在 vue3 + vite 项目里用 xlsx-style 做 Excel 导入导出,算得上是后台管理系统里绕不开的老操作了。可问题是,这个老插件在新项目里一装一引就报错,而且报错还五花八门,从process is not defined到fs is not defined都有。我在两个项目里分别踩过这些坑,这次把排查过程、根因分析和可直接抄的方案完整写出来,如果你正好被 xlsx-style 卡住,按下面这几步处理基本就能顺利导出。
1. 为什么扯上 vite 就报错
1.1 先看三个高频报错现场
在 vue3 + vite 项目里,只要执行npm install xlsx-style,然后写一行:
import XLSX from 'xlsx-style'大概率会在浏览器控制台看到下面这些报错中的一种。
Uncaught ReferenceError: process is not defined at xlsx.js:...Module "fs" has been externalized for browser compatibility. Cannot access 'fs' in client codeUncaught TypeError: Cannot read properties of undefined (reading 'utils')第一次遇到这种报错,很多人会觉得是 vite 配置有问题,或者 vue3 版本不兼容。实际上这三条报错指向的是同一个病根:xlsx-style 这个库的代码还停留在 CommonJS 时代,内部直接用了 Node 核心模块,而 vite 在做依赖预构建和浏览器端打包时,默认不会替这些模块做 polyfill。
1.2 xlsx-style 的老底
xlsx-style 是从 SheetJS 远古版本里 fork 出来的一个样式扩展库,核心功能是在xlsx基础上增加了单元格样式支持,比如字体、边框、背景色、对齐方式、合并单元格。当年用 webpack 打包时,webpack 会帮开发者在浏览器环境里补齐一部分 Node 模块,所以很多人没怎么感觉到异常。但 vite 的设计理念是“原生 ESM、按需预构建、浏览器能跑就不 polyfill”,它不会像 webpack 那样默认注入fs、crypto、stream这些包。
更麻烦的是 xlsx-style 的 npm 包最后发布停留在 0.8.0,内部还依赖了老版本的xlsx和cptable。在浏览器里cptable经常会出现未定义的情况,这个错误有时候藏在深层模块里,报错信息很难一眼看懂。
还有个细节:xlsx-style 的入口文件是 CommonJS 格式,Vite 预构建时虽然能用@rollup/plugin-commonjs转译,但转译只解决模块格式,不解决 Node 核心模块的引用问题。于是 vite 依赖预构建阶段就会冒出Module "fs" has been externalized for browser compatibility,这就是告诉你:fs这个模块在浏览器代码里不能访问。
1.3 为什么不是 vue3 的问题
我一开始也怀疑是 vue3 的响应式代理把 xlsx 对象弄坏了,特意写了个最小 demo 验证,结果发现纯import就报错了,还没轮到组件逻辑。所以这个问题的定位顺序很重要:先确认是不是库本身构建不兼容,再去看是不是 vue3 使用方式的问题。xlsx-style 在 vite 生态里的问题,属于构建工具和旧库之间的摩擦,不是ref、reactive或者生命周期钩子能影响的。
2. 四个解决思路对比
2.1 思路一:换社区维护的 fork 版本
目前最省事的方式是直接换成xlsx-js-style。这个库是社区对xlsx-style的兼容 fork,API 基本一致,同样支持单元格样式,并且把 Node 核心模块的依赖处理得干净很多。对大部分 vue3 + vite 项目来说,安装后可以直接替换,不用改业务代码。
npm install xlsx-js-style引入时改成:
import * as XLSX from 'xlsx-js-style'后面生成工作簿、设置单元格样式、写文件的代码,跟xlsx-style几乎完全相同。这个方案我最推荐,也是我后来的首选。
2.2 思路二:用 vite 插件打 polyfill
如果因为某些原因必须使用原来名字的xlsx-style,也可以考虑给 vite 加 polyfill 插件。社区里有现成的vite-plugin-node-polyfill,它会像 webpack 那样给浏览器环境补齐一部分 Node 内置模块。安装后配置到 vite 插件里:
import { nodePolyfills } from 'vite-plugin-node-polyfill' export default defineConfig({ plugins: [nodePolyfills()] })这个方案的优点是改动小,但我测下来并不算完美。xlsx-style 报错不只来自fs,还有全局变量process、Buffer以及内部cptable的不确定性。polyfill 插件能解决一部分问题,但遇到深层代码里的细节,还是得继续打补丁。另外,polyfill 会引入较多额外的 polyfill 代码,打包体积会变大,项目中如果还有别的旧库,这样做容易把问题搞复杂。
2.3 思路三:patch-package 直接改源码
另一个思路是把 xlsx-style 的源码拉下来,手动改掉 Node 核心模块的引用,然后用 patch-package 固化补丁。这样做的好处是包名不变、API 不变,适合存量代码已经到处import XLSX from 'xlsx-style',又不想大规模替换的历史项目。
缺点是补丁可能因为安装路径、npm 版本或者不同 node_modules 结构出现偏差,而且对不熟悉源码结构的人来说,第一眼找不到改哪里。后面我会在实操章节里完整演示一次。
2.4 思路四:用 URL 参数方式规避
还有一个投机取巧的办法是绕开 exce 导出时用到的cptable,只使用xlsx官方库的新版本,然后自己给单元格加样式再序列化。但这样等于把 xlsx-style 的样式逻辑重写一遍,工程量不小,我不建议普通业务场景这么做。
四种方案放在一起对比:
| 方案 | 维护成本 | 是否改源码 | 推荐场景 |
|---|---|---|---|
| xlsx-js-style | 低 | 不需要 | 新项目、可改包名的存量项目 |
| vite-plugin-node-polyfill | 中 | 不需要 | 临时绕开报错、快速验证 |
| patch-package 改源码 | 中高 | 需要 | 包名不能变的存量项目 |
| 重写样式逻辑 | 高 | 不适用 | 有特殊定制需求的项目 |
3. 实操:把导出功能完整跑起来
3.1 搭一个最小复现环境
先用 Vite 创建一个干净的 vue3 项目,这样能排查出是不是业务代码导致的额外问题。
npm create vue@latest demo-export cd demo-export npm install npm install xlsx-js-style最小环境下,我在src/components/ExportButton.vue里写一个按钮,点击后直接导出 Excel。这样一旦报错,能很快判断是库的问题还是业务逻辑的问题。
3.2 封装一个带样式的导出工具
下面这个工具函数是我在实际项目里裁剪出来的版本,支持表头背景色、边框、对齐方式、列宽和自动换行。使用xlsx-js-style时,样式对象的写法和xlsx-style一致。
import * as XLSX from 'xlsx-js-style' const headerStyle = { font: { name: '微软雅黑', sz: 11, bold: true, color: { rgb: 'FFFFFFFF' } }, fill: { fgColor: { rgb: 'FF4472C4' } }, alignment: { horizontal: 'center', vertical: 'center' }, border: { top: { style: 'thin', color: { rgb: 'FF000000' } }, bottom: { style: 'thin', color: { rgb: 'FF000000' } }, left: { style: 'thin', color: { rgb: 'FF000000' } }, right: { style: 'thin', color: { rgb: 'FF000000' } } } } const bodyStyle = { font: { name: '微软雅黑', sz: 11 }, alignment: { vertical: 'center' }, border: { top: { style: 'thin', color: { rgb: 'FFCCCCCC' } }, bottom: { style: 'thin', color: { rgb: 'FFCCCCCC' } }, left: { style: 'thin', color: { rgb: 'FFCCCCCC' } }, right: { style: 'thin', color: { rgb: 'FFCCCCCC' } } } } export function exportExcel({ columns = [], rows = [], filename = '导出.xlsx' }) { const header = columns.map((col) => col.title) const data = rows.map((row) => columns.map((col) => (row[col.key] === undefined || row[col.key] === null ? '' : row[col.key])) ) const sheetData = [header, ...data] const ws = XLSX.utils.aoa_to_sheet(sheetData) ws['!cols'] = columns.map((col) => ({ wch: col.width || 12 })) const range = XLSX.utils.decode_range(ws['!ref']) for (let col = range.s.c; col <= range.e.c; col++) { const headerAddr = XLSX.utils.encode_cell({ r: 0, c: col }) if (ws[headerAddr]) { ws[headerAddr].s = headerStyle } for (let row = 1; row <= range.e.r; row++) { const bodyAddr = XLSX.utils.encode_cell({ r: row, c: col }) if (ws[bodyAddr]) { ws[bodyAddr].s = bodyStyle } } } const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, 'Sheet1') XLSX.writeFile(wb, filename) }这里我特意用aoa_to_sheet而不是json_to_sheet,是因为aoa_to_sheet接受二维数组,方便我单独设置表头文案,同时保持列顺序稳定。json_to_sheet虽然写起来更简单,但遇到自定义中文列名、字段顺序调整时,反而要多做一次 map。
3.3 在 vue3 组件里触发导出
在组件里调用封装好的方法:
<script setup> import { ref } from 'vue' import { exportExcel } from '../utils/exportExcel' const list = ref([ { id: 1, name: '张三', amount: 298.5 }, { id: 2, name: '李四', amount: 1099 } ]) function handleExport() { exportExcel({ columns: [ { title: '编号', key: 'id', width: 8 }, { title: '姓名', key: 'name', width: 16 }, { title: '金额', key: 'amount', width: 12 } ], rows: list.value, filename: '人员列表.xlsx' }) } </script> <template> <button @click="handleExport">导出 Excel</button> </template>如果项目里有 Element Plus,直接把按钮替换成el-button就行,导出逻辑完全一样。关键是导出工具函数不要和 UI 组件耦合,后续可以复用到多个页面。
3.4 如果非要用 xlsx-style,怎么打补丁
假设项目里已经到处使用xlsx-style,暂时没时间改包名,可以考虑打补丁。我这里演示 patch-package 的完整流程,你可以对着操作。
第一步,安装 patch-package:
npm install patch-package --save-dev第二步,打开node_modules/xlsx-style/xlsx.js,搜索require('fs')、require('crypto')和require('stream')这类 Node 内置模块引用。我遇到的实际版本里,常见做法是把它们直接置空或注释掉。
- var fs = require('fs'); - var crypto = require('crypto'); + var fs = undefined; + var crypto = undefined;cptable相关代码在浏览器里也会出问题,可以找到类似下面的位置:
- if (typeof cptable == 'undefined') cptable = require('./cptable'); + if (typeof cptable == 'undefined' && typeof window === 'undefined') cptable = require('./cptable');第三步,修改后先在浏览器里跑通,确认没有报错,然后执行:
npx patch-package xlsx-style这会在项目根目录生成patches/xlsx-style+0.8.0.patch文件。
第四步,在package.json的 scripts 里加上:
"postinstall": "patch-package"这样团队成员执行npm install后,补丁会自动应用。要注意,不同 Node 版本、不同 npm 安装策略可能导致node_modules结构变化,如果补丁应用失败,可以用npx patch-package重新生成。
4. 常见报错与排查技巧实录
4.1 报错速查表
我把实际排查中遇到的几类问题整理成一张表,方便快速对号入座。
| 报错信息 | 根因 | 解决方式 |
|---|---|---|
process is not defined | 代码引用了 Node 全局变量 process,vite 未注入 polyfill | 换 xlsx-js-style,或配置 vite polyfill |
Module "fs" has been externalized for browser compatibility | 库内部引用了 Node 核心模块 fs | 用 fork 版本,或 patch 掉 fs 引用 |
cptable is not defined | xlsx-style 内部老依赖 cptable 未正确加载 | 换库,或对 cptable 做兼容处理 |
Cannot read properties of undefined (reading 'utils') | 默认导入方式不正确,模块解析到 CommonJS 导出对象上 | 改成import * as XLSX from '...' |
Buffer is not defined | 库内部使用 Buffer 构造二进制数据 | 使用vite-plugin-node-polyfill,或换成兼容 fork |
| vite build 打包报错,dev 环境却正常 | rollup 在构建阶段对模块分析更严格,暴露隐藏的 Node 引用 | 按前面方案处理,并清理.vite缓存后重新构建 |
4.2 我的排查顺序
遇到xlsx-style报错,我习惯按下面的顺序排查,效率最高。
第一,先确认报错是发生在import语句,还是发生在调用导出的运行时。如果 import 就报错,基本就是库本身和 vite 不兼容;如果运行时才报错,可能是样式对象写法有问题或者book_append_sheet参数传错。
第二,查看依赖树:
npm ls xlsx npm ls xlsx-style如果同一项目里同时出现xlsx、xlsx-style、xlsx-js-style,非常容易出问题。比如xlsx-style内部依赖老版xlsx,而业务代码又直接安装了新版xlsx,两个实例混在一起,会导致导出的文件内容正常但样式丢失,或者出现诡异报错。
第三,打开浏览器的 Sources 面板,把报错点定位到具体文件,看它是来自node_modules/.vite的预构建产物,还是来自业务代码。如果是预构建产物里的代码报错,可以试试删除node_modules/.vite缓存目录再重启 dev server。
第四,检查 vite 配置里有没有把xlsx-style排除出预构建:
export default defineConfig({ optimizeDeps: { exclude: ['xlsx-style'] } })有时候 xlsx-style 这种老库在预构建时会被转译出问题,把它排除掉反而能保持 CommonJS 原样,配合@rollup/plugin-commonjs一起处理。这个办法不是万能,但值得一试。
4.3 避坑清单
第一,不要同时安装多个 xlsx 变体。xlsx、xlsx-style、xlsx-js-style的模块结构不完全一样,底层用到的utils对象可能是不同副本,混用轻则样式丢失,重则直接报错。一个项目里尽量只保留一个导出库。
第二,如果你只需要最简单的表格导出,没有单元格样式、合并单元格、字体颜色这些需求,直接用官方xlsx就够了,不要为了一个样式功能引入一个老库给自己添堵。
第三,中文文件名的导出,在 Windows 环境下偶尔会出现乱码。推荐在writeFile前用XLSX.write生成 Buffer,再通过 Blob 下载,或者直接给文件名拼上\ufeff前缀。但这个方案在不同浏览器里的表现有差异,稳妥起见,文件名保持中文其实问题不大,更常见的是单元格内容里中文乱码,那就是编码声明的问题,可以在生成 workbook 时设置bookType: 'xlsx',再用type: 'buffer'输出。
第四,样式数量较多时,导出性能会下降。尤其是几百行、每行循环给单元格赋样式,可能会明显卡顿。我的做法是:如果行数超过 500 行,只给表头加样式,正文不加边框;如果超过 1000 行,连表头都只加粗,不填充背景色。这样能显著缩短导出时间。
4.4 一个另类的排查技巧
如果某个报错在 dev 环境不出现,只在npm run build后出现,可以先执行:
npm run build -- --debug或者用npx vite build加--watch观察构建输出。vite 构建时 rollup 对 CommonJS 模块的处理比 dev 模式更严格,容易暴露出一些隐藏的require调用。遇到这种情况,我先看构建日志里有没有externalized字样,再针对性用 polyfill 或补丁解决。
5. 我最终在项目里的方案和体会
5.1 两个项目的不同选择
第一个项目是维护多年的旧后台系统,代码里到处是import XLSX from 'xlsx-style',大概有三四个模块都在用。直接换包名风险有点大,我当时选择的是 patch-package 补丁,把fs和crypto引用处理掉后,dev 和 build 都跑通了,样式也正常。
第二个项目是全新启动的管理端,没有任何历史包袱,我直接用了xlsx-js-style。整体体验顺畅很多,不需要处理补丁,也不用担心cptable这种隐藏依赖。从成本角度看,新项目用 fork 版本明显更划算。
5.2 后续可以扩展的思路
如果你只是被导出功能卡住,可以先按上面的最小 demo 跑通再迁移业务代码。后续如果要导出几千行甚至几万行数据,可以把 Excel 生成逻辑放到 Web Worker 里,避免阻塞主线程。xlsx-js-style这类纯 JS 库在 Worker 里也能正常跑,只是需要额外处理 Worker 的打包问题。
另外提醒一句:xlsx-style 这类老样式库的样式能力并不是 Excel 全量支持,像条件格式、数据验证这些复杂功能它处理不了。如果业务需求到了那个程度,直接考虑exceljs之类的库更合适,不要试图在 xlsx-style 上死磕。
最后再分享一个小技巧:换库后如果发现某些单元格边框少了,先别急着怀疑库有问题,检查一下样式对象里border的四个方向是不是都写全了,尤其是left和right。这是我踩过最多的地方,导出工具函数里的默认样式如果能统一管理,后面维护会轻松很多。