如何在 Vuex/Pinia 中实现一个插件,例如用于状态持久化(Persisted State)或日志记录?

各位观众老爷,大家好! 今天咱们来聊聊 Vuex/Pinia 插件这玩意儿,这可是让你代码起飞、状态管理更上一层楼的秘密武器。 别害怕,虽然听起来高大上,但其实一点也不难。 今天我就像个老司机一样,带你一步一步玩转它,保证你听完之后也能轻松写出自己的插件。

开场白:插件是啥? 为啥要用它?

想象一下,你的 Vuex/Pinia store 就像一个百宝箱,里面装着各种各样的状态数据。 但是,这个百宝箱默认情况下是“一次性”的, 页面一刷新,里面的东西就都没了。 这可不行! 咱们得想办法让它变得“持久”,或者让它有个“日记本”,记录下都发生了些啥。 这时候,插件就派上用场了!

简单来说,插件就是一个可以扩展 Vuex/Pinia 功能的小工具。 它可以让你在 store 的生命周期中插入一些“钩子”,在特定的时机做一些你想要做的事情,比如:

  • 状态持久化 (Persisted State): 把 store 里的数据保存到 localStorage 或者 sessionStorage 里,下次打开页面的时候自动恢复。
  • 日志记录 (Logging): 记录 store 里的每一个 mutation,方便你调试和排错。
  • 数据同步: 将多个组件的状态同步,特别是在使用了组件库的情况下,比如同步表单的状态。

Vuex 插件:让状态持久化更简单

首先,咱们先来看看 Vuex 的插件怎么写。 以状态持久化为例,我们创建一个 vuex-persist 插件。

// vuex-persist.js

const vuexPersist = (store) => {
  // 在 store 初始化的时候,从 localStorage 中读取状态
  if (localStorage.getItem('vuex-state')) {
    store.replaceState(
      JSON.parse(localStorage.getItem('vuex-state'))
    );
  }

  // 在每次 mutation 发生后,将状态保存到 localStorage 中
  store.subscribe((mutation, state) => {
    localStorage.setItem('vuex-state', JSON.stringify(state));
  });
};

export default vuexPersist;

这段代码很简单,就两部分:

  1. 初始化 (Initialization): 当 Vuex store 刚创建好的时候,插件会尝试从 localStorage 中读取之前保存的状态。 如果找到了,就用 replaceState 方法把 store 的状态替换成 localStorage 里的数据。
  2. 订阅 (Subscription): 插件会订阅 store 的 subscribe 方法,这个方法会在每次 mutation 发生之后被调用。 在回调函数里,我们把最新的状态转换成 JSON 字符串,然后保存到 localStorage 中。

使用这个插件也很简单:

// store.js

import Vue from 'vue';
import Vuex from 'vuex';
import vuexPersist from './vuex-persist';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
    username: 'Guest'
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    setUsername(state, username) {
      state.username = username;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  plugins: [vuexPersist] // 注册插件
});

export default store;

store.js 文件中,我们只需要把 vuexPersist 插件添加到 plugins 数组里就行了。 这样,每次刷新页面,countusername 的值都会被自动恢复。

高级 Vuex 插件: 更多配置选项

上面的插件虽然简单,但是功能也比较单一。 比如,我们可能想要:

  • 自定义存储位置: 不一定都要存在 localStorage 里,也许想存在 sessionStorage 或者 Cookie 里。
  • 选择性持久化: 只想保存一部分状态,而不是全部。
  • 数据转换: 在保存之前对数据进行一些处理,或者在读取之后进行一些转换。

为了满足这些需求,我们可以对插件进行一些扩展:

// vuex-persist.js (高级版本)

