警告
Vue 團隊已棄用 Vuex,轉而支持 Pinia。
在大型應用程式中,由於多個狀態分散在許多組件以及它們之間的互動,狀態管理通常變得複雜。經常被忽略的是,Vue 實例中的真理來源是原始資料物件 - Vue 實例只是代理對它的存取。因此,如果您有一個狀態片段應該由多個實例共享,您應該避免複製它,並通過身份共享它。
如果您希望組件共享狀態,建議的方式是 Vuex。在深入研究之前,請查看其文件。當與 Vue 開發者工具 瀏覽器擴充功能(如 Time Travel debugging)一起使用時,它有一個很棒的功能。
我們不會詳細介紹如何設定或使用 Vuex,因為它有很棒的文件。相反,我們只會向您展示在 Quasar 專案中使用它時的資料夾結構。
預設情況下,如果您在創建 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 模組。這是一個快速範例。假設我們在 state 中設定了 drawerState
並新增了 updateDrawerState
mutation。
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 store,您需要像這樣修改您的 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,目前只有 state 是強型別的。如果您想要使用類型化的 getters/mutations/actions,您將需要使用 Vuex 之上的額外套件或 Vuex 的替代品。
使用 Vuex Smart Module
完全類型化 store 的選項之一是名為 vuex-smart-module
的套件。您可以透過執行以下指令來新增此套件
$ yarn add vuex-smart-module
安裝完成後,您需要編輯 src/store/index.ts
檔案以使用此套件來建立 store。編輯您的 store 索引檔案以類似於以下內容
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 一樣使用模組,並且在該模組中,您可以選擇將所有內容放在一個檔案中,或為 state、getters、mutations 和 actions 使用單獨的檔案。或者,當然,這兩者的組合。
只需在 src/store/index.ts
中匯入模組,並將其新增至您的 rootConfig
。有關範例,請查看此處
在 Vue 檔案中使用類型化的 store 非常簡單,這是一個範例
<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>
在 Boot 檔案中使用類型化的 store
在 Boot 檔案中使用 store 時,也可以使用類型化的 store。這是一個非常簡單的 Boot 檔案範例
import { boot } from 'quasar/wrappers'
import { root } from 'src/store'
export default boot(({ store }) => {
root.context(store).mutations.add(5);
});
在 Prefetch 中使用類型化的 store
同樣地,當使用 Prefetch 功能時,您也可以使用類型化的 store。這是一個範例
<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>
Store Code Splitting
您可以利用 PreFetch 功能來程式碼分割 Vuex 模組。
Code splitting Vuex Smart Module
與常規 Vuex 相比,使用 Vuex Smart Module 進行程式碼分割的工作方式略有不同。
假設我們有以下模組範例
// 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 提供的 PreFetch 功能,類似於常規 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
鉤子來註冊/取消註冊我們的動態 store 模組。 如果你的應用程式中有一個部分只有少數訪客會使用,那麼這就很有意義。 例如,你的網站的 /admin
部分,你在其下有多個子路由。 然後你可以檢查路由是否以 /admin
開頭,並根據這一點為每個以 /admin/...
開頭的路由載入 store 模組。
要做到這一點,你可以在 Quasar 中使用一個 Boot File,看起來像這樣
提示
下面的範例旨在與 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 stores 中存取 router
只需在 Vuex stores 中使用 this.$router
即可存取 router。
這是一個範例
export function whateverAction (state) {
this.$router.push('...')
}