前言
其他业务组想要一个在线编辑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-model 是 value 和 change 的语法糖,但只停留在概念中,自己需要实现的时候一头雾水。以下贴出了部分配置代码,但是已经包含了基础配置的核心代码了,完整代码可以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 找到了解决方式
import 'x-data-spreadsheet/dist/locale/zh-cn';
Spreadsheet.locale('zh-cn');
文档没有查看数据的方法
自己查看源码,找到了对应的使用方式,实例的原型上有挂载多个方法,包含获取数据的方法
this.sheet.getData();
删除 sheet 时没有触发 change 事件
这个有点小坑,查看源码时发现其实是有暴露对应的事件,但是在其发布的最新版本的 npm 包的代码,却没有实现触发 change 事件,我去提对应的 issure 也是无果哈,只能直接先打补丁解决。
这个时源码的删除事件代码
这个是 npm 包的对应的删除事件代码
打补丁
这个了解和使用的人应该比较少,比较偏,虽然挺好用的,但是还是不推荐使用,只能说用来修复一些燃眉之急或者比较简单是问题,避免后续代码不好维护或者其他不可预估的问题。
准备
查看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_modules的x-data-spreadsheet源码中,找到删除Sheet的方法,并且添加change触发事件就可以了,至于我为什么知道添加什么代码,只要看一下源码,搜索触发change事件就知道,当然说着比较简单,还是要有一定的看源码积累,看多了,挺容易就能想到是对应的事件发布订阅了。
/node_modules/x-data-spreadsheet/src/index.js
this.sheet.trigger('change');
生成补丁
执行 npx patch-package package-name,package-name 是对应打补丁的依赖名称,我们这边就是x-data-spreadsheet
在命令行执行 npx patch-package x-data-spreadsheet, 会在当前项目生成一个补丁文件patches,保留对应的补丁信息。
npx patch-package x-data-spreadsheet
这样子,这个小小的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
扩展
注意:npm规定上传一个包后可以在24h内删除,删除一个包后在24h内不能再次上传相同包,即版本号不能相同,这会有点限制,当然如果是公司的私服就没这个限制了。没有公司私服的话,我们自己搭建一个本地私服也是很简单
verdaccio npm 私服
安装依赖
npm i verdaccio -g
运行 verdaccio
verdaccio
查看本地仓库
http://localhost:4873/
使用方式
将 npm 镜像源设置为我们自己本地镜像私服的源就可以了。
npm config set registry http://localhost:4873/
这个就能正常的发布和安装包,这也是一种测试方式吧,方便我们更好的开发。
小结
在线Excel编辑组件,到这里就走完终点了,虽然是二次封装,但是过程还是比较健全的,有对于开源库进行二次封装需要准备的点,怎么去进行分析,组件内容也涉及正常js组件进行 Vue 封装,自定义v-model,对于组件功能的扩展,实现第三方npm的包的补丁修复,webpack的打包配置,npm包的发布,以及本地npm私服搭建,相信对于大家去做组件的封装有点借鉴意义。
到这里相信学会了vue组件的二次封装了,对应的代码也发布到 Github,组件也发布到npm
如果觉得这篇文章对您有帮助,欢迎点赞收藏,有问题也欢迎指出。