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

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

aaa.gif

一. 原理

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

二. websocket数据格式

1. 心跳

{
  type: 'checkalive'
}

2. 获取当前全部数据

  • 客户端发送请求
{
  type: 'new'
}
  • 服务端发送全部数据
{
  type: 'history',
  has: true,
  data: []
}

3. 表格修改

{
  type: 'excelChange',
  data: {
    x: '', // x坐标
    y: '', // y坐标
    value: '' // 数据
  },
  total: [] // 全部数据, 用于后台缓存全部数据
}

5. excel导入

{
  type: 'importExcel',
  data: []
}

三. websocket后端实现

1. 使用库

2. 源码

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

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()创建

    // 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创建

// 创建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对象传入

const client = new WSClient('ws://122.51.46.108:23333')
client.init(mySpreadsheet)

4. excel导入逻辑

/**
 * 导入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

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容