Pinia:下一代状态管理工具

https://pinia.vuejs.org/introduction.html

安装

1
npm install pinia@next

vue3 中初始化

1
2
import { createPinia } from "pinia";
app.use(createPinia());

与 vuex 比较

  • 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见
  • 支持多个 Store
  • 支持 Vue devtools、SSR 和 webpack 代码拆分

State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineStore } from "pinia";

// 设置全局唯一Id
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.counter++;
// 重置
store.$reset();

修改 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 常规修改state
store.$patch({
counter: store.counter + 1,
name: "Abalam"
});

// 如果是数组,使用函数方式修改
cartStore.$patch((state) => {
state.items.push({ name: "shoes", quantity: 1 });
state.hasChanged = true;
});

// 全部替换
store.$state = { counter: 666, name: "Paimon" };

监听 state

1
2
3
4
5
// 和watch一样了,粒度更高
main.$subscribe((mutation, state) => {
console.log(mutation);
console.log(state);
});

getters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const getters = {
doubleCount: (state) => state.counter * 2,
// 穿透使用当前的getter属性时要使用this,不能用箭头函数
doublePlusOne() {
// autocompletion and typings for the whole store ✨
return this.doubleCount + 1;
},
// 可以传递参数,使用这种方式就不会有缓存了,本质上变成调用函数
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId);
},
//变相使用缓存
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active);
return (userId) => activeUsers.find((user) => user.id === userId);
},
// 使用其他state里的getters
otherGetter(state) {
const otherStore = useOtherStore();
return state.localData + otherStore.data;
}
};
1
2
3
4
//getUserById使用方式:
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>

Actions

1
2
3
4
5
6
7
8
const actions = {
increment() {
this.counter++;
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random());
}
};

可异步发请求,通过 this 获取 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}
}
});

// 可在setup中直接使用
const main = useMainStore();
// call the action as a method of the store
main.registerUser(1, 123);

监听 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 当someStore的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}.`);
});
}
);

// 手动移除监听
unsubscribe();

Plugins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createPinia } from "pinia";

// add a property named `secret` to every store that is created after this plugin is installed
// this could be in a different file
function SecretPiniaPlugin() {
return { secret: "the cake is a lie" };
}

const pinia = createPinia();
// give the plugin to pinia
pinia.use(SecretPiniaPlugin);

// 和传递SecretPiniaPlugin效果一样,都具备响应性,
// 区别在于,使用函数赋值这种方案devtools可追踪
pinia.use(({ store }) => {
store.hello = "world";
});

// in another file
const store = useStore();
store.secret; // 'the cake is a lie'

plugins 常用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 给每一个store增加路由路径
import { markRaw } from "vue";
// adapt this based on where your router is
import { router } from "./router";

pinia.use(({ store }) => {
// 防止响应性,处理成原始值
store.router = markRaw(router);
});

// 可以在内部调用$subscribe和$onAction
pinia.use(({ store }) => {
store.$subscribe(() => {
// react to store changes
});
store.$onAction(() => {
// react to store actions
});
});

plugin 增加新属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defineStore("search", {
actions: {
searchContacts() {
// ...
}
},

// 注意:增加的options需要和actions,state平级
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300
}
});

// lodash库
import debounce from "lodash/debunce";

// 给全局增加方法拦截,只要有debounce对象所对应的action都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-plugin-persistedstate 配合 pinia 的持久化插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
import { defineStore } from "pinia";

export const useStore = defineStore("main", {
state: () => {
return {
someState: "hello pinia",
nested: {
data: "nested pinia"
}
};
},
persist: {
key: "store-key",
storage: window.sessionStorage,
paths: ["nested.data"],
beforeRestore: (context) => {
console.log("Before hydration...");
},
afterRestore: (context) => {
console.log("After hydration...");
}
}
});