Luckysheet 实现excel多人在线协同编辑

前言

        前些天看到Luckysheet支持协同编辑Excel,正符合我们协同项目的一部分,故而想进一步完善协同文章,但是遇到了一下困难,特此做声明哈,若侵权,请联系我删除文章!

        若侵犯版权、个人隐私,请联系删除哈!!!(我可不想踩缝纫机)

        Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。当然,也原生支持协同,下面,我们针对协同部分做详细讲解。官网使用的是Java,也有协同的Demo,我就不说了,下面用 Node 实现协同,完整的样例如下,我们开始吧

Luckysheet 基础使用

引入依赖

CDN

<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script>

本地打包

Luckysheet: 🚀Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。icon-default.png?t=N7T8https://gitee.com/mengshukeji/Luckysheet        官网建议我们在上网址下载完整的包,这样,我们得到的是luckysheet的源码,可以进行二次开发。很重要哈,最后我们也会这样做。

npm i --s  // 执行 npm 命令,进行依赖包的下载

npm run build  // 执行打包命令(二次开发是需要修改源码的)

         把dist包放到自己的项目中,我已经更名了哈:

        然后,index.html 直接引入这个地址的文件就行了(二开一定是引这个地址哈)。 

     <!-- 引入 luck Sheet 二次开发地址  就是你刚才 build 的那个 dist 包 -->
    <link rel='stylesheet' href='./luckysheet/dist/plugins/css/pluginsCss.css' />
    <link rel='stylesheet' href='./luckysheet/dist/plugins/plugins.css' />
    <link rel='stylesheet' href='./luckysheet/dist/css/luckysheet.css' />
    <link rel='stylesheet' href='./luckysheet/dist/assets/iconfont/iconfont.css' />

    <script src="./luckysheet/dist/plugins/js/plugin.js"></script>
    <script src="./luckysheet/dist/luckysheet.umd.js"></script>

        这个方式建议大家都试试,二次开发一定是这个方式哈!

npm

        如果大家觉得不用二开,就是用原生的功能 ,那直接使用 npm 下载就行了。

npm i luckysheet

    <link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/css/pluginsCss.css' />
    <link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/plugins.css' />
    <link rel='stylesheet' href='./node_modules/luckysheet/dist/css/luckysheet.css' />
    <link rel='stylesheet' href='./node_modules/luckysheet/dist/assets/iconfont/iconfont.css' />
    <script src="./node_modules/luckysheet/dist/plugins/js/plugin.js"></script>
    <script src="./node_modules/luckysheet/dist/luckysheet.umd.js"></script>

初始化

指定容器

<div id="luckysheet" style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;"></div>

创建表格

onMounted(() => {
  // 初始化表格
  var options = {
    container: "luckysheet", //luckysheet为容器id
  };
  luckysheet.create(options);
});

         这样就已经是一个完善的表格编辑器了,支持函数、图表、填充等多项功能。

协同编辑

        因此,我们分别配置这几个参数:

loadUrl

        配置loadUrl接口地址,加载所有工作表的配置,并包含当前页单元格数据,与loadSheetUrl配合使用。参数为gridKey(表格主键)

$.post(loadurl, {"gridKey" : server.gridKey}, function (d) {})

        源码写法如上,因此,我们需要创建一个 post请求的地址:

 app.use("/excel", excelRouter); // 添加公共前缀

         配置 loadUrl,加了 baseURL是做了请求代理哈

 allowUpdate: true,
 loadUrl: "/baseURL/excel",

        接口要求返回以下数据,我们直接复制,然后返回:

"[	
	//status为1的sheet页,重点是需要提供初始化的数据celldata
	{
		"name": "Cell",
		"index": "sheet_01",
		"order":  0,
		"status": 1,
		"celldata": [{"r":0,"c":0,"v":{"v":1,"m":"1","ct":{"fa":"General","t":"n"}}}]
	},
	//其他status为0的sheet页,无需提供celldata,只需要配置项即可
	{
		"name": "Data",
		"index": "sheet_02",
		"order":  1,
		"status": 0
	},
	{
		"name": "Picture",
		"index": "sheet_03",
		"order":  2,
		"status": 0
	}
]"

         本例中,只返回一个sheet表,初始化 0 0 单元格内容为 ‘默认数据’

