网站怎么在百度搜到,为什么不自己做购物网站,企业站点,百度推广方案前言 感谢大家对 Multi person online edit(多人在线编辑器) 项目的支持#xff0c;mpoe 项目使用 quill、luckysheet、canvas-editor 实现的 md、excel、word 在线协同编辑#xff0c;欢迎大家Fork 代码#xff0c;多多 Start哦~
Multi person online edit 多人协同编辑器…
前言 感谢大家对 Multi person online edit(多人在线编辑器) 项目的支持mpoe 项目使用 quill、luckysheet、canvas-editor 实现的 md、excel、word 在线协同编辑欢迎大家Fork 代码多多 Start哦~
Multi person online edit 多人协同编辑器项目https://gitee.com/wfeng0/mpoe 经过大家反馈和咨询还是对 luckysheet 的协同更加感兴趣但是原项目有些乱有些功能也没有完善因此单独将Luckysheet 抽离成新项目争取实现完整的协同功能。 本项目仅实现luckysheet协同哈使用 sequelize 作为ORM数据库连接方便大家迁移同时也做了兼容没有数据库的用户只是不能持久化数据协同功能不受任何影响。为了规范代码使用 typescript 构建没有使用任何前端框架实现最简单的luckysheet协同增强版。 创建两个实例对象 由于luckysheet是挂载在window上因此同一个页面不能直接创建两个实例对象但是可以通过 iframe 实现 实现效果如下 初始化协同 配置Lucky sheet的协同非常简单 const options {allowUpdate: true, // 配置协同功能loadUrl: /api/loadLuckysheet, // 初始化 celldata 数据updateUrl: WS_SERVER_URL, // 协同服务转发服务// ...other option};
配置 allowUpdate 是否允许操作表格后的后台更新与updateUrl配合使用。如果要开启共享编辑此参数必须设置为true.
配置 loadUrl loadUrl是初始化 celldata 数据的一个http接口请求底层实现是通过post发送请求初始化sheet 数据
$.post(loadurl, {gridKey : server.gridKey}, function (d) {}) 因此需要在服务端创建一个 post 请求的接口处理并返回数据 配置 updateUrl 操作表格后实时保存数据的websocket地址此接口也是共享编辑的接口地址过共享编辑功能可以实现Luckysheet实时保存数据和多人同步数据每一次操作都会发送不同的参数到后台具体的操作类型和参数参见表格操作。
/*** 创建 Web Socket 服务*/
export function createWebSocketServer(port: number) {const wsServer new WebSocketServer({ port });logger.info(ws server is running at: ws://localhost:${port});wsServer.on(connection, (client) {console.log( user connected);client.on(error, console.error);client.on(close, () {});client.on(message, (data) {console.log(received: %s, data);});});
} 进行数据解析根据官网的描述发送给后端的数据默认是经过pako压缩过后的需要进行解析转换为可识别对象操作
/*** Pako 数据解析*/
export function unzip(str: string) {const chartData str.toString().split().map((i) i.charCodeAt(0));const binData new Uint8Array(chartData);const data pako.inflate(binData);return decodeURIComponent(String.fromCharCode(...Array.from(new Uint16Array(data))));
} 解析数据如下 配置协同数据结构 上面的讲述的都是 前台向后台发送数据那么协同服务应该返回什么数据结构给 luckysheet呢 根据 luckysheet/src/controller/server.js 中的返回参数分析协同服务需要按照下列数据返回
/*** 处理广播给其他客户端事件客户端接收服务端要求数据结构* * data: 修改的命令* id: 7a websocket的id* username: 用户名用于显示 xxx 正在编辑* type: * # message 用户退出 用户退出时关闭协同编辑时其提示框* # type 1 send 成功或失败* # type 2 更新数据* # type 3 多人操作不同选区(t: mv)用不同颜色显示其他人所操作的选区* # type 4 批量指令更新* # type 5 showloading* # type 6 hideloading*/if (data exit) return JSON.stringify({ message: 用户退出, id: userid });// 这里仅做 2 3 类型处理其他类型自行拓展哈
const info { data, id: userid, username, type: data.t mv ? 3 : 2 }
return JSON.stringify(info); 配置上诉后即可实现初步协同如下 Sequelize Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。本项目使用其构建意在只需要书写表模型即可完成复杂的 luckysheet数据结构存储。同时还能检测连接状态使得没有数据库的用户也可以体验协同。
class DataBase {private _connected: boolean false; // 连接状态private _sequelize: Sequelize | null null; // 连接对象/*** 初始化数据库*/public init() {// 创建连接const URL mysql://${user}:${password}${host}:${port}/${database};this._sequelize new Sequelize(URL, { logging });// 测试连接this._sequelize.authenticate().then(...).catch(...)}
}
初始化模型 Sequelize 是通过模型进行数据操作的因此我们需要提供对应的模型结构
/*** Worker Books 工作簿模型表*/import { Model, Sequelize } from sequelize;export class WorkerBookModel extends Model {// 通过 declare 定义模型类型declare gridKey: string;declare title: string;declare lang?: string;// 需要向外提供 注册模型的静态方法static registerModule(sequelize: Sequelize) {WorkerBookModel.init(....)}
}
同步模型
Model.sync() - 如果表不存在,则创建该表(如果已经存在,则不执行任何操作)Model.sync({ force: true }) - 将创建表,如果表已经存在,则将其首先删除Model.sync({ alter: true }) - 这将检查数据库中表的当前状态(它具有哪些列,它们的数据类型等),然后在表中进行必要的更改以使其与模型匹配. force: true 会导致表数据丢失请谨慎使用
在这里就不过多介绍 sequelize 相关知识了大家自行查阅文档哈。 协同存储实现 Luckysheet 每一次操作都会保存历史记录用于撤销和重做如果在表格初始化的时候开启了共享编辑功能则会通过websocket将操作实时更新到后台。因此我们根据传递到后台的操作类型更新数据库状态不就实现了协同存储了嘛。 单个单元格刷新
async function v(data: string) {// 1. 解析 rc 单元格const { t, r, c, v, i } OperateDataJSON.parse(data);logger.info([CRDT DATA]:, data);// 纠错判断if (t ! v) return logger.error(t is not v.);if (isEmpty(i)) return logger.error(i is undefined.);if (isEmpty(r) || isEmpty(c)) return logger.error(r or c is undefined.);// 场景一单个单元格插入值if (v v.v v.m) {// 判断表内是否存在当前记录const exist await CellDataService.hasCellData(i, r, c);if(exist) CellDataService.updateCellData() else CellDataService.createCellData()}// 场景二剪切/粘贴到某个单元格 - 会触发两次广播if (v null) {// 删除该记录await CellDataService.deleteCellData(i, r, c);}// 场景三 删除单元格内容if (v !v.v !v.m){// 删除记录await CellDataService.deleteCellData(i, r, c);}
} 范围单元格刷新 上诉是一个标准的范围单元格协同消息我们需要根据 range row column 和 v 的数组循环处理每一条数据项 // 循环列取 v 的内容然后创建记录for (let index 0; index v.length; index) {// 这里面的每一项都是一条记录for (let j 0; j v[index].length; j) {// 解析内部的 r c 值const item v[index][j];const r range.row[0] index;const c range.column[0] j;// 根据 r c 存储数据}} 隐藏行/列 行高/列宽处理 行高列宽及隐藏行列均触发在 tcg 中 边框及合并单元格处理
// k borderInfo 边框处理// {t:cg,i:e73f971d606...,v:[{rangeType:range,borderType:border-all,color:#000,style:1,range:[{row:[0,0],column:[0,0],row_focus:0,column_focus:0,left:0,width:73,top:0,height:19,left_move:0,width_move:73,top_move:0,height_move:19}]}],k:borderInfo}// {t:cg,i:e73f971d......,v:[{rangeType:range,borderType:border-all,color:#000,style:1,range:[{row:[2,7],column:[1,2],row_focus:2,column_focus:1,left:74,width:73,top:40,height:19,left_move:74,width_move:147,top_move:40,height_move:119,}]}],k:borderInfo}// {t:cg,i:e73f971d......,v:[{rangeType:range,borderType:border-bottom,color:#000,style:1,range:[{left:148,width:73,top:260,height:19,left_move:148,width_move:73,top_move:260,height_move:19,row:[13,13],column:[2,2],row_focus:13,column_focus:2}]}],k:borderInfo}if (k borderInfo) {// 处理 rangeTypefor (let idx 0; idx borderInfo.length; idx) {const border borderInfo[idx];const { rangeType, borderType, color, style, range } border;// 这里能拿到 i range 判断是否存在// declare row_start?: number;// declare row_end?: number;// declare col_start?: number;// declare col_end?: number;const info: ConfigBorderModelType {worker_sheet_id: i,rangeType,borderType,row_start: range[0].row[0],row_end: range[0].row[1],col_start: range[0].column[0],col_end: range[0].column[1],};const exist await ConfigBorderService.hasConfigBorder(info);if (exist) {// 更新await ConfigBorderService.updateConfigBorder({config_border_id: exist.config_border_id,...info,color,style: Number(style),});} else {// 创建新的边框记录await ConfigBorderService.createConfigBorder({...info,style: Number(style),color,});}}} 合并单元格的处理可能麻烦些 // 合并单元格 - 又是一个先删除后新增的操作由luckysheet 前台设计决定的// {t:all,i:e73f971....,v:{merge:{1_0:{r:1,c:0,rs:3,cs:3}},},k:config}// {t:all,i:e73f971....,v:{merge:{1_0:{r:1,c:0,rs:3,cs:3},9_1:{r:9,c:1,rs:5,cs:3}},},k:config}// {t:all,i:e73f971....,v:{merge:{9_1:{r:9,c:1,rs:5,cs:3}},},k:config}// 先删除await ConfigMergeService.deleteMerge(i);// 再新增for (const key in v.merge) {if (Object.prototype.hasOwnProperty.call(v.merge, key)) {const { r, c, rs, cs } v.merge[key];await ConfigMergeService.createMerge({worker_sheet_id: i,r,c,rs,cs,});}}
获取数据的时候需要处理两个地方 config 及 celldata /* eslint-disable */// 4. 查询 merge 数据 - 这里不仅要体现在 config 中还要体现在 celldata.mc 中const merges await ConfigMergeService.findAll(worker_sheet_id);merges?.forEach((merge) {// 拼接 r_c 格式const { r, c } merge.dataValues;// ts-ignoretemp.config.merge[${r}_${c}] merge.dataValues;// 配置 celldata mc 属性const currentMergeCell temp.celldata.find(// ts-ignore(i) i.r r i.c c);// ts-ignoreif (currentMergeCell) currentMergeCell.v.mc merge.dataValues;}); 图片及统计图处理 这块内容还有些前台的东西需要二开后面会同步更新 git 大家关注下仓库start 下。
luckysheet-crdt: Luckysheet 协同增强版全功能实现https://gitee.com/wfeng0/luckysheet-crdt 图片上传需要使用到两个新的 APIuploadImage、imageUrlHandle默认情况下插入的图片是以base64的形式放入sheet数据中但是图片放入 sheet 中进行协同传输会导致node 解析数据堆栈溢出因此需要自定义图片上传方法 // 处理协同图片上传uploadImage: async (file: File) {// 此处拿到的是上传的 file 对象进行文件上传 配合 node 接口实现const formData new FormData();formData.append(image, file);const { data } await fetch({url: /api/uploadImage,method: POST,data: formData,});// *** 关键步骤需要返回一个地址给 luckysheet 用于显示图片if (data.code 200) return Promise.resolve(data.url);else return Promise.resolve(image upload error);}, 看大家的接口设计哈如果直接返回能访问的服务器路径其实不用第二个接口也能实现这里就都简单介绍一下 // 处理上传图片的地址imageUrlHandle: (url: string) {// 已经是 // http data 开头则不处理if (/^(?:\/\/|(?:http|https|data):)/i.test(url)) {return url;}// 不然拼接服务器路径return SERVER_URL url;}, 在协同存储上处理如下 查询数据库并处理为 luckysheet 初始化数据类型 即可实现图片协同存储 统计图的后面再更新哈还在研究中~
总结
1. luckysheet 的协同并不难很多东西源码底层已经封装好了我们只需要按照官网说明处理响应的操作即可
2. 当然库还有些没有完善的功能需要大家自行拓展
3. 后续会持续更新关注大家的需求也会考虑封装一个 npm 包提供给大家下载即用
4. 大家多多start 支持呀~这样才有动力更新哦