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

团购网站制作临安规划建设局网站

团购网站制作,临安规划建设局网站,北京网站建设可选兴田德润,怎么做网站空间前言 多人协同开发确实是比较难的知识点#xff0c;在技术实现上有一定挑战#xff0c;但随着各种技术库的发展#xff0c;目前已经有了比较成熟的解决方案。今介绍 Yjs 基于CRDT算法#xff0c;用于构建自动同步的协作应用程序#xff0c;与Quill富文本编辑器#xff0c…前言 多人协同开发确实是比较难的知识点在技术实现上有一定挑战但随着各种技术库的发展目前已经有了比较成熟的解决方案。今介绍 Yjs 基于CRDT算法用于构建自动同步的协作应用程序与Quill富文本编辑器快速构建多人协同编辑器。 前几章是介绍QuillYjs的基础看项目示例的直接前往  整体样例实现 章节。实现的整体效果如下 协同编辑数据模型 想要实现协同开发就要对数据模型进行约束,目前比较有代表性的协同数据模型为 Delta 数据模型 Deltas数据模型的实现是Quill.js富文本编辑器Deltas是一种简单而富有表现力的格式可以用来描述Quill的内容和改变。这种格式本质上是JSON是人类可读的也很容易被机器解析。Deltas可以描述任何Quill文档包括所有的文本和格式信息其中没有HTML的歧义和复杂性。 {ops: [{ insert: Gandalf, attributes: { bold: true } },{ insert: the },{ insert: Grey, attributes: { color: #cccccc } }] } 如上Deltas数据我们解析下 { insert: Gandalf, attributes: { bold: true } }【插入 Gandalf并加粗】    { insert: the },【插入 the 】 { insert: Grey, attributes: { color: #cccccc } }【插入 Grey,并设置颜色 #ccc】, 因此实现的效果如下在线版没有颜色我就不加了他是对每一项要操作的字符串进行属性描述 Slate 数据模型 而Slate数据模型的实现是Slate.js,Slate.js 是一款支持完全自定义的富文本编辑器它在可扩展性、可定制性、丰富的 API 和 React 集成方面有着出色的表现。 {type: insert_text,path: [0, 0],offset: 15,text: A new string of text to be inserted., } 我就不解析上面的Slate数据模型了也比较简单。Quill与Slate.js在底层实现上还是有很大差别的如下仅是一个简单的文本两者渲染的DOM结构完全不同 Slate.js嵌套的DOM太多了可能这样才能实现 支持完全自定义更多定制化功能。但是我更倾向于Quill因此本文采用Quill来实现。 协同编辑的问题所在 协同编辑最大的问题就是如何保持数据一致性 这便是协同编辑需要解决的问题。 数据一致性算法 OT算法与CRDT 算法应该算是目前比较好的协同算法了具体的算法实现我也没有深入了解如果大家有需要后续会出文章讲解算法部分。 大家可以看看这篇文章文档多人协同编辑底层算法是如何实现的我的开发也受到该作者的启发写的很好包括文档编辑锁等协同思想大家可以去看看。 Yjs 在官网的介绍中Yjs是一个高性能CRDT用于构建自动同步的协作应用程序。它将其内部CRDT模型公开为可以并发操作的共享数据类型。共享类型类似于常见的数据类型如Map和Array。它们可以被操纵在发生更改时触发事件并在没有合并冲突的情况下自动合并。 Yjs支持以下的富文本编辑器可以看出其生态还是非常完善的。 到此还是希望大家明确概念哈Yjs仅是处理协同数据一致性算法的具体实现我们很容易与Quill的功能相混淆认为是Yjs提供了所有的技术支持并不是。Quill才是文本编辑、协同数据的生产者而Yjs仅是保证了多人的Delta数据一致性这个很重要的要分清楚你的操作对象。 我们还是先搭建Quill Yjs 协同编辑吧然后再跟大家介绍API。 搭建QuillYjs协同编辑器 下载 Quill、Yjs 依赖 // 下载 Quill npm install quill1.3.4// 下载Yjs npm install yjs 初始化Quill编辑器  templatediv idedit/div /templatescript setup import Quill from quill; import quill/dist/quill.snow.css; // 使用了 snow 主题色import { onMounted } from vue;onMounted(() {// 获取dom需要在mounted后new Quill(#edit, {theme: snow,}); }); /scriptstyle langless scoped/style到这里Quill编辑器已经配置好了。 初始化Yjs Yjs提供了三种连接形式websocket rtc dat我们稍后会介绍websocket形式rtc是官网的样例我们先直接用 。 npm i y-webrtc # or npm i y-websocket # or npm i y-dat 下载yjs与quill的连接器 npm i y-quill // 初始化YJS// A Yjs document holds the shared dataconst ydoc new Y.Doc();// Define a shared text type on the document这个是拿到需要协同的 Delta 数据const ytext ydoc.getText(quill);// 绑定 quill与YJSconst binding new QuillBinding(ytext, quill);// 使用webrtc实现连接const provider new WebrtcProvider(quill-demo-room, ydoc); ytext对象是用于表示文本的共享数据结构。它还支持格式化属性即粗体和斜体。Yjs会自动解决共享数据上的并发更改因此我们不再需要担心冲突的解决。然后我们将ytext与quill编辑器同步并使用QuillBinding使它们保持同步几乎所有的编辑器绑定都是这样工作的。 创建绑定后直接利用rtc实现数据共享就能实现协同编辑了 封装类 因为后续的操作都需要使用到quill及yjs对象考虑封装为类实现 // 导出Quill实体类import Quill from quill; import quill/dist/quill.snow.css; // 使用了 snow 主题色export class myQuill {constructor(selector) {// 初始化 quill 文档操作对象this.quill new Quill(selector, {theme: snow,placeholder: 请输入内容...,});} }// 导出 Yjs 实体类 import * as Y from yjs; import { WebrtcProvider } from y-webrtc; import { QuillBinding } from y-quill;export class myYjs {// 需要传入绑定对象constructor(quill) {// A Yjs document holds the shared datathis.ydoc new Y.Doc();// Define a shared text type on the document这个是拿到需要协同的 Delta 数据const ytext this.ydoc.getText(quill);// 绑定 quill与YJSconst binding new QuillBinding(ytext, quill.quill);// 使用webrtc实现连接const provider new WebrtcProvider(quill-demo-room, this.ydoc);} }直接初始化即可后续在拿的是对象进行操作 onMounted(() {// 获取dom需要在mounted后const quill new myQuill(#edit);// 初始化YJSconst yjs new myYjs(quill); }); 添加用户光标 我们需要添加用户光标区分编辑用户 npm i quill-cursors 绑定光标信息 这样在协同开发时就能出现用户光标了 同时还支持修改光标用户信息 // 完善代码 创建自己的光标信息 createAwareness(name) {let { awareness } this.provider;// 定义随机颜色let color # Math.random().toString(16).split(.)[1].slice(0, 6);awareness.setLocalStateField(user, { name, color });return awareness; } Yjs Shared Types Yjs也有自己的数据类型允许我们通过API进行操作但是我还是上面所说这不是Yjs的事情文档的编辑、删除、更新都应该是Quill富文本编辑器的事因此我不会介绍Yjs的API下面章节会介绍Quill的API。 yarray.insert(0, [some content]) // delta: [{ insert: [some content] }] Quill Apis 我们已经搭建了最简单最基础的协同开发编辑器用到的Yjs仅是做数据绑定冲突处理是Yjs内部自己实现的我们不需要过多关注。下面需要介绍Quill的相关API因为我们编辑的是Quill富文本编辑器因此熟悉Quill API是非常重要的。 Quill支持多种方式格式化包括UI控件和API调用UI控件就是顶部的菜单栏我们重点看API调用的方式 Quill菜单栏配置 Quill支持我们自定义菜单栏传入什么就显示什么支持下列属性 属性后面的简写才是tabbar配置项 toolbar: [[background]], // 添加背景颜色 有些图标已经不显示了因此我们可以使用 iconfont图标自定义菜单栏通过调用API实现相同功能。 向编辑器中插入文本 insertText quill.insertText(0, Hello, bold, true);quill.insertText(5, Quill, {color: #ffff00,italic: true }); 如何向末尾追加文本呢 获取文本编辑器长度 getLength 检索返回编辑器的内容长度。注意即使Quill是空的编辑器仍然有一个‘\n’表示的空行所以getLength将返回1。 var length quill.getLength(); var length quill.getLength();// 向末尾追加quill.insertText(length, quill.getLength()); 效果换行了考虑下原因即使Quill是空的编辑器仍然有一个‘\n’表示的空行,因此 quill.insertText(0, Hello, bold, true);var length quill.getLength();// 向末尾追加quill.insertText(length, quill.getLength()); 就被解析为【\n】hello 【‘hello’,\n】 length2 (向2 的位置添加文本)【‘hello’,\n‘getLength’】。就跟数组的索引跟下标的关系类似因此正确的做法是 length -1 不再换行。 insertText实际使用中的问题 1. 仅支持插入字符串 源码中text是需要进行正则匹配去除特殊符号的因此不支持传入其他。 2.  getLength 使用需谨慎 // 测试变量[1, 2, 3, 4, 5].forEach((i) {console.log(i);quill.insertText(length, i.toString());}); 上述代码理论上应该插入 12345但是实际的效果是 原因是length是实时变化的因此动态获取长度能避免很多问题 [1, 2, 3, 4, 5].forEach((i) {console.log(i);quill.insertText(quill.getLength(), i.toString());}); formatting 格式化API quill.formatText(0, 5, bold, true); // 加粗 hello quill.formatText(0, 5, { // 取消加粗 hello 并且设置颜色为bluebold: false,color: rgb(0, 0, 255) }); api比较简单 用户选择 quill.getSelection(focus false);这个是比较重要的API可以实现外部API的格式化操作对用户选中的内容进行单独格式化可以进行参数传递控制是否聚焦输入框不然点击输入框外就不能选中了。 撤销与重做 quill.history.undo(); quill.history.redo(); 整体样例实现 我们利用上面的知识做一个完整的案例来体验一下多人协同编辑吧。 登录页实现 我们协同是基于用户体系的同时协同用户光标也有用户因此需要登录才能加入编辑。 首页实现 协同编辑页实现 接口开发 需要初始化 express、ws、socket的服务ws的服务我们用在Yjs的y-websocket服务上后面细说这次使用数据库实现持久化数据存储webAPI采用SSM的三层分离架构controller、serviceImpl、xmlMapper分离在node中还多了路由模块,因此数据流向是 axios node_router node_conrtoller  node_service node_mapper axios.then() 有过SSM开发经验的一看就懂了不懂的可以琢磨一下不然看不懂这个看代码也比较难。详细的接口设计开发部分我就不展开说了这是后端的知识如果大家感兴趣可以单独出一篇文章说说前后端的开发让大家都能成为全栈开发 初始化WS服务 Yjs提供了三种连接模式嘛ws是可以自己实现服务器使用也更稳定因此使用node创建ws服务供Yjs调用实现双向即时通信  module.exports () {console.log(等待初始化 WS 服务...);// 搭建ws服务器const { WebSocketServer } require(ws);const wss new WebSocketServer({port: 9000,});console.log( WS 服务初始化成功连接地址ws://localhost:9000);wss.on(connection, (ws, req) {console.log(Yjs 客户端连接 ws 服务);// ws.send(我是服务端); // 向当前客户端发送消息}); };Yjs客户端调用 import * as Y from yjs import { WebsocketProvider } from y-websocketconst doc new Y.Doc() const wsProvider new WebsocketProvider(ws://localhost:1234, my-roomname, doc)wsProvider.on(status, event {console.log(event.status) // logs connected or disconnected }) 在这里使用监听的目的是根据用户连接状态决定是否启用本地连接实现更加稳定的协同编辑到此已经完成了所有的静态开发接口也差不多了我们来实现关键的协同编辑 协同编辑 我们不使用Quill 原生的tabbar自定义了icon通过调用API实现富文本编辑。 撤销与重做 我们实现的思想还是封装的公共类哈 // MyQuill 类// 撤销undo() {this.quill.history.undo();}// 重做redo() {this.quill.history.redo();} 调用 // 撤销case icon-chexiao:quill.undo();break;// 重做case icon-zhongzuo:quill.redo();break; 格式化 // 格式化format(opt, color) {// 将加粗\斜体\删除线\下划线\颜色等操作 封装一个函数,因此,就需要先获取样式,才能判断是否已经有样式// 还需要获取用户的选择,可能是给某些字符添加样式// 获取用户选择 ** 这里需要传递参数,不然会导致焦点移出编辑器,选中失效这个 true 非常关键var range this.quill.getSelection(true);if (!range) return console.warn(User cursor is not in editor);let { index, length } range; // index 是当前光标的索引,length 表示当前选择的长度// 获取样式 检索给定范围内文本的所用格式(加粗 斜体都是块作用域,是需要指定长度的,因此,用户没有选择,则默认不作用,不像标题等,是行作用域)let { bold, italic, strike, underline } this.quill.getFormat(index,length);// icon-cuti: bold,// icon-italic: italic,// icon-strikethrough: strike,// icon-zitixiahuaxian: underline,// icon-zitiyanse: color,// 拿到用户操作的映射,判断有没有当前属性,没有则添加,有,则删除if (opt icon-cuti)this.quill.formatText(index, length, bold, !bold);if (opt icon-italic)this.quill.formatText(index, length, italic, !italic);if (opt icon-strikethrough)this.quill.formatText(index, length, strike, !strike);if (opt icon-zitixiahuaxian)this.quill.formatText(index, length, underline, !underline);if (opt color) this.quill.formatText(index, length, color, color);} 实现图片上传 insertEmbed 向编辑器中插入嵌入式内容返回一个改变后的Delta对象: quill.insertEmbed(10, image, http://quilljs.com/images/cloud.png); 因此我们需要一个图片的服务器地址才能实现插入图片下面来说说文件上传 前端文件上传无非是两种方式一个base64 一个FormData是二进制文件的载体两种方式都可以在node中解析并保存文件 // 文件上传 const upload async (e) {// 创建的本地浏览文件无法实现 quill 中的url请求需要借助服务器// let url window.URL.createObjectURL(files[0]);// quill.insertEmbed(0, image, url);let baseURL http://localhost:5000;let { files } e.target;let form new FormData();form.append(file, files[0]);let res await editUploadFile_API(form);// 上传成功后直接拿到地址添加到编辑器中if (res.code ! 200) return ElMessage.error(res.msg);quill.insertEmbed(null, image, baseURL res.data); }; 使用 express-fileupload 中间件中间件作用在该上传文件之前哈可以快速解析文件放在 req.files上大家也可以使用Multer // 上传文件 exports.uploadFile async (req, res, next) {console.log(req.files);if (req.files null)return res.status(400).json({ code: 400, msg: no file uploaded });// 不然转存数据let { file } req.files;let newfilename file.md5 . file.name.split(.)[1];let newpath path.join(process.cwd(), /public/images/) newfilename;// 移动文件到第一参数指定位置 若有错误 返回500file.mv(newpath, (err) {if (err) return res.status(500).json({ msg: 文件上传失败 });return httpCode(res, 200, 文件上传成功, /static/images/${newfilename});}); }; 实现效果 实现文件共享 通过分享链接实现接口数据传递绑定文件进而实现文件共享: 跳到页面后是没有登录的状态因此进行登录后返回invited页面进行确认。 考虑 router的特性将当前路由信息转存到login页面才能在login页面直接跳转到确认邀请页面 // 考虑是否登录const user JSON.parse(sessionStorage.getItem(user));if (to.path ! /login) {if (!user) {ElMessage.error(请先登录);// 进行数据转存if (to.matched[0].path /invited/:fileid) {// 向 login 添加信息let { fileid } to.params;return next({ path: /login, query: { fileid, ...to.query } });}return next({ path: /login });}} 登录按钮 if (router.currentRoute.value.query.fileid) {let { fileid, filename, username } router.currentRoute.value.query;return router.push({path: /invited/${fileid},query: { filename, username },});}router.push(/home); 页面开发 效果如下 实现粘贴板 const execContent (text) {if (navigator.clipboard) {// clipboard api 复制navigator.clipboard.writeText(text);} else {var textarea document.createElement(textarea);document.body.appendChild(textarea);// 隐藏此输入框textarea.style.position fixed;textarea.style.clip rect(0 0 0 0);textarea.style.top 10px;// 赋值textarea.value text;// 选中textarea.select();// 复制document.execCommand(copy, true);// 移除输入框document.body.removeChild(textarea);}}; 文件版本控制 这里有一个注意事项 /*** 版本控制说明* 1. 客户端一定是永远调用一个接口,因从需要处理是否处于创建状态,* 2. 根据files 表的 currenthead 当前指针 是否为空 判断是否是第一次创建* createVersion 中可以直接 next 跳过创建过程* 3. 更新版本还需要控制时间* 4. 更新版本的同时,还需要更新文件表信息 currenthead 字段*/// 更新版本(有一定的时间周期,不然一个文件会有很多版本) router.post(/updateVersion, versionCtrl.createVersion, fileCtrl.updateFiles); 创建文件的时候是没有初始化版本currenthead 字段的因此当我们保存的时候需要先判断当前是否有版本没有则正常创建如果已经有了版本则需要判断当前版本是否超过时限不然保存一次创建一个版本是不合理的。 客户端初始化quill的时候需要延时判断当前编辑器是否有内容 不能直接覆盖因为可能别的编辑者正在编辑会导致内容覆盖还涉及到Delta的数据转换 // 初始化文本编辑器init(data) {// 处理数据(最大程度还原数据)let _T data.replace(/[\r]/g, #r#).replace(/[\n]/g, #n#).replace(/[\t]/g, #t#);let delta JSON.parse(_T);/*** 需要先处理特殊字符不然转不了JSON* 然后再根据特性转回来不然该换行的地方没有换行*/delta.forEach((i, index) {i.insert i.insert.toString().replace(/#n#/g, \n).replace(/#r#/g, \r).replace(/#t#/g, \t);});this.quill.setContents(delta);} 这里有一个小问题哈Emoji表情是不可以直接存再 UTF8的数据库中需要做转换不然报错。 // 表情转码 export const utf16toEntities (str) {const patt /[\ud800-\udbff][\udc00-\udfff]/g; // 检测utf16字符正则str str.replace(patt, (char) {let H;let L;let code;let s;if (char.length 2) {H char.charCodeAt(0); // 取出高位L char.charCodeAt(1); // 取出低位code (H - 0xd800) * 0x400 0x10000 L - 0xdc00; // 转换算法s #${code};;} else {s char;}return s;});return str; }; // 表情解码 export const entitiestoUtf16 (strObj) {const patt /#\d;/g;const arr strObj.match(patt) || [];let H;let L;let code;for (let i 0; i arr.length; i 1) {code arr[i];code code.replace(#, ).replace(;, );// 高位H Math.floor((code - 0x10000) / 0x400) 0xd800;// 低位L ((code - 0x10000) % 0x400) 0xdc00;code #${code};;const s String.fromCharCode(H, L);strObj strObj.replace(code, s);}return strObj; };初始化 socket 服务 socket服务这块我已经讲了很多次了就不细说了不过这次使用的是 room 更贴合房间概念只有同一个编辑文件中才能交流。可以细看代码。 io.on(connection, (socket) {socket.join(room 237);console.log(socket.rooms); // Set { socket.id, room 237 }socket.join([room 237, room 238]);io.to(room 237).emit(a new user has joined the room); // broadcast to everyone in the room }); 实现效果如下 整体效果 可优化点  文件导入、删除、回收站、文档搜索等项目基本上已经是完整的项目了vuenodemysql也有数据存储大家可以继续创作。 总结 从Yjs的应用到Quill编辑器的API介绍算是比较完整的讲述了协同编辑的思想与实现方案同时拓展了MySQL的应用这个项目还是比较不错的大家可以 fork 继续创作最后大家多多支持呀点赞收藏哦
http://www.w-s-a.com/news/612383/