router.post("/", (req, res, next) => {
  //   console.log("lucySheet");
  let sheetData = [
    //status为1的sheet页,重点是需要提供初始化的数据celldata
    {
      name: "Cell",
      index: "sheet_01",
      order: 0,
      status: 1,
      celldata: [
        {
          r: 0,
          c: 0,
          v: { v: "默认数据", m: "111", ct: { fa: "General", t: "n" } },
        },
      ],
    },
  ];
  res.json(JSON.stringify(sheetData));
});

updateUrl

        操作表格后,实时保存数据的websocket地址,此接口也是共享编辑的接口地址。注意,发送给后端的数据默认是经过pako压缩过后的。后台拿到数据需要先解压。通过共享编辑功能,可以实现Luckysheet实时保存数据和多人同步数据,每一次操作都会发送不同的参数到后台

因此,我们需要初始化一个 ws 连接:

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("用户连接");
  });
};

        打开控制台,可以看到连接成功的提示,我们可以一下源码是怎么处理的:

        除了看到输出语句外,我们更应该关注一个 send 事件,因为 websocket 是通过send 发送数据的,还有的是pako.gzip()压缩。因此,服务端监听 message 获取数据:

 至此,我们可以获取一些基础信息:

  1.  每次操作都会发送 send 事件;
  2. 每次发送的数据都经过 pako.gzip 压缩
  3. node 获取的都是 buffer 数据

        也就是这样,我也不知道如何进行下去了,就加了官方的微信,就发生了篇头的那张截图。但是革命还在继续。加了官网微信群,特此感谢【小李飞刀刀】的指导。

解析Buffer

const pako = require("pako");

/**
 * @DESC 导出解压方法
 * @param { string } str
 * @returns
 */
exports.unzip = (str) => {
  let chartData = str
    .toString()
    .split("")
    .map((i) => i.charCodeAt(0));

  let binData = new Uint8Array(chartData);

  let data = pako.inflate(binData);

  return decodeURIComponent(
    String.fromCharCode.apply(null, new Uint16Array(data))
  );
};

        得到上图,就知道该怎么办了吧,映射的是用户的所有操作哈。需要添加用户标记

    let id = Math.random().toString().split(".")[1].slice(0, 3);

    // 需要添加自定义属性
    ws.wid = id;

    ws.wname = "user_" + id;

处理用户光标

        我们一定要看源码是如何处理的哈,官网文档并没有那么详细:

        因此,同步光标的时候,我们应该发送type =3 的数据,我们封装ws的事件响应中心:

// wss.clients 所有的客户端
wss.clients.forEach((conn) => {
  // 不发送给自己
  if (conn.wid === ws.wid) return;
  // 使得 this 指向当前连接对象
  wshandle.call(conn, unzip(data));
});

        我们还没做数据同步哈,因此数据没有显示,不影响,先显示用户光标。

同步数据

/**
 * ws 事件响应中心
 *  根据不同的事件,返回不同的数据
 *  type 1 成功/失败
 *  type 2 更新数据
 *  type 3 用户光标
 *  type 4 批量处理数据
 */
function wshandle(data) {
  // 表示用户移动鼠标 实际是需要根据指令实现不同的响应的,但是这里统一做 更新数据
  this.send(callbackdata.call(this, data, JSON.parse(data).t === "mv" ? 3 : 2));
}

        至此,协同好像已经实现了,但是还没完。

用户退出

        源码中需要返回 {message ,id} 两个数据,因此直接封装 退出函数:

/**
 * 用户退出
 */
function exit() {
  this.send(JSON.stringify({ message: "用户退出", id: this.wid }));
}

        监听ws close 事件:

 ws.on("close", (ws) => {
      try {
        // 实现用户退出
        wss.clients.forEach((conn) => {
          if (conn.wid === ws.wid) return;
          // 使得 this 指向当前连接对象
          exit.call(conn);
        });
      } catch (error) {
        console.log(error);
      }
    });

