编辑器历史记录功能(2)快照模式
最近在重构一个 h5 编辑器,需要实现历史记录功能,让用户可以撤销和重做。 之前有写过文章 #12 介绍过利用命令模式实现编辑器历史记录,今天就来讲讲快照模式的实现。
细节
快照模式的实现很简单,就是在数据变化时,将数据备份一份推入栈中。撤销和重做都是从栈中取出对应的备份进行恢复。接下来就用具体代码来演示下,技术栈: Vue 2.7 + TS + Composition Api
首先是自定义 hook
const historyArr = [] const historyIdx = 0 const maxCount = 30 export function useHistory<T>(cb?: (data: T) => any) { const push = (data: T) => { // 存在撤销 if(historyArr.length - 1 !== historyIdx) { historyArr.splice(historyIdx + 1, historyArr.length) } // if(historyArr.length === maxCount) { historyArr.shift() } historyArr.push(JSON.stringify(data)) historyIdx = historyArr.length - 1 } const redo = () => { if(historyIdx < historyArr.length - 1) { historyIdx++ const item = historyArr[historyIdx] } } const undo = () => { if(historyIdx > 0) { historyIdx-- const item = historyArr[historyIdx] } } const dataChange = (item: T) => { cb && cb(item) } return { push, undo, redo, } }
在使用的时候,监听数据,然后存入数据即可, 这里也封装成一个 hook
function useHistoryInit() { // 使用 store const store = useStore() const { push, redo, undo } = useHistor<EditorData>((v) => { store.data = v }) watch(() => store.data, (cur) => { push(data) }, { immediate: true }) } // 数据初始化后的地方 useHistoryInit()
上面的代码直接执行是有问题的。撤销时,重新改动了数据,导致又 push 了一次记录,这样是不符合我们预期的。因此还需要加一个是否允许填充历史的变量。
+ let allowRecord = true ... const push = (item: T) => { + if(!allowRecord) { + return + } ... } const dataChange = (item: T) => { + allowRecord = false cb && cb(item) + nextTick(() => { + allowRecord = false + }) } ...
这样数据取出的时候,就不会重新填入记录了。这里用 nextTick
是因为 Vue 的批量更新机制,数据取出后改变数据会在下个 Tick 触发 wacth ,所以这里我们需要等一个 Tick 才能恢复。
在编辑器时,如果拖拽一个元素,可能会频繁出发数据的改变,我们其实希望拖拽开始到结束只触发一次。这个功能怎么实现呢? 其实也很简单,重复的数据变化时,禁止存入数据,变化结束后才允许存入数据。这里我们也会用到 上面使用的那个变量,不过用法就得封装一下了。
... const stop = () => { allowRecord = false } const start = () => { allowRecord = true } ... return { ... stop, start, } ... // 使用时 const {start, stop, push} = useHistory() const store = useStore() const resizeHandle = (x: number, ...) => { stop() // 禁止存入 ... } const resizeEndHandle = (x: number, ...) => { start() // 允许存入,并将当前数据主动存入 push(store.data) ... }
通过引入 stop
和 start
我们能够控制 历史记录的写入。
除了基本的操作,我们的 UI 也需要知道 是否能够进行 撤销和重做的操作,这里我们再暴露两个变量即可。
+ const historyIdx = ref(0) - let historyIdx = 0 ... + const undoEnable = computed(() => { + return historyIdx.value > 0 + }) + const redoEnable = computed(() => { + return historyIdx.value < historyArr.length - 1 + }) return { + redoEnable, + undoEnable, } ...
historyIdx
也要从普通变量变为 响应式变量.
到这里我们就已经实现了差不多了,我们再把 useHistory
和数据封装在一起,最终代码如下:
exportfunction createHistoryHook(maxCount: number = 30) { const historyArr: string[] = [] const historyIdx = ref(0) let allowRecord = true let changeHandles: Set<Fn> = new Set() return function useHistoryHook<T>() { const push = (data: T) => { if (!allowRecord) { return } // 存在撤销 if (historyArr.length - 1 !== historyIdx.value) { historyArr.splice(historyIdx.value + 1, historyArr.length) } // if (historyArr.length === maxCount) { historyArr.shift() } historyArr.push(JSON.stringify(data)) historyIdx.value = historyArr.length - 1 } const undoEnable = computed(() => { return historyIdx.value > 0 }) const redoEnable = computed(() => { return historyIdx.value < historyArr.length - 1 }) const dataChange = (item: string) => { stop() const obj = JSON.parse(item) changeHandles.forEach(cb => cb(obj)) nextTick(() => { start() }) } const redo = () => { if (redoEnable.value) { historyIdx.value++ const item = historyArr[historyIdx.value] dataChange(item) } } const undo = () => { if (undoEnable.value) { historyIdx.value-- const item = historyArr[historyIdx.value] dataChange(item) } } const stop = () => { allowRecord = false } const start = () => { allowRecord = true } const addHandle= (handle: FnPair<T, any>) => { changeHandles.add(handle) } const removeHandle = (handle: FnPair<T, any>) => { changeHandles.delete(handle) } return { push, undo, redo, redoEnable, undoEnable, stop, start, addHandle, removeHandle, } } }
具体使用
// hook export const useHistory = createHistoryHook<EditorDataType>(30) export function useInitHistory() { const store = useStore() const { push, addHandle } = useHistory() addHandle((data) => { store.data = data }) watch(() => store.data, cur => push(cur), {deep: true}) } // 数据初始化后 useInitHistory() // UI 显示 const { undo,redo,undoEnable,redoEnable } = useHistory()
最后
利用快照实现历史记录的功能十分简单,但是需要注意的细节还是有一些的。结合 Vue Composition Api, 整体开发体验十分友好。平时开发中,将通用逻辑抽离,能够极大的方便我们进行维护和复用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论