Vue自定义指令实现快速读取Excel

1,678 阅读3分钟


前几天因为业务需求,所维护的而后台中出现了大量关于上传下载Excel的操作。因为我们的后台是基于Vue,并且是在 vue-element-admin 的基础上结合实际需求开发而来。vue-element-admin 中也有一些相关操作 Excel 的示例,都十分清晰明了,很快就能上手。而我们当然首要参考了 vue-element-admin 的操作方式,如上传 Excel:
<template>
  <div>
    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
      Drop excel file here or
      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
        Browse
      </el-button>
    </div>
  </div>
</template>

<script>
import XLSX from 'xlsx'
///
</script>

在上传 Excel 中,vue-element-admin 的做法是,点击上传按钮时触发事先放在组件内的 input 的 click ,在通过监听 input 的 change 事件,获取读取到的 Excel 文档。事实上,对文件的处理也只能这样了,读取到 file 后通过 xlsx 工具库,对 file 进行 JSON 化处理再发给后端。(不要问我为什么这些事情要前端来做,问就是我乐意)。

刚刚说到这样做没得啥子问题,但是在实际项目中,尤其是后台管理系统,。几乎很多页面几乎都是表格、查询、批量操作等。最开始的时候,我就是直接把 input + 按钮 放在业务页面,但是随着项目慢慢变大,这样就显得有些臃肿了。不仅增加了代码量,也不利于维护。于是我把这个功能封装成了一个组将,就像 vue-element-admin 就类似那样。但是后来随着项目越来越来,越来越多的页面需要 Excel 操作,我对这种频繁引入此组件的方式也开始不厌其法。这个时候其实就有两种选择了:将组件注册为全局组件,或者使用自定义指令达到相同的效果。正如标题写的那样,我选择了后者。
因为 Excel 这个需求,体现上无非就是:点击了某个按钮,弹出文件选择,用户选择 Excel 后直接读取。因此,直接参与在业务中的只有按钮,至于用户在选择 Excel 后,我需要把这部操作封装一下,因为逐步操作和业务没有直接关系。因此,我需要实现一个针对选择 Excel 按钮的自定义指令:
  // 注册全局自定义快速读取 excel `v-read-excel`
  Vue.directive('read-excel', {
    inserted: (el, { value }) => {
      const id = Date.now()
      const input = document.createElement('input')
      el['read-excel-id'] = id
      input.id = id
      input.type = 'file'
      input.accept = '.xlsx, .xls'
      input.onchange = ({ target: { files: [excel] }}) => {
        if(!excel) return  
        const XLSX = require('xlsx')
        const reader = new FileReader()
        reader.onload = async({ target: { result }}) => {
          const workbook = XLSX.read(result, { type: 'array' })
          value && value(XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]))
        }
        reader.readAsArrayBuffer(excel)
      }
      input.style.display = 'none'
      document.body.appendChild(input)
      el.addEventListener('click', () => document.getElementById(id).click())
    },
    unbind: el => document.getElementById(el['read-excel-id']).remove()
  })
使用起来无需引入组件,更无需 input ,只需要这样:
<template>
  <div class="PageUploadExcel">
    <el-button v-read-excel="upload" type="primary">上传Excel</el-button>
  </div>
</template>

<script>
export default {
  name: 'PageUploadExcel',
  data() {
    return {
      list: []
    }
  },
  methods: {
    upload(list) {
      this.list = list
    }
  }
}
</script>
原理很简单:在被绑定按钮插入文档后,给这个按钮配套一个 input 放在 body 里,点击按钮就会触发 input ... 在被绑定按钮被移除文档同时也删除掉自己所对应的 input。
这就是关于上传 Excel 的自定义指令封装操作。
至于下载,也是参考 vue-element-admin 的做法,不过也是为了使用简便,就直接把方法挂在 Vue 原型上了:
  Vue.prototype.$excel = function(list, name) {
    !list.length ? list = [{ '暂无数据': '' }] : ''
    import('@/utils/Export2Excel').then(excel => {
      excel.export_json_to_excel({
        header: Object.keys(list[0]),
        data: list.map(listItem => Object.keys(list[0]).map(j => listItem[j])),
        filename: name || '下载Excel',
        bookType: 'xlsx'
      })
    })
  }
这个用起来更简单:
<template>
  <div class="PageDownloadExcel">
    <el-button type="primary" @click="download">下载Excel</el-button>
  </div>
</template>

<script>
export default {
  name: 'PageDownloadExcel',
  data() {
    return {
      list: [
        { '姓名': '张三', '年龄': 18, '爱好': '旅游' },
        { '姓名': '李四', '年龄': 19, '爱好': '游泳' },
        { '姓名': '王五', '年龄': 20, '爱好': '吃鸡' }
      ]
    }
  },
  methods: {
    download() {
      this.$excel(this.list, '数据表格')
    }
  }
}
</script>
可能你也注意到了,我在这里使用的数据是:
[
    { '姓名': '张三', '年龄': 18, '爱好': '旅游' },
    { '姓名': '李四', '年龄': 19, '爱好': '游泳' },
    { '姓名': '王五', '年龄': 20, '爱好': '吃鸡' }
]
是的,key - value 都是直接用来展示的汉字。这样做,除了方便外,也可以实现后端实时控制导出的字段,我司目前使用的就是这种方式。当然,这个要看具体的业务需求了。