简介:这份资源面向需要在浏览器端将网页表格导出为Excel并保留样式的开发者,尤其适合使用谷歌浏览器、希望快速落地导出功能的前端人员。内容围绕两种样式保留思路展开:一是在td行内直接写style,二是把CSS规则写入导出模板,并给出可直接运行的完整代码与tableToExcel函数实现,帮助读者理解Blob、base64与模板拼接的配合方式。资源包共1个文件,为PDF文档,大小约62KB,篇幅精炼,便于快速查阅与对照练习。目前已有2077人学习下载,说明该方案在实际项目中具有一定参考价值。读者可从中获得可复用的导出脚本、样式保留的取舍思路以及常见局限说明,例如复杂CSS在部分Excel版本中可能无法完全还原,并了解js-xlsx、SheetJS等第三方库的进阶方向,适合作为表格导出功能的入门与排错参考。
1. 前端表格导出 Excel 保留样式:为什么原生 API 总是丢边框和背景色
做过管理后台的同行大概率都遇到过这个场景:产品经理指着页面上一张带斑马纹、表头深色背景、金额列红色高亮的 table,说「导出成 Excel,要和页面长得一样」。你打开搜索,抄了一段Blob+table.innerHTML的代码,导出成功,但打开文件一看——边框没了,背景色没了,列宽全乱,中文还偶尔乱码。这不是你代码写错了,而是路线选错了。
浏览器原生能做的,是把 HTML 表格序列化成一个.xls后缀的 HTML 文件,Excel 打开时靠自身的 HTML 解析器去还原。它只认最基础的border、bgcolor这类上古属性,对 CSS 里的background-color、border-collapse、padding基本视而不见。想真正保留样式,必须换一条路:用 SheetJS 这类库生成真正的 xlsx,再叠加样式层。这篇就把这条链路从选型、实现、参数到踩坑完整走一遍,适合正在做后台导出、报表下载、对账文件的前端和全栈同学。
2. 选型先想清楚:HTML 直出、SheetJS 还是 ExcelJS
2.1 三条路线的能力边界对比
在动手前,先把可选方案摆到台面上。很多人一上来就搜「JS 导出 Excel」,拿到的答案五花八门,本质上是三类完全不同的技术路线,能力上限差得很远。
| 方案 | 原理 | 样式支持 | 体积 | 适用场景 |
|---|---|---|---|---|
| HTML 直出 | table 序列化为 HTML,伪装 xls | 仅 border/bgcolor | 0 依赖 | 纯数据、无样式要求 |
| SheetJS (xlsx) | 生成标准 xlsx 二进制 | 社区版样式弱,需 Pro | ~400KB | 数据导出为主 |
| ExcelJS | 生成标准 xlsx,完整样式 API | 字体/填充/边框/列宽全支持 | ~900KB | 报表、对账、需还原样式 |
结论很直接:只要标题里出现「保留样式」四个字,HTML 直出就可以直接排除,SheetJS 社区版在样式上也会让你难受——它的cell.s样式对象在开源版本里对填充和边框的支持并不完整,很多写法在导出后不生效。真正能稳定还原表头背景、单元格边框、字体颜色的,是 ExcelJS。
那 SheetJS 是不是就没用了?也不是。如果只是导数据、对性能敏感、包体积卡得死,SheetJS 依然是首选。选型的判断标准就一条:样式是不是硬需求。是,就 ExcelJS;不是,SheetJS 更轻。
2.2 为什么样式必须走 xlsx 二进制而不是 HTML
这里补一句原理,理解了就不会再走回头路。xlsx 本质是一个 zip 包,里面是若干 XML:xl/styles.xml定义所有样式,xl/worksheets/sheet1.xml定义单元格内容和它引用的样式索引s。ExcelJS 做的事,就是帮你把这些 XML 结构用 JS 对象描述出来,再打包成 zip。
而 HTML 直出根本没有 styles.xml 这一层,Excel 打开时只能用它内置的 HTML 兼容解析器去猜,猜得到 border 就算运气好,猜不到 background-color 是必然的。所以「保留样式」这个需求,从原理上就决定了必须生成真正的 xlsx。这也是为什么很多同学换了三四个库,样式还是丢——路线没对,换库无用。
3. 用 ExcelJS 在浏览器里跑通最小导出
3.1 安装与最小可运行代码
先装依赖。ExcelJS 同时支持 Node 和浏览器,浏览器端直接用打包工具引入即可。
npm install exceljs file-saver下面是一段能在浏览器里直接跑通的最小代码,导出一张带表头背景和边框的表。
import ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; async function exportTable(tableEl, fileName = 'export.xlsx') { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet('Sheet1'); // 1. 读取页面 table 的表头和表体 const rows = Array.from(tableEl.querySelectorAll('tr')); const data = rows.map(tr => Array.from(tr.querySelectorAll('th,td')).map(td => td.innerText.trim()) ); // 2. 写入数据 data.forEach(row => sheet.addRow(row)); // 3. 给表头加样式:深色背景 + 白色加粗字体 + 居中 const headerRow = sheet.getRow(1); headerRow.eachCell(cell => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF2F54EB' } // 注意是 ARGB,前面两位是透明度 }; cell.font = { color: { argb: 'FFFFFFFF' }, bold: true, size: 12 }; cell.alignment = { vertical: 'middle', horizontal: 'center' }; cell.border = { top: { style: 'thin', color: { argb: 'FFD9D9D9' } }, left: { style: 'thin', color: { argb: 'FFD9D9D9' } }, bottom: { style: 'thin', color: { argb: 'FFD9D9D9' } }, right: { style: 'thin', color: { argb: 'FFD9D9D9' } } }; }); // 4. 生成 buffer 并触发下载 const buffer = await workbook.xlsx.writeBuffer(); saveAs(new Blob([buffer]), fileName); }逻辑说明:addRow按行写入,第一行默认就是表头;getRow(1)拿到表头行后逐格设置样式。参数上最容易翻车的是颜色格式——ExcelJS 用的是ARGB,FF2F54EB里前两位FF是不透明度,写成2F54EB六位会直接报错或渲染异常。border的四个方向必须分别写,只写bottom就只有下边框。
3.2 列宽、行高与合并单元格怎么设
数据能导出了,接下来是让版式接近页面。列宽和行高不设,Excel 会按默认宽度铺开,长文本挤成一团。
// 按页面列宽估算,Excel 列宽单位约等于字符数 sheet.columns = [ { width: 12 }, // 序号列 { width: 24 }, // 名称列 { width: 18 }, // 金额列 { width: 30 } // 备注列 ]; // 行高,单位是磅 sheet.getRow(1).height = 28; // 合并单元格:从 A1 到 D1 合并做标题 sheet.mergeCells('A1:D1'); sheet.getCell('A1').value = '2024 年度对账单'; sheet.getCell('A1').alignment = { horizontal: 'center', vertical: 'middle' };参数说明:width是字符宽度,中文大约占 2 个字符宽,所以中文列要按字数乘 2 再留点余量。mergeCells之后只有左上角单元格能写值,其他格子的值会被清掉,这是 xlsx 的规范决定的,不是库的 bug。合并后如果还要加边框,得手动给合并区域的每个格子都设一遍,否则只有左上角有框。
3.3 从页面 table 自动映射样式的思路
如果表格列很多,手写每一列的样式不现实。常见做法是遍历页面 DOM,把getComputedStyle拿到的背景色、字体色、对齐方式映射到 ExcelJS 的样式对象上。
function mapStyle(td) { const cs = getComputedStyle(td); return { fill: { type: 'pattern', pattern: 'solid', // rgb() 转 ARGB,补上 FF 不透明度 fgColor: { argb: 'FF' + rgbToHex(cs.backgroundColor) } }, font: { color: { argb: 'FF' + rgbToHex(cs.color) }, bold: parseInt(cs.fontWeight) >= 600 }, alignment: { horizontal: cs.textAlign === 'center' ? 'center' : 'left', vertical: 'middle' } }; }这里的关键是rgbToHex转换函数,把rgb(47, 84, 235)转成2F54EB。注意getComputedStyle返回的可能是rgba,带透明度的要单独处理,Excel 的填充不支持半透明,遇到rgba只能取近似实色。这套自动映射能覆盖 80% 的常规样式,剩下的特殊列再手动覆盖。
4. 大数据量与中文场景下的性能与编码处理
4.1 万行以上表格的分批写入与内存控制
ExcelJS 在浏览器里是纯内存操作,一万行以内问题不大,超过五万行就可能把标签页卡死甚至崩溃。血泪经验是:不要一次性addRow几万次,也不要在主线程里同步跑完。
const CHUNK = 2000; async function writeInChunks(sheet, data) { for (let i = 0; i < data.length; i += CHUNK) { const slice = data.slice(i, i + CHUNK); slice.forEach(row => sheet.addRow(row)); // 让出主线程,避免页面假死 await new Promise(r => setTimeout(r, 0)); } }参数说明:CHUNK取 1000 到 3000 之间比较稳,太小会让出太频繁拖慢总时长,太大又起不到防卡死的作用。如果数据量真的到了十万级,更靠谱的做法是把导出放到 Web Worker 里,主线程只负责接收 buffer 和触发下载,这样页面完全不会卡。
4.2 中文乱码与日期格式的两个必调项
中文乱码在 xlsx 路线里其实很少见,因为 xlsx 内部就是 UTF-8 的 XML。真正会出问题的是从 HTML 直出那条路,需要加 BOM 头。既然我们走的是 ExcelJS,重点应该放在日期和数字格式上。
// 日期列:写入 Date 对象,再设 numFmt sheet.getColumn(3).numFmt = 'yyyy-mm-dd'; sheet.getColumn(4).numFmt = '#,##0.00'; // 金额千分位保留两位 // 如果数据源是字符串日期,先转 Date const d = new Date('2024-01-15'); sheet.getCell('C2').value = d;参数说明:numFmt用的是 Excel 的格式代码,yyyy-mm-dd是日期,#,##0.00是千分位金额。如果直接把字符串'2024-01-15'写进去,Excel 会当文本处理,排序和求和都会失效。金额同理,写数字再设格式,不要写带逗号的字符串。
5. 导出保留样式的避坑清单:5 个真实翻车现场
5.1 现象:表头背景色导出来是黑的
原因:颜色写成了六位2F54EB,ExcelJS 按 ARGB 解析时把前两位当成了不透明度,剩下的位数错位,渲染成异常颜色。解决:所有颜色统一补FF前缀,写成FF2F54EB。
5.2 现象:边框只显示一部分,或者完全没有
原因:只给单元格设了bottom,或者合并单元格后只设了左上角。解决:四个方向分别写全;合并区域要给区域内每个格子都设边框,或者用sheet.getCell(range)遍历。
5.3 现象:导出的文件 Excel 提示「格式与扩展名不一致」
原因:文件后缀写成了.xls,但内容是 xlsx 二进制。解决:ExcelJS 生成的就是 xlsx,后缀必须用.xlsx,不要为了兼容老版本改后缀。
5.4 现象:大数据量导出时页面卡死几秒
原因:几万次addRow同步执行,阻塞主线程。解决:分批写入并await让出主线程,或直接上 Web Worker。
5.5 现象:中文列宽怎么调都显示不全
原因:按英文字符数估的列宽,中文实际占两个字符宽。解决:中文列宽按「字数 × 2 + 4」估算,或者干脆设一个偏大的固定值再让用户自己拖。
6. 把导出封装成可复用工具:一个进阶技巧
走到这里,导出功能基本能用了。但如果项目里有十几个页面都要导出,每个页面复制一遍样式代码就是灾难。我一般的做法是封装一个exportWithStyle(tableEl, options),把「读 DOM → 映射样式 → 分批写入 → 下载」整条链路收进去,页面只传一个 table 元素和文件名。
进阶技巧在于样式模板的抽离。把表头、斑马纹、金额列这些常用样式定义成配置对象,页面按需引用,而不是每次重写。
const STYLE_PRESETS = { header: { fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF2F54EB' } }, font: { color: { argb: 'FFFFFFFF' }, bold: true }, alignment: { horizontal: 'center', vertical: 'middle' } }, zebra: { fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF5F5F5' } } }, money: { numFmt: '#,##0.00', alignment: { horizontal: 'right' } } }; function applyPreset(cell, preset) { Object.assign(cell, STYLE_PRESETS[preset]); }这样页面里只需要声明「第 1 行用 header,偶数行用 zebra,第 4 列用 money」,样式逻辑集中在一处,改一次全局生效。
验证方法也很简单:导出后不要只看文件能不能打开,一定要用 Excel 的「检查」或者直接解压 xlsx 看xl/styles.xml,确认样式索引真的写进去了。我踩过最深的坑就是代码没报错、文件能打开,但样式静默丢失,最后靠解压对比 XML 才定位到是颜色格式写错。
一个习惯送给你:任何导出功能上线前,我都会拿三种数据测一遍——空表格、超长中文、一万行以上。这三种能过,基本就不会在用户那里翻车。希望帮到你。
本文还有配套的精品资源,点击获取