在线Excel编辑组件

1,195 阅读9分钟

前言

其他业务组想要一个在线编辑Excel的组件,同时也支持导入、导出。有需求,那就开整吧,自己从0到1实现不那么现实,不管是能力,以及时间成本。所以还是在开源社区寻找,进行了一波调研,最终选择了x-data-spreadsheet做二次封装,对其做二次封装的原因,1.可以集成进公司内部的组件库、2.用户使用起来更加方便,高效、3.扩展一些原本组件不支持的功能,比如导入、导出。当前组件源码以及示例已经发布到Github,同时也发布到了npm

组件实现

准备

对于组件的二次封装,也不算是很容易的一件事,还是需要经过调研,根据文档去实践开源库具有的功能,对于其存在的问题有一定的了解,同时去查看 Github 的 start 数量,issure 对应的已解决和还存在的问题,代码提交频率,这些都是我们是否选择使用这个开源库的因素。最后查看源码也是很重要的一件事,明明具有很多功能,但是文档上却没有体现,或者写的不完整,这是屡见不鲜的事情,毕竟写文档确实不是一件容易的事,比写代码痛苦许多,我也是深有体会,这是看源码的其一原因;另外一个原因是当开源库出现问题,你是否能具有解决问题的能力,不管是提交相应的Pr,自己打补丁,或者把源码fork下来,自己进行二次开发,去完善自己的功能,毕竟你没办法去预测作者是否还继续维护开源库,或者想不想合并你的Pr,想不想去实现你需要的功能,自己具有一定的解决问题的能力,才不会处于太被动的地步,未来需求团队提出的紧急或者出乎意料的需求,才能更加从容的去解决。

这次封装的过程中,也发现x-data-spreadsheet一些不完善的地方,实现一些扩展,比如 Excel 的导入、导出, 自定义 v-model 实现数据绑定,还有发现了不满足我需求的问题,我去 Github 提交了 issure,不过没人鸟我哈,因为作者已经将近一年没发布新版本了,这时候提交 Pr 也是没用的,这就很尴尬了哈,因为暂时只是一个小功能,所以我采用打补丁的方式先解决了。

基础配置封装

开始封装时,需要认真的查看官方文档

由于x-data-spreadsheet不是用vue实现的,所以需要对其做进一步的封装,这一步比较简单,大家平常封装组件的时候经常操作,将需要配置的参数通过Props传递进来,然后对于需要暴露的方法,采用this.$emit发布出去就可以了。大家可能比较生疏的点在于数据的双向绑定,可能之前知道 v-modelvaluechange 的语法糖,但只停留在概念中,自己需要实现的时候一头雾水。以下贴出了部分配置代码,但是已经包含了基础配置的核心代码了,完整代码可以Github自行查看。

<template>
  <div ref="element"></div>
</template>
<script>
// 引入组件
import Spreadsheet from 'x-data-spreadsheet';
// 切换中文
import 'x-data-spreadsheet/dist/locale/zh-cn';
Spreadsheet.locale('zh-cn');
import _ from 'lodash';
export default {
  name: 'EcSheet',
  // 定义model
  model: {
    prop: 'value',
    event: 'change',
  },
  props: {
    value: Array,
    // 模式
    mode: {
      type: String,
      default: 'edit',
    },
    // 是否显示工具栏
    showToolbar: {
      type: Boolean,
      default: true,
    },
    // 是否显示底部工具栏工具栏
    showBottomBar: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {};
  },
  mounted() {
    const { mode, showToolbar, showBottomBar } = this;
    const value = _.isEmpty(this.value) ? {} : this.value;
    // 实例化 new Spreadsheet(el,options)
    const sheet = new Spreadsheet(this.$refs.element, {
      mode,
      showToolbar,
      showBottomBar,
      view: {
        height: () => document.documentElement.clientHeight,
        width: () => document.documentElement.clientWidth,
      },
    })
      .loadData(value) // 数据初始化
      .change(() => {
        // change 事件
        const data = this.handleResult();
        this.$emit('change', data);
      })
      .on('cell-selected', (cell, ri, ci) => {
        // 选择单元格
        this.$emit('cell', cell, ri, ci);
      });
    sheet.validate();
    this.sheet = sheet;
  },
  methods: {
    handleResult() {
      // 获取数据
      const result = this.sheet.getData();
      // 处理最终数据
      return result.map((item) => {
        const { name, rows } = item;
        return { name, rows };
      });
    },
  },
};
</script>

踩坑点

切换中文无效

根据文档提供的使用方式,还是显示英文,切换没有生效

