这篇文章我首先是在掘金中发出来的,期间也收获了一些朋友们的支持,同时也是觉得 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-jsregenerator-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,暂时就总结了这么多,欢迎交流。


Logo

前往低代码交流专区

更多推荐