Canvas-Editor 实现类似 Word 协同编辑

文章介绍了如何使用Canvas-Editor富文本编辑器实现协同编辑功能,重点在于协同部分的代码实现,包括用户选区管理、Yjs的CRDT应用、WebSocket通信以及Yjs库的使用。作者分享了源码修改和API设计,讨论了存在的技术挑战和未来优化方向。
摘要由CSDN通过智能技术生成

前言

    对于word的协同编辑,已经构思很久了,但是没有找到合适的插件。今天推荐基于canvas/[svg](https://so.csdn.net/so/search?q=svg&spm=1001.2101.3001.7020) 的富文本编辑器  canvas-editor,能实现类似word的基础功能,如果后续有更好的,也会及时更新。

Canvas-Editor

效果图

官方文档

canvas-editor | rich text editor by canvas/svgrich text editor by canvas/svgicon-default.png?t=N7T8https://hufe.club/canvas-editor-docs/

官方DEMO

canvas-editoricon-default.png?t=N7T8https://hufe.club/canvas-editor/

Gitee

canvas-editor: 同步自https://github.com/Hufe921/canvas-editoricon-default.png?t=N7T8https://gitee.com/mr-jinhui/canvas-editor

前置条件与实现思路

    虽然canvas-editor做的还不错,API都比较完善,但是对协同部分还是空缺,因此我们此次的重点是实现协同部分的代码,难免会修改源码部分。因此,我们需要阅读源码,实现 ts 代码的编写,修改其源码,实现协同。

下载源码并运行

    大家可以直接从 github下载 ,也可以从刚才给的 gitee 下。

npm i // 下载相关依赖

npm run dev // 启动服务

npm run build // 打包项目

    启动后,能出来与demo一致的页面,即完成了这一步。

实现用户选区

    用户闪烁的光标目前还没有思路实现,后面会攻克技术难点,但是用户选取可以通过API实现:

     但是这个API会导致我的选取也会发生改变,因此,不能直接使用,需要添加新的API

    简单解释一下文件,command文件向外暴露了API, command 指向 commandAdapt 文件,Adapt 文件中,有需要的全部对象,包括 画布、选取对象等,可以直接进行底层绘制。

  public setUserRange(startIndex: number, endIndex: number, payload?: string) {    if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) return    const isReadonly = this.draw.isReadonly()    if (isReadonly) return    // 根据 index 获取 domList 设置颜色    const elementList = this.draw.getElementList()    for (let i = startIndex; i <= endIndex; i++) {      elementList[i].highlight = payload||'#F5EEA0'    }    this.draw.render({      isSetCursor: false,      isCompute: false    })  }

     这样用户选取,才不会影响我的选取,而取消选取就是设置透明色即可。

  // 用户取消选取  public setUserUnRange(startIndex: number, endIndex: number) {   if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) return    const isReadonly = this.draw.isReadonly()    if (isReadonly) return    // 根据 index 获取 domList 设置颜色    const elementList = this.draw.getElementList()    for (let i = startIndex; i <= endIndex; i++) {      elementList[i].highlight = 'transparent'    }    this.draw.render({      isSetCursor: false,      isCompute: false    })  }

**用户的光标是无状态的,因此需要记录光标信息,不然我重新设置了选取,上次的选取是需要取消哦,**这个后面再说。

搭建CRDT

    协同的核心就是数据一致性,因此,我们需要根据现有的数据结构实现CRDT。

新建yjs文件

// editor/core/websocketimport * as Y from 'yjs'import { WebsocketProvider } from 'y-websocket'import { IWebsocketProviderStatus } from '../../interface/Websocket' export class Ydoc {  private ydoc: Y.Doc  private ymap: Y.Map<unknown>  private ytext: Y.Text  private provider: any | undefined  private connect: boolean | undefined  private url: string  private roomname: string   constructor(url: string, roomname: string) {    console.log('new Ydoc')    this.url = url    this.roomname = roomname    this.connect = false     // 创建 YDoc 文档    this.ydoc = new Y.Doc()     this.ymap = this.ydoc.getMap('map')     this.ytext = this.ydoc.getText('text')     this.ymap.observe(() => {})     this.ytext.observe(() => {})     // 【方案二】 websocket 方式实现协同(已自己搭建 websocket 服务)    this.provider = new WebsocketProvider(this.url, this.roomname, this.ydoc)     // 监听链接状态F·    this.provider.on('status', (event: IWebsocketProviderStatus) => {      let { status } = event      if (status === 'connected') this.connect = true      else this.connect = false    })  }   public disConnection() {    if (!this.connect) return    this.provider.disconnect()  }}

初始化 yjs

    入口文件 index.ts 实现创建并传参

 // 创建 websocket    if (ydocInfo) {      let { url, roomname, userid, username, color } = ydocInfo      if (!url || !roomname || !userid || !username)        throw Error('参数错误,url、roomname、userid、username必传!')      // 1. 如果存在,则创建协同      ydoc = new Ydoc(url, roomname, userid, this.command, color)      Reflect.set(window, 'ydoc', ydoc)      console.log(`用户${username}初始化`)      ydoc.userInitEditor(`用户${username}`)    }

     这样,整个编辑器需要实现协同的地方,都能调用 ydoc 实现。

实现用户登录

    Yjs 的基本使用中,通过Map设置数据,observe观察器实现数据获取,协同部分不懂得可以看上一篇文章:

[深度解析 Yjs 协同编辑原理【看这篇就够了】_深度 解析yjs原理-CSDN博客文章浏览阅读1k次,点赞21次,收藏16次。本文带大家分析了Yjs的API、y-websocket 的实现原理、Yjs的应用及底层协同模型,并使用Logic Flow 简单实现了其协同。大致的协同实现都有类似的思想,大家以后需要协同的场景,希望也能自行开发。_深度 解析yjs原理在这里插入图片描述
![](https://img-blog.csdnimg.cn/direct/c66b5078deb34ddc8f8bca8d3f0d2dca.png

    这样,用户每次初始化 Editor的时候,都会广播其他用户:

实现用户选区

    用户每次操作鼠标抬起,都会触发setRangeStyle事件:

     因此,在这个事件中捕获用户的选区操作;

     yjs中则是正常转发,然后调用上面实现的选区API:

 public userRange({ data }: IYMapObserve) {    let { startIndex, endIndex, userid, color } = data    this.command.setUserRange(startIndex, endIndex, userid, color)  }

    效果如下:

实现用户取消选区

    现在的选区还是有bug的,用户退出后,无法识别,还有就是单击时,无法优化选区。

    如上图,我点击时,理论上只占用一个格子,不应该有选区【**用户光标目前还没能实现**】  if (startIndex === endIndex) return 如果点击的开始与结束相同,则不进行渲染。还有用户退出时,清空用户选区:

     实现删除历史选区,并删除lastRange 记录即可。

实现文本输入与删除

   CanvasEvent监听了input 事件,实现监听用户的输入,修改参数实现在draw 中获取用户数据,文档变化时,会调用 draw 中的方法:

因此,在这里通过yjs广播事件,修改参数后,就能拿到用户新增的数据了:

 // 内容区变化  public contentChangeHandle(payload: IEditorData) {    /**     * 因此在这里需要重新解析用户的选区设置,不然会导致选区异常 BUG     */    // 这里要解析 userRange    let { header, footer, main } = payload     main.forEach(item => {      if (item.userRange) {        delete item.highlight        delete item.userRange      }    })     this.setValue({ header, footer, main })  }

    实现效果:

删除实现:

    keydown.ts 中对每个事件做了监听,在该文件实现广播,还是拿到本地的数据,进行数据解析,重新渲染。

    效果如下:

实现样式协同

    样式的协同,就是基于API实现的,因为在main.ts中,所有的菜单栏操作,都是基于API实现,因此,我们需要在API调用处,进行统一处理即可

  // 选区样式改变  public rangeStyleChange(payload: IRangeStyle) {    // 样式只能针对 用户的当前选区    // 直接使用 element 的事件机制     let { startIndex = 0, endIndex = 0, attr, value } = payload    const isReadonly = this.draw.isReadonly()    if (isReadonly) return    if (startIndex === endIndex) return    // 根据 index 获取 domList 设置颜色    const elementList = this.draw.getElementList()    for (let i = startIndex; i <= endIndex; i++) {      let el = elementList[i]      if (el) {        switch (attr) {          case 'color':            value ? (el.color = <string | undefined>value) : delete el.color            break           case 'bold':            value ? (el.bold = true) : delete el.bold            break           case 'italic':            value ? (el.italic = true) : delete el.italic            break           case 'fontSize':            break           case 'underline':            value ? (el.underline = true) : delete el.underline            break           case 'highlight':            // 这里还有BUG,因为用户选区结束又被设置透明            value              ? (el.highlight = <string | undefined>value)              : delete el.highlight            break          default:            break        }      }    }    this.draw.render({      isSetCursor: false,      isCompute: false    })  }

    效果如下:

    用户协同选区与高亮冲突了,这个还得在想办法处理。

打包在项目中使用

    想要打包,需要注释 main.ts 中的window.onload 事件,将Editor 暴露到window身上

    打包后,将dist 放置到项目 public/libs.canvas-editor下【**如果你打包报错,基本上是TS语法检查的问题 let const 引入没用的模块等**】

这样已经实现了基本的协同编辑了,至于说 菜单栏、目录,其实也是它自己加上的,然后调用API实现:

剩下的就是自行实现菜单栏,调用API即可。

总结

    对这个文章简单说一下:
  1. 这个版本的代码肯定是粗糙的哈,大家稍微谅解一下,自己的TS还有点差;
  2. 功能实现上还有些缺陷,有些功能底层限制了,修改起来难度非常大,比如协同选区问题,后续会再优化;
  3. 协同的底层一定是数据一致性、广播监听、调用相应API实现相同功能;
  4. 后续可能会完善这部分代码,争取能实现基本的、稳定的协同环境,包括也会更新在 mpoe 项目中,有一个稳定版本支撑协同编辑;
  5. 文章在书写过程中,会发现BUG,然后调整代码,可能会出现页面与实际代码不匹配,大家以实际代码为主哈
  6. 也会持续关注大家的问题与需求,大家可以提一些好的建议。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值