BUG修复

        不知道大家发现没有,当多人协作时,我们的用户id 是错的,原因是我们move时,传的参数不对:

// 使得 this 指向当前连接对象 ,并且保证,操作对象始终是当前用户
wshandle.call(conn, { id: ws.wid, name: ws.wname }, unzip(data));

// 表示用户移动鼠标 实际是需要根据指令实现不同的响应的,但是这里统一做 更新数据
// 手动传输 user
this.send(callbackdata(user, data, JSON.parse(data).t === "mv" ? 3 : 2));

// function callback:

return JSON.stringify({
    createTime: dayjs().format("YYYYMMHH mm:hh:ss"),
    data,
    id: user.id,
    returnMessage: "success",
    status: 0,
    type,
    username: user.name,
  });

数据库存储

全量存储

        表格操作完成后,使用luckysheet.getAllSheets()方法获取到全部的工作表数据,全部发送到后台存储。

协同存储

        协同存储就是用户的每次操作,都会触发 websocket,因此,我们直接在websocket中调用控制层,实现数据的更新,举例说明:

[
    {
        "data":[], // 每个工作表参数组成的一维数组
        "name": "Cell", //工作表名称
        "color": "", //工作表颜色
        "index": 0, //工作表索引
        "status": 1, //激活状态
        "order": 0, //工作表的下标
        "hide": 0,//是否隐藏
        "row": 36, //行数
        "column": 18, //列数
        "defaultRowHeight": 19, //自定义行高
        "defaultColWidth": 73, //自定义列宽
        "celldata": [], //初始化使用的单元格数据
        "config": {
            "merge":{}, //合并单元格
            "rowlen":{}, //表格行高
            "columnlen":{}, //表格列宽
            "rowhidden":{}, //隐藏行
            "colhidden":{}, //隐藏列
            "borderInfo":{}, //边框
            "authority":{}, //工作表保护
            
        },
        "scrollLeft": 0, //左右滚动条位置
        "scrollTop": 315, //上下滚动条位置
        "luckysheet_select_save": [], //选中的区域
        "calcChain": [],//公式链
        "isPivotTable":false,//是否数据透视表
        "pivotTable":{},//数据透视表设置
        "filter_select": {},//筛选范围
        "filter": null,//筛选配置
        "luckysheet_alternateformat_save": [], //交替颜色
        "luckysheet_alternateformat_save_modelCustom": [], //自定义交替颜色	
        "luckysheet_conditionformat_save": {},//条件格式
        "frozen": {}, //冻结行列配置
        "chart": [], //图表配置
        "zoomRatio":1, // 缩放比例
        "image":[], //图片
        "showGridLines": 1, //是否显示网格线
        "dataVerification":{} //数据验证配置
    },
   // ... 其他 sheet 页数据与上类似
]

        上是整个sheet的配置项,数据库表可以根据这个来构建,数据表单独分开、样式表也单独分开,还有基础配置表:

        这样就不用存储很多无效的数据,能实现对某一条数据的精确控制与存储,节省数据库存储空间。

文件导入

        两种方式实现哈,先隐藏默认,然后自定定位实现添加按钮,或者根据配置项实现配置

/deep/.luckysheet_info_detail_save,
/deep/.luckysheet_info_detail_update {
  display: none;
}

npm i luckyexcel

         绑定了一个 input ref='importFileRef'

const importFileHandle = (e) => {
  let { files } = e.target;
  LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) => {
    luckysheet.create({
      container: "luckysheet", // luckysheet is the container id
      data: exportJson.sheets,
      title: exportJson.info.name,
      userInfo: exportJson.info.name.creator,
    });
    // 清空
    importFileRef.value.value = "";
  });
};

         但是这样会丢失协同性:

// 文件导入
const importFileHandle = (e) => {
  let { files } = e.target;
  LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) => {
    // 【会丢失协同性】
    // luckysheet.create({
    //   container: "luckysheet", // luckysheet is the container id
    //   data: exportJson.sheets,
    //   title: exportJson.info.name,
    //   userInfo: exportJson.info.name.creator,
    // });

    let { info, sheets } = exportJson;

    luckysheet.setWorkbookName(info.name);

    sheets.forEach((sheet) => {
      // sheet 便是每一个 sheet 页,需要根据实际的数量动态创建
      luckysheet.setSheetAdd({
        sheetObject: sheet,
      });
    });
    // 清空
    importFileRef.value.value = "";
  });
};

文件导出

npm i exceljs file-saver

import Excel from "exceljs";

import FileSaver from "file-saver";

import { ElMessage } from "element-plus";

export const exportExcel = async (name, luckysheet) => {
  // 获取 buffer
  let buffer = await getBuffer(luckysheet);
  download(name, buffer);
};

/**
 *  使用 fileSaver 进行文件保存操作
 * @param {Buffer} buffer
 */
function download(name, buffer) {
  try {
    const blob = new Blob([buffer], {
      type: "application/vnd.ms-excel;charset=utf-8",
    });
    FileSaver.saveAs(blob, `${name}.xlsx`);
    ElMessage.success("文件导出成功");
  } catch (error) {
    ElMessage.error("文件导出失败");
  }
}

/**
 *
 * @param { Array as luckysheet.getluckysheetfile() } luckysheet
 * @returns
 */
async function getBuffer(luckysheet) {
  // 参数为luckysheet.getluckysheetfile()获取的对象
  // 1.创建工作簿,可以为工作簿添加属性
  const workbook = new Excel.Workbook();
  // 2.创建表格,第二个参数可以配置创建什么样的工作表
  luckysheet.every(function (table) {
    if (table.data.length === 0) return true;
    const worksheet = workbook.addWorksheet(table.name);
    // 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
    setStyleAndValue(table.data, worksheet);
    setMerge(table.config.merge, worksheet);
    setBorder(table.config.borderInfo, worksheet);
    return true;
  });
  // 4.写入 buffer
  const buffer = await workbook.xlsx.writeBuffer();
  return buffer;
}

var setMerge = function (luckyMerge = {}, worksheet) {
  const mergearr = Object.values(luckyMerge);
  mergearr.forEach(function (elem) {
    // elem格式:{r: 0, c: 0, rs: 1, cs: 2}
    // 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
    worksheet.mergeCells(
      elem.r + 1,
      elem.c + 1,
      elem.r + elem.rs,
      elem.c + elem.cs
    );
  });
};

