Vue3+Vite项目使用xlsx-style导出Excel报错解决指南
在 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的兼容 forkAPI 基本一致同样支持单元格样式并且把 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 vuelatest 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 clickhandleExport导出 Excel/button /template如果项目里有 Element Plus直接把按钮替换成el-button就行导出逻辑完全一样。关键是导出工具函数不要和 UI 组件耦合后续可以复用到多个页面。3.4 如果非要用 xlsx-style怎么打补丁假设项目里已经到处使用xlsx-style暂时没时间改包名可以考虑打补丁。我这里演示 patch-package 的完整流程你可以对着操作。第一步安装 patch-packagenpm 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-style0.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 全局变量 processvite 未注入 polyfill换 xlsx-js-style或配置 vite polyfillModule fs has been externalized for browser compatibility库内部引用了 Node 核心模块 fs用 fork 版本或 patch 掉 fs 引用cptable is not definedxlsx-style 内部老依赖 cptable 未正确加载换库或对 cptable 做兼容处理Cannot read properties of undefined (reading utils)默认导入方式不正确模块解析到 CommonJS 导出对象上改成import * as XLSX from ...Buffer is not defined库内部使用 Buffer 构造二进制数据使用vite-plugin-node-polyfill或换成兼容 forkvite 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。这是我踩过最多的地方导出工具函数里的默认样式如果能统一管理后面维护会轻松很多。