当前位置: 首页 > news >正文

惠州网站建设是什么渠道查官网

惠州网站建设是什么,渠道查官网,企业网站建设的优势,企业网站的建立流程的第一步是前言 历史记录管理器是系统中的重要功能,而实现历史记录,有多种方案,例如: 存储数据快照、使用命令模式等。 在协同场景中,分布式撤销是非常重要的功能,如果历史记录通过快照存储,是很难实现的&…

前言

        历史记录管理器是系统中的重要功能,而实现历史记录,有多种方案,例如: 存储数据快照、使用命令模式等。

        在协同场景中,分布式撤销是非常重要的功能,如果历史记录通过快照存储,是很难实现的;命令式需要调整添加协同用户标识,在执行命令时,进行业务判断。        

        今天,我们使用 Yjs 的一个工具函数 UndoManager 功能,实现一个分布式撤销功能,支持协同场景下,独立撤销操作(自己只能撤销自己的操作)。

撤销实现案例

        例如基于 Konva 图层快照:

	public patchHistory() {// 当前图层的JSON串 - 不直接使用 toJSON(),避免影响原图层const layerClone = mainLayer.clone();// 当前图层的 MD5const layerJson = JSON.stringify(layerClone.toObject());const layerMD5 = MD5(layerJson);// 被添加图层与最后缓存的记录是否一致const lastLayer = this.undoStack[this.undoStack.length - 1];const lastLayerJson = JSON.stringify(lastLayer?.toObject());const lastLayerMD5 = MD5(lastLayerJson);// 如果最后一个记录与当前记录一致,则不添加记录if (layerMD5 === lastLayerMD5) return console.log("历史记录一致");this.undoStack.push(layerClone);// 如果记录数大于 HISTORY_MAX_RECORED,则删除最前的记录while (this.undoStack.length > HistoryManager.MaxHistoryCount) this.undoStack.shift();}

      例如基于命令模式:

// 执行命令并添加到历史记录
executeCommand(command) {// 如果当前不是最新状态,移除后面的历史记录if (this.currentIndex < this.history.length - 1) {this.history = this.history.slice(0, this.currentIndex + 1);}// 执行命令command.execute();// 添加到历史记录this.history.push(command);this.currentIndex++;// 如果历史记录超过最大数量,移除最旧的记录if (this.history.length > this.maxSize) {this.history.shift();this.currentIndex--;}this.updateUI();
}

        上诉仅是实现历史记录的一种方法,具体的还得根据自己的项目实际,选择合适的方案。今天,为大家介绍下 Yjs 协同中的历史记录管理器使用案例,支持分布式撤销(自己只能撤销自己的操作)。

Y.UndoManager

        官网链接:https://docs.yjs.dev/api/undo-manager,其用法也非常简单,如下:

import * as Y from 'yjs'// 可以是任何的 Yjs AbstractType 
const ytext = doc.getText('text')
const undoManager = new Y.UndoManager(ytext)ytext.insert(0, 'abc')
undoManager.undo()
ytext.toString() // => ''
undoManager.redo()
ytext.toString() // => 'abc'

        请注意,Y.UndoManager 是自带两个操作栈(undoStack|redoStack),堆栈中存放的是由 Yjs 维护的堆栈对象 StackItem :

构造函数

new Y.UndoManager(scope: Y.AbstractType | Array<Y.AbstractType> [{captureTimeout: number, trackedOrigins: Set<any>, deleteFilter: function(item):boolean}])

        构造函数中,必须传递一个 Yjs 的共享数据类型;可选参数中,`captureTimeout` 表示时间间隔(默认为 500 毫秒)内创建的编辑,会合并成一个记录,将其设置为 0 以单独捕获每个更改;`trackedOrigins` 则表示跟踪不同源的修改;

        而 `deleteFilter` 则是删除过滤函数,源码中,当记录被弹出时,会判断当前记录项是否需要删除,文档中没有指明其作用,不过判断应该是为了弹出时,可以保留其记录,某些特殊场景下做拓展使用。

API 详解

undoManager.undo()

        撤消 UndoManager 堆栈上的最后一个作,反向作将放在重做堆栈上。

undoManager.redo()

        重做重做堆栈上的最后一个操作,即之前的撤消被撤消。

undoManager.stopCapturing()

        调用 stopCapturing()确保放在 UndoManager 上的下一个作不会与上一个作合并 。前面说了,`captureTimeout` 是一定时间内的操作,都会被合并为一个记录存储,如果在下一个操作之前,手动调用 `stopCapturing`,则下一个操作会被记录为一个独立的操作。

// without stopCapturing
ytext.insert(0, 'a')
ytext.insert(1, 'b')
undoManager.undo()
ytext.toString() // => '' (note that 'ab' was removed)// with stopCapturing
ytext.insert(0, 'a')
// 手动调用 stopCapturing 那么下一个操作会被单独记录
undoManager.stopCapturing()
ytext.insert(0, 'b')
undoManager.undo()
ytext.toString() // => 'a' (note that only 'b' was removed)

undoManager.on('stack-item-added'  ... )

undoManager.on('stack-item-popped' ...)

undoManager.on('stack-item-updated', ...)

        事件都比较简单,这里不展开说了哈

指定跟踪的来源

        共享文档上的每个更改都有一个来源,也就是每一个共享数据类型的变更,都会有一个 origin 对象,标记着更改来源,默认是 null:

        官网推荐我们,将每一个修改,都封装到事务中,以便减少事件调用及指定事件源,因此,我们通过事务来调整共享数据类型。

        这个没这么难理解的,之前我们都是直接调用的数据方法,下面的两次 insert 会引起两次数据更新:

ytext.insert(0, 'a')
ytext.insert(1, 'b')
// ymap.set(key,value)
// yarray.push(...)

        官网推荐我们这样,事务中的执行的更改,只会引起一次数据更新:

doc.transact(()=>{ytext.insert(0, 'a')ytext.insert(1, 'b')// ymap.set(key,value)// yarray.push(...)
},origin)

        而 UndoManager 中的 `trackedOrigins`,就是跟踪指定源的修改(这个源的修改需要记录,其他源的修改不需要记录)。

画布案例

        有了以上的基础知识后,我们来实现基础的分布式撤销就简单多了。

        但是请注意!StackItem 存储的是共享数据类型哈!并不是快照、命令,我们需要结合实际的项目,去做适配。下面简单实现下多人协同场景下的撤销,实现一个基础的画布应用:

Konva

        Konva 的知识我就不多介绍了,它不是本章的重点,仅是作为绘制工具类使用。

	constructor(container: HTMLDivElement, width: number, height: number) {this.stage = new Konva.Stage({ container, width, height });this.layer = new Konva.Layer();this.stage.add(this.layer);}

        这是最简单的 Konva 代码了。

Yjs

        yjs 主要处理的事,是创建 Doc,维护共享数据类型,维护历史记录管理器:

export class Collaborate {private doc: Y.Doc;private yArray: Y.Array<unknown>;private undoManager: Y.UndoManager;private localOrigin: { userid: string; clientID: number };constructor(room: string, url?: string) {this.doc = new Y.Doc();// 初始化 originthis.localOrigin = { userid: "local", clientID: this.doc.clientID };// provider 不一定需要创建,如果非协同场景,则不需要,但是一定需要 Yjs 的数据结构,使用 Array 实现数据存储this.yArray = this.doc.getArray();// 如果用户传递了 url 则尝试连接该地址if (url) {// this.provider = new WebsocketProvider(url, room, this.doc);}this.undoManager = new Y.UndoManager(this.yArray, {trackedOrigins: new Set([this.localOrigin]),captureTimeout: 400,});// 监听变化 - 数据驱动更新,因此此处应该直接调用 draw.render 方法this.yArray.observeDeep(() => draw.render());}
}

        当然!别忘了,所有的共享数据类型,需要使用 doc.transact 事务封装!!!不然,UndoManager无法捕获!

HistoryManager

        这里的历史记录管理器仅作为外部调用使用,内部不创建堆栈,依赖 Y.UndoManager 实现:

export class HistoryManager {private collaborate: Collaborate;constructor(collaborate: Collaborate) {this.collaborate = collaborate;}undo() {this.collaborate.getUndoManager().undo();}redo() {this.collaborate.getUndoManager().redo();}
}

        其原理图大致如下:用户A执行操作,使用事务跟踪并记录当前的操作,此时,会引起共享数据类型的更新,(如果协同场景下,那么会广播此更新,使得所有客户端的共享数据类型同步),远端监听到更新后,进行视图更新。

关键步骤

        协同中心 yjs 提供事务执行

	// 提供事务执行transact(fn: () => void) {this.doc.transact(fn, this.localOrigin);}

        draw中提供视图更新方法(本例执行数据驱动,大家可以根据自己项目实际调整)

	/*** @description render 渲染函数 - konva 图形由 Yjs Doc 数据驱动*/render() {// 先清空当前图层,再根据 YArray 重建视图this.layer.destroyChildren();// 从 YArray 解析数据const yArray = this.collaborate.getYArray();for (const yShape of yArray.toArray() as Array<Konva.ShapeConfig>) {const { type } = yShape as { type?: string };if (type === "rect") {const rect = new Konva.Rect(yShape);this.layer.add(rect);}if (type === "circle") {const circle = new Konva.Circle(yShape);this.layer.add(circle);}}this.layer.batchDraw();}

        用户操作封装到事务中,目前是将所有的图形属性添加到 shape 中

	/*** @description 添加图形 - 添加的图形是需要同步到 Yjs Doc*/addShape(shape: Konva.ShapeConfig) {// 同步到 Yjs Doc(放入带有本地来源的事务中,便于撤销/重做)const yArray = this.collaborate.getYArray();this.collaborate.transact(() => {yArray.push([shape]);});}

        实现效果:

拓展:

        目前我们Array 中直接存储的是 shape 的配置项,但是我们想更新一个数据(移动元素|更改颜色)等场景时,该如何操作?

draw.getStage().on("dragend", (e) => {const node = e.target;const id = node.getAttr("id") as string | undefined;if (!id) return;const collaborate = draw.getCollaborate();const yarray = collaborate.getYArray();const arrayMap = yarray.toArray();const idx = arrayMap.findIndex((m) => m.id === id);if (idx === -1) return;const newShapeAttrs = Object.assign({}, yarray.get(idx), node.attrs);console.log("==> ", newShapeAttrs);// 重新更新数据collaborate.transact(() => {// 删除原数据yarray.delete(idx, 1);// 在同一位置新增yarray.insert(idx, [newShapeAttrs]);});
});

        这样设计,每次更新位置,都会引起 Array 的先删除后增加,但是实际的元素又没有变化!但是,YArray 不支持修改单个属性,array[idx] = [newShapeAttrs] 类似这样修改是不支持的

        可以单个修改属性的是 YMap ,因此,可以设计为 YArray<YMap> 的数据结构,拿到单个数据项,做属性修改,这部分大家可以自行优化下。

富文本案例

        上述画布案例中,画布数据就是一个个对象属性 (konva.ShapeConfig),因此使用数组存储,如何是富文本实现呢? Yjs 为我们提供了 Y.Text 类型,做富文本最合适。

定义基础页面结构

 监听输入:

/*** @description 监听输入事件*/
editor.addEventListener("compositionend", (e) => {const text = e.data;handleInput(text);
});editor.addEventListener("input", (e: Event) => {// 需要兼容换行 空格等其他特殊的字符const event = e as InputEvent;if (event.isComposing) return;const text = (e as InputEvent).data ?? "";handleInput(text);
});

封装协同控制中心

        还是上一个案例类似,协同控制中心仅负责创建共享数据类型,维护 UndoManager,

const ydoc = new Y.Doc();
const ytext = ydoc.getText("text");// 创建本地 origin
const localOrigin = { userId, clientID: ydoc.clientID };// 创建 y-websocket provider
const provider = new WebsocketProvider("ws://localhost:9999", "easy-painting", ydoc);// 创建 Y.UndoManager
const undoManager = new Y.UndoManager([ytext], { trackedOrigins: new Set([localOrigin]) });// 监听更新
ytext.observeDeep((_e, transaction) => {// 本地更新不执行if (transaction.origin === localOrigin) return;updateView();
});// 封装 doc.transact
function transact(fn: () => void) {ydoc.transact(() => {fn();undoManager.stopCapturing();}, localOrigin);
}

创建历史记录管理

        核心思想还是调用 undomanager 哈

// 创建 history
const history = {undo: () => {undoManager.undo();},redo: () => {undoManager.redo();},
};

数据处理

        本例采用 YText.Delta 作为共享数据类型,插入数据时使用 insert ,执行格式化时使用 format,删除时用 delete,更多的操作大家自行拓展哈。

// 输入框输入事件执行函数
function handleInput(text: string) {if (!text) return;console.log("input text: ", text);// 取当前光标位置 || ytext.length 作为插入位置const index = getTextIndex().start || ytext.length;// 执行 ytext.insert 命令transact(() => {ytext.insert(index, text);});
}
/*** @description 执行命令*/
function executeCommand(command: string, value?: string) {// 调用命令document.execCommand(command, false, value);// 获取当前的 索引const { start, end } = getTextIndex();transact(() => {// 对选择区域进行 formatif (start !== end) {ytext.format(start, end - start, { [command]: value ?? true });} else {// 否则对当前光标位置进行 formatytext.format(start, 0, { [command]: value ?? true });}});
}
// 监听 delete 实现
editor.addEventListener("keydown", (e) => {if (e.key === "Backspace") {// 获取当前光标位置const { start, end } = getTextIndex();if (start === end) {// 如果当前光标位置为0,则不执行删除if (start === 0) return;// 否则删除当前光标位置前一个字符transact(() => {ytext.delete(start, end - start);});}}
});

视图更新

        这里采用的简单的实现方式哈,执行 `document.execCommand(command, false, value);`,实现简单的样式,因此视图更新,就是识别 Delta 数据,转换为 HTML,赋给编辑器:


/*** @description 视图更新*/
function updateView() {console.log("==> 视图更新", ytext.toDelta());editor.innerHTML = ytext.toDelta().map((item: DeltaItem) => {// 如果没有属性,则直接返回文本if (!item.attributes) {// 处理换行符return item.insert.replace(/\n/g, "<br>");}// 需要根据属性类型,返回对应的 html 标签let content = item.insert;// 处理换行符content = content.replace(/\n/g, "<br>");// 按照优先级和兼容性顺序应用样式// 多个属性需要嵌套应用,例如: {"strikeThrough": true, "bold": true} => <s><strong>text</strong></s>if (item.attributes.bold) {content = `<strong>${content}</strong>`;}if (item.attributes.italic) {content = `<em>${content}</em>`;}if (item.attributes.strikeThrough) {content = `<s>${content}</s>`;}if (item.attributes.underline) {content = `<u>${content}</u>`;}if (item.attributes.foreColor) {content = `<span style="color: ${item.attributes.foreColor}">${content}</span>`;}return content;}).join("");
}

实现效果如下:

        这就是协同场景下的分布式撤销,这仅是一个简单的示例哈,还有很多细节需要完善,例如,直接执行 editor.innerHTML 会导致用户光标丢失,协同用户光标优化、用户光标存储等等,大家可以自行处理。

总结

        Y.UndoManager 不仅仅可以用于协同场景下的分布式撤销,当然,也可以用于本地实现历史记录管理,只是一般情况下,都是自己适配项目实现。

        上诉两个案例,从协同控制、历史记录,以及底层数据操作,原理都是类似的,都需要初始化一个 UndoManager ,并且指定追踪的源,通过保持一致的共享数据类型,实现页面展示的一致性。

        大家可以针对Yjs 官网的各个案例进行阅读,相信大家对协同的理解会更加深刻。

http://www.dtcms.com/a/431166.html

相关文章:

  • 个人做网站有什么条件网站备案信息填写
  • 自建网站代理服务器深圳建设网站推荐
  • 2025 AI 图景:从工具革命到生态重构的生存逻辑
  • 基于人工智能的电信经营分析系统架构研究
  • 环保部网站建设项目验收方案上海哪家做公司网站
  • RoCE V2 深度解析
  • PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限?
  • 小马厂网站建设商业信息发布平台
  • 随机过程:从理论到Python实践
  • 做国外网站用什么颜色建站行业的发展前景
  • Google Earth Pro(谷歌地球)2025年7月大陆版安装教程
  • C++与Open CASCADE中的STEP格式处理:从基础到高级实践
  • 【大模型】ubuntu搭建ollama 使用ollama本地部署deepseek qwen等大模型
  • Win32 托盘图标弹出菜单使用
  • MATLAB中SIL 和 PIL 仿真
  • 基于NUC和STM32F103的无人车
  • wordpress网站的配置文件进出口外贸公司名字
  • 【报错】qt.qpa.plugin: Could not find the Qt platform plugin “windows“ in ““
  • 彩票网站给实体店做代销个人网站设计论文道客巴巴
  • 学校网站建设注意点美妆网站设计模板
  • 【算法训练营Day28】动态规划part4
  • PandasAI:ChatBI的极简上手版学习(一)
  • 鸿蒙NEXT星闪数据传输实战:重新定义无线连接体验
  • 使用WireGuard组建大内网环境
  • 网站的建设服务中心上海市建设工程安全质量监督总站网站
  • TDengine 时序函数 TWA 用户手册
  • 从“人治”到“数治”:信用报告在现代化社会治理中的角色与演变
  • (基于江协科技)51单片机入门:8.DS1302
  • 01.如何使用 JavaScript 创建游戏 | 图片拼图
  • 【2025年Q3】AI生产力再探再报:社恐专用写作、动嘴剪视频、AI点外卖?这波AI工具太野了!