var setBorder = function (luckyBorderInfo, worksheet) {
  if (!Array.isArray(luckyBorderInfo)) {
    return;
  }
  // console.log('luckyBorderInfo', luckyBorderInfo)
  luckyBorderInfo.forEach(function (elem) {
    // 现在只兼容到borderType 为range的情况
    // console.log('ele', elem)
    if (elem.rangeType === "range") {
      let border = borderConvert(elem.borderType, elem.style, elem.color);
      let rang = elem.range[0];
      // console.log('range', rang)
      let row = rang.row;
      let column = rang.column;
      for (let i = row[0] + 1; i < row[1] + 2; i++) {
        for (let y = column[0] + 1; y < column[1] + 2; y++) {
          worksheet.getCell(i, y).border = border;
        }
      }
    }
    if (elem.rangeType === "cell") {
      // col_index: 2
      // row_index: 1
      // b: {
      //   color: '#d0d4e3'
      //   style: 1
      // }
      const { col_index, row_index } = elem.value;
      const borderData = Object.assign({}, elem.value);
      delete borderData.col_index;
      delete borderData.row_index;
      let border = addborderToCell(borderData, row_index, col_index);
      // console.log('bordre', border, borderData)
      worksheet.getCell(row_index + 1, col_index + 1).border = border;
    }
    // console.log(rang.column_focus + 1, rang.row_focus + 1)
    // worksheet.getCell(rang.row_focus + 1, rang.column_focus + 1).border = border
  });
};
var setStyleAndValue = function (cellArr, worksheet) {
  if (!Array.isArray(cellArr)) {
    return;
  }
  cellArr.forEach(function (row, rowid) {
    // const dbrow = worksheet.getRow(rowid+1);
    // //设置单元格行高,默认乘以1.2倍
    // dbrow.height=luckysheet.getRowHeight([rowid])[rowid]*1.2;
    row.every(function (cell, columnid) {
      if (rowid == 0) {
        const dobCol = worksheet.getColumn(columnid + 1);
        //设置单元格列宽除以8
        dobCol.width = luckysheet.getColumnWidth([columnid])[columnid] / 8;
      }
      if (!cell) {
        return true;
      }
      //设置背景色
      let bg = cell.bg || "#FFFFFF"; //默认white
      bg = bg === "yellow" ? "FFFF00" : bg.replace("#", "");
      let fill = {
        type: "pattern",
        pattern: "solid",
        fgColor: { argb: bg },
      };
      let font = fontConvert(
        cell.ff,
        cell.fc,
        cell.bl,
        cell.it,
        cell.fs,
        cell.cl,
        cell.ul
      );
      let alignment = alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr);
      let value = "";

      if (cell.f) {
        value = { formula: cell.f, result: cell.v };
      } else if (!cell.v && cell.ct && cell.ct.s) {
        // xls转为xlsx之后,内部存在不同的格式,都会进到富文本里,即值不存在与cell.v,而是存在于cell.ct.s之后
        // value = cell.ct.s[0].v
        cell.ct.s.forEach((arr) => {
          value += arr.v;
        });
      } else {
        value = cell.v;
      }
      //  style 填入到_value中可以实现填充色
      let letter = createCellPos(columnid);
      let target = worksheet.getCell(letter + (rowid + 1));
      // console.log('1233', letter + (rowid + 1))
      for (const key in fill) {
        target.fill = fill;
        break;
      }
      target.font = font;
      target.alignment = alignment;
      target.value = value;

      return true;
    });
  });
};

var fontConvert = function (
  ff = 0,
  fc = "#000000",
  bl = 0,
  it = 0,
  fs = 10,
  cl = 0,
  ul = 0
) {
  // luckysheet:ff(样式), fc(颜色), bl(粗体), it(斜体), fs(大小), cl(删除线), ul(下划线)
  const luckyToExcel = {
    0: "微软雅黑",
    1: "宋体(Song)",
    2: "黑体(ST Heiti)",
    3: "楷体(ST Kaiti)",
    4: "仿宋(ST FangSong)",
    5: "新宋体(ST Song)",
    6: "华文新魏",
    7: "华文行楷",
    8: "华文隶书",
    9: "Arial",
    10: "Times New Roman ",
    11: "Tahoma ",
    12: "Verdana",
    num2bl: function (num) {
      return num === 0 ? false : true;
    },
  };
  // 出现Bug,导入的时候ff为luckyToExcel的val

  //设置字体颜色
  fc = fc === "red" ? "FFFF0000" : fc.replace("#", "");
  let font = {
    name: typeof ff === "number" ? luckyToExcel[ff] : ff,
    family: 1,
    size: fs,
    color: { argb: fc },
    bold: luckyToExcel.num2bl(bl),
    italic: luckyToExcel.num2bl(it),
    underline: luckyToExcel.num2bl(ul),
    strike: luckyToExcel.num2bl(cl),
  };

  return font;
};

var alignmentConvert = function (
  vt = "default",
  ht = "default",
  tb = "default",
  tr = "default"
) {
  // luckysheet:vt(垂直), ht(水平), tb(换行), tr(旋转)
  const luckyToExcel = {
    vertical: {
      0: "middle",
      1: "top",
      2: "bottom",
      default: "top",
    },
    horizontal: {
      0: "center",
      1: "left",
      2: "right",
      default: "left",
    },
    wrapText: {
      0: false,
      1: false,
      2: true,
      default: false,
    },
    textRotation: {
      0: 0,
      1: 45,
      2: -45,
      3: "vertical",
      4: 90,
      5: -90,
      default: 0,
    },
  };

  let alignment = {
    vertical: luckyToExcel.vertical[vt],
    horizontal: luckyToExcel.horizontal[ht],
    wrapText: luckyToExcel.wrapText[tb],
    textRotation: luckyToExcel.textRotation[tr],
  };
  return alignment;
};

