107.vue插件机制piniaStoragePlugins

我们来看下面的代码

image

 storage.js

export function piniaStoragePlugins({ store }) {
  const key = `pinia-${store.$id}`

  // 2️⃣ 初始化恢复:只恢复 count
  const saved = localStorage.getItem(key)
  if (saved) {
    // 注意:这里取出来的是数字,需要包进对象里给 $patch
    store.$patch({
      count: JSON.parse(saved)
    })
  }

  // 3️⃣ 监听变化:只存 count
  store.$subscribe((_, state) => {
    // 核心改动:只序列化 state.count
    
    localStorage.setItem(key, JSON.stringify(state.count))
  })

  store.$onAction(({ name, after }) => {
    after(() => {
      console.log(`[Action] ${name} completed`)
    })
  })
}

在main.ts中如何使用插件了

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
// main.js


// 必须加上 .js 后缀
import { piniaStoragePlugins } from './plugins/storage.js'


// 创建 Vue 应用实例
const app = createApp(App)

// 创建 Pinia 实例
const pinia = createPinia()
pinia.use(piniaStoragePlugins)

// 使用 Pinia(必须在 mount 之前)
app.use(pinia)

// 挂载到页面
app.mount('#app')

hell.vue中的代码如下

<template>
  <p>hello.vue</p>
  <p>原始count: {{ store.count }}</p>

  <p>hello22.vue</p>
  <p>双倍count: {{ store.doubleCount }}</p>
  <p>{{ store.getCount }}</p>
  
  <!-- 操作按钮 -->
  <div class="btn-group">
    <button @click="store.count++">+1</button>
    <button @click="store.addCount">+10</button>
    <button @click="store.decrease">-5</button>
  </div>

  <h3>网络请求的结果如下所示</h3>
  <ul v-if="store.data.length">
    <li v-for="(item, index) in store.data" :key="index">
      <h3>{{ item.title }}</h3>
      <p>{{ item.content }}</p>
      <img :src="item.img" :alt="item.title" style="width: 200px; margin: 10px 0;">
    </li>
  </ul>
  <p v-else>加载中...</p>
</template>

<script setup>
import { onMounted } from 'vue'
import piniaCountStore from '../store/countstore'

const store = piniaCountStore()

onMounted(() => {
  // 使用你提供的接口地址
  store.getData('http://iwenwiki.com/api/blueberrypai/getIndexBanner.php')
})
</script>

<style scoped>
p {
  font-size: 18px;
}
.btn-group {
  margin: 15px 0;
}
button {
  margin-right: 10px;
  padding: 5px 15px;
}
li {
  border-bottom: 1px solid #eee;
  padding: 15px 0;
}
</style>

app.vue的代码如下

<template>
  <h1>app vue</h1>
  <hello />
</template>

<script setup>
// 引入子组件
import hello from './components/hello.vue'
</script>

<style scoped>
h1 {
  color: #42b983;
}
</style>

我们来看下运行的效果

image

 

这是一段非常经典、非常地道的 Pinia 持久化插件代码。它实现了 “局部持久化”(只存 count,不管其他字段)。

我会按 “代码执行顺序”​ + “设计思想”​ 两个维度,给你拆解得明明白白。


一、整体在干什么?(一句话版)

这个插件会在 Store 创建时,从 localStorage 恢复 count;在 count变化时,自动把最新的 count存回 localStorage。


二、逐行详细解析

1️⃣ 函数定义与参数解构