import zhCN from 'x-data-spreadsheet/dist/locale/zh-cn';
Spreadsheet.locale('zh-cn', zhCN);

在官方Github的 issure 找到了解决方式

image.png

import 'x-data-spreadsheet/dist/locale/zh-cn';
Spreadsheet.locale('zh-cn');

文档没有查看数据的方法

自己查看源码,找到了对应的使用方式,实例的原型上有挂载多个方法,包含获取数据的方法

this.sheet.getData();

删除 sheet 时没有触发 change 事件

这个有点小坑,查看源码时发现其实是有暴露对应的事件,但是在其发布的最新版本的 npm 包的代码,却没有实现触发 change 事件,我去提对应的 issure 也是无果哈,只能直接先打补丁解决。

这个时源码的删除事件代码

image.png

这个是 npm 包的对应的删除事件代码

image.png

打补丁

这个了解和使用的人应该比较少,比较偏,虽然挺好用的,但是还是不推荐使用,只能说用来修复一些燃眉之急或者比较简单是问题,避免后续代码不好维护或者其他不可预估的问题。

准备

查看patch-package使用方式,文档还是比较全的。

安装依赖

注意如果是要提供给别人使用的组件,需要将依赖安装到dependencies,所以照着我的操作来就可以了。

npm

npm i patch-package

yarn

yarn add patch-package postinstall-postinstall

使用yarn包管理器的话,需要多安装postinstall-postinstall,为什么需要这个呢,文档也是很贴心,写的很详细,可以查看官方解释,Why use postinstall-postinstall

修改 package.json

在 package.json 的 scripts 添加如下命令:

"scripts": {
  "postinstall": "patch-package"
}

修改node_modules中源码

我们这次的补丁还是比较简单的,只需要在node_modulesx-data-spreadsheet源码中,找到删除Sheet的方法,并且添加change触发事件就可以了,至于我为什么知道添加什么代码,只要看一下源码,搜索触发change事件就知道,当然说着比较简单,还是要有一定的看源码积累,看多了,挺容易就能想到是对应的事件发布订阅了。

/node_modules/x-data-spreadsheet/src/index.js

this.sheet.trigger('change');

image.png

生成补丁

执行 npx patch-package package-name,package-name 是对应打补丁的依赖名称,我们这边就是x-data-spreadsheet

在命令行执行 npx patch-package x-data-spreadsheet, 会在当前项目生成一个补丁文件patches,保留对应的补丁信息。

npx patch-package x-data-spreadsheet

image.png

这样子,这个小小的bug就被我们修复了。

导入、导出功能

这个功能主要是借助了xlsx插件的数据文件转换功能

安装依赖

npm i xlsx

导出

/**
 * @params {string} name 文件名称
 * @params {string} ext 文件格式
 */
export(name = 'sheet', ext = 'xlsx') {
  const { sheet } = this;
  // 读取数据
  const list = sheet.getData();
  // 处理数据
  const wb = this.handleData(list);
  // 导出Excel
  XLSX.writeFile(wb, `${name}.${ext}`);
},
/**
 * @param {Array} list Excel多表数据
 */
handleData(list) {
  // 创建一个工作簿
  const wb = XLSX.utils.book_new();
  list.forEach(function (xws) {
    const aoa = [[]];
    // 读取每一个sheet的数据,并处理成xlsx的数据格式
    const rows = xws.rows;
    for (let i = 0; i < rows.len; i++) {
      const row = rows[i];
      if (!row) continue;
      aoa[i] = [];
      Object.keys(row.cells).forEach(function (k) {
        const idx = +k;
        if (isNaN(idx)) return;
        aoa[i][idx] = row.cells[k].text;
      });
    }
    // 创建工作表;
    const ws = XLSX.utils.aoa_to_sheet(aoa);
    // 工作表添加到工作簿
    XLSX.utils.book_append_sheet(wb, ws, xws.name);
  });
  // 返回处理后的工作簿
  return wb;
},

导入

/**
 * @param {file} files 文件
 */
