Store
什麼是 Store?
隨著應用程式變得越來越大,您通常需要在多個組件之間共享狀態.在 Vue.js 生態系統中,Pinia 提供了這個功能.
在本章中,我們將實現 Pinia 的基本功能作為 chibivue-store.
為什麼需要函式庫?
如果您只是想在組件之間共享狀態,在模組作用域導出 ref 和 computed 就足夠了:
// stores/counter.ts
import { ref, computed } from "chibivue";
export const count = ref(0);
export const doubleCount = computed(() => count.value * 2);
export const increment = () => count.value++;這在 CSR(客戶端渲染)中沒有問題.但是,在 SSR(伺服器端渲染)中會導致嚴重的問題.

在 SSR 中,您必須注意「Cross-Request State Pollution(跨請求狀態污染)」.
由於伺服器只初始化模組一次,上述模組作用域的狀態會在所有請求之間共享. 這可能導致一個使用者的狀態洩漏給另一個使用者.
使用像 Pinia 這樣的狀態管理函式庫,只需在 setup 中呼叫 useXxxStore(),函式庫就會自動處理每個請求的狀態隔離.
如果您使用 Nuxt,它提供了 useState,一個 SSR 友好的狀態管理組合式函式. 對於簡單的狀態共享,useState 可能足夠,無需引入 Pinia.
本章涵蓋從基本的 CSR 使用到 SSR 水合.
有關 SSR 的更多詳細資訊,請參閱 SSR 章節.
套件結構
chibivue-store 在 @extensions/chibivue-store 套件中提供.
@extensions/chibivue-store/src/
├── index.ts # 導出
├── createStore.ts # 根 store 創建
├── rootStore.ts # Store 介面和符號
└── store.ts # defineStore 實現類型定義
StateTree
表示 store 持有的狀態的類型.
// rootStore.ts
export type StateTree = Record<string | number | symbol, any>;Store 介面
定義根 store 的公共 API.
// rootStore.ts
export interface Store {
install: (app: App) => void;
use(plugin: StorePlugin): Store;
state: Ref<Record<string, StateTree>>;
_p: StorePlugin[];
_a: App | null;
_e: EffectScope;
_s: Map<string, StoreGeneric>;
}install: 作為 Vue 插件的安裝方法use: 添加插件的方法state: 保存所有 store 狀態的 ref(用於 SSR)_p: 已安裝的插件_a: 連結到此 store 的 App_e: store 附加的 EffectScope_s: 按 ID 管理已定義 store 的 Map
StoreInstance 介面
定義每個 store 實例可用的方法.
// store.ts
export interface StoreInstance<
Id extends string = string,
S extends StateTree = StateTree,
G extends _GettersTree<S> = _GettersTree<S>,
A = Record<string, (...args: any[]) => any>,
> {
$id: Id;
$state: S;
$patch: (partialState: Partial<S> | ((state: S) => void)) => void;
$reset: () => void;
}$id: Store 識別符$state: Store 狀態(僅 Options API 風格)$patch: 批量狀態更新$reset: 重置狀態為初始值(僅 Options API 風格)
依賴注入鍵
定義通過 provide/inject 共享 store 的鍵.
// rootStore.ts
import type { InjectionKey } from "chibivue";
export const storeSymbol: InjectionKey<Store> = Symbol();此符號用於在整個應用程式中 provide 由 createStore() 創建的 store.
createStore 實現
創建根 store 的函式.
// createStore.ts
import { effectScope, markRaw, ref } from "chibivue";
import { type Store, setActiveStore, storeSymbol } from "./rootStore";
export function createStore(): Store {
const scope = effectScope();
const state = scope.run(() => ref({}))!;
let _p: StorePlugin[] = [];
let toBeInstalled: StorePlugin[] = [];
const store: Store = markRaw({
install(app) {
setActiveStore(store);
store._a = app;
app.provide(storeSymbol, store);
toBeInstalled.forEach((plugin) => _p.push(plugin));
toBeInstalled = [];
},
use(plugin) {
if (!this._a) {
toBeInstalled.push(plugin);
} else {
_p.push(plugin);
}
return this;
},
_p,
_a: null,
_e: scope,
_s: new Map(),
state,
});
return store;
}關鍵點:
effectScope()創建 detached scope,管理 store 的生命週期state是ref({}),集中管理所有 store 的狀態(用於 SSR)markRaw使 store 對象本身不被響應式化install方法呼叫app.provide使 store 在整個應用程式中可用
管理 activeStore
// rootStore.ts
export let activeStore: Store | undefined;
export const setActiveStore = (store: Store | undefined): Store | undefined =>
(activeStore = store);
export const getActiveStore = (): Store | undefined => {
const store = hasInjectionContext() && inject(storeSymbol, null);
if (__DEV__ && !store && typeof window === "undefined") {
console.warn(
`[chibivue-store]: Store instance not found in context. ` +
`This falls back to the global activeStore which exposes you to ` +
`cross-request state pollution on the server.`,
);
}
return store || activeStore;
};activeStore 用於從組件外部存取 store(例如,在其他 store 內部).
getActiveStore 使用 hasInjectionContext() 確認 injection context,在 SSR 環境中如果沒有 context 則發出警告.這可以讓開發者了解 Cross-Request State Pollution 的風險.
defineStore 實現
定義單個 store 的函式.與 Pinia 一樣,它支援兩種定義風格.
Composition API 風格
// Composition API style (setup function)
export function defineStore<Id extends string, SS extends StateTree>(
id: Id,
setup: () => SS,
): () => SS;傳遞 setup 函式並使用 ref 和 computed 定義狀態.
Options API 風格
// Options API style
export function defineStore<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A extends Record<string, (...args: any[]) => any>,
>(options: StoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>;使用包含 state,getters 和 actions 的物件定義.
使用範例
Composition API 風格
// stores/counter.ts
import { ref, computed } from "chibivue";
import { defineStore } from "chibivue-store";
export const useCounterStore = defineStore("counter", () => {
// State
const count = ref(0);
// Getters(使用 computed)
const doubleCount = computed(() => count.value * 2);
// Actions
const increment = () => {
count.value++;
};
const reset = () => {
count.value = 0;
};
return {
count,
doubleCount,
increment,
reset,
};
});Options API 風格
// stores/counter.ts
import { defineStore } from "chibivue-store";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2;
},
},
actions: {
increment() {
this.count++;
},
},
});在應用程式中註冊
// main.ts
import { createApp } from "chibivue";
import App from "./App.vue";
import { createStore } from "chibivue-store";
const app = createApp(App);
app.use(createStore());
app.mount("#app");在組件中使用
<!-- Counter.vue -->
<script setup>
import { useCounterStore } from "../stores/counter";
const counterStore = useCounterStore();
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
</div>
</template>使用 $patch
$patch 允許一次更新多個狀態屬性.
物件形式
const store = useCounterStore();
store.$patch({
count: 10,
});函式形式
const store = useCounterStore();
store.$patch((state) => {
state.count += 5;
});使用 $reset
對於使用 Options API 風格定義的 store,$reset 將狀態重置為初始值.
const store = useCounterStore();
store.increment(); // count: 1
store.increment(); // count: 2
store.$reset(); // count: 0(回到初始值)SSR 支援
chibivue-store 支援伺服器端渲染(SSR).
store.state 屬性
根 store 的 state 屬性允許您序列化和水合所有 store 狀態.
// Store interface
interface Store {
install: (app: App) => void;
state: Ref<Record<string, StateTree>>; // 保存所有 store 的狀態
_e: EffectScope;
_s: Map<string, StoreGeneric>;
}state 作為 ref({}) 創建,每個 store 的狀態保存在 state.value[storeId] 中. 這樣可以:
- SSR 序列化伺服器端狀態:
JSON.stringify(store.state.value) - 客戶端水合:
store.state.value = serverState
伺服器端:序列化狀態
// server.ts
import { createApp } from "chibivue";
import { renderToString } from "@chibivue/server-renderer";
import { createStore } from "chibivue-store";
import App from "./App.vue";
export async function render() {
// 重要:為每個請求創建新實例
// 這可以防止 Cross-Request State Pollution
const store = createStore();
const app = createApp(App);
app.use(store);
const html = await renderToString(app);
// 序列化 store 狀態
const storeState = JSON.stringify(store.state.value);
return { html, storeState };
}
注意 createStore() 和 createApp() 是在 render() 函式內部呼叫的. 您不能在模組作用域創建它們作為單例.
// 錯誤:在模組作用域創建是危險的
const store = createStore(); // 在所有請求之間共享!
const app = createApp(App);
export async function render() {
// store 和 app 在所有請求之間共享
}嵌入 HTML
<!DOCTYPE html>
<html>
<head>
<script>
window.__STORE_STATE__ = ${storeState};
</script>
</head>
<body>
<div id="app">${html}</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>客戶端:水合狀態
// main.ts (client)
import { createApp } from "chibivue";
import { createStore } from "chibivue-store";
import App from "./App.vue";
const store = createStore();
const app = createApp(App);
app.use(store);
// 使用伺服器狀態水合
if (window.__STORE_STATE__) {
store.state.value = window.__STORE_STATE__;
}
app.mount("#app");
chibivue-store 現在支援 SSR. 通過將伺服器計算的狀態傳輸到客戶端,您可以在水合後保持一致的狀態.
未來擴展
當前實現涵蓋了基本功能,但 Pinia 還有:
- $subscribe: 訂閱狀態變更
- $onAction: 監控 action 執行
- 插件系統: 擴展 store 功能
- Devtools 整合: 狀態視覺化和時間旅行除錯
- mapState / mapActions: Options API 組件的輔助函式
總結
chibivue-store 實現包括:
- 根 Store 創建: 使用
createStore作為 Vue 插件安裝 - 依賴注入: 通過
provide/inject在組件樹中共享 store - 兩種定義風格: 支援 Composition API 和 Options API
- Getters: 使用
computed定義派生狀態 - Actions: 可以存取 state 和 getters 的方法
- $patch: 批量狀態更新
- $reset: 重置狀態為初始值(僅 Options API)
- 單例模式: 每個 store ID 只創建一個實例
- SSR 支援: 通過
store.state序列化和水合狀態
通過結合 Vue 的插件系統,provide/inject 和響應式系統,我們實現了全域狀態管理.