var borderConvert = function (borderType, style = 1, color = "#000") {
  // 对应luckysheet的config中borderinfo的的参数
  if (!borderType) {
    return {};
  }
  const luckyToExcel = {
    type: {
      "border-all": "all",
      "border-top": "top",
      "border-right": "right",
      "border-bottom": "bottom",
      "border-left": "left",
    },
    style: {
      0: "none",
      1: "thin",
      2: "hair",
      3: "dotted",
      4: "dashDot", // 'Dashed',
      5: "dashDot",
      6: "dashDotDot",
      7: "double",
      8: "medium",
      9: "mediumDashed",
      10: "mediumDashDot",
      11: "mediumDashDotDot",
      12: "slantDashDot",
      13: "thick",
    },
  };
  let template = {
    style: luckyToExcel.style[style],
    color: { argb: color.replace("#", "") },
  };
  let border = {};
  if (luckyToExcel.type[borderType] === "all") {
    border["top"] = template;
    border["right"] = template;
    border["bottom"] = template;
    border["left"] = template;
  } else {
    border[luckyToExcel.type[borderType]] = template;
  }
  // console.log('border', border)
  return border;
};

function addborderToCell(borders, row_index, col_index) {
  let border = {};
  const luckyExcel = {
    type: {
      l: "left",
      r: "right",
      b: "bottom",
      t: "top",
    },
    style: {
      0: "none",
      1: "thin",
      2: "hair",
      3: "dotted",
      4: "dashDot", // 'Dashed',
      5: "dashDot",
      6: "dashDotDot",
      7: "double",
      8: "medium",
      9: "mediumDashed",
      10: "mediumDashDot",
      11: "mediumDashDotDot",
      12: "slantDashDot",
      13: "thick",
    },
  };
  // console.log('borders', borders)
  for (const bor in borders) {
    // console.log(bor)
    if (borders[bor].color.indexOf("rgb") === -1) {
      border[luckyExcel.type[bor]] = {
        style: luckyExcel.style[borders[bor].style],
        color: { argb: borders[bor].color.replace("#", "") },
      };
    } else {
      border[luckyExcel.type[bor]] = {
        style: luckyExcel.style[borders[bor].style],
        color: { argb: borders[bor].color },
      };
    }
  }

  return border;
}

function createCellPos(n) {
  let ordA = "A".charCodeAt(0);

  let ordZ = "Z".charCodeAt(0);
  let len = ordZ - ordA + 1;
  let s = "";
  while (n >= 0) {
    s = String.fromCharCode((n % len) + ordA) + s;

    n = Math.floor(n / len) - 1;
  }
  return s;
}

关联文件

       在excel协同的时候,还需要跟我们quill编辑器类似,绑定fileid:

updateUrl:

      "ws://localhost:9000?fileid=" + router.currentRoute.value.params.fileid, // 实现传参,

        二开实现websocket的关闭连接:

// 源码中 server.js 添加方法
closeWebSocket: function () {
    let _this = this;
    if ("WebSocket" in window) {
      _this.websocket.close();
    } else console.error("## closeWebSocket", locale().websocket.support);
  },

global.api(api.js 文件)
/**
 * 导出 websocket 的关闭方法:
 * luckysheet.wsclose() 进行调用
 */
export function wsclose() {
  console.log('调用自定义方法 server.closeWebSocket()')
  server.closeWebSocket();
}

        重新打包,在需要的地方进行调用:

但是每次关闭连接后,都会alert,把这个关了:

        与文件关联后,不是同一个文件的不能协同编辑。