import(files) {
  // 判断 files 是否是文件,解决 input 的 type=file 直接上传和 
  // UI 框架 upload 上传文件参数获取不一致问题
  const file = files instanceof File ? files : files.target.files[0];
  const reader = new FileReader();
  reader.onload = (e) => {
    // 获取文件数据
    const data = e.target.result;
    const fixedData = this.fixData(data);
    const workbook = XLSX.read(btoa(fixedData), { type: 'base64' });
    this.sheet.loadData(this.handleExcel(workbook));
  };
  reader.readAsArrayBuffer(file);
},
fixData(data) {
  let o = '';
  let l = 0;
  const w = 10240;
  for (; l < data.byteLength / w; ++l) {
    o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w, l * w + w)));
  }
  o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)));
  return o;
},
handleExcel(wb) {
  const out = [];
  wb.SheetNames.forEach(function (name) {
    const o = { name: name, rows: {} };
    const ws = wb.Sheets[name];
    const aoa = XLSX.utils.sheet_to_json(ws, { raw: false, header: 1 });
    aoa.forEach(function (r, i) {
      const cells = {};
      r.forEach(function (c, j) {
        cells[j] = { text: c };
      });
      o.rows[i] = { cells: cells };
    });
    out.push(o);
  });
  return out;
},

打包

对于组件打包,随着前端的日益繁华,可以选择的方式很多,渐渐淡出视线的gulp, 专注于 js 的 rollup, 什么都想要的 webpack,速度极快的 esbuild 等等,虽然都很好,我还是选择自己比较熟悉的 webpack,也许在现在它暴露了一些缺点,但是这么多年实践的考验,他还是很稳定,依旧是很多项目的选择。

打包配置

const path = require("path");
// 清楚 dist 目录
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块
const VueLoaderPlugin = require("vue-loader/lib/plugin");
// 抽离css文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  // 入口
  entry: {
    sheet: "./src/index.js",
  },
  // 文件输出,打包成 umd 格式
  output: {
    // 文件名,eg: sheet.js
    filename: "[name].js",
    // 输出目录,需要配置,不然 CleanWebpackPlugin 插件无效
    path: path.resolve(__dirname, "./dist"),
    // 输出库的名称
    library: "sheet",
    // export default
    libraryExport: "default",
    // umd 格式
    libraryTarget: "umd",
  },
  mode: "none",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: "babel-loader",
      },
      {
        test: /\.vue$/,
        use: "vue-loader",
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
      {
        test: /\.less$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    new CleanWebpackPlugin(),
    new VueLoaderPlugin(),
  ],
};

npm包发布

准备

修改 package.json 相关字段

{
    "name": "@lbingxin/sheet",
     "version": "1.0.0",
     "description": "提供一个在线的Excel编辑组件—支持导入、导出",
     "main": "index.js",
     "repository": "git+https://github.com/LBINGXIN/sheet.git",
     "keywords": ["sheet","excel","excel 在线Excel","sheet 在线Excel","excel 导入导出","excel 编辑组件"],
     "files": ["package.json","README.md","dist","index.js"],
     "author": "LBINGXIN",
}
  • name:包名,这里加上我的账号名称前缀,采用 scope 作用域的包名,这样子的好处有利于分类,同时也避免包重名
  • version:版本号
  • description:简介
  • main:包入口,根据自己的入口文件配置
  • repository:仓库地址
  • keywords:关键词,别人通过什么样的关键词搜到你的包
  • files:发布到npm上的文件
  • author:作者

发布

执行 npm login 登录账号

npm login

执行npm publish --access public,发布包,我们加上属性--access public的原因是因为我们是npm scope私有作用域包,所以需要添加上这个属性,发布成功,就能在npm上查看到自己的包

npm publish --access public

image.png

扩展

注意:npm规定上传一个包后可以在24h内删除,删除一个包后在24h内不能再次上传相同包,即版本号不能相同,这会有点限制,当然如果是公司的私服就没这个限制了。没有公司私服的话,我们自己搭建一个本地私服也是很简单

verdaccio npm 私服

安装依赖

npm i verdaccio -g

运行 verdaccio

verdaccio

image.png

查看本地仓库

 http://localhost:4873/

image.png

使用方式

将 npm 镜像源设置为我们自己本地镜像私服的源就可以了。

npm config set registry http://localhost:4873/

这个就能正常的发布和安装包,这也是一种测试方式吧,方便我们更好的开发。

小结

在线Excel编辑组件,到这里就走完终点了,虽然是二次封装,但是过程还是比较健全的,有对于开源库进行二次封装需要准备的点,怎么去进行分析,组件内容也涉及正常js组件进行 Vue 封装,自定义v-model,对于组件功能的扩展,实现第三方npm的包的补丁修复,webpack的打包配置,npm包的发布,以及本地npm私服搭建,相信对于大家去做组件的封装有点借鉴意义。

到这里相信学会了vue组件的二次封装了,对应的代码也发布到 Github,组件也发布到npm

如果觉得这篇文章对您有帮助,欢迎点赞收藏,有问题也欢迎指出。

参考