警告
Vue 团队已弃用 Vuex,转而支持 Pinia。
在大型应用程序中,状态管理通常会变得很复杂,因为多个状态片段分散在许多组件中,并且它们之间存在交互。人们往往忽视了 Vue 实例中真相来源是原始数据对象 - Vue 实例只是代理对它的访问。因此,如果您有一部分状态应由多个实例共享,则应避免重复它并通过身份共享它。
如果您希望组件共享状态,推荐的方法是使用 Vuex。在深入研究之前,请查看其 文档。它在与 Vue 开发者工具 浏览器扩展(如时间旅行调试)一起使用时具有出色的功能。
我们不会详细介绍如何配置或使用 Vuex,因为它有很好的文档。相反,我们只会向您展示在 Quasar 项目中使用 Vuex 时文件夹结构的外观。
默认情况下,如果您选择在使用 Quasar CLI 创建项目文件夹时使用 Vuex,它会将您设置为使用 Vuex 模块。 /src/store
的每个子文件夹代表一个 Vuex 模块。
如果您在创建项目期间没有选择 Vuex 选项,但希望稍后添加它,那么您只需要查看下一节并创建 src/store/index.js
文件。
提示
如果 Vuex 模块对您的网站应用程序来说太大了,您可以更改 /src/store/index.js
并避免导入任何模块。
添加 Vuex 模块。
Quasar CLI 通过 $ quasar new
命令简化了添加 Vuex 模块的过程。
$ quasar new store <store_name> [--format ts]
它将在 /src/store
中创建一个文件夹,其名称来自上面命令中的 “store_name”。它将包含您需要的全部样板代码。
假设您想创建一个名为 “showcase” 的 Vuex 模块。您将执行 $ quasar new store showcase
。然后您会注意到新创建的 /src/store/showcase
文件夹,它包含以下文件
我们已经创建了新的 Vuex 模块,但尚未通知 Vuex 使用它。因此,我们编辑 /src/store/index.js
并添加对它的引用
import { createStore } from 'vuex'
import showcase from './showcase'
export default function (/* { ssrContext } */) {
const Store = createStore({
modules: {
showcase
},
// enable strict mode (adds overhead!)
// for dev mode and --debug builds only
strict: process.env.DEBUGGING
})
return Store
}
提示
如果您正在开发 SSR 应用程序,则可以查看服务器端提供的 ssrContext 对象。
现在我们可以在 Vue 文件中使用此 Vuex 模块。以下是一个快速示例。假设我们在状态中配置了 drawerState
并添加了 updateDrawerState
变异。
export const updateDrawerState = (state, opened) => {
state.drawerState = opened
}
// src/store/showcase/state.js
// Always use a function to return state if you use SSR
export default function () {
return {
drawerState: true
}
}
在 Vue 文件中
<template>
<div>
<q-toggle v-model="drawerState" />
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup () {
const $store = useStore()
const drawerState = computed({
get: () => $store.state.showcase.drawerState,
set: val => {
$store.commit('showcase/updateDrawerState', val)
}
})
return {
drawerState
}
}
}
</script>
TypeScript 支持
如果您选择在使用 Quasar CLI 创建项目文件夹时使用 Vuex 和 TypeScript,它将在 src/store/index.ts
中添加一些类型代码。要获得组件中的类型化 Vuex 存储,您需要修改 Vue 文件,使其类似于以下内容
<template>
<!-- ... -->
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from 'src/store';
export default defineComponent({
setup () {
const $store = useStore();
// You can use the $store, example: $store.state.someStoreModule.someData
},
});
</script>
警告
使用 Vuex 时,目前只有状态是强类型化的。如果您想使用类型化获取器/变异/操作,则需要使用 Vuex 之上的额外包或 Vuex 的替代品。
使用 Vuex 智能模块
完全类型化存储的选项之一是名为 vuex-smart-module
的包。您可以通过运行以下命令来添加此包
$ yarn add vuex-smart-module
安装完成后,您需要编辑 src/store/index.ts
文件以使用此包创建存储。编辑存储索引文件使其类似于以下内容
import { store } from 'quasar/wrappers';
import {
createStore,
Module,
createComposable,
Getters,
Mutations,
} from 'vuex-smart-module';
class RootState {
count = 1;
}
class RootGetters extends Getters<RootState> {
get count () {
return this.state.count;
}
multiply (multiplier: number) {
return this.state.count * multiplier;
}
}
class RootMutations extends Mutations<RootState> {
add (payload: number) {
this.state.count += payload;
}
}
// This is the config of the root module
// You can define a root state/getters/mutations/actions here
// Or do everything in separate modules
const rootConfig = {
state: RootState,
getters: RootGetters,
mutations: RootMutations,
modules: {
//
},
};
export const root = new Module(rootConfig);
export default store (function (/* { ssrContext } */) {
const rootStore = createStore(root, {
strict: !!process.env.DEBUGGING,
// plugins: []
// and other options, normally passed to Vuex `createStore`
});
return rootStore;
});
export const useStore = createComposable(root);
您可以像使用普通 Vuex 一样使用模块,并且您可以在该模块中选择将所有内容放在一个文件中,或者为状态、获取器、变异和操作使用单独的文件。当然,也可以将这两种方法结合使用。
只需将模块导入 src/store/index.ts
并将其添加到您的 rootConfig
中即可。例如,请查看 此处
在 Vue 文件中使用类型化存储非常简单,以下是一个示例
<template>
<q-page class="column items-center justify-center">
<q-btn @click="store.mutations.add(3)" label="Add count" />
<div>Count: {{ store.getters.count }}</div>
<div>Multiply(5): {{ store.getters.multiply(5) }}</div>
</q-page>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore, root } from 'src/store';
export default defineComponent({
name: 'PageIndex',
setup () {
const store = useStore();
return { store };
},
});
</script>
在引导文件中使用类型化存储
在引导文件中使用存储时,也可以使用类型化存储。以下是一个非常简单的引导文件的示例
import { boot } from 'quasar/wrappers'
import { root } from 'src/store'
export default boot(({ store }) => {
root.context(store).mutations.add(5);
});
在预取中使用类型化存储
同样,您也可以在使用 预取功能 时使用类型化存储。以下是一个示例
<script lang="ts">
import { defineComponent } from 'vue';
import { root } from 'src/store';
export default defineComponent({
name: 'PageIndex',
preFetch ({ store }) {
root.context(store).mutations.add(5);
},
setup () {
//
},
});
</script>
存储代码拆分
您可以利用 预取功能 来代码拆分 Vuex 模块。
代码拆分 Vuex 智能模块
与常规 Vuex 相比,使用 Vuex 智能模块进行代码拆分的工作方式略有不同。
假设我们有以下模块示例
// simple module example, with everything in one file
import { Getters, Mutations, Actions, Module, createComposable } from 'vuex-smart-module';
class ModuleState { greeting = 'Hello'}
class ModuleGetters extends Getters<ModuleState> {
get greeting () {
return this.state.greeting;
}
}
class ModuleMutations extends Mutations<ModuleState> {
morning () {
this.state.greeting = 'Good morning!';
}
}
class ModuleActions extends Actions<ModuleState, ModuleGetters, ModuleMutations, ModuleActions> {
waitForIt (payload: number) {
return new Promise<void>(resolve => {
setTimeout(() => {
this.commit('morning');
resolve();
}, payload);
});
}
}
export const admin = new Module({
state: ModuleState,
getters: ModuleGetters,
mutations: ModuleMutations,
actions: ModuleActions,
});
export const useAdmin = createComposable(admin);
然后,我们只想在访问特定路由组件时加载此模块。我们至少可以通过两种不同的方式来实现。
第一种方法是使用 Quasar 提供的 预取功能,类似于常规 Vuex 的示例,可以在 此处 找到。要实现此方法,我们在 router/routes.ts
文件中定义一个路由。在本示例中,我们有一个 /admin 路由,它是 MainLayout 的子路由。
{ path: 'admin', component: () => import('pages/Admin.vue') }
我们的 Admin.vue
文件如下所示
<template>
<q-page class="column items-center justify-center">
{{ greeting }}
<q-btn to="/" label="Home" />
</q-page>
</template>
<script lang="ts">
import { defineComponent, onUnmounted } from 'vue';
import { registerModule, unregisterModule } from 'vuex-smart-module'
import { admin, useAdmin } from 'src/store/module';
import { useStore } from 'vuex';
export default defineComponent({
name: 'PageIndex',
preFetch ({ store }) {
if (!store.hasModule('admin')) {
registerModule(store, 'admin', 'admin/', admin);
}
},
setup() {
const $store = useStore();
// eslint-disable-next-line
if (!process.env.SERVER && !$store.hasModule('admin') && (window as any).__INITIAL_STATE__) {
// This works both for SSR and SPA
registerModule($store, ['admin'], 'admin/', admin, {
preserveState: true,
});
}
const adminStore = useAdmin();
const greeting = adminStore.getters.greeting;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line
if (module.hot) {
module.hot.accept(['src/store/module'], () => {
// This is necessary to prevent errors when this module is hot reloaded
unregisterModule($store, admin);
registerModule($store, ['admin'], 'admin/', admin, {
preserveState: true,
});
});
}
onUnmounted(() => {
unregisterModule($store, admin);
});
return { greeting };
},
});
</script>
第二种方法是使用 router.beforeEach
钩子来注册/注销动态存储模块。如果您应用程序的某个部分仅被一小部分访问者使用,这种方法就很有意义。例如,您网站上的 /admin
部分,其中包含多个子路由。然后,您可以在路由导航时检查路由是否以 /admin
开头,并根据此条件为每个以 /admin/...
开头的路由加载存储模块。
要实现此方法,您可以使用 Quasar 中的 启动文件,代码如下所示
提示
下面的示例旨在与 SSR 和 SPA 兼容。如果您只使用 SPA,可以通过完全删除 registerModule
的最后一个参数来简化代码。
import { boot } from 'quasar/wrappers';
import { admin } from 'src/store/module';
import { registerModule, unregisterModule } from 'vuex-smart-module';
// If you have never run your app in SSR mode, the ssrContext parameter will be untyped,
// Either remove the argument or run the project in SSR mode once to generate the SSR store flag
export default boot(({ store, router, ssrContext }) => {
router.beforeEach((to, from, next) => {
if (to.fullPath.startsWith('/admin')) {
if (!store.hasModule('admin')) {
registerModule(store, ['admin'], 'admin/', admin, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
preserveState: !ssrContext && !from.matched.length && Boolean(window.__INITIAL_STATE__),
})
}
} else if (store.hasModule('admin')) {
unregisterModule(store, admin);
}
next();
});
});
在您的组件中,您可以直接使用动态模块,而无需担心注册问题。例如
<template>
<q-page class="column items-center justify-center">
{{ greeting }}
<q-btn to="/" label="Home" />
</q-page>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useAdmin } from 'src/store/module';
export default defineComponent({
name: 'PageIndex',
setup() {
const adminStore = useAdmin();
const greeting = adminStore.getters.greeting;
return { greeting };
},
});
</script>
在 Vuex 存储中访问路由
只需在 Vuex 存储中使用 this.$router
即可访问路由器。
以下是一个示例
export function whateverAction (state) {
this.$router.push('...')
}