总结

        到此,功能都已经开发完了。还是那句话哈:

        如果侵权了,请联系删除!

        如果侵权了,请联系删除!

        如果侵权了,请联系删除!

        对luckysheet的协同做一下总结吧:

  1. 对pako压缩数据进行解析,这是第一个难点;
  2. 数据存储按照分布式存储会更快;这里是结合着 loadUrl的哈,后端返回保存后的数据进行渲染;
  3. luckyexcel 进行文件导入;
  4. exceljs file-saver 实现文件导出;
  5. 对源码进行二次开发,实现手动关闭 websocket 连接;
  6. 还有很多细节哈,大家根据需要可以自行定义,有问题欢迎留言讨论。

制作不易,点赞收藏~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/121529.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图及谱聚类商圈聚类中的应用

背景 在O2O业务场景中&#xff0c;有商圈的概念&#xff0c;商圈是业务运营的单元&#xff0c;有对应的商户BD负责人以及配送运力负责任。这些商圈通常是一定地理围栏构成的区域&#xff0c;区域内包括商户和用户&#xff0c;商圈和商圈之间就通常以道路、河流等围栏进行分隔。…

酷开科技持续推动智能投影行业创新发展

近年来&#xff0c;投影仪逐渐成为年轻人追捧的家居时尚单品。据国际数据公司&#xff08;IDC&#xff09;报告显示&#xff0c;2022年中国投影机市场总出货量505万台&#xff0c;超80%为家用投影仪。相比于电视&#xff0c;投影仪外观小巧、屏幕大小可调节&#xff0c;无论是卧…

PostgreSql中解析JSON字段和解析TEXT中的JSON字段

