前端处理 xlsx,标红备注不在话下

背景:最近在处理和 xlsx 相关的需求,需要具备 xlsx 的读写能力。写的话包含标红和批注。

结论:基于 exceljs 做二次封装。


xlsx 的读写能力,前端已经具备。以前这种都是交给后端来解析。前端扬眉吐气了一把。

笔者这次面对的需求是这样的

  • 读取 xlsx 文件,并且能正确获取日期类型的数据。
  • 写 xlsx 文件,并且能标红和备注

选型 1 -sheetjs(xlsx)

首先搜索到的是 sheetjs,功能非常强大。 看到 antd 也在依赖此库。

sheetjs

体积

库比较大,记得异步加载哦。

兼容性

docs.sheetjs.com/docs/gettin...

For broad compatibility with JavaScript engines, the library is written using ECMAScript 3 language dialect. A "shim" script provides implementations of functions for older browsers and environments.

为了与JavaScript引擎进行广泛的兼容性,该库是使用Ecmascript 3语言方言编写的。 一个"垫片"脚本为较旧的浏览器和环境提供了实现功能。

从描述上看,连 IE 都支持,兼容性非常好。

读写能力

最终代码和 Demo 在 codesandbox.io/s/zealous-v...

读能力

我们的 xlsx 文件内容如下

使用 XLSX 读文件

ini 复制代码
async function xlsxToJSON(file: File): Promise<any[][]> {
  const ab = await file.arrayBuffer();
  const workbook = XLSX.read(ab);
  // 读取第一个 sheet
  const sheet1 = workbook.Sheets[workbook.SheetNames[0]];
  // 转成数组形式的数据
  const json: any[][] = XLSX.utils.sheet_to_json(sheet1, {
    header: 1,
  });

  return json;
}

得到的结果是这样。可以看到 A 列因为是字符串,能读取正确。但是 B 列得到的是数字。这是因为 xlsx 文件存储日期就是数字。

改动下代码, 关键代码是 XLSX.read(ab, { cellDates: true })

ini 复制代码
async function xlsxToJSON(file: File): Promise<any[][]> {
  const ab = await file.arrayBuffer();
  const workbook = XLSX.read(ab, { cellDates: true });
  // 读取第一个 sheet
  const sheet1 = workbook.Sheets[workbook.SheetNames[0]];
  // 转成数组形式的数据
  const json: any[][] = XLSX.utils.sheet_to_json(sheet1, {
    header: 1,
  });

  return json;
}

得到的结果是这样。可以看到把日期数据读取成 Date 对象了。 但是好像日期不对。2023-10-01 读取出来是 2023-09-30。

了解后,是时区的问题。于是网络上找了资料和解法,得到这样的代码。

ini 复制代码
/**
 * 时区 bug
 * https://zhuanlan.zhihu.com/p/89914219
 * https://github.com/SheetJS/sheetjs/issues/2350
 */
const importBugHotfixDiff = (function () {
  function getTimezoneOffsetMS(date) {
    const time = date.getTime();
    const utcTime = Date.UTC(
      date.getFullYear(),
      date.getMonth(),
      date.getDate(),
      date.getHours(),
      date.getMinutes(),
      date.getSeconds(),
      date.getMilliseconds()
    );
    return time - utcTime;
  }

  const baseDate = new Date(1899, 11, 30, 0, 0, 0);
  const nD = new Date();
  const dnthreshAsIs =
    (nD.getTimezoneOffset() - baseDate.getTimezoneOffset()) * 60000;
  const dnthreshToBe = getTimezoneOffsetMS(nD) - getTimezoneOffsetMS(baseDate);
  return dnthreshAsIs - dnthreshToBe;
})();

function fixPrecisionLoss(date) {
  return new Date(date.getTime() - importBugHotfixDiff);
}

export { fixPrecisionLoss };

再改下一开始的代码是这样,关键代码是 fixPrecisionLossformatDate。其中 formatDate 是为了统一日期数据字符串。

ini 复制代码
async function xlsxToJSON(
  file: File,
  options?: xlsxToJSONOptions
): Promise<any[][]> {
  const { formatDate = defaultFormatDate } = options || {};

  const XLSX = await importXLSX();

  const ab = await file.arrayBuffer();
  // cellDates 日期转换成 Date
  const workbook = XLSX.read(ab, { cellDates: true });
  // 读取第一个 sheet
  const sheet1 = workbook.Sheets[workbook.SheetNames[0]];
  // 转成数组形式的数据
  const json: any[][] = XLSX.utils.sheet_to_json(sheet1, {
    header: 1
  });

  // 日期数据转换,修复时区的bug。
  json.forEach((row, rowIndex) => {
    row.forEach((cell, colIndex) => {
      if (cell instanceof Date) {
        const nd = fixPrecisionLoss(cell);
        json[rowIndex][colIndex] = formatDate(nd);
      }
    });
  });

  return json;
}

得到的结果是这样。大功告成。

写能力

上代码

css 复制代码
JSONToXlsx([["日期"], ["2023-10-01"], [new Date("2023-10-01")]]);
ini 复制代码
async function JSONToXlsx(json: any[][], options?: JSONToXlsxOptions) {
  const {
    filename = "date_file.xlsx",
    sheetName = "sheet1",
    formatDate = defaultFormatDate
  } = options || {};

  const XLSX = await importXLSX();

  const nJson = cloneDeep(json);

  // 日期数据转换,修复时区的bug。
  nJson.forEach((row, rowIndex) => {
    row.forEach((cell, colIndex) => {
      if (cell instanceof Date) {
        nJson[rowIndex][colIndex] = formatDate(cell);
      }
    });
  });

  const ws = XLSX.utils.aoa_to_sheet(nJson);

  ws.A2.c = [
    {
      t: "这里错啦,这里错啦"
    }
  ];
  ws.A2.s = {
    font: {
      color: {
        rgb: "FF0187FA"
      }
    }
  };

  console.log("ws", ws);

  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, sheetName);

  XLSX.writeFile(wb, filename, {
    cellStyles: true
  });
}

得到的结果是

批注实现了。 但是标红并没有成功。后来找了很久才知道需要 xlsx Pro 即付费版本才支持修改颜色。

于是得寻找其他方案

选型 2-exceljs

最终代码和 Demo 在 codesandbox.io/s/zealous-v...

得到的结果是

由于 exceljs 和 xlsx 关注的问题都类似,只是 API 不一样而已,所以这里不打算讲的太细,更多还是看代码吧。

这里讲写关键点。

1 exceljs 同样存在时区问题,解决时区的方法见 utils 中的 numberToDate方法。

2 exceljs 对于日期类型返回 number (xlsx 可调整参数返回 Date),所以需要调用方告知那一列数据需要做转换。具体见 XlsxToJSONOptions 的 isDateCell。

3 exceljs 导出对于 Date 对象也是按字符串到处 YYYY-MM-DD。

4 exceljs 更原始,读文件需要额外使用 FileReader 处理,写文件需要额外使用 file-saver Blob 处理。

5 写文件的注意文件名的约束。比如需要做一些处理规避特殊字符 fileName.replace(/[<>\:;?/*|]/g, "-")

相关推荐
ZJ_.10 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营14 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood40 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端41 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248942 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235612 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O4 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js