In my previous article about frontend state persistence, I mentioned I'd explain the vuex-persistedstate plugin — after all, it's not good to leave promises unfulfilled. Here I'll cover the more important parts of the vuex-persistedstate source code. Let's start with the full source:
Source Code
import { Store, MutationPayload } from "vuex";
import merge from "deepmerge";
import * as shvl from "shvl";
interface Storage {
getItem: (key: string) => any;
setItem: (key: string, value: any) => void;
removeItem: (key: string) => void;
}
interface Options {
key?: string;
paths?: string[];
reducer?: (state: any, paths: string[]) => object;
subscriber?: (
store: typeof Store
) => (handler: (mutation: any, state: any) => void) => void;
storage?: Storage;
getState?: (key: string, storage: Storage) => any;
setState?: (key: string, state: typeof Store, storage: Storage) => void;
filter?: (mutation: MutationPayload) => boolean;
arrayMerger?: (state: any, saved: any) => any;
rehydrated?: (store: typeof Store) => void;
fetchBeforeUse?: boolean;
overwrite?: boolean;
assertStorage?: (storage: Storage) => void | Error;
}
export default function(options?: Options) {
options = options || {};
const storage = options.storage || (window && window.localStorage);
const key = options.key || "vuex";
function getState(key, storage) {
let value;
try {
return (value = storage.getItem(key)) && typeof value !== "undefined"
? JSON.parse(value)
: undefined;
} catch (err) {}
return undefined;
}
function filter() {
return true;
}
function setState(key, state, storage) {
return storage.setItem(key, JSON.stringify(state));
}
function reducer(state, paths) {
return Array.isArray(paths)
? paths.reduce(function(substate, path) {
return shvl.set(substate, path, shvl.get(state, path));
}, {})
: state;
}
function subscriber(store) {
return function(handler) {
return store.subscribe(handler);
};
}
const assertStorage =
options.assertStorage ||
(() => {
storage.setItem("@@", 1);
storage.removeItem("@@");
});
assertStorage(storage);
const fetchSavedState = () => (options.getState || getState)(key, storage);
let savedState;
if (options.fetchBeforeUse) {
savedState = fetchSavedState();
}
return function(store) {
if (!options.fetchBeforeUse) {
savedState = fetchSavedState();
}
if (typeof savedState === "object" && savedState !== null) {
store.replaceState(
options.overwrite
? savedState
: merge(store.state, savedState, {
arrayMerge:
options.arrayMerger ||
function(store, saved) {
return saved;
},
clone: false,
})
);
(options.rehydrated || function() {})(store);
}
(options.subscriber || subscriber)(store)(function(mutation, state) {
if ((options.filter || filter)(mutation)) {
(options.setState || setState)(
key,
(options.reducer || reducer)(state, options.paths),
storage
);
}
});
};
}If you can understand this, you're good. For those who find it confusing but partially understandable, let's break it down step by step:
Interface Parameter Fields Explained with Source Code
// Defines methods for local storage operations
interface Storage {
getItem: (key: string) => any;
setItem: (key: string, value: any) => void;
removeItem: (key: string) => void;
}
// Configuration options
interface Options {
key?: string;
paths?: string[];
reducer?: (state: any, paths: string[]) => object;
subscriber?: (
store: typeof Store
) => (handler: (mutation: any, state: any) => void) => void;
storage?: Storage;
getState?: (key: string, storage: Storage) => any;
setState?: (key: string, state: typeof Store, storage: Storage) => void;
filter?: (mutation: MutationPayload) => boolean;
arrayMerger?: (state: any, saved: any) => any;
rehydrated?: (store: typeof Store) => void;
fetchBeforeUse?: boolean;
overwrite?: boolean;
assertStorage?: (storage: Storage) => void | Error;
}Let's analyze the most important parameter fields in the Options interface:
key
key is the name used to store data in local cache, defaulting to "vuex". If you don't specify a Storage type, vuex-persistedstate defaults to localStorage, equivalent to localStorage.setItem('vuex', data).
paths & reducer
These two fields work together. paths is an array, and reducer is a function. You can pass just paths or just reducer to specify which values to persist. Looking at this source code snippet makes their purpose clear:
function reducer(state, paths) {
return Array.isArray(paths)
? paths.reduce(function(substate, path) {
return shvl.set(substate, path, shvl.get(state, path));
}, {})
: state;
}subscriber, storage, getState, setState, filter
These four fields should be discussed together. storage defines the local storage methods to use — currently supports three types: localStorage, sessionStorage, and cookie. getState and setState define methods for getting and setting local storage values. subscriber sets up the Vuex mutation subscription function, defaulting to handler => {}. filter accepts a function whose parameter is a Vuex mutation — when this function returns false, setState is not triggered. These parameters work together, as shown in the source:
// subscriber definition
function subscriber(store) {
return function(handler) {
return store.subscribe(handler);
};
}
// filter definition
function filter() {
return true;
}
(options.subscriber || subscriber)(store)(function(mutation, state) {
if ((options.filter || filter)(mutation)) {
(options.setState || setState)(
key,
(options.reducer || reducer)(state, options.paths),
storage
);
}
});From the code above, subscriber returns a function after the first call, which is defined as:
subscriber?: (
store: typeof Store
) => (handler: (mutation: any, state: any) => void) => void;It needs to be a mutation, same type as filter's parameter. So if you don't define subscriber and filter, setState is called by default to persist Vuex state. If you pass a Vuex mutation (make sure it exists in your Vuex store) and the function returns false, the state won't be saved to the defined storage.
Summary
This library is great for TypeScript beginners to study and learn from — the logic isn't too simple or too complex, and it helps solidify TypeScript knowledge. This坑 is now filled.
