pinia
pinia 是 vue 官方推荐的存储管理库,可用于管理全局的数据.
基础
import { createPinia } from "pinia";
app.use(createPinia());
这样我们就创建了一个 pinia(root store)并注册为 vue 插件.
在pinia
中,store
指的是一个存放 state, getters and actions的实体,它们分别对应data
, computed
and methods
.
定义store
:
import { defineStore } from "pinia";
// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useStore = defineStore("main", {
// other options...
});
使用:
import { useStore } from "@/stores/counter";
export default {
setup() {
const store = useStore();
return {
// you can return the whole store instance to use it in the template
store,
};
},
};
注意 store 使用的是 vue 的reactive
,故若想使用 destruction 语法,需要:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useStore()
// `name` and `doubleCount` are reactive refs
// This will also create refs for properties added by plugins
// but skip any action or non reactive (non ref/reactive) property
const { name, doubleCount } = storeToRefs(store)
// the increment action can be just extracted
const { increment } = store
return {
name,
doubleCount
increment,
}
},
})
state
在 pinia,我们通过一个函数定义定义state
:
import { defineStore } from "pinia";
const useStore = defineStore("storeId", {
// arrow function recommended for full type inference
state: () => {
return {
// all these properties will have their type inferred automatically
counter: 0,
name: "Eduardo",
isAdmin: true,
};
},
});
修改:
const store = useStore();
//重置为初始值
store.$reset();
//合并多个修改
store.$patch({
counter: store.counter + 1,
name: "Abalam",
});
cartStore.$patch((state) => {
state.items.push({ name: "shoes", quantity: 1 });
state.hasChanged = true;
});
//替换所有state
store.$state = { counter: 666, name: "Paimon" };
我们还可以监视更改:
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type; // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId; // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload; // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem("cart", JSON.stringify(state));
});
使用$subscribe
而不是 watch 的好处是它在更改发送后只会触发一次.
默认情况下它被限制在 component 的范围,如果想要让 component 取消挂载后仍有些,需要设置detached
:
export default {
setup() {
const someStore = useSomeStore();
// this subscription will be kept after the component is unmounted
someStore.$subscribe(callback, { detached: true });
// ...
},
};
如果是通过前面的 composition api 的形式使用store
,我们直接通过store
访问 state,不过如果使用 option api(不使用 setup)的话,需要使用辅助的函数.
只读:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.counter inside the component
// same as reading from store.counter
...mapState(useCounterStore, ['counter'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'counter',
// you can also write a function that gets access to the store
double: store => store.counter * 2,
// it can have access to `this` but it won't be typed correctly...
magicValue(store) {
return store.someGetter + this.counter + this.double
},
}),
},
}
读写:
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.counter inside the component and allows setting it
// this.counter++
// same as reading from store.counter
...mapWritableState(useCounterStore, ['counter'])
// same as above but registers it as this.myOwnName
...mapWritableState(useCounterStore, {
myOwnName: 'counter',
}),
},
}
getter
定义getter
(可带参数也可以不带而使用this
):
export const useStore = defineStore("main", {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
});
注意如果使用了其他的不使用 arrow function 定义getter
,且我们使用了this
,则在 typescript 中我们必须指明返回值类型:
export const useStore = defineStore("main", {
state: () => ({
counter: 0,
}),
getters: {
// automatically infers the return type as a number
doubleCount(state) {
return state.counter * 2;
},
// the return type **must** be explicitly set
doublePlusOne(): number {
// autocompletion and typings for the whole store ✨
return this.doubleCount + 1;
},
},
});
我们可以通过返回一个函数的形式使得getter
可以带参数:
export const useStore = defineStore("main", {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId);
},
},
});
但注意这种情况下它不会被缓存.
使用其他 store 的 getter 只需要使用它的 store 即可:
import { useOtherStore } from "./other-store";
export const useStore = defineStore("main", {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore();
return state.localData + otherStore.data;
},
},
});
在 option api 中使用 getter 和之前一样使用mapstate
:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.doubleCounter inside the component
// same as reading from store.doubleCounter
...mapState(useCounterStore, ['doubleCount'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCounter',
// you can also write a function that gets access to the store
double: store => store.doubleCount,
}),
},
}
action
定义action
:
import { mande } from "mande";
const api = mande("/api/users");
export const useUsers = defineStore("users", {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password });
showTooltip(`Welcome back ${this.userData.name}!`);
} catch (error) {
showTooltip(error);
// let the form component display the error
return error;
}
},
},
});
注意它可以是异步的.
action
一般可以直接通过 store instance 访问.
当使用不用 setup 的 option api 时:
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
methods: {
// gives access to this.increment() inside the component
// same as calling from store.increment()
...mapActions(useCounterStore, ['increment'])
// same as above but registers it as this.myOwnName()
...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
},
}
我们还可以监视action
:
const unsubscribe = someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
// a shared variable for this specific action call
const startTime = Date.now();
// this will trigger before an action on `store` is executed
console.log(`Start "${name}" with params [${args.join(", ")}].`);
// this will trigger if the action succeeds and after it has fully run.
// it waits for any returned promised
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
);
});
// this will trigger if the action throws or returns a promise that rejects
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
);
});
}
);
// manually remove the listener
unsubscribe();
默认情况下这种监视会限制在组件的范围,想在组件取消挂载时仍然可用:
export default {
setup() {
const someStore = useSomeStore();
// this subscription will be kept after the component is unmounted
someStore.$onAction(callback, true);
// ...
},
};
plugin
pinia 中 plugin 为一个函数,通过pinia.use()
使用,它有如下功能:
- Add new properties to stores
- Add new options when defining stores
- Add new methods to stores
- Wrap existing methods
- Change or even cancel actions
- Implement side effects like Local Storage
- Apply only to specific stores
该函数可以带参数:
export function myPiniaPlugin(context) {
context.pinia; // the pinia created with `createPinia()`
context.app; // the current app created with `createApp()` (Vue 3 only)
context.store; // the store the plugin is augmenting
context.options; // the options object defining the store passed to `defineStore()`
// ...
}
plugin
只在 pinia 传给 app 后创建的 store 起效.
为 store 添加 property:
pinia.use(() => ({ hello: "world" }));
也可以通过参数:
// from the example above
pinia.use(({ store }) => {
store.hello = "world";
// make sure your bundler handle this. webpack and vite should do it by default
if (process.env.NODE_ENV === "development") {
// add any keys you set on the store
store._customProperties.add("hello");
}
});
注意后者为了让 devtools 可跟踪多了一个步骤.
store 会自动unwrapping
,不需要加.value:
const sharedRef = ref("shared");
pinia.use(({ store }) => {
// each store has its individual `hello` property
store.hello = ref("secret");
// it gets automatically unwrapped
store.hello; // 'secret'
// all stores are sharing the value `shared` property
store.shared = sharedRef;
store.shared; // 'shared'
});
添加 state 则需要在以下两个地方添加:
- 在
store
以通过 store 访问 - 在
store.$state
所以在 devtools 中可用,在 SSR 过程中会被serialized
const globalSecret = ref('secret')
pinia.use(({ store }) => {
// `secret` is shared among all stores
store.$state.secret = globalSecret
store.secret = globalSecret
// it gets automatically unwrapped
store.secret // 'secret'
const hasError = ref(false)
store.$state.hasError = hasError
// this one must always be set
store.hasError = toRef(store.$state, 'hasError')
// in this case it's better not to return `hasError` since it
// will be displayed in the `state` section in the devtools
// anyway and if we return it, devtools will display it twice.
注意在 plugin 中的修改不会被subscriptions
跟踪到,因为它发生在 store 可用之前.
当加入外部的不是 reactive 的属性:
import { markRaw } from "vue";
// adapt this based on where your router is
import { router } from "./router";
pinia.use(({ store }) => {
store.router = markRaw(router);
});
在 plugin 可正常地使用$subscribe
和onAction
.
我们还可以创建新的 option 然后在 plugin 中使用:
defineStore("search", {
actions: {
searchContacts() {
// ...
},
},
// this will be read by a plugin later on
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300,
},
});
// use any debounce library
import debounce from "lodash/debounce";
pinia.use(({ options, store }) => {
if (options.debounce) {
// we are overriding the actions with new ones
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
);
return debouncedActions;
}, {});
}
});
pinia 有完整的 typescript 支持,故 plugin 也可以很好地组织类型.
参数:
import { PiniaPluginContext } from "pinia";
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}
添加新 store property:
import "pinia";
declare module "pinia" {
export interface PiniaCustomProperties {
// by using a setter we can allow both strings and refs
set hello(value: string | Ref<string>);
get hello(): string;
// you can define simpler values too
simpleNumber: number;
}
}
PiniaCustomProperties
是一个 generic type,当我们需要使用 store 的类型时可使用:
import "pinia";
declare module "pinia" {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id;
state?: () => S;
getters?: G;
actions?: A;
};
}
}
/*
S: State
G: Getters
A: Actions
SS: Setup Store / Store
*/
对于新的 state:
import "pinia";
declare module "pinia" {
export interface PiniaCustomStateProperties<S> {
hello: string;
}
}
对于新的 option:
import "pinia";
declare module "pinia" {
export interface DefineStoreOptionsBase<S, Store> {
// allow defining a number of ms for any of the actions
debounce?: Partial<Record<keyof StoreActions<Store>, number>>;
}
}