Pinia 撤銷!重做!
使用 Vue 的開發(fā)者,對 Pinia 一定不會陌生。

作為 Vue 官方新一代狀態(tài)管理庫,Pinia 用更簡潔的語法、更完善的 TypeScript 支持,以及去除了 mutations 的“直接改 state”風(fēng)格,迅速取代了 Vuex,成為 Vue2 / Vue3 項目的首選。
然而,狀態(tài)管理越集中,用戶就越容易“手滑”——一不小心把表單清空、把配置覆蓋、把畫布誤刪。
此時,如果像 Photoshop 那樣 Ctrl + Z 就能撤銷,該多好?

這正是本文主角 pinia-undo 的價值所在:給任何 Pinia Store 一鍵加上“時間旅行”能力。
什么是 pinia-undo?
pinia-undo 是一個開箱即用的 Pinia 插件,它會在你的 Store 里自動注入:
- undo() – 撤銷到上一狀態(tài)
- redo() – 重做剛才被撤銷的變更
- history – 只讀的歷史棧,方便調(diào)試或顯示“可撤銷步數(shù)”
核心特點:
- 零侵入:不改動你原有的 state / action
- 可配置:想忽略某些字段?想禁用歷史?想自定義序列化?都支持
- 輕量化:僅 1 個依賴,gzip 后 < 2 kB
- 兼容 Vue2 & Vue3:只要 Pinia 能跑,它就能跑
如何使用(3 分鐘上手)
(1) 安裝
pnpm add pinia-undo
# 或 npm / yarn(2) 注冊插件
// main.ts
import { createPinia } from 'pinia'
import { PiniaUndo } from 'pinia-undo'
const pinia = createPinia()
pinia.use(PiniaUndo) // 一行代碼,全局生效
app.use(pinia)(3) 像平常一樣寫 Store
// stores/counter.ts
export const useCounter = defineStore('counter', {
state: () => ({
count: 10,
name: 'Pinia'
}),
actions: {
inc() { this.count++ },
dec() { this.count-- }
}
})(4) 模板里直接調(diào)用
<script setup>
import { useCounter } from '@/stores/counter'
const counter = useCounter()
</script>
<template>
<h1>{{ counter.count }}</h1>
<button @click="counter.inc">+1</button>
<button @click="counter.dec">-1</button>
<div>
<button @click="counter.undo()" :disabled="!counter.canUndo">? 撤銷</button>
<button @click="counter.redo()" :disabled="!counter.canRedo">? 重做</button>
</div>
</template>提示:undo() / redo() 在棧頂/棧底會自動空操作,不會產(chǎn)生異常;你也可以通過 canUndo / canRedo 禁用按鈕。
高階玩法:讓插件更貼合業(yè)務(wù)
(1) 忽略敏感字段
有些字段(如 loading、updatedAt)不需要進入歷史:
export const useForm = defineStore('form', {
state: () => ({ name: '', age: 0, loading: false }),
// 告訴插件:別管 loading
undo: { omit: ['loading'] }
})(2) 臨時關(guān)閉歷史
某些批處理操作想一次性提交,不想生成中間態(tài):
export const useCanvas = defineStore('canvas', {
undo: { disable: false }, // 默認(rèn)開啟
actions: {
batchUpdate(list) {
this.$patch((state) => {
// 先關(guān)歷史
this.$pinia.state.value.canvas.__UNDO_DISABLE = true
list.forEach((op) => { /* 大量修改 */ })
// 再開歷史
this.$pinia.state.value.canvas.__UNDO_DISABLE = false
})
}
}
})(3) 自定義序列化
當(dāng) state 里包含 Map、Set、Dayjs 等不可 JSON 直接序列化的對象時:
import devalue from '@nuxt/devalue'
export const useAdvanced = defineStore('advanced', {
undo: {
serializer: {
serialize: devalue,
deserialize: (str) => eval(`(${str})`) // devalue 逆向
}
}
})常見問答
問題 | 回答 |
兼容 Pinia 2 嗎? | ? 實測 Pinia 2.0+ 無問題 |
支持 SSR 嗎? | ? 歷史棧保存在客戶端,不影響服務(wù)端渲染 |
內(nèi)存會不會爆? | 默認(rèn)保留 100 步,可自己裁剪 |
能同時監(jiān)聽多個 Store 嗎? | 插件全局注冊后,所有 Store 都自動擁有 undo/redo |
寫在最后
狀態(tài)管理給了 Vue 應(yīng)用“單一數(shù)據(jù)源”的便利,也帶來了“一錯全錯”的風(fēng)險。
pinia-undo 用不到 200 行源碼的代價,把“撤銷 / 重做”這一桌面軟件級體驗帶進 Web 端:
- 不寫 mutations,不改業(yè)務(wù)邏輯
- 不依賴后端,不額外存儲
- 配置一下,就能讓任何 Pinia Store 回到過去
下次產(chǎn)品說“用戶誤刪能不能還原”時,你可以淡定回答:已經(jīng)自帶 Ctrl + Z 了。
Github 地址:https://github.com/wobsoriano/pinia-undo





























