前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >十几分钟写个实时协作表格

十几分钟写个实时协作表格

作者头像
治电小白菜
发布2021-03-02 15:13:51
6370
发布2021-03-02 15:13:51
举报
文章被收录于专栏:技术综合技术综合

最近在使用jexcel, 所以顺便尝试下写个简单的协作表格 源码地址: klren0312/realtimeExcel: ws + jexcel, create simple realtime excel (github.com)

aaa.gif

一. 原理

使用jexcel的api监听表格的修改, 将修改的相关数据通过websocket传给后台, 后台再将数据广播给其他websocket客户端, 客户端接收到修改后修改表格

二. websocket数据格式

1. 心跳
代码语言:javascript
复制
{
  type: 'checkalive'
}
2. 获取当前全部数据
  • 客户端发送请求
代码语言:javascript
复制
{
  type: 'new'
}
  • 服务端发送全部数据
代码语言:javascript
复制
{
  type: 'history',
  has: true,
  data: []
}
3. 表格修改
代码语言:javascript
复制
{
  type: 'excelChange',
  data: {
    x: '', // x坐标
    y: '', // y坐标
    value: '' // 数据
  },
  total: [] // 全部数据, 用于后台缓存全部数据
}
5. excel导入
代码语言:javascript
复制
{
  type: 'importExcel',
  data: []
}

三. websocket后端实现

1. 使用库
2. 源码

ws开启端口为23333, redis用来缓存数据, 也可以改成全局变量存, 这样就不用还要额外安装redis, 毕竟求

代码语言:javascript
复制
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 23333 })
const Redis = require("ioredis")
const redis = new Redis()

wss.on('connection', (ws, req) => {
  ws.on('message', msg => {
    try {
      const data = JSON.parse(msg)
      switch (data.type) {
        case 'checkalive': // 心跳
          ws.send(JSON.stringify({type: 'checkalive'}))
          break
        case 'new': // 新打开页面加载历史数据
          redis.get('cacheData').then(res => {
            if (res) {
              const cacheData = JSON.parse(res)
              const newObj = {
                type: 'history',
                has: cacheData.length === 0 ? false : true,
                data: cacheData
              }
              ws.send(JSON.stringify(newObj))
            }
          })
          
          break
        case 'excelChange': // 表格修改
          redis.set('cacheData', JSON.stringify(data.total))
          wss.clients.forEach(function each(client) {
            // 通知除了当前客户端的其他客户端
            if (client !== ws && client.readyState === WebSocket.OPEN) {
              client.send(msg)
            }
          })
          break
        case 'excelInsert': // 表格插入
          redis.set('cacheData', JSON.stringify(data.total))
          wss.clients.forEach(client => {
            // 通知除了当前客户端的其他客户端
            if (client !== ws && client.readyState === WebSocket.OPEN) {
              client.send(msg)
            }
          })
          break
        case 'importExcel': // 表格导入
          redis.set('cacheData', JSON.stringify(data.data))
          wss.clients.forEach(client => {
            // 通知除了当前客户端的其他客户端
            if (client !== ws && client.readyState === WebSocket.OPEN) {
              client.send(msg)
            }
          })
          break
      }
    } catch (error) {
    }
  })
})

四. 前端页面实现

1. 使用库
  • jexcel
  • jsuite jexcel依赖
  • sheetjs
2. websocket客户端代码封装

封装为类, 方便调用, 使用 new WSClient()创建

代码语言:javascript
复制
    // ws连接对象
    class WSClient {
      constructor (url) {
        this.url = url
        this.ws = ''
        this.timeoutId = null
      }

      init (excelInstance) {
        this.ws = new WebSocket(this.url)
        this.ws.onopen = () => {
            if (this.ws.readyState === 1) {
              // 心跳
              this.ws.send(JSON.stringify({type:'checkalive'}))
              this.ws.keepAliveTimer = setInterval(() => {
                if (this.ws.bufferedAmount === 0 && this.ws.readyState === 1) {
                  this.ws.send(JSON.stringify({type:'checkalive'}))
                }
              }, 60000)
              // 重新进入页面, 获取历史数据
              this.ws.send(JSON.stringify({type: 'new'}))
            }
          }
          this.ws.onmessage = res => {
            try {
              const msg = JSON.parse(res.data)
              const data = msg.data
              switch (msg.type) {
                case 'excelChange':
                  const oldData = excelInstance.getValueFromCoords(data.x, data.y)
                  if (data.value !== oldData) {
                    comeFromData = `${data.x}${data.y}${data.value}`
                    excelInstance.setValueFromCoords(data.x, data.y, data.value)
                  }
                  break
                case 'history':
                  if (msg.has) {
                    excelInstance.setData(data)
                  }
                  break
                case 'excelInsert':
                  comeFromInsert = `${data.mixed}${data.rowNumber}${data.insertBefore}`
                  excelInstance.insertRow({
                    mixed: data.mixed,
                    rowNumber: data.rowNumber,
                    insertBefore: data.insertBefore
                  })
                  break
                case 'importExcel':
                  excelInstance.setData(data)
                  break
              }
            } catch (error) {
            }
          }
          this.ws.onerror = () => {}
          this.ws.onclose = e => {
            if (e.code === 23333) return
            clearInterval(this.ws.keepAliveTimer)
            // 判断是否断网
            if (!window.navigator.onLine) {
              this.ws.close(23333)
            } else {
              // 一分钟重连一次
              this.timeoutId = setTimeout(() => {
                this.ws.close(23333)
                this.ws = new WebSocket(this.url)
                clearTimeout(this.timeoutId)
              }, 60000)
            }
          }
      }
    }
