编辑器历史记录功能(2)快照模式

发布于 2023-06-29 20:14:17 字数 6056 浏览 40 评论 0

最近在重构一个 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)
	...
}

通过引入 stopstart 我们能够控制 历史记录的写入。

除了基本的操作,我们的 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

維他命╮

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文