相关文章:

  • 建设部建造师强制注销网站h5响应式网站模板下载
  • 蛋糕网站内容规划建设网站需要多少钱济南兴田德润o厉害吗
  • 企业如何建设网站呢做网站的高手
  • 为什么打开网址都是站长工具开发一款网站需要多少钱
  • 做一个网站app需要多少钱分类信息网站建设计划
  • 怎样下载建设部网站建模培训
  • 北流网站建设制作旅游网站开发目的和目标
  • 网站公司怎么做的网站建设论文二稿
  • 网站建设服务商都有哪些html项目答辩
  • 网站上传到万网主机wordpress视频防盗链
  • 西安建设商城类网站广告设计公司文案
  • 如何建设好高校网站麻辣烫配方教授网站怎么做
  • 宁波网站建设计品牌推广策略分析
  • 网站自建设需要买什么时候开始深圳市建筑市场信息公开平台
  • 平台营销型网站建设小城镇建设的网站文献
  • 燕郊个人做网站小企业网站模板
  • 网站ip需要备案新开河街做网站公司
  • 网站定制设计方案wordpress批量传图片
  • 做外贸兼职的网站设计福州网站开发私人
  • 金华建站模板目前国内有哪些网站做家具回收
  • 个人做网站还是公众号赚钱好部门网站建设和维护
  • 系列图标设计网站推荐建商城网站
  • 中牟建设工程信息网站黑龙江 哈尔滨
  • 网站设计基本结构wap自助建论坛网站
  • 专业番禺网站建设爱做网站外国
  • 深圳罗湖网站设计公司价格制作网站的公司办什么营业执照
  • 长清网站建设价格群辉NAS搭建wordpress
  • 变更股东怎样在工商网站做公示网站建设和网站优化哪个更重要
  • 西安手机网站python网站开发效率
  • 深圳建站的公司羽毛球赛事2022直播