Pinia
Vue 专属状态管理库, 可以 跨组件或页面共享状态.
- 测试工具集
- 插件
- 自动补全功能
- 支持服务端渲染
- Devtools 支持
- 热更新
Store
store 是一个保存状态和业务逻辑的实体, 并不与组件树绑定.
像一个永远存在的组件, 每个组件都可以读取和写入它.
三个概念
- state 类比 vue 的 data
- getter 类比 vue 的 computed
- action 类比 vue 的 methods
什么时候使用 Store ?
一个 Store 应该包含在整个应用中访问的数据. 应该避免引入原本可以在组件中保存的本地数据.
定义 Store
你应该在不同的文件中去定义 store。 (为了 typescript 推断, 构建工具进行代码分割等.)
export const useAlertsStore = defineStore("alerts", {});
- 返回值建议使用 use 开头, Store 结尾的格式进行命名
- 第一个参数为 Store 的Id, 需全局唯一
- 第二个参数为 Setup 函数或 Option 对象
Option Store: 传入带有 state, actions, getters 属性的 Option 对象.
export const useCounterStore = defineStore("counter", {
// data 数据
state: () => ({ count: 0 }),
// computed 计算数学
getters: {
double: (state) => state.count * 2,
},
// methods 方法
actions: {
increment() {
this.count++;
},
},
});
Setup Store: 传入一个 Setup 箭头函数.
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const name = ref("wangzhy");
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, name, doubleCount, increment };
});
对应关系:
ref --> data
computed() --> getters
function() --> actions
Setup Store 需要返回 state 的所有属性
要让 pinia 正确识别 state,你必须在 setup store 中返回 state 的所有属性。这意味着,你不能在 store 中使用私有属性。
需要将所有通过 ref 定义的 state 进行显式返回. 否则 Pinia 无法将其识别为响应式状态.
pinia Setup Store 可以在 inject 中使用 app.provide 提供的数据.
import { inject } from "vue";
import { useRoute } from "vue-router";
import { defineStore } from "pinia";
export const useSearchFilters = defineStore("search-filters", () => {
const route = useRoute();
// 这里假定 `app.provide('appProvided', 'value')` 已经调用过
const appProvided = inject("appProvided");
// ...
return {
// ...
};
});
需要注意的是,不要在 Setup Store 中将 appProvided 进行返回. 因为这并不是 Store 的东西.
使用 Store
import { useCounterStore } from "@/stores/counter";
const counter = useCounterStore();
Store 是一个用 reactive 包装的对象, 这意味着不需要在 getters 后面写 .value.就像 setup 中的 props 一样, 不能对 store 解构
从 Store 解构: storeToRefs()
data, getters 需要通过 storeToRefs() 进行解构.
actions 可以直接进行解构.
import { useCounterStore } from "@/stores/counter";
import { storeToRefs } from "pinia";
const counter = useCounterStore();
// storeToRefs 会跳过所有的 actions 和非响应式(即非ref/非reactive)属性
const { count } = storeToRefs(counter);
const { increment } = counter;
State
Store 的核心.
定义 state
export const useUserStore = defineStore("storeId", {
state: () => {
return {
// 会自动推断出它们的类型
count: 0,
name: "wangzhy",
isAdmin: true,
items: [],
hasChanged: true,
};
},
});
export const useUserInfoStore = defineStore("storeId2", {
state: (): UserInfoState => {
return {
// 初始化空列表
userList: [] as UserInfo[],
// 用于尚未加载的数据
user: null as UserInfo | null,
};
},
});
interface UserInfo {
name: string;
age: number;
}
interface UserInfoState {
userList: UserInfo[];
user: UserInfo | null;
}
访问 state
通过 store.xxx 可直接访问
重置 state
- 在选项式 api 中,可以直接使用 store.$reset() 方法
- 在组合式 api 中,需要自己定义 $reset() 方法
store.$reset();
变更 state
- store.xxx=xxx
- store.$patch
store.$patch({
count: store.count + 1,
});
store.$patch((state) => {
state.items.push({ name: "shoes", quantity: 1 });
state.hasChanged = true;
});
替换 state
- 通过
store.$patch({xxx:xxx}) - pinia.state.value =
订阅 state
counter.$subscribe(
(mutation, state) => {
console.log("mutation.type", mutation.type, "mutation.storeId", mutation.storeId);
console.log(mutation.storeId);
localStorage.setItem("counter", JSON.stringify(state.count));
},
{
// 刷新时机
flush: "sync",
}
);
getters
Getter 只是幕后的计算属性, 所有不可以向它们传递任何参数。 不过,可以从 getter 返回一个函数, 该函数可以接受任意参数。
getters: {
getUserById: (state) => {
return (userName: string) => state.userList.find((user) => user.name === userName);
},
},
此时, getter 将不再被缓存。(指的是 getUserById(1) 的结果不会被缓存.) 如果想要缓存 getUserById(1) 的结果, 需要在 getUserById 函数定义上下功夫.
export const useStateCacheStore = defineStore("main", () => {
const products = ref([
{ id: 1, name: "iPhone", price: 999 },
{ id: 2, name: "iPad", price: 799 },
{ id: 3, name: "MacBook", price: 1999 },
]);
const getProductById = computed(() => {
const cache = new Map();
console.log("定义了 cache 对象");
return (id: number) => {
if (cache.has(id)) {
console.log(`从缓存获取产品 ${id}`);
return cache.get(id);
}
// 没有缓存则计算
console.log(`计算产品 ${id}`);
const product = products.value.find((p) => p.id === id);
cache.set(id, product);
return product;
};
});
return { products, getProductById };
});
Action
- 选项式通过 actions 进行定义
- 组合式通过 function 进行定义
订阅 action
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now();
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(", ")}].`);
// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`);
});
// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`);
});
}
);
// 手动删除监听器
unsubscribe();
插件
- 为 store 添加新属性
- 定义 store 时增加新的选项
- 为 store 增加新 的方法
- 包装现有的方法
- 改变甚至取消 action
- 实现 effect, 如本地存储
- 仅应用于特定 store
pinia 插件是一个函数, 可以选择性返回要添加到 store 的属性. 接收一个可选参数, context
定义插件
export function myPiniaPlugin({
pinia, // createPinia 创建的 pinia 对象
app, // createApp 创建的 vue 对象
store, // 要扩展的 store
options, // 传给 defineStore() 的 store 的可选对象
}) {
// ...
}
使用插件
pinia.use(myPiniaPlugin);
扩展 Store
- 返回一个对象(推荐,不需要额外操作,devtools 可自动跟踪)
pinia.use(() => ({
hello: "pinia",
}));
- 通过 context 的 store 进行扩展(需要进行额外操作才能被 devtools 跟踪)
pinia.use(({ store }) => {
store.abc = "123";
// 需要下面的代码才能被 devtools 跟踪
if (process.env.NODE_ENV === "development") {
store._customProperties.add("abc");
}
});
每个 store 都被 reactive 包装过, 所以可以自动解包
添加新的 state
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty("hasError")) {
const hasError = ref(false);
store.$state.hasError = hasError;
}
store.hasError = toRef(store.$state, "hasError");
});