ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端Table带样式导出Excel:HTML壳与xlsx-js-style实践

前端Table带样式导出Excel:HTML壳与xlsx-js-style实践 简介在后台管理系统与数据报表场景中将页面表格导出为带样式的Excel文件是一个常见诉求。虽然CSV与纯XLSX导出简单但往往丢失边框、背景色、表头样式等关键信息。理解XLSX文件内部结构与样式存储原理styles.xml中的cellXfs后可实现真正保留格式的导出方案。工程实践中基于Excel内置HTML解析能力的HTML .xls方案配合getComputedStyle动态内联化页面样式能快速输出与页面同款的报表而xlsx-js-style则提供标准XLSX的逐格样式控制适合对文件格式有严格要求的下游数据交换场景。两种方案覆盖“给人看”和“给程序读”的典型应用本文围绕这两条路径介绍具体实现、参数边界与样式映射方法。1. 把页面 table 带样式导成 Excel先想清楚要哪一种导出后台管理系统里最常见的需求不是导出数据是导出一份给领导看的 Excel 表。页面上 Table 表头蓝底白字加粗状态列带彩色标签交付时长出现最多的一句话是样式丢了。多数人第一版用 SheetJS但官方社区版没有样式写入参数aoa_to_sheet只把值写进单元格导出的文件白底黑字、列宽挤在一起、长单号变成科学计数法。保留样式有两条落地路径一条利用 Excel 内置的 HTML 解析能力把页面 table 连同内联 CSS 包成 .xls另一条用社区维护的 xlsx-js-style在标准 XLSX 里逐格写样式对象。前者适合给人看的报表后者适合给程序读的文件下面分别给完整代码、参数说明和边界条件。2. JS 导出 Excel 的三条路线与样式保真度对比动手写代码之前先花十分钟确认三个问题文件最终用什么软件打开Excel 桌面版、WPS 还是下游程序解析、样式要保到哪一层字体颜色、边框、合并单元格还是列宽、浏览器兼容范围到哪。这三个答案决定了走哪条路线。这一章把 CSV、HTML 壳 .xls、xlsx-js-style 三条路线放一起对比说清楚各自能保什么、不能保什么以及为什么完整的样式信息在 XLSX 文件里是需要专门写入的。2.1 为什么 CSV 和普通 xlsx 导出会丢样式CSV 本质是逗号分隔的纯文本Excel 打开时按默认格式渲染字体、边框、背景色这些信息在格式里根本无法表达中文编码不对还会乱码。它适合做数据备份和系统间回传不适合做人看的报表。普通 xlsx 丢样式的原因在文件结构里。XLSX 是一个 zip 包单元格值写在xl/worksheets/sheet1.xml的c节点里每个c有可选的s属性指向xl/styles.xml中cellXfs数组的索引!-- 无样式s 属性缺省所有单元格用第 0 个默认样式 -- c rA1 tsv订单号/v/c !-- 有样式s1 指向 styles.xml 里定义的加粗字体与边框组合 -- c rA1 ts s1v订单号/v/cSheetJS 官方社区版在 write 阶段只写值和!cols列宽不会生成带格式的s索引所以打开后全是默认样式。这是社区版的能力边界不是写法问题理解这一点再遇到为什么文档里没有 style 参数这类疑问就不会兜圈子。2.2 HTML 壳 .xls利用 Excel 内置的表格解析能力Excel 从 97 到现在的桌面版都保留了打开 HTML 文件的能力只要文件是带 table 结构的 HTML 文档即使扩展名是 .xlsExcel 也会按表格解析并保留内联 CSS 里的字体、边框、背景色、文本对齐和行高列宽。这套做法在前端领域用了十几年很多老系统导出的Excel本质就是这种 HTML 文件。它的限制同样明显只认内联 style 和有限的一套 CSS 属性class 里定义的样式、Vue 组件里写的 scoped 样式都不认不支持渐变背景和阴影文件本质是 HTMLWPS 或个别版本 Excel 的解析会有细微差异。所以它最适合页面有现成 table、样式不复杂的管理系统报表不适合对文件格式有严格校验、必须切到真实单元格类型的下游场景。2.3 xlsx-js-style社区分支补上标准 XLSX 的样式通道如果对方明确要求标准 .xlsx 文件比如下游系统要读取单元格内容或做数据校验HTML 壳就不合适了。常见做法是换用 xlsx-js-style这是 SheetJS 的社区分支保持writeFile、aoa_to_sheet等 API 兼容的同时在 write 阶段把每个单元格上的s样式对象序列化进 styles.xml 的 fonts、fills、borders 和 cellXfs。用起来和 SheetJS 几乎一样只是 npm 包名从xlsx换成xlsx-js-style并且单元格可以挂样式对象。注意它和官方版是两个发布源同一项目混装会出问题如果代码里已经引了官方 SheetJS要整体替换而不是两个都保留。三条路线的取舍对比如下路线输出格式样式保真度中文处理文件体积典型场景CSV.csv 纯文本无需手动加 BOM最小数据备份、系统间回传HTML 壳 .xls.xls内容为 HTML中字体、边框、背景、列宽需手动加 BOM中管理系统报表、与页面同款xlsx-js-style.xlsx 标准格式高字体、填充、边框、对齐、合并、行高列宽直接 UTF-8较大下游解析、交付文件选型标准其实就一条文件给人看还是给程序读。给人看HTML 壳最快当天能上线给程序读或要求真实 xlsx 后缀和纯数字单元格直接上 xlsx-js-style。后面两章分别给这两条路线的完整实现和参数坑位。3. 用 HTML 壳导出带样式的 .xls从 DOM 拿样式到输出文件的完整代码这一章给一个能直接粘进项目的exportTableAsExcel函数并说明每一步在干什么。核心思路三步克隆页面 table 并给单元格补内联样式套上 Excel 能识别的 HTML 外壳用 Blob 触发下载并处理中文乱码。3.1 直接 table.outerHTML 导出的样式是丢的很多人的第一版是这样const html table.outerHTML包上 Blob 就下载。结果导出的文件连边框都没有。原因是页面样式绝大多数挂在 class 上CSS 规则不会出现在 outerHTML 里Vue 的 scoped CSS 还会给选择器加 data 属性而 Excel 的 HTML 解析器不加载任何外部样式表只认元素上的内联 style。所以第一步先做内联化遍历克隆节点里的 td/th用getComputedStyle把渲染后的样式写回 style 属性。function inlineTableStyles(clone) { const cells clone.querySelectorAll(td, th); cells.forEach(function (cell) { const cs getComputedStyle(cell); // border 拆成三个值拼比直接读简写属性兼容性好 cell.style.border cs.borderTopWidth cs.borderTopStyle cs.borderTopColor; cell.style.backgroundColor cs.backgroundColor; cell.style.color cs.color; cell.style.fontWeight cs.fontWeight; cell.style.textAlign cs.textAlign; cell.style.padding cs.padding; }); return clone; }说明getComputedStyle返回的 border 在浏览器里拆成 width/style/color 三个值拼回去是和原页面一致的字符串原单元格没设边框时 borderTopStyle 是 none拼出来也不会多出线条。不必把所有样式都塞进去Excel 对超出它理解范围的属性反而会解析不稳定下表是实际验证过能被识别的字段范围内联样式字段Excel 是否识别备注border、border-top/bottom/left/right识别颜色不支持 transparent 或 rgbabackground-color识别只认纯色渐变和图片忽略color、font-weight、font-style识别与 CSS 语义一致text-align、vertical-align识别分别对应水平、垂直对齐padding部分识别能撑开行高但像素换算不精确line-height、letter-spacing忽略Excel 里无对应概念3.2 Excel 外壳、工作表名与 UTF-8 BOMExcel 对 HTML 文件的识别依赖几个约定最外层必须是完整 html 文档head 里的 mso 条件注释用来声明工作表名x:Name标签内容就是 Excel 左下角的 sheet 名meta charsetUTF-8负责字符集声明。中文表名可以直接写但两个 sheet 重名时 Excel 会提示修复所以表名要保证唯一。中文乱码是另一个高频问题Blob 构造时把 UTF-8 BOM\ufeff放在 HTML 字符串最前面绝大多数 Excel 和 WPS 能按 UTF-8 解码否则某些系统会用本地编码打开导致整表乱码。BOM 只影响文件头一个字节对 HTML 内容本身无副作用。3.3 完整函数与参数说明function exportTableAsExcel(selector, options) { const opts options || {}; const filename opts.filename || 导出表格.xls; const sheetName opts.sheetName || Sheet1; const table document.querySelector(selector); if (!table) { console.warn(exportTableAsExcel: 未找到节点, selector); return; } const clone table.cloneNode(true); inlineTableStyles(clone); const html html xmlns:ourn:schemas-microsoft-com:office:office xmlns:xurn:schemas-microsoft-com:office:excel headmeta charsetUTF-8 / !--[if gte mso 9] xmlx:ExcelWorkbookx:ExcelWorksheetsx:ExcelWorksheet x:Name sheetName /x:Name x:WorksheetOptionsx:DisplayGridlines //x:WorksheetOptions /x:ExcelWorksheet/x:ExcelWorksheets/x:ExcelWorkbook/xml ![endif]--/headbody clone.outerHTML /body/html; const blob new Blob([\ufeff, html], { type: application/vnd.ms-excel;charsetutf-8 }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); }参数说明selector支持任意 querySelector 表达式#tableId最直接如果页面有多个小表格想合成一个 sheet先把它们拼到一个隐藏 table 里再传。filename必须以.xls结尾内容虽是 HTML但 Excel 会按兼容模式打开起成.xlsx会被按严格格式解析直接报文件损坏。提示cloneNode(true)只复制结构和内联 style复制不了虚拟滚动场景下尚未渲染的行。table 开启 LazyRender 时先让数据源全量渲染再导出。使用示例// 导出 antd Table 渲染出来的真实 DOM边框和表头背景由 getComputedStyle 内联化后带走 exportTableAsExcel(#report-table, { filename: 月度报表.xls, sheetName: 月报 });3.4 单元格内容类型避免公式识别和超长数字变形HTML 壳方案有个高频坑td 内容以开头时Excel 会当成公式执行导出的表可能出现错误值超过 11 位的数字会被转成科学计数法。常见做法是在数据列上标记类型内联化之后单独处理。给 td 加>// 只处理页面渲染时标记过的数值列 clone.querySelectorAll(td[data-typenumber]).forEach(function (cell) { const old cell.getAttribute(style) || ; cell.setAttribute(style, old ;mso-number-format:0.00;); });mso-number-format是 Excel 私有属性浏览器不认但会原样留在 style 字符串里Excel 解析 HTML 时读取它来控制显示格式。0.00表示固定两位小数想保留长单号就写成表示按文本显示。此方案只影响显示单元格在 Excel 里仍是文本或常规类型需要真实数字类型时应在数据源层面处理。4. 用 xlsx-js-style 逐格写样式表头、边框、合并单元格与列宽参数HTML 壳在数据量大、样式规则多、或必须交付真实 .xlsx 时会碰到天花板。xlsx-js-style 是更可控的路线值、样式、合并信息都写成结构化对象由库在 write 阶段生成合法的 XLSX 包。这一章先讲样式对象的字段结构再给一个带表头样式、数据行边框、列宽和合并单元格的完整函数最后说大表性能怎么取舍。4.1 样式对象结构font / fill / alignment / border 字段对照xlsx-js-style 里每个单元格的s属性是一个普通对象直接对应 styles.xml 里的组件。常用字段和取值分组字段可选值与说明fontboldtrue / falsefontsz字号数字如 11、12fontcolor.rgb8 位 ARGB 字符串如FF333333fillfgColor.rgb背景色同样写 8 位如FFF2F2F2alignmenthorizontalleft / center / rightalignmentverticaltop / center / bottomalignmentwrapTexttrue / false长文本自动换行bordertop / bottom / left / right{ style: thin, color: { rgb: FFCCCCCC } }style 取 thin / medium / thick / dashed / dottedz单元格属性数字格式0.00、yyyy-mm-dd等写在单元格对象上而不是 s 里两个容易翻车的地方fill 必须写fgColorpatternType 可省库默认按 solid 处理border 的 style 写错大小写或颜色不带 8 位会被整条忽略。颜色统一用带FF前缀的 ARGB 是兼容性最好的写法。4.2 表头和数据行分开上样式避免全表逐格构造单元格样式是逐格设置的一万行的表如果每格都新建独立样式对象文件膨胀且打开变慢。常见做法是定义两个共享样式对象表头一个、数据行一个。表头只循环一行数据行遍历!ref范围内非空单元格赋值同一个对象引用内存只多一份样式import XLSX from xlsx-js-style; function exportRowsToXlsx({ columns, rows, filename, sheetName }) { const ws XLSX.utils.aoa_to_sheet([]); const header columns.map(function (col) { return col.title; }); XLSX.utils.sheet_add_aoa(ws, [header], { origin: A1 }); XLSX.utils.sheet_add_aoa(ws, rows, { origin: A2 }); const headerStyle { font: { bold: true, sz: 12, color: { rgb: FFFFFFFF } }, fill: { fgColor: { rgb: FF2F54EB } }, alignment: { horizontal: center, vertical: center }, border: { top: { style: thin, color: { rgb: FFD9D9D9 } }, bottom: { style: thin, color: { rgb: FFD9D9D9 } }, left: { style: thin, color: { rgb: FFD9D9D9 } }, right: { style: thin, color: { rgb: FFD9D9D9 } } } }; const cellStyle { alignment: { vertical: center }, border: { top: { style: thin, color: { rgb: FFE8E8E8 } }, bottom: { style: thin, color: { rgb: FFE8E8E8 } }, left: { style: thin, color: { rgb: FFE8E8E8 } }, right: { style: thin, color: { rgb: FFE8E8E8 } } } }; header.forEach(function (_, c) { const addr XLSX.utils.encode_cell({ r: 0, c: c }); ws[addr].s headerStyle; }); const range XLSX.utils.decode_range(ws[!ref]); for (let r 1; r range.e.r; r) { for (let c 0; c range.e.c; c) { const addr XLSX.utils.encode_cell({ r: r, c: c }); if (!ws[addr]) { ws[addr] { t: s, v: }; } ws[addr].s cellStyle; } } ws[!cols] columns.map(function (col) { return { wch: col.width || 16 }; }); ws[!rows] [{ hpt: 26 }].concat( rows.map(function () { return { hpt: 22 }; }) ); const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, sheetName || Sheet1); XLSX.writeFile(wb, filename || 导出.xlsx); }这个循环里容易写错三处。sheet_add_aoa第二次调用不传 origin 会把数据从 A1 重新写、覆盖表头所以必须写{ origin: A2 }。encode_cell的行列从 0 开始和第一行的直观行号差一位循环边界要看ws[!ref]译出来的范围。!rows是按行号排列的完整数组长度不够时后面的行高不生效所以这里用 concat 保证和数据行数等长。4.3 合并单元格、列宽与不转科学计数法的处理合并单元格通过工作表!merges声明每一项是{ s: {r, c}, e: {r, c} }即左上角和右下角坐标从 0 开始。标题行跨列合并是典型场景把 A1 到最后一列合并值保留在左上角ws[!merges] [ { s: { r: 0, c: 0 }, e: { r: 0, c: columns.length - 1 } } ]; ws[A1].s { font: { bold: true, sz: 14 }, alignment: { horizontal: center, vertical: center } };注意合并后除了左上角范围内其他单元格对象的引用要清理掉否则部分 Excel 打开会报文件已损坏清理方式是把这些地址的ws[addr]设为undefined。这里的合并和页面 Table 的 rowSpan/colSpan 是两个体系从 DOM 反向推导合并范围时要用表头的 colspan 累加列偏移不能直接拿列索引用。超长数字在 Excel 里默认转科学计数法。aoa_to_sheet会把纯数字自动写作数字类型想按原样显示整数给对应列单元格设置z属性// 第 2 列B 列按整数原样显示 for (let r 1; r range.e.r; r) { const addr XLSX.utils.encode_cell({ r: r, c: 1 }); if (ws[addr]) { ws[addr].z 0; } }z是单元格上的数字格式属性和样式对象分开写。0表示整数原样显示日期列换成yyyy-mm-dd hh:mm:ss百分比列用0.0%。若想保留前导零单号、证件号需要在数据源里把值处理成字符串再交给aoa_to_sheet它遇到字符串会写文本类型不会自动转数字。5. 样式自动映射与导出自检把 getComputedStyle 变成 Excel 样式最后落一个能直接收工的技巧从页面真实渲染样式自动生成 xlsx-js-style 样式对象并在导出后校验样式确实写进了文件。5.1 用 getComputedStyle 生成表头样式对象页面表头的颜色和字号由设计稿定死在 CSS 里与其在导出代码里另抄一份颜色不如直接从 DOM 读。表头只有一行逐格读开销可忽略function cssColorToExcel(cssColor, fallback) { if (!cssColor || cssColor transparent) return fallback || FFFFFF; const parts cssColor.match(/[\d.]/g); if (!parts || parts[3] 0) return fallback || FFFFFF; // alpha 为 0 return parts.slice(0, 3).map(function (n) { return Number(n).toString(16).padStart(2, 0); }).join().toUpperCase(); } function headerStyleFromDom(th) { const cs getComputedStyle(th); return { font: { bold: cs.fontWeight bold || parseInt(cs.fontWeight, 10) 600, sz: parseInt(cs.fontSize, 10), color: { rgb: FF cssColorToExcel(cs.color, 333333) } }, fill: { fgColor: { rgb: FF cssColorToExcel(cs.backgroundColor, FFFFFF) } }, alignment: { horizontal: cs.textAlign, vertical: center }, border: { bottom: { style: thin, color: { rgb: FFCCCCCC } } } }; }说明cs.color在 Chrome 里返回rgb(r, g, b)或rgba(r, g, b, a)不能直接当十六进制用cssColorToExcel负责把三通道转成 6 位 hex 并处理透明底色数据行样式通常统一逐行跑getComputedStyle上万次会有可见卡顿所以这个函数只用于表头数据行沿用 4.2 里的共享cellStyle。5.2 导出后自检直接读 styles.xml 验证样式落盘xlsx-js-style 生成的文件是 zip 包样式落在xl/styles.xml的 fills 和 cellXfs 里。导出完成后不需要打开 Excel 肉眼核对用 Node 直接解包查颜色即可const fs require(fs); const JSZip require(jszip); async function verifyStyle(path, color) { const buf fs.readFileSync(path); const zip await JSZip.loadAsync(buf); const styles await zip.file(xl/styles.xml).async(string); console.log(styles.includes(color) ? 样式已写入: color : 样式丢失: color); } // 校验导出.xlsx 里是否存在表头背景色 FF2F54EB verifyStyle(导出.xlsx, FF2F54EB);命令行的快速替代是unzip -p 导出.xlsx xl/styles.xml | grep FF2F54EB适合交付前手动抽查。如果查不到颜色字符串优先检查三点样式对象的 rgb 是否带FF前缀、fill 用的字段是不是fgColor、目标单元格是否真的存在——只设置!cols不会触发 styles.xml 生成样式块。把这套自检放进发版用例里样式回归问题能在交付前被拦住。本文还有配套的精品资源点击获取
返回列表