现代浏览器的功能越来越强大,前端需要处理的业务逻辑也越来越复杂,提供良好的交互是我们一直追求的事,而我们在做的可视化报表工具,有一个重要的提升用户体验的功能,撤销 & 重做,这个功能给用户以安全感和保障,用户不会担心所做的操作以及交互会消失掉,不可追溯。
为了实现这个功能,我调研了一些实现方式,有基于 Immutable 数据结构的,有基于栈 数据结构去管理的,我们的实际项目使用 Vuex 作为全局状态管理工具。而真正适合我们实际项目的其实是基于 Vuex 的 store
实例API的实现方式,将所有通过 Vuex 存储的状态,在触发 actions
/mutations
时存为历史记录,当我们想用的时候就可随意“穿梭”。
鉴于此我结合 stateshot
开发了一个可以 “时间旅行” 的 Vuex 插件。
本文将介绍一种基于 Vuex API 实现的 “时间旅行” 插件,以动态网格布局为例子,使用 stateshot.js
实现的 撤销 & 重做 功能,大致效果如下图:
时间旅行
Why
Time Gem
,强大的时间宝石,拥有操控时间的能力。
When
后悔,人类很神奇的感觉,当我做了一件事之后后悔了,想如果没做该多好,想要回到当初。
How
那么我们需要一颗拥有时间旅行强大功能的“时间宝石”,让我们去创造它
实现“时间宝石“
vuex-stateshot
的实现借助了 Vuex 的一些API,以及 stateshot.js
用来记录历史状态,以及进行撤销 & 重做。
当我们触发一个 actions
/mutations
,可以通过订阅的方式,触发一次 snapshot
,记录下历史状态快照,这样就方便我们进行撤销 & 重做。
下面让我们认识一下这些API,subscribe
、subscribeAction
、registerModule
、createNamespacedHelpers
Vuex API
Vuex.Store 实例方法以及辅助函数中提供了一些可能平时用不到的 API,这些 API 在开发 Vuex 插件很好用。
store.subscribe
订阅 store 的 mutation。handler
会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数:
store.subscribe((mutation, state) => {
console.log(mutation.type)
console.log(mutation.payload)
})
store.subscribeAction
从 3.1.0
起,subscribeAction
提供了常用于开发 Vuex 插件的用法,可以指定订阅处理函数的被调用时机应该在一个 action 分发之前(before)还是之后(after) (默认行为是之前):
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`)
},
after: (action, state) => {
console.log(`after action ${action.type}`)
}
})
store.registerModule
在 store 创建之后,你可以使用 store.registerModule
方法注册模块:
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。
createNamespacedHelpers
通过使用 createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
使用
vuex-stateshot
插件的使用方式是无侵入的,可插拔的,插件通过 registerModule
API动态创建了名为 vuexstateshot
的命名空间,来存储一些时间旅行需要用到的状态、方法。
安装
可以通过如下命令安装:
npm i vuex-stateshot -S
or
yarn add vuex-stateshot -S
创建插件
在创建插件(createPlugin)的时候可以指定有哪些模块(__MODULE__NAME__)以及模块下的哪些 actions
/mutations
需要订阅,可选择性地传入 stateshot
的History Options API
一个栗子🌰
import { createPlugin } from 'vuex-stateshot'
const subscribes = {
// The special root module key
rootModule: {
// The actions you want snapshot
actions: [],
// The mutations you want snapshot
mutations: []
},
// The custom module name
__MODULE__NAME__: {
// The actions you want snapshot
actions: [],
// The mutations you want snapshot
mutations: []
}
}
const options = {
maxLength: 20
}
const store = new Vuex.Store({
state: {},
...,
plugins: [createPlugin(subscribes, options)]
})
组件内部使用
在组件内部,可以通过 createNamespacedHelpers
API,指定插件的命名空间 vuexstateshot
来映射组件绑定辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapGetters, mapActions } = createNamespacedHelpers('vuexstateshot')
export default {
...,
computed: {
...mapGetters([ 'undoCount', 'redoCount', 'hasUndo', 'hasRedo' ])
},
methods: {
...mapActions(['undo', 'redo', 'reset'])
}
}
Undo/Redo 方法
通过组件绑定辅助函数 mapActions
,我们可以得到 undo
/redo
方法,用来管理状态。
方法名 | 描述 | 回调 |
---|---|---|
undo | 如果有可撤销的历史记录,则可以得到上一个记录的状态 | () => prevState |
redo | 当执行过 undo 后,可以通过 redo 获取到最近一次 undo 过的历史记录 |
() => nextState |
reset | 清除历史记录 | - |
历史记录状态
通过组件绑定辅助函数 mapGetters
,我们可以得到 hasUndo
、hasRedo
、undoCount
、redoCount
等状态,用来逻辑处理。
当触发一次状态同步, 此时undoCount = 1
/hasUndo = true
;
这是使用插件的一个开端;
当你调用一次undo
后, 会有一次redo
记录
状态 | 描述 | 类型 | 初始值 |
---|---|---|---|
undoCount | 可以撤销的历史记录计数. | Number | 0 |
redoCount | 可以重做的历史记录技术 | Number | 0 |
hasUndo | 是否可以撤销 | Boolean | false |
hasRedo | 是否可以重做 | Boolean | false |
任意门
当需求复杂时,可能我想撤销的不是一次 actions
/mutations
,而是需要撤销若干个 actions
/mutations
,对于这种需求,vuexstateshot
提供了自定义时机同步历史记录的方法,让多次复杂的操作“一键还原”。
Methods
名称 | 描述 | 回调 |
---|---|---|
syncState | 自定义方法同步历史记录快照 | - |
unsubscribeAction | 停止订阅 Actions
|
- |
subscribeAction | 重新订阅 Actions ,通常搭配 unsubscribeAction 使用 |
- |
unsubscribe | 停止订阅 Mutations
|
- |
subscribe | 重新订阅 Mutations ,通常搭配 unsubscribe 使用 |
- |
一个场景
假设我们订阅了changeTheme
、changeColor
、changeLang
三个Actions,每个 Action 触发的时候,都会记录一次历史记录快照,可实际场景的需求是,需要这些状态全部改变后才同步历史记录快照,以便撤销时还原多个状态。
import { mapActions } from 'vuex'
export default {
name: 'xxx',
...
methods: {
...mapActions([
'changeTheme',
'changeColor',
'changeLang'
]),
handleChange () {
// 停止订阅 Actions
this.$stateshot.unsubscribeAction()
// 多次触发已订阅的 Actions
this.changeTheme('dark')
this.changeColor('#fa4')
this.changeTheme('zh')
// 重新订阅 Actions
this.$stateshot.subscribeAction()
// 同步历史记录快照
this.$stateshot.syncState()
}
}
}
Tips:vuex-stateshot
同时提供了可以停止订阅Actions
/Mutations
的方法
在线Demo
结语
在可视化工具项目中,我们已经在使用 vuex-stateshot
来管理历史状态了。性能表现良好,完美达成了“时间旅行”的需求。为用户提供了操作交互上的安全保障
感谢 @doodlewind 提供了出色的工具 stateshot
以及 Vuex 3.1.0+ 提供的 subscribeAction
API,让操作变得有序可依。
vuex-stateshot
插件特性✨:
- 无侵入、可插拔的插件调用
- 严谨的逻辑业务状态
- 稳定的撤销 & 重做方法
- 任意时机同步快照的方法
- 100% 测试场景覆盖率
资源
原文:http://xlbd.me/create-a-time-...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。