pinia-plugin-persistedstate源码浅析
# 概述
Pinia
是vue3
的官方推荐用于数据共享的库,但是Pinia🍍
中的数据是存在于浏览器的内存中,当浏览器刷新后,这些数据就会消失。因此我们需要对数据做持久化存储,这个时候就需要用到pinia-plugin-persistedstate
。
pinia-plugin-persistedstate
本质上利用浏览器持久化存储地能力,默认使用localStorage
。本文将详细介绍pinia-plugin-persistedstate
的使用以及源码剖析,以vue3
项目为例。
# pinia-plugin-persistedstate
的使用
使用pinia-plugin-persistedstate
之前需要安装pinia
和pinia-plugin-persistedstate
。其使用主要分为两部分
- 在
main.js
中引用,如示例:
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
2
3
- 在
store
中使用,pinia
的defineStore
的第二个参数,提供一个persist
属性,配置该属性即可。
export const useCommonStore = defineStore("common", {
persist: {
key: "common", // localStorage的建
storage: localStorage, //指定存储方式,默认为localStorage
paths: ["menuActiveIndex", "collapse", "state", "currentMenuList"], // 数组中的为state的键,需要持久化的state放在paths中
},
});
2
3
4
5
6
7
- 其效果如下
# 源码剖析
pinia-plugin-persistedstate
是为 pinia
量身定制的持久化方案,因此我们需要对pinia
有一定的了解,可以参考之前的文章Pinia 源码浅析 (opens new window),PS:该文章会持续更新。本文会只讲解pinia
中与pinia-plugin-persistedstate
有关的部分。
# pinia.use
方法
由上可知,通过pinia.use
去使用pinia-plugin-persistedstate
,pinia.use
实现如下:
use(plugin) {
if (!this._a && !isVue2) {
toBeInstalled.push(plugin);
}
else {
_p.push(plugin);
}
return this;
},
2
3
4
5
6
7
8
9
该use
方法是在createPinia
中定义的,调用use
方法,首先会判断_a
是否存在以及当前是vue2
还是vue3
,如果当前_a
不存在(即还没有调用app.use(pinia)
)并且是vue3
,则将pinia-plugin-persistedstate
插件放到toBeInstalled
变量中,否则将插件放大_p
中。
放到toBeInstalled
的插件会在pinia
被vue3
实例调用use
方法时push
到_p
中:
toBeInstalled.forEach((plugin) => _p.push(plugin));
由此可知,pinia
中的插件全部在_p
中。
# pinia
使用插件
pinia
内部使用插件的部分是在createSetupStore
中实现的,即defineStore
——>useStore
——>createSetupStore
,其实现如下:
pinia._p.forEach((extender) => {
/* istanbul ignore else */
if (USE_DEVTOOLS) {
//调试部分
const extensions = scope.run(() =>
extender({
store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
);
Object.keys(extensions || {}).forEach((key) =>
store._customProperties.add(key)
);
assign(store, extensions);
} else {
assign(
store,
scope.run(() =>
extender({
store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)
);
}
});
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
30
这段代码就是在 vue 组件中使用store
的useStore
方法时去遍历pinia
安装的插件,调用插件暴露的方法extender
,对应pinia-plugin-persistedstate
就是该插件暴露的方法createPersistedState
,将store
、app
(即 vue 实例)、pinia
、options: optionsForPlugin
传给插件
# pinia-plugin-persistedstate
pinia-plugin-persistedstate
只暴露了一个接口:createPersistedState
,默认暴露createPersistedState()
的实例
# createPersistedState
createPersistedState
方法返回的是一个函数,其实现如下:
function createPersistedState(factoryOptions = {}) {
return (context) => {
const { auto = false } = factoryOptions;
const {
options: { persist = auto },
store,
pinia,
} = context;
if (!persist) return;
if (!(store.$id in pinia.state.value)) {
const original_store = pinia._s.get(store.$id.replace("__hot:", ""));
if (original_store)
Promise.resolve().then(() => original_store.$persist());
return;
}
const persistences = (
Array.isArray(persist)
? persist.map((p) => normalizeOptions(p, factoryOptions))
: [normalizeOptions(persist, factoryOptions)]
)
.map(parsePersistence(factoryOptions, store))
.filter(Boolean);
store.$persist = () => {
persistences.forEach((persistence) => {
persistState(store.$state, persistence);
});
};
store.$hydrate = ({ runHooks = true } = {}) => {
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence;
if (runHooks) beforeRestore == null ? void 0 : beforeRestore(context);
hydrateStore(store, persistence);
if (runHooks) afterRestore == null ? void 0 : afterRestore(context);
});
};
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence;
beforeRestore == null ? void 0 : beforeRestore(context);
hydrateStore(store, persistence);
afterRestore == null ? void 0 : afterRestore(context);
store.$subscribe(
(_mutation, state) => {
persistState(state, persistence);
},
{
detached: true,
}
);
});
};
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
接受的参数context
就是pinia
中传过来的store
、app
、pinia
、options
。
若使用defineStore
时,没有配置persist
属性,则context
中options
的persist
取factoryOptions
的auto
的值,默认为false
,此时,函数就终止,return
。
然后插件会检查store
是否存在于pinia.state.value
中,pinia.state.value
是一个包含所有当前存储实例的对象。若不存在,则处理下热加载的前缀并从pinia._s
中获取原始的存储实例,如果原始实例存在,则用promise
将其$persist
方法放在微任务对列中去执行。
若store
存在,则判断persist
是数组还是对象。若persist
是数组,则对它遍历,执行normalizeOptions
方法;否则直接调用normalizeOptions
,然后包装成数组,遍历,执行parsePersistence
方法。
然后插件定义了两个方法store.$persist
和store.$hydrate
。$persist
就是遍历persistences
,去执行persistState
方法;$hydrate
水合,就是用来从存储中恢复数据到store
中,但是在pinia
和pinia-plugin-persistedstate
中未看到相关调用。
最后插件遍历了persistences
,在调用hydrateStore
的前后会判断beforeRestore
和afterRestore
是否定义了,若存在,则直接调用,最后还调用了store.$subscribe
方法进行监听state
的改变,若state
发生了改变,则会执行回调函数persistState
,其过程如下:
const { beforeRestore, afterRestore } = persistence;
beforeRestore == null ? void 0 : beforeRestore(context);
hydrateStore(store, persistence);
afterRestore == null ? void 0 : afterRestore(context);
store.$subscribe(
(_mutation, state) => {
persistState(state, persistence);
},
{
detached: true,
}
);
2
3
4
5
6
7
8
9
10
11
12
# 辅助函数或方法
# hydrateStore
方法
hydrateStore
方法就是从storage
中去取数据,然后进行反序列化,调用store.$patch
进行更新state
的数据,其实现如下:
function hydrateStore(store, { storage, serializer, key, debug }) {
try {
const fromStorage = storage == null ? void 0 : storage.getItem(key);
if (fromStorage)
store.$patch(
serializer == null ? void 0 : serializer.deserialize(fromStorage)
);
} catch (e) {
if (debug) console.error("[pinia-plugin-persistedstate]", e);
}
}
2
3
4
5
6
7
8
9
10
11
# persistState
方法
persistState
方法就是监听state
的变化,将新数据进行序列化后存储到storage
中,其实现如下:
function persistState(state, { storage, serializer, key, paths, debug }) {
try {
const toStore = Array.isArray(paths) ? pick(state, paths) : state;
storage.setItem(key, serializer.serialize(toStore));
} catch (e) {
if (debug) console.error("[pinia-plugin-persistedstate]", e);
}
}
2
3
4
5
6
7
8
pick
方法就是通过store
中定义的paths
数组去获取state
中对应的值。
# normalizeOption
normalizeOption
就是通过Proxy
创建了一个代理对象,在访问options
时,若options
不存在该属性,则从factoryOptions
中获取。
其实现如下:
function normalizeOptions(options, factoryOptions) {
options = isObject(options) ? options : /* @__PURE__ */ Object.create(null);
return new Proxy(options, {
get(target, key, receiver) {
if (key === "key") return Reflect.get(target, key, receiver);
return (
Reflect.get(target, key, receiver) ||
Reflect.get(factoryOptions, key, receiver)
);
},
});
}
2
3
4
5
6
7
8
9
10
11
12
# parsePersistence
parsePersistence
函数就是用来解析store
中定义的persist
,其主要实现如下:
const {
storage = localStorage, //默认存储
beforeRestore = void 0,
afterRestore = void 0,
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse,
},
key = store.$id,
paths = null,
debug = false,
} = o;
return {
storage,
beforeRestore,
afterRestore,
serializer,
key: ((_a = factoryOptions.key) != null ? _a : (k) => k)(
typeof key == "string" ? key : key(store.$id)
),
paths,
debug,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
从上述代码可以看出,如果key
值未定义,会采用默认的store.$id
,若key
是字符串,则直接采用;否则执行key(store.$id)
;另外默认的序列化和反序列化的函数serializer
是JSON.stringify
和JSON.parse
。这里还提供了两个钩子函数beforeRestore
和afterRestore
。
# 总结
pinia-plugin-persistedstate
插件短小而精悍,利用的还是localStorage
和pinia
的能力进行数据的监听、存储和恢复。