3. jexcel创建
代码语言:javascript
复制
// 创建excel实例
const mySpreadsheet = jexcel(document.getElementById('spreadsheet'), {
  data: defaultData,
  columns: [{
      type: 'text',
      width: 200,
    },
    {
      type: 'text',
      width: 200,
    },
    {
      type: 'text',
      width: 200,
    },
    {
      type: 'text',
      width: 200,
    },
    {
      type: 'text',
      width: 200,
    },
    {
      type: 'text',
      width: 200,
    }
  ],
  tableOverflow: true, // 允许滚动
  tableHeight: window.innerHeight, // 最大高度
  allowDeleteRow: false,
  allowDeleteColumn: false,
  // allowManualInsertRow: false,
  allowManualInsertColumn: false,
  oninsertrow: excelInsertRow,
  onchange: excelChange
})

/**
 * 表格修改触发
 */
function excelChange (el, currentel, x, y, nv, ov) {
  console.log(comeFromData, `${x}${y}${nv}`)
  if (comeFromData === `${x}${y}${nv}`) {
    comeFromData = ''
    return
  }
  if (nv !== ov) {
    const obj = {
      x: x,
      y: y,
      value: nv
    }
    if (client.ws.readyState === 1) {
      client.ws.send(JSON.stringify({
        type: 'excelChange',
        data: obj,
        total: mySpreadsheet.getData()
      }))
    }
  }
}
/**
 * 表格插入新行触发
 */
function excelInsertRow (el, rowNumber, numOfRows, rowRecords, insertBefore) {
  if (comeFromInsert === `${numOfRows}${rowNumber}${insertBefore}`) {
    comeFromInsert = ''
    return
  }
  const obj = {
    rowNumber: rowNumber,
    mixed: numOfRows,
    insertBefore: insertBefore
  }
  if (client.ws.readyState === 1) {
    client.ws.send(JSON.stringify({
      type: 'excelInsert',
      data: obj,
      total: mySpreadsheet.getData()
    }))
  }
}

创建ws连接, 并把jexcel对象传入

代码语言:javascript
复制
const client = new WSClient('ws://122.51.46.108:23333')
client.init(mySpreadsheet)
4. excel导入逻辑
代码语言:javascript
复制
/**
 * 导入excel文件
 */
function fileUpload(e) {
  if (!e)  return
  const files = e.target.files, f = files[0]
  const reader = new FileReader()
  reader.onload = function(e) {
    const data = new Uint8Array(e.target.result)
    const workbook = XLSX.read(data, {type: 'array'})
    const workSheet = workbook.Sheets[workbook.SheetNames[0]]
    const res = XLSX.utils.sheet_to_json(workSheet)
    let arr = []
    res.forEach(d => {
      const filterArr = Object.keys(d).map(v => {
        if (v.indexOf('__EMPTY') !== -1) {
          return ''
        } else {
          return v
        }
      })
      arr.push(filterArr)
    })
    if (mySpreadsheet) {
      mySpreadsheet.setData(arr)
      if (client.ws.readyState === 1) {
        client.ws.send(JSON.stringify({
          type: 'importExcel',
          data: mySpreadsheet.getData()
        }))
      }
    }
    e = null
  }
  reader.readAsArrayBuffer(f)
}

结果

http://iexcel.zzes1314.cn

参考资料

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 原理
  • 二. websocket数据格式
    • 1. 心跳
      • 2. 获取当前全部数据
        • 3. 表格修改
          • 5. excel导入
          • 三. websocket后端实现
            • 1. 使用库
              • 2. 源码
              • 四. 前端页面实现
                • 1. 使用库
                  • 2. websocket客户端代码封装
                    • 3. jexcel创建
                      • 4. excel导入逻辑
                      • 结果
                      • 参考资料
                      相关产品与服务
                      云数据库 Redis
                      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档