初始化操作 创建表 CREATE TABLE orders ( "ID" int8 NOT NULL,"info_j" json NOT NULL,"info_t" text NOT NULL );初始化表 INSERT INTO orders("ID", "info_j","info_t") VALUES (1, {"name":&qu…

setViaGenMode

1.命令描述 setViaGenMode用于设置vias的全局变量&#xff0c;包括使用addRing / addStripe命令连接rings 、stripes&#xff0c;editPowerVia、sroute、addSplitPowerVia以及手拉线使用的editAddRoute/editCommitRoute。 2.-optimize_cross_via true false 未完待续

人大金仓三大兼容:SQL Server迁移无忧

SQL Server在数据库领域一直占据着重要地位。作为一款成熟稳定的关系型数据库管理系统&#xff0c;SQL Server在国内有着广泛的用户群体&#xff0c;医疗、海关、政务等行业的核心业务系统多采用SQL Server数据库。随着政策与市场的双重驱动&#xff0c;信息技术应用创新产业的…

Spring RabbitMQ那些事(1-交换机配置消息发送订阅实操)

这里写目录标题 一、序言二、配置文件application.yml三、RabbitMQ交换机和队列配置1、定义4个队列2、定义Fanout交换机和队列绑定关系2、定义Direct交换机和队列绑定关系3、定义Topic交换机和队列绑定关系4、定义Header交换机和队列绑定关系 四、RabbitMQ消费者配置五、Rabbit…

C语言面试

数据类型&#xff08;基本内置类型&#xff09; char //字符数据类型 short //短整型 int //整型 long //长整型 long long //更长的整型 float //单精度浮点数 double //双精度浮点数 类型的基本归类 整形家族&#xff1a; …

英伟达发布RAPIDS cuDF框架 pandas在GPU上运行速度快了150倍

11月9日 消息&#xff1a;Nvidia 发布了一款名为 RAPIDS cuDF 的新版本&#xff0c;据称可以将 pandas 运行在 GPU 上&#xff0c;并且性能提升了150倍。pandas 是一款流行的基于 Python 的数据框架库&#xff0c;用于数据处理和分析。它的开源版本由 Wes McKinney 开发和发布&…

RT-Thread提供的网络世界入口 -net组件

作为一款在RTOS领域对网络支持很丰富的RT-Thread&#xff0c;对设备联网功能的支持的工具就是net组件。 位于/rt-thread/components/net路劲下&#xff0c;作为一款基础组件&#xff0c;env与Studio的工程配置项界面的配置项都依赖该目录下的Kconfig。 我们对网络功能的选择&am…

关于卷积神经网络的步幅(stride)

认识步幅&#xff08;stride&#xff09; 卷积核从输入数组的最左上方开始&#xff0c;按从左往右、从上往下的顺序&#xff0c;依次在输入数组上滑动&#xff0c;我们将每次滑动的行数和列数称为步幅。 计算步幅 假设输入的形状n∗n&#xff0c;卷积核的形状为f∗f&#xff0…

css排版—— 一篇优雅的文章(中英文) vs 聊天框的特别排版

文章 <div class"contentBox"><p>这是一篇范文——仅供测试使用</p><p>With the coming of national day, I have a one week holiday. I reallyexpect to it, because it want to have a short trip during these days. Iwill travel to Ji…

机器学习模板代码(期末考试复习)自用存档

机器学习复习代码 利用sklearn实现knn import numpy as np import pandas as pd from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import GridSearchCVdef model_selection(x_train, y_train):## 第一个是网格搜索## p是选择查找方式:1是欧…

Vue3 + Vite + Ts + Router搭建项目

1、新建文件夹 从新建的文件夹cmd进入终端 2、安装vite—依据vite创建vue3项目 2.1、运行 npm init vitelatest2.2.1、输入项目名称 2.2.2、选择vue 2.2.3、选择TypeScript语言 3、安装依赖项 3.1、进入刚才创建的文件夹 cd vite-project 3.2、查看镜像 #查看当前源 npm con…

【uniapp+vue3/vue2】ksp-cropper高性能图片裁剪工具,详解

效果图&#xff1a; 1、ksp-cropper是hbuilder插件市场中的一款插件&#xff0c;兼容vue2和vue3 ksp-cropper插件安装地址&#xff0c;直接点击跳转 2、插件用法相对简单 &#xff08;1&#xff09;只要url有值就会显示插件&#xff0c;为空就会隐藏插件 &#xff08;2&#…

自动化测试框架 —— pytest框架入门篇

今天就给大家说一说pytest框架。 今天这篇文章呢&#xff0c;会从以下几个方面来介绍&#xff1a; 1、首先介绍一下pytest框架 2、带大家安装Pytest框架 3、使用pytest框架时需要注意的点 4、pytest的运行方式 5、pytest框架中常用的插件 01、pytest框架介绍 pytest 是 pytho…

国产猫罐头可以长期作为主食吗?我家的优质TOP的猫罐头分享

我最近一直在调查国产猫罐头可以长期作为主食吗&#xff1f;看看我的购物订单&#xff0c;我已经尝试了几十款了。今天&#xff0c;我想和大家分享一些关于国产猫罐头的经验和见解。 近年来&#xff0c;国产宠粮市场取得了突破性的进展&#xff0c;各个猫粮商在配方、营养数据…

win10添加回环网卡步骤

打开命令行输入hdwwiz 添加新硬件向导 结果

Visual Studio 2022 + OpenCV 4.5.2 安装与配置教程

目录 OpenCV的下载与配置Visual Studio 2022的配置新建工程新建文件新建项目属性表环境配置测试先写一个输出将OpenCV的动态链接库添加到项目的 x64 | Debug下测试配置效果 Other OpenCV的下载与配置 参考这个OpenCV的下载与环境变量的配置&#xff1a; Windows10CLionOpenCV4…

CUDA学习笔记7——CUDA内存组织

CUDA内存组织 CUDA设备内存的分类与特征 内存类型物理位置访问权限可见范围生命周期1全局内存芯片外可读写所有线程和主机端由主机分配与释放2常量内存芯片外只读所有线程和主机端由主机分配与释放3纹理和表面内存芯片外一般只读所有线程和主机端由主机分配与释放4寄存器内存…

OpenSSL生成自签名证书

生成之前首先需要明白以下内容&#xff1a; 第三点的验证数字签名解释下&#xff1a;客户端将使用颁发机构的公钥解密得到的原始数据&#xff0c;再将原始数据通过哈希算法计算得到的哈希值&#xff08;此处应该是使用CA证书提供的哈希算法&#xff09;进行比对。如果两者一致&…