export function piniaStoragePlugins({ store }) {
  • export function:这是一个命名导出的插件函数,方便在 main.js中通过 import { piniaStoragePlugins }引入。

  • { store }:这是解构赋值。Pinia 在调用插件时,会传入一个 Context 对象。这里直接取出其中的 store实例。

  • store:这就是你在组件中通过 useCountStore()拿到的那个 Store 实例。插件在这里拿到了它的“控制权”。


2️⃣ 生成 localStorage 的 Key

const key = `pinia-${store.$id}`
  • store.$id:这是 Store 的唯一标识符。它来自于你定义 Store 时的第一个参数:

    defineStore('count', {...}) // 这里的 'count' 就是 $id
  • 拼接 Key:最终生成的 Key 是 pinia-count

  • 为什么这么做?

    • 防冲突:如果你有多个 Store(user, cart, settings),它们不会互相覆盖。

    • 可追溯:一眼就能看出这个 Key 属于哪个 Store。


3️⃣ 初始化恢复(核心逻辑)

const saved = localStorage.getItem(key)
if (saved) {
  store.$patch({
    count: JSON.parse(saved)
  })
}

这是页面加载时执行的代码。

  • localStorage.getItem(key):尝试从浏览器本地存储中读取数据。

  • if (saved):判断是否存在缓存。

    • 如果是第一次访问,或者清除了缓存,savednull,直接跳过,使用 Store 定义的初始值 count: 10

    • 如果之前存过,saved是字符串(比如 "15")。

  • JSON.parse(saved):把字符串转换回 JavaScript 数字(比如 15)。

  • store.$patch({ count: 15 }):这是最关键的一步

    • 为什么不用 store.count = 15

      虽然直接赋值在插件里也能生效,但 $patch是 Pinia 推荐的批量更新 API。它确保了这次更新能被 Pinia 的 DevTools 追踪,并且能正确地触发响应式更新。

    • 这里只更新 count,其他 state(比如 name)保持原样。

📌 执行时机:这发生在 Store 刚创建、组件还没开始渲染的时候。所以用户看不到“闪一下”的变化。


4️⃣ 监听变化并持久化(核心逻辑)

store.$subscribe((_, state) => {
  localStorage.setItem(key, JSON.stringify(state.count))
})

这是订阅(监听)逻辑,只要 Store 状态变了,它就可能执行。

  • store.$subscribe:Pinia 内置的监听器。它会在 state 变化之后​ 自动执行回调函数。

  • (_, state)

    • _:代表第一个参数 mutation(变化的原因)。这里用下划线忽略它,因为我们只关心结果,不关心是 +1还是 +10触发的。

    • state:代表 变化后的最新 state 快照

  • JSON.stringify(state.count)

    • state.count:拿到最新的数字(比如 16)。

    • JSON.stringify:把数字转换成字符串("16"),因为 localStorage 只能存字符串

  • localStorage.setItem(key, ...):把最新的 count值存入浏览器。

📌 执行时机:每当你在组件中调用 store.count++store.increment()$patch修改 count时,这个函数就会立即执行。


5️⃣ Action 日志(调试辅助)

store.$onAction(({ name, after }) => {
  after(() => {
    console.log(`[Action] ${name} completed`)
  })
})

这部分和持久化无关,是调试利器

  • store.$onAction:监听 Store 中 Actions​ 的调用。

  • ({ name, after }):解构出 Action 的名称(如 increment)和 after钩子。

  • after(() => { ... }):注册一个回调,当 Action 成功执行完毕后触发。

  • 效果:当你点击按钮触发 increment后,控制台会打印:

    [Action] increment completed

三、代码的完整执行流程图

1. main.js 加载
       ↓
2. createPinia().use(piniaStoragePlugins)
       ↓
3. 组件调用 useCountStore()
       ↓
4. Pinia 创建 Store 实例
       ↓
5. 插件开始执行
       ↓
6. 【恢复阶段】
   读取 localStorage → 找到 pinia-count
   如果找到 → $patch 更新 count
       ↓
7. 【监听阶段】
   $subscribe 开始监听 state 变化
   $onAction 开始监听 action 调用
       ↓
8. 组件渲染(显示恢复后的 count)
       ↓
9. 用户点击按钮(count 改变)
       ↓
10.【持久化阶段】
    $subscribe 触发
    将新的 count 存入 localStorage
       ↓
11. 页面刷新 → 回到第 6 步

四、为什么这是“高级写法”?

  1. 精准打击:不像有些插件傻瓜式地存整个 State,它只存 count,节省 I/O 和空间。

  2. 利用 $patch:遵循 Pinia 的最佳实践,保证响应式系统的完整性。

  3. 忽略 mutation:体现了开发者对 $subscribe机制的深刻理解(只关心结果,不关心过程)。

  4. 生命周期管理:在 Store 初始化时恢复数据,在变化时保存数据,时机把握得非常准。

五、一句话总结

这段代码是一个“单向数据流”的典范:

Store 创建 → 读缓存 → 更新 State → State 变化 → 写缓存。

它巧妙地利用了 $subscribe作为桥梁,连接了内存中的 State 和磁盘上的 localStorage。

现在你不仅读懂了代码,还读懂了作者的意图。这段代码完全可以直接用在生产环境中。👍

需要我帮你把它改造成支持多个字段配置(比如通过 paths: ['count', 'name'])的版本吗?那是企业级项目的标准写法。

 

Pinia 的 Store 实例身上挂了一堆 “美元方法”($xxx,它们是 Pinia 的底层武器库。除了你刚弄明白的 $subscribe,我把它们按实战频率从高到低给你捋一遍,并告诉你什么时候用、为什么用


一、核心必会(日常开发 90% 都在用)

1️⃣ $patch—— 批量修改 State(最重要)

作用:一次性修改多个 state,性能更好,逻辑更清晰。

// ❌ 不推荐:多次触发响应式更新
store.count++
store.name = 'pinia'
store.age = 18

// ✅ 推荐:只触发一次更新
store.$patch({
  count: store.count + 1,
  name: 'pinia',
  age: 18
})

函数形式(适合复杂逻辑):

store.$patch((state) => {
  state.list.push({ id: 1, text: 'hello' })
  state.count = state.list.length
})

📌 为什么需要它

  • 避免多次触发组件重新渲染

  • 让修改逻辑集中、可读


2️⃣ $state—— 直接替换整个 State

作用:用一个新对象完全替换当前 state(类似 window.location.reload的感觉)。

// 重置为初始值(SSR / 表单重置常用)
store.$state = {
  count: 0,
  name: '',
  list: []
}

⚠️ 注意

  • 必须包含所有 state 字段,否则会丢失数据

  • 通常不如 $patch精细


3️⃣ $reset—— 重置到初始状态

作用:把 state 恢复到 defineStore时的初始值。

store.$reset()

前提

  • 仅适用于 Option Store

  • Setup Store 需要自己实现

📌 典型场景

  • 关闭弹窗时清空表单

  • 退出登录时重置用户状态


4️⃣ $onAction—— 监听 Actions(调试/埋点神器)

作用:监听所有 action 的调用、成功、失败。

store.$onAction(({
  name,      // action 名称
  args,      // 参数数组
  after,     // 成功后钩子
  onError,   // 失败后钩子
}) => {
  console.log(`action ${name} 开始`, args)

  after((result) => {
    console.log(`action ${name} 成功`, result)
  })

  onError((error) => {
    console.log(`action ${name} 失败`, error)
  })
})

📌 典型场景

  • 全局埋点

  • 统一的 loading 状态管理

  • 错误上报


二、进阶 & 插件级(你正在学的)

5️⃣ $subscribe—— 监听 State 变化(你刚学的)

store.$subscribe((mutation, state) => {
  console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
})

插件开发、持久化、日志的核心


6️⃣ $dispose—— 销毁 Store(内存管理)

作用:停止该 store 的所有响应式效果和订阅(防止内存泄漏)。

store.$dispose()

📌 什么时候用

  • 手动创建的 store(非 useXXXStore()

  • 组件卸载时需要清理的特殊场景

⚠️ 注意:Vue 组件里正常使用 useXXXStore()不需要手动调用


三、调试 & 开发工具(DevTools)

7️⃣ $id—— Store 的唯一标识

store.$id // 'count'

📌 插件里最常用,用来区分不同的 store。


8️⃣ $onError—— 捕获 State 修改错误

作用:捕获 state 修改过程中的错误(较少用,偏底层)。

store.$onError((error) => {
  console.error('State 修改出错:', error)
})

四、不常用但要知道的

9️⃣ $hotUpdate—— HMR(热更新)

store.$hotUpdate(newStore)

📌 用于 Vite / Webpack HMR,普通开发几乎碰不到。


五、一张表总结(面试/速查)

 

方法

核心作用

使用频率

典型场景

$patch

批量修改 state

⭐⭐⭐⭐⭐

表单提交、复杂更新

$state

替换整个 state

⭐⭐⭐

重置、SSR

$reset

恢复初始值

⭐⭐⭐⭐

表单清空、登出

$onAction

监听 actions

⭐⭐⭐⭐

埋点、日志、loading

$subscribe

监听 state

⭐⭐⭐⭐

持久化、插件

$dispose

销毁 store

⭐⭐

内存管理

$id

store 标识

⭐⭐⭐

插件开发


六、核心区别(必背)

$patchvs 直接修改

// 直接修改:触发 N 次更新
store.count++
store.name = 'a'

// $patch:只触发 1 次更新
store.$patch({ count: 1, name: 'a' })

$subscribevs $onAction

  • $subscribestate 变了(结果)

  • $onActionaction 调用了(过程)

$resetvs $state =

  • $reset()→ 用初始定义(安全)

  • $state =→ 用你给的对象(危险,可能缺字段)


七、实战组合拳(你现在的插件)

export function piniaStoragePlugin({ store }) {
  // 1️⃣ 用 $id 做 key
  const key = `pinia-${store.$id}`

  // 2️⃣ 初始化恢复
  const saved = localStorage.getItem(key)
  if (saved) store.$patch(JSON.parse(saved))

  // 3️⃣ 用 $subscribe 监听变化
  store.$subscribe((_, state) => {
    localStorage.setItem(key, JSON.stringify(state))
  })

  // 4️⃣ 用 $onAction 做日志(可选)
  store.$onAction(({ name, after }) => {
    after(() => {
      console.log(`[Action] ${name} completed`)
    })
  })
}

八、一句话总结

Pinia 的 $方法是操作 Store 的“内功心法”:

  • 改数据用 $patch

  • 监听结果用 $subscribe

  • 监听过程用 $onAction

  • 重置状态用 $reset

你现在问的这个问题,已经是 Pinia 进阶 → 插件开发​ 的必经之路了。

要不要我接着帮你把 $patch的源码实现原理​ 也拆一下?那一块对理解 Vue 响应式更新特别有帮助。

 

posted on 2026-07-15 16:27  luzhouxiaoshuai  阅读(8)  评论(0)    收藏  举报

导航