const vuexPersist = (options = {}) => {
  const {
    key = 'vuex-state', // 存储的 key
    storage = localStorage, // 存储的方式 (localStorage, sessionStorage, Cookie)
    reducer = (state) => state, // 选择需要持久化的 state
    rehydrater = (state) => state, // 恢复 state 前的数据转换
  } = options;

  return (store) => {
    // 初始化
    if (storage.getItem(key)) {
      try {
        const storedState = JSON.parse(storage.getItem(key));
        const rehydratedState = rehydrater(storedState); // 使用 rehydrater 进行数据转换
        store.replaceState(rehydratedState);
      } catch (e) {
        console.error("Failed to rehydrate state from storage:", e);
        // 处理解析错误,例如移除无效的存储数据
        storage.removeItem(key);
      }
    }

    // 订阅
    store.subscribe((mutation, state) => {
      const reducedState = reducer(state); // 使用 reducer 选择需要持久化的 state
      storage.setItem(key, JSON.stringify(reducedState));
    });
  };
};

export default vuexPersist;

在这个高级版本中,我们添加了几个配置选项:

  • key: 指定存储数据的 key,默认是 'vuex-state'
  • storage: 指定存储的方式,默认是 localStorage。 你可以把它改成 sessionStorage 或者自己实现一个 Cookie 存储。
  • reducer: 一个函数,用来选择需要持久化的 state。 默认是返回整个 state,你可以根据需要选择一部分 state。
  • rehydrater: 一个函数,在恢复 state 前进行数据转换。这在处理不同版本的数据结构时特别有用。

使用方法也稍微有点变化:

// store.js (高级版本)

import Vue from 'vue';
import Vuex from 'vuex';
import vuexPersist from './vuex-persist';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
    username: 'Guest',
    token: null,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    setUsername(state, username) {
      state.username = username;
    },
    setToken(state, token) {
      state.token = token;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  plugins: [
    vuexPersist({
      key: 'my-app-state',
      storage: sessionStorage,
      reducer: (state) => ({ count: state.count, username: state.username }), // 只保存 count 和 username
      rehydrater: (state) => {
          // 示例:对username进行解密,假设你的username被加密存储
          if (state.username) {
              try {
                  // 假设 decryptUsername 是一个解密函数
                  state.username = decryptUsername(state.username);
              } catch (error) {
                  console.error("Failed to decrypt username:", error);
              }
          }
          return state;
      }
    }),
  ],
});

export default store;

在这个例子中,我们只保存了 countusername 两个状态,并且把存储方式改成了 sessionStoragerehydrater 函数用于在从 sessionStorage 恢复数据时,解密 username (假设 username 被加密存储)。

Pinia 插件:更现代、更简洁

Pinia 的插件机制和 Vuex 类似,但是更加简洁和灵活。 我们同样以状态持久化为例,创建一个 pinia-plugin-persist 插件。

// pinia-plugin-persist.js

import { defineStore } from 'pinia';

const piniaPluginPersist = (options = {}) => {
  return ({ store }) => {
    const {
      key = store.$id, // 使用 store 的 id 作为 key
      storage = localStorage,
      serializer = {
        serialize: JSON.stringify,
        deserialize: JSON.parse,
      },
      beforeRestore = () => {},
      afterRestore = () => {},
      paths = null // 默认为 null,表示持久化所有状态
    } = options;

    const storageKey = `pinia-plugin-persist-${key}`;

    // 在 store 初始化时,从 storage 中读取状态
    if (storage.getItem(storageKey)) {
      try {
          beforeRestore(); // 在恢复之前执行
          const storedState = serializer.deserialize(storage.getItem(storageKey));

          if (paths) {
              // 选择性地合并状态,只保留 paths 中指定的状态
              store.$patch(state => {
                  paths.forEach(path => {
                      if (Object.prototype.hasOwnProperty.call(storedState, path)) {
                          state[path] = storedState[path];
                      }
                  });
              });
          } else {
            store.$patch(storedState); // 使用 $patch 方法更新状态
          }
          afterRestore(); // 恢复后执行
      } catch (e) {
          console.error("Failed to rehydrate state from storage:", e);
          // 处理解析错误,例如移除无效的存储数据
          storage.removeItem(storageKey);
      }
    }

    // 订阅 store 的 $subscribe 方法,在状态变化时保存到 storage 中
    store.$subscribe(
      () => {
          if (paths) {
              // 选择性地序列化状态,只保存 paths 中指定的状态
              const partialState = paths.reduce((acc, path) => {
                  if (Object.prototype.hasOwnProperty.call(store.$state, path)) {
                      acc[path] = store.$state[path];
                  }
                  return acc;
              }, {});
              storage.setItem(storageKey, serializer.serialize(partialState));
          } else {
            storage.setItem(storageKey, serializer.serialize(store.$state));
          }

      },
      { detached: true } // 使用 detached 选项,避免循环更新
    );
  };
};

