前端 Excel 导出的特殊需求

在内部系统开发时,经常会有业务部门会要求开发Excel导入/导出的功能。这种需求非常的常见,如果只是基础的需求开发起来也比较容易。只是有一些特殊的业务下会有对导出的工作表样式会有要求以及会有锁定的需求。

一般来说这些相关的功能都会让后端同学来帮忙。后端同学忙不过来的时候我们也会使用 SheetJS 这个库来处理,社区也有很多基于它的各种增强。但因为社区版的样式处理非常麻烦,也经常会看到有小伙伴在社群里面吐槽自定义样式不好做。所以项目后期就切换到了 ExcelJS,开发体感更加友好且支持的功能更多。

对于 ExcelJS 介绍的部分就不过多赘述了,社区里面有很多相关的文章可以自行检索。

这篇笔记主要是解决日常业务开发中遇到的几个常见需求,比如说:

  • 单元格的下拉选择
  • 单元格的数据校验
  • 单元格和工作表的编辑保护

💥 单元格的数据下拉功能

我们最先遇到的特殊需求肯定就是单元格数据的下拉选择功能了。下拉功能需要借助单元格的 数据验证 来实现。

官方文档中的示例也非常的直截了当,很简单的就可以实现下拉功能。

// 指定有效值的列表(One,Two,Three,Four)。
// Excel 将提供一个包含这些值的下拉列表。
worksheet.getCell('A1').dataValidation = {
  type: 'list',
  allowBlank: true,
  formulae: ['"One,Two,Three,Four"']
};

// 指定范围内的有效值列表。
// Excel 将提供一个包含这些值的下拉列表。
worksheet.getCell('A2').dataValidation = {
  type: 'list',
  allowBlank: true,
  formulae: ['$D$5:$F$5']
};

这就不需要多说了,属于是看一眼就能学会的功能。我们开发时遇到的问题其实时如何批量设置这个下拉功能。比如说某一列我都想设置为下拉的。


💥 批量设置下拉选择

这个确实在 ExcelJS 的文档中并没有全部都提到。只是在样式的部分提到了 工作表的列整列整行单元格 的几种设置方式。其他的一些API中,只是简单的写了两个示例。

比如说上面提到的数据验证功能就只有给单元格设置的Demo,并没有提供批量设置的示例。我们自己通过getRow()getColumn() 给整行/整列去设置并不会生效。
所以很多人会自然而然的认为只能通过给单元格设置的方式。如果要批量设置就得使用 for/forEach 或者使用提供的 eachCell 方法循环单元格设置。

其实我们使用 addWorksheet 创建的 worksheet 工作表中是有一个 dataValidations 属性的,并提供了 add()find()remove() 这三个API,但是在文档中并没有提到。
我们批量设置下拉选择(数据校验)功能时可以使用 ws.dataValidations.add('address', validation) 这样来批量设置:

worksheet.dataValidations.add(
  'A2:A100', // 就是{列}{行}:{列}{行} 框定作用范围
  {
    type: 'list',
    allowBlank: true,
    formulae: ['"One,Two,Three,Four"']
  }
)

💥 单元格的数据校验功能

数据校验功能,除了可以给单元格设置实现下拉功能。还以对用户输入的内容做校验,并且在用户输入时和输入错误后提示用户。
示例Demo:

// 指定单元格必须为非5的整数。
// 向用户显示适当的错误消息(如果他们弄错了)
worksheet.getCell('A1').dataValidation = {
  type: 'whole',
  operator: 'notEqual',
  formulae: [5],
  showErrorMessage: true,
  errorStyle: 'error',
  errorTitle: 'Five',
  error: 'The value must not be Five'
};

// 指定单元格必须为1.5到7之间的十进制数字。
// 添加“工具提示”以帮助指导用户
worksheet.getCell('A2').dataValidation = {
  type: 'decimal',
  operator: 'between',
  formulae: [1.5, 7],
  allowBlank: true,
  showInputMessage: true,
  promptTitle: 'Decimal',
  prompt: 'The value must between 1.5 and 7'
};

按照示例代码设置输入的内容错误有提示,用户在输入的的时候也会有提示。

但是我自己在同时设置 errorprompt 相关属性后,只会在输入完成后弹出错误信息,并没有在用户输入时提示辅助信息。
这是因为我把 showErrorMessageshowInputMessage 搞混了,如果要开启输入提示( prompt 相关的属性)还需要把 showInputMessage 属性置为 true

所以同时开启 errorprompt 的话就是下面这样:

worksheet.getCell('A2').dataValidation = {
  type: 'decimal',
  operator: 'between',
  formulae: [1.5, 7],
  allowBlank: true,
  // 输入时显示的提示
  showInputMessage: true,
  promptTitle: 'Decimal',
  prompt: 'The value must between 1.5 and 7',
  // 输入后弹出的错误提醒
  showErrorMessage: true,
  errorStyle: 'error',
  errorTitle: 'Decimal',
  error: 'The value must between 1.5 and 7'
};

这块的批量设置也是一样的,通过上方提到的 ws.dataValidations.add('address', validation) API设置即可。


💥 编辑保护功能

工作表保护的设置也很简单,直接看文档就行了 👉 🔗 工作表保护
但是保护开启之后会使整个 worksheet (工作表)用户都无法编辑了,会弹出提示:
您试图更改的单元格或图表位于受保护的工作表中。若要进行更改,请取消工作表保护。您可能需要输入密码

一般我们只是需要做部分的单元格锁定使其不可编辑,或者只锁定单元格数据校验,用户还是可以正常编辑内容这样两个需求。那么就需要我们根据实际情况来组合使用 单元格保护 了。 👉 🔗 单元格保护

比如说我现在的一个需求:

用户可以正常编辑单元格内容,只保护单元的数据校验规则。防止用户在操作Excel时破坏掉单元格的数据校验
比如说用户复制粘贴其他Excel中的数据内容,就会破坏和覆盖掉下拉选项的情况。

我可以先使用 worksheet.protect('the-password') 锁定整个工作表,然后使用 worksheet.getCell('A1').protection = { locked: false } 给单元格设置解除锁定。这样就可以满足用户编辑时误操作也不会破坏掉我们预设的数据校验了。

但又遇到不知道如何批量设置单元格保护的问题了。尝试过使用 eachCell() 这个API给每一个单元格设置并不生效。因为大多数情况下我们导出的空白 Excel 模板,工作表中除了表头并没有实际数据。所以在使用 eachCell() 循环当前列单元格时只会循环表头这一格单元格。并不能如预想一般循环当前列的所有单元(即使设置了 eachCell({ includeEmpty: true }, fn(cell, rowNum)))。

我们可以在使用 worksheet.getColumn() 获取到整列之后,直接给返回的整列对象设置单元格保护属性。比如说:

worksheet.getColumn('id').protection = { locked: false }
await worksheet.protect('the-password')

worksheet.xlsx.writeBuffer().then((buffer) => ....)

同理的,也可以使用 getRow() 获取到整行之后,给整行设置单元格保护。

暂时我能回想起来会在实际开发中就遇到那么多问题。如果后期又遇到新的问题了再单独补充。
预计未来应该会补充一个我自己抽象封装好的导出和导入的utils。只需要和一些UI库的Table组件一样传入表头配置和表格数据就行了。


相关资源

exceljs/exceljs: Excel Workbook Manager
这一定是前端导出Excel界的天花板~