前端导出Excel?使用js/ts导出Excel?背景色、指定单元格合并等高定制化?选它——Exceljs
这篇文章我首先是在掘金中发出来的,期间也收获了一些朋友们的支持,同时也是觉得 exceljs 这个工具库在鱼龙混杂的前端excel导出、js导出excel等工具下是非常好的一个。不需要担心是vue还是react还是nodejs环境,它统统能做。特此也在此发布一下,希望能帮到更多的朋友。
这篇文章我首先是在掘金中发出来的,期间也收获了一些朋友们的支持,同时也是觉得 exceljs 这个工具库在鱼龙混杂的前端excel导出、js导出excel等工具下是非常好的一个。特此也在此发布一下,希望能帮到更多的朋友。加油!大前端!前端不会亡,只会越来越好!
大背景
首先,介绍一下自己:大家好!我是今年7月毕业然后11月刚入职前端的菜鸟程序员。
前段时间工作上遇到一个需求,需要导出带样式(例如合并单元格,大面积颜色填充,指定颜色线)excel,原本的使用的工具包为js-export-excel
。我在网络上暂时没有找到这个包导出图片和样式的文章和解决办法,所以我果断选择换一个包。不知道有没有懂js-export-excel
的大佬,欢迎来讨论。
最后我为什么选择了exceljs呢?除了能满足我上述需求以外,还有两个非常重要的原因:1️⃣它主要就是通过js进行文件的导出的。这也就意味着它不依赖网页,不需要像某些库一样需要在网页上用html画一个表格再导出,并且可以支持在nodejs环境下直接导出(也就是说,我不需要关心我现在使用的是什么框架,而且即使我要在nodejs端进行excel导出也是可以直接使用这个库来实现的)。2️⃣有中文文档,免去了我阅读英文文档的麻烦。
这里呢,我分享几点需要注意的地方
,当然也会简单说说怎么快速上手
。(也欢迎来讨论)
ok,需求很明确了——导出excel,带样式和图片,样式包含筛选、大面积颜色背景、指定颜色边框。开干!!!
快速上手
首先附上官方中文文档。
exceljs (github.com)
exceljs (gitee.com)(镜像仓库)
一、安装
常规导入
根据项目使用的包管理器对应去安装依赖即可。
npm install exceljs
cnpm install exceljs
yarn add exceljs
pnpm add exceljs
bun install exceljs
浏览器导入
ExcelJS 在 dist/ 文件夹内发布了两个支持浏览器的包:
一个是隐式依赖 core-js
polyfills 的…
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.js"></script>
<script src="exceljs.js"></script>
另一个则没有…
<script src="--your-project's-pollyfills-here--"></script>
<script src="exceljs.bare.js"></script>
二、导入
const ExcelJS = require('exceljs'); // 这是常规的导入方式,能适用于大部分应用场景 - 20240109 LisEcho
看到这个导入方式不要怕,我们访问 npm - exceljs 后就会发现它有个 TS 的图标,也就是说其本身就是基于 ts 所以ts是天然支持的,不需要考虑说导入什么 @types/exceljs ,去找了也没用,因为那个库已经被弃用了。
所以如果是和我一样日常使用 ts 的小伙伴可以直接用下面的方式导入使用。No problem!
import ExcelJs from 'exceljs';
ES5 解决方案
(2024年1月9日)
ES5 版本对许多 polyfill 都具有隐式依赖,而 exceljs 不再明确添加。 需要要在依赖中添加 core-js
和 regenerator-runtime
// exceljs 所需的 polyfills
require('core-js/modules/es.promise');
require('core-js/modules/es.string.includes');
require('core-js/modules/es.object.assign');
require('core-js/modules/es.object.keys');
require('core-js/modules/es.symbol');
require('core-js/modules/es.symbol.async-iterator');
require('regenerator-runtime/runtime');
const ExcelJS = require('exceljs/dist/es5');
对于 IE 11,您还需要一个 polyfill 以支持 unicode regex 模式。
const rewritePattern = require('regexpu-core');
const {generateRegexpuOptions} = require('@babel/helper-create-regexp-features-plugin/lib/util');
const {RegExp} = global;
try {
new RegExp('a', 'u');
} catch (err) {
global.RegExp = function(pattern, flags) {
if (flags && flags.includes('u')) {
return new RegExp(rewritePattern(pattern, flags, generateRegexpuOptions({flags, pattern})));
}
return new RegExp(pattern, flags);
};
global.RegExp.prototype = RegExp.prototype;
}
三、开始使用
(2024年1月9日 添加)
先要有一些概念:
Workbook -> 一个 Excel 文件
Worksheet -> 一个Excel中用于承载表的 “画布”,我们所有的表格都将画在这张“画布”上
搭建“舞台”
// 新建一个工作簿
const workbook = new ExcelJs.Workbook();
这一步可先可后,自己决定。需要注意的就是新建sheet是需要使用到这个实例的。如果你需要导出多个excel文件,按需要new实例就好。
(2024年1月9日) 目前 Exceljs 官方已经在 workbook 上添加了更多的属性,例如:强制工作簿计算属性、工作簿视图、作者信息、创建日期、修改日期、最新一次打印日期…需要的友友们移步官网文档详细了解,有点意思。
// 添加工作表
const worksheet = workbook.addWorksheet( 'sheet1',
{
views: [{ showGridLines: false }], //<<<---这里指创建的sheet隐藏表格网格线
});
ok啊,到了这里舞台已经搭建好了,那么我们可以接着奏乐、接着舞了。
实现需求
1.添加表头字段
后续给表格添加数据时就是通过key的字段取数据。
worksheet.columns = [
{
header: '',
key: 'index',
width: 3,
},
{
header: '姓名',
key: 'name',
width: 20,
},
{
header: '性别',
key: 'sex',
width: 5,
},
{
header: '年龄',
key: 'age',
width: 12,
},
...
];
2.筛选
形式不一,我就喜欢简单易用的,所以我是毫不犹豫选择了第一种方式。
// 方式1
worksheet.autoFilter = 'A1:C1';
// 方式2
worksheet.autoFilter = {
from: 'A1',
to: 'C1',
}
// 方式3
// 将自动筛选器设置为从第3行第1列的单元格到第5行第12列的单元格
worksheet.autoFilter = {
from: {
row: 3,
column: 1
},
to: {
row: 5,
column: 12
}
}
// 方式4
// 将自动筛选器设置为从D3到第7行第5列中的单元格
worksheet.autoFilter = {
from: 'D3',
to: {
row: 7,
column: 5
}
}
3.添加大面积背景 和 插入图片
// 用红色深色垂直条纹填充A1
ws.getCell('A1').fill = {
type: 'pattern',
pattern:'darkVertical',
fgColor:{argb:'FFFF0000'}
};
// 给单元格填充纯色
ws.getCell('A1').fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFFFFF' },
};
上面是对单个单元格进行填充,但是我们的需求是大面积填充。
那么这个需求就非常有意思了。添加大面积的背景我在官方的文档里没有看到可行的办法。难道我们要一行一行去遍历给所有单元格添加颜色嘛?不可能。本本菜鸟才不做这么菜鸟的事儿。但是不能和领导说我做不到啊。咋办呢?我这时注意到了官方文档里的图片的一个用法:
将图片添加到工作表背景使用。
Workbook.addImage
中的图像 ID
,可以使用 addBackgroundImage
函数设置工作表的背景
// 设置背景
worksheet.addBackgroundImage(imageId1);
这办法不就来了啊!我只要有一张纯色图片,然后铺满整个工作表的背景不就是纯色大背景。(没想到本菜鸟小小年纪就学会了走旁门左道。)于是我开始了研究图片的使用。正好我们的需求中也有图片也需要加上去。
插入图片的逻辑:首先,要有一个图片的id,将图片转为id的方法官方提供了,直接使用workbook.addImage()
,其返回值即为图片的id。然后就可以使用图片的id把图片添加到指定的位置worksheet.addImage(imageId, 'B2:D6');
。注意区分,一个使用的是工作簿实例,一个使用的是sheet表实例。
事情来到添加图片。官方提供了3种插入图片的方法:(推荐第三种)
// 通过文件名将图像添加到工作簿
const imageId1 = workbook.addImage({
filename: 'path/to/image.jpg',
extension: 'jpeg',
});
// 通过 buffer 将图像添加到工作簿
const imageId2 = workbook.addImage({
buffer: fs.readFileSync('path/to.image.png'),
extension: 'png',
});
// 推荐
// 通过 base64 将图像添加到工作簿
const myBase64Image = "data:image/png;base64,iVBORw0KG...";
const imageId2 = workbook.addImage({
base64: myBase64Image,
extension: 'png',
});
这里就有我和插入图片的爱恨情仇极致拉扯纠缠。
我尝试了第一个和第二个方法,都会有报错。不信可以试试。如果有使用前两个方法成功了的朋友,欢迎来友好讨论。只有使用了使用第三个方式添加图片才能成功。但这里又有了一个问题,我怎么让项目内的本地图片成base64呢?
通过搜索我们可以轻松获得各式各样的通过图片url地址获得base64的代码,然后我们只要解决本地图片怎么才能变成那些方法的url就可以了。这里可以留给大家思考,下面我先把url转base64的一种方式奉上:
/**
* 通过图片url将图片转为base64格式。
*/
const getBase64Image = (url: any) => {
const img = new Image();
//因为是网络资源所以会有图片跨域问题产生,此属性可以解决跨域问题,下文详解
img.setAttribute('crossOrigin', 'anonymous');
//如果需要兼容ios,这两个顺序一定不能换,先设置crossOrigin后设置src
img.src = url;
return new Promise((resolve, reject) => {
img.onload = () => {
//canvas基本配置
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx!.drawImage(img, 0, 0, canvas.width, canvas.height);
resolve({
success: true,
//canvas.toDataURL的方法将图片的绝对路径转换为base64编码
base64: canvas.toDataURL(),
});
};
img.onerror = () => {
reject({ success: false });
};
});
};
使用很简单,使用该方法后.then即可拿到并使用。感觉上是有点麻烦,但是这其实还是考虑到了一些东向西,所以我当时选择了它,也有可以直接使用的版本,大家可以自行去查找。
拿到base64图片之后就好办了,直接使用:
// 在 B2:D6 上插入图片
worksheet.addImage(imageId2, 'B2:D6');
4.添加有颜色的边框
// 在A3周围设置双细绿色边框
ws.getCell('A3').border = {
top: {style:'double', color: {argb:'FF00FF00'}},
left: {style:'double', color: {argb:'FF00FF00'}},
bottom: {style:'double', color: {argb:'FF00FF00'}},
right: {style:'double', color: {argb:'FF00FF00'}}
};
有效边框样式
- thin
- dotted
- dashDot
- hair
- dashDotDot
- slantDashDot
- mediumDashed
- mediumDashDotDot
- mediumDashDot
- medium
- double
- thick
5.合并单元格
worksheet.mergeCells('A4:B5');
添加数据
就是添加每一行数据了,就是方法addRow(value)
和addRows(value)
。这两个方法是在最后一个有值行之后新增行并插入给定的数据,数据支持了对象和数组的方式。
值得一提的是这两个方法都可选第二参数。第二参数默认为’i’, 第二参数有三种:‘i’、‘i+’、‘n’,‘i’的含义是继承上一单元格的样式,‘n’则是不继承并使用默认样式。‘+’则是决定空单元格是否继承样式,有+则继承,否则不继承(会有一个小bug,第一个空单元格会继承上一单元格样式,后面的空单元格则不继承)。
// 通过一个对象直接进行添加,这里展示的是单行
worksheet.addRow({id: 1, name: 'John Doe', dob: new Date(1970,1,1)});
worksheet.addRow({id: 2, name: 'Jane Doe', dob: new Date(1965,1,7)});
// 数组型数据添加到行
worksheet.addRow([3, 'Sam', new Date()]);
// 这种方式可以在插入数据时对数据进行处理
const rowValues = [];
rowValues[1] = 4;
rowValues[5] = 'Kyle';
rowValues[9] = new Date();
worksheet.addRow(rowValues);
// 进阶,一次性添加多行
const newRow = worksheet.addRow(rowValues, 'i');
const rows = [
[5,'Bob',new Date()], // row by array
{id:6, name: 'Barbara', dob: new Date()}
];
const newRows = worksheet.addRows(rows);
// 继承样式
const newRowsStyled = worksheet.addRows(rows, 'i');
导出表格
const EXCEL_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8';
workbook.xlsx.writeBuffer().then((data) => {
const blob = new Blob([data], { type: EXCEL_TYPE });
console.log(blob.stream(), 6664744);
FileSaver.saveAs(blob, `${projname}.xlsx`);
});
ok,到这里基本已经实现了需求。另外还有一种table的构建方式,因为时间问题,我没有学习使用,大家可自行观摩学习使用,后续如果有使用到我会再补充。
注意事项
一、addRow()中的坑。
通过addRow
得到的有值单元格样式不可再操作。 对于这点我也很苦恼,迟迟未找到原因,所以,如果需要对添加表格进行样式定制需要先给表头应用需要的样式,然后addrow继承下去,添加完数据之后再操作表头,定义表头样式。
如果不是通过table对象添加的表格,不能remove,不可删除。 我也不清楚为什么没有设计这个,可能官方就希望我们使用table方式添加表格和数据吧。但是那样有一定的局限性,大家可以自己尝试。我没有使用table,因为我已经使用了上面的方式,效果已经出来了,所以没有改了。
二、插入图片中的坑
使用第1、2种方式会导致报错。 报错描述很类似,都是xxx不是一个方法,e.g. readFileSync not
a function……最后还是得去使用第三种方式。我暂时没有找到前两种方式报错的解决办法,欢迎大家一起讨论。
三、设置宽度和高度种的坑
宽度欺骗。 当我们设置高度为10的时候,往往我们实际上得到的不是10,而是9.22。这个类似的问题我在exceljs的issue中也有看到。有兴趣的同学可以移步去看看。
ok,暂时就总结了这么多,欢迎交流。
更多推荐
所有评论(0)