export default piniaPluginPersist;

这个 Pinia 插件的功能和 Vuex 的高级版本类似,也提供了一些配置选项:

  • key: 指定存储数据的 key,默认是 store 的 $id
  • storage: 指定存储的方式,默认是 localStorage
  • serializer: 一个对象,包含 serializedeserialize 两个方法,用于序列化和反序列化数据。 默认使用 JSON.stringifyJSON.parse
  • beforeRestore: 一个函数,在从 storage 恢复数据之前执行。
  • afterRestore: 一个函数,在从 storage 恢复数据之后执行。
  • paths: 可选的字符串数组,指定要持久化的 state 属性的路径。如果省略,则持久化整个状态对象。

使用方法如下:

// stores/counter.js

import { defineStore } from 'pinia';
import piniaPluginPersist from '../pinia-plugin-persist';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    username: 'Guest',
    settings: {
        theme: 'light',
        notificationsEnabled: true
    }
  }),
  actions: {
    increment() {
      this.count++;
    },
    setUsername(username) {
      this.username = username;
    },
    toggleNotifications() {
        this.settings.notificationsEnabled = !this.settings.notificationsEnabled;
    }
  },
  persist: {
    storage: sessionStorage,
    paths: ['count', 'username', 'settings.theme'], //只持久化 count, username, 和 settings.theme
    beforeRestore: () => {
      console.log('开始恢复状态...');
    },
    afterRestore: () => {
      console.log('状态恢复完成!');
    }
  }
});

// pinia.js
import { createPinia } from 'pinia'
import piniaPluginPersist from './pinia-plugin-persist'

const pinia = createPinia()
pinia.use(piniaPluginPersist)

export default pinia

注意几个关键点:

  1. defineStore 中使用 persist 选项: Pinia 插件通过 persist 选项直接在 store 定义中配置,使得配置更加集中和易于管理。
  2. paths 数组: 允许你指定要持久化的状态属性。 在上面的例子中,我们只持久化 countusername,以及 settings.theme 属性。
  3. beforeRestoreafterRestore 钩子: 提供了在状态恢复前后执行自定义逻辑的机会,比如记录日志或执行一些初始化操作。
  4. Pinia 实例 use 插件 需要先创建 Pinia 实例,然后用 pinia.use() 注册插件。

插件开发最佳实践

  • 保持插件的简洁性: 插件应该只负责特定的功能,不要把太多的逻辑塞到一个插件里。
  • 提供配置选项: 让用户可以根据自己的需求定制插件的行为。
  • 处理错误: 在插件中添加错误处理机制,避免因为插件的问题导致整个应用崩溃。 比如,在解析 localStorage 中的数据时,要使用 try...catch 语句来捕获异常。
  • 编写测试用例: 确保插件的功能正常,并且不会与其他插件冲突。
  • 文档化: 编写清晰的文档,让其他开发者可以轻松地使用你的插件。

表格:Vuex 和 Pinia 插件的对比

| 特性 | Vuex 插件 | Pinia 插件

总结:插件的无限可能

插件是一个强大的工具,可以让你扩展 Vuex/Pinia 的功能,满足各种各样的需求。 通过本文的学习,相信你已经掌握了插件的基本概念和使用方法。 现在,你可以开始尝试编写自己的插件,让你的代码更加强大、更加灵活! 记住,好的插件能够让你的应用更加出色,也能让你成为更优秀的开发者!

好了,今天的讲座就到这里,感谢大家的观看! 希望对你有所帮助! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注