做过后台报表导出的同学大概都经历过这个场面:运营跑过来说“对账单抬头要加 logo,金额列要千分位,最后补一行合计”,你打开代码一看,是八百行用单元格坐标硬编码出来的写值调用,改个列宽都得重新数格子。我最早接这类需求时,一份月度结算单改了七版,前后耗了三天,其中两天半在调样式。后来把这套东西换成了模板渲染的思路——用 ejsExcel 这类把 EJS 语法直接写进 Excel 单元格的 Node.js Excel 模板引擎,样式交给业务方在 Excel 里画,数据交给代码填,动态报表的生成就从“改代码”变成了“改模板”。这篇把我这两年用下来觉得最值钱的 10 个技巧,连同它的工作原理、模板规范、行循环机制和线上踩过的坑,一次讲透。适合已经会用 Node.js、正被复杂报表格式折磨的后端和全栈同学,也适合刚接触 ejsExcel 想快速跑通第一条链路的初学者。
1. 先搞清楚 ejsExcel 到底在干什么
1.1 Excel 的本质是一个装了 XML 的压缩包
很多人对 xlsx 有误解,以为它是某种二进制格式。把任意一个 .xlsx 文件的后缀改成 .zip 再解压,你会看到一堆目录:xl/worksheets/sheet1.xml、xl/sharedStrings.xml、xl/styles.xml、xl/media/、[Content_Types].xml。真正决定表格长什么样的,是sheet1.xml里那一串<row>、<c>(cell)、<v>(value)节点。列宽、行高、合并区域、字体边框,全部在这些 XML 的属性里。
这个认知非常关键,因为它直接决定了 ejsExcel 的能力边界。它不是去“理解”Excel 的渲染逻辑,而是在 XML 文本层面做字符串替换:把单元格里的<%= xxx %>换成真实值,把带循环标记的那一行<row>节点整体复制 N 份。理解了这一层,后面所有“为什么这里会出问题”就都有了答案。
1.2 EJS 语法凭什么能写进单元格
你写好一个带占位符的 xlsx 模板,ejsExcel 读进来之后做的事分三步。第一步解压,拿到各个 sheet 的 XML。第二步找到所有单元格的文本内容,把每一段文本当成一个 EJS 模板片段去编译和执行,执行时的上下文就是你传进去的 data 对象。第三步把渲染结果写回 XML,重新打包成 zip,输出一个可下载的 buffer。
所以你会发现它的定位特别清晰:它是一个“文本替换器 + 行复制器”,而不是一个“Excel 对象模型库”。这一点和 exceljs 那种用 API 一格一格去写的方式完全不同。ejsExcel 的强项是“格式已经画好了,我只填数据”,弱项是“我要动态增删列、动态改结构”——后者超出了它的设计目标。
我见过 A 同学拿它去做“列数不固定的透视表”,折腾两天最后放弃,换成先算好数据再拼模板的方案。这就是典型的用错了工具:模板引擎擅长的是“结构稳定、内容变化”,不是“结构也变化”。
1.3 三种主流方案的取舍对比
在 Node.js 里生成 Excel,常见的路就这么几条。把它们的特性摊开对比,你就能一眼看出自己该选哪条。
| 方案 | 代表库 | 样式还原度 | 复杂格式维护成本 | 适合场景 |
|---|---|---|---|---|
| 代码直写单元格 | exceljs、xlsx | 中,需手写样式对象 | 高,改样式等于改代码 | 结构简单、列数固定的导出 |
| 模板渲染 | ejsExcel | 极高,Excel 里画什么就是什么 | 低,改模板不动代码 | 对账单、报表、申报表等格式敏感型 |
| 在线报表服务 | 各类 BI 工具 | 中 | 低但受限于产品能力 | 需要用户自助拖拽分析 |
我自己的工作判断标准很粗暴:如果业务方会拿着打印出来的纸质件跟你对样式,就用模板渲染;如果只是后台列表“导出成 Excel 看看”,代码直写更省事。
拿 ejsExcel 最舒服的地方在于,那份对账单的字体、边框、合并、页边距、打印区域,全是业务方自己在 Excel 里调好的。他们调完把文件发我,我只负责把里面几个格子的内容换成<%= %>,交付周期从三天压到两小时。这不是技术多先进,是流程对齐了——谁最懂样式,就让谁去改样式。
2. 从零跑通第一个动态报表
2.1 依赖安装与版本注意点
安装本身没什么好说的,一条命令:
npm i ejsexcel真正需要注意的是 Node 版本。ejsExcel 的思路是解压—渲染—打包,中间大量依赖 Buffer 和流操作,在 Node 14 以下跑老版本时我遇到过解压后字符编码错乱、中文变问号的情况。现在的 LTS 版本(18 及以上)跑下来很稳,建议直接对齐团队的 Node 版本,不要在生产机上留多个大版本。
另外一个实践建议:把 ejsExcel 单独封装成一个内部模块,不要让业务代码直接 require 它。原因后面第 6 章会讲,主要是为了做模板缓存、异常兜底和输出校验,这几个东西一旦散落在业务代码里,出问题时排查成本会翻好几倍。
2.2 制作模板的三条硬性规范
我踩过的坑里,一大半出在模板本身不规范。下面这三条是我要求团队所有人必须遵守的,写进内部规范文档的:
第一,模板必须是 .xlsx,不能是 .xls。老格式的底层是二进制 BIFF,不是 zip + XML,ejsExcel 根本解析不了。有些同事觉得“另存为”一下就行,但要注意有些旧版办公软件另存出来的 xlsx 兼容性有问题,建议用较新的版本重新另存。
第二,所有写模板标记的单元格,格式先设成“文本”。这条极其重要。Excel 很“聪明”,你在格子里输入<%= a %>,它可能自动把<和>处理成转义字符,或者把以=开头的内容当公式解析。我吃过一次亏:某个单元格内容变成<%= a %>,渲染出来原样输出,查了两小时才发现是单元格格式问题。做法很简单,选中整列,把数字格式设成“文本”,再去输标记。
第三,循环的开始标记和结束标记必须各占一个完整单元格,不能跟其他内容混在一起。这一条是行循环机制决定的,第 4 章会展开。
注意:模板里不要用“插入表格”的方式做美化。Excel 的表格对象会生成独立的 table 部件,渲染后行数变了,表格范围不会跟着扩展,边框会断掉。要边框就直接选中区域设边框。
2.3 最小可运行示例与数据契约
先把最小链路跑通,别的都是后话。模板我通常放在项目根的tpl/目录,输出放out/,都通过配置指定,不写死绝对路径。
const fs = require('fs'); const path = require('path'); const ejsexcel = require('ejsexcel'); async function renderMonthly() { // 模板读成 Buffer,后面做缓存也用它 const tplBuffer = fs.readFileSync(path.join(__dirname, 'tpl', 'monthly.xlsx')); // data 就是渲染时的上下文,模板里能访问到的变量全从这里来 const data = { title: '月度结算单', period: '2024-05', list: [ { idx: 1, name: '技术服务费', amount: '1200.50', date: '2024-05-03' }, { idx: 2, name: '材料采购费', amount: '880.00', date: '2024-05-11' }, { idx: 3, name: '运输费', amount: '460.00', date: '2024-05-18' } ], total: '2540.50', generatedAt: '2024-06-01 09:30:00' }; const buf = await ejsexcel.renderExcel(tplBuffer, data); const outPath = path.join(__dirname, 'out', 'monthly.xlsx'); fs.writeFileSync(outPath, buf); return outPath; } renderMonthly().catch(err => { console.error('[render failed]', err); process.exit(1); });对应模板里各个格子的内容大概是这样:
| 位置 | 单元格内容 | 说明 |
|---|---|---|
| A1 | <%= title %> | 标题,单值替换 |
| A2 | 结算周期:<%= period %> | 混排文本,注意标记别被转义 |
| A4 到 G4 之间 | <% for (var i = 0; i < list.length; i++) { %>放在 A4,<% } %>放在 G4 | 整行作为循环体 |
| B4 | <%= list[i].idx %> | 循环体内的取值 |
| C4 | <%= list[i].name %> | 同上 |
| D4 | <%= list[i].amount %> | 金额已在数据层格式化 |
| A5 | 合计:<%= total %> | 循环体外,单值 |
跑通之后你能得到一个和你模板长得一模一样、内容却是动态的 xlsx。这时候第一步就算成功了,接下来全是在这个骨架上做加固。
关于数据契约,我有一个很硬的原则要强调:模板里出现的每一个变量,数据层必须保证它有值。ejsExcel 拿到的如果是undefined,渲染出来就是空白单元格,不会报错,于是问题会一路带到用户手里。第 3 章的第一个技巧就是解决这个。
3. 十个真正省时间的模板技巧
这一章是全文的干货密度最高的部分。我把两年里反复用到的经验收敛成十条,按“数据处理层”和“样式输出层”分成两组。每一条都配有我实际用过的写法。
3.1 技巧一到五:把复杂度挡在模板之外
技巧 1:业务逻辑全部前置到 Node 侧,模板只做取值。
这是最重要的一条,没有之一。很多人一上手就喜欢在模板里写<% if (...) { %>、<% list.filter(...).forEach(...) %>,觉得灵活。等模板长到两百行,你会发现这玩意儿既没有语法高亮,也没法断点调试,改一个条件得在 Excel 和编辑器之间来回切十几次。
正确姿势是:在 Node 侧组装一份“视图模型”,字段名直白、值已经格式化好、该算的都算完。模板里只有<%= vm.xxx %>和最简单的循环。我给团队定的线是:模板里不允许出现超过一层的判断和循环,不允许出现数组方法调用。这条执行下来,模板维护成本降了一大截。
function buildViewModel(rawOrders) { const list = rawOrders.map((o, i) => ({ idx: i + 1, name: o.name, amount: o.amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','), date: o.date.slice(0, 10), status: o.paid ? '已结清' : '待结算' // 布尔值在这里就翻译成人话 })); return { title: '月度结算单', list, total: list.reduce((s, x) => s + Number(x.amount.replace(/,/g, '')), 0) .toFixed(2), hasData: list.length > 0 }; }技巧 2:给每个字段加默认值兜底,杜绝空白单元格。
模板渲染最阴的失败方式不是报错,是静默输出空白。用户拿着空表格来问你“这里怎么没数”,你再去翻数据源,链路已经断了。我的做法是在组装视图模型时统一过一遍兜底:
const safe = (v, fallback = '—') => (v === undefined || v === null || v === '') ? fallback : v;金额、日期这种格式敏感的字段,兜底值用—而不是0,这样业务方一眼能看出是“缺数据”而不是“金额为零”。这个细节看起来小,但能省掉大量来回确认的沟通。
技巧 3:循环标记必须成对且独占单元格,整行才是循环体。
ejsExcel 判断“这一行要不要复制”,靠的是扫描单元格里有没有循环开始标记。所以<% for (...) { %>必须完完整整待在一个格子里,<% } %>也一样,中间不能被其他文字打断。我见过有人写成序号<% for(...){ %>,结果整行没被复制,只渲染出了最后一条数据。
还有一个容易忽略的点:开始标记和结束标记必须落在同一行。跨行的循环在 ejsExcel 里是不成立的,因为它复制的最小单位就是行。如果你需要主从表那种“一个主记录下面跟多行明细”,做法是把主记录和明细拆到不同的行区域,用两次独立渲染或嵌套的两个行块来处理。
技巧 4:合并单元格只在循环体内侧处理,别让标记跨合并区。
循环行里有合并单元格时,复制出来的行会出现合并错位。原因在于合并区域是在 sheet 的<mergeCells>节点里按“起始行:起始列”登记的,行数一多,后面的合并区间不会自动平移。
我的处理原则是:循环体内部只做列内合并(同一行内横向合并),绝对不做跨行合并。如果业务上确实需要“同一个客户的多条明细只显示一次客户名”,那就在数据层把不需要重复的字段置空,让视觉上看起来是合并的,而不是真的去合并单元格。这个土办法在实际交付里从来没出过问题。
技巧 5:日期和金额在数据层就格式化成字符串,别指望 Excel 格式。
Excel 里的日期本质上是一个数字序列号,金额是浮点数。你在模板里写<%= date %>,如果传的是2024-05-03,Excel 会把它当普通文本,排序和筛选就废了;如果传的是时间戳,又会变成一串看不懂的五位数。
两条路可选:一是数据层直接.toFixed(2)和slice(0, 10)转成字符串,牺牲排序能力换稳定;二是保留数字,在模板单元格上设置好数字格式,让 Excel 自己显示。我一般选前者,因为这类报表 99% 是用来打印和归档的,排序需求极少。真要排序,就再加一列隐藏的原始值列。
3.2 技巧六到十:样式复用与输出侧加固
技巧 6:用条件渲染控制“无数据”和分页占位。
空列表渲染出来是一张只有表头的空表,客户看着心里发毛。做法是在循环外面加一层判断,准备一行“暂无数据”的样式行:
模板里放两行,一行是真数据行(带循环标记),一行是空态行(内容为“本期无数据”,带居中和灰色字体)。用<% if (!list.length) { %>包住空态行,渲染时只有其中一行会留下来。ejsExcel 对这种整行条件渲染是支持的,因为它在行级别做判断和复制。
技巧 7:把重复片段抽成数据层函数,而不是去搞模板 include。
EJS 原生有<%- include(...) %>,但在 Excel 模板里搞 include 会带来路径和共享字符串的麻烦,我试过两次都放弃了。更稳的做法是把重复内容在数据层算成字符串,模板只负责摆位置。
比如页脚要显示“第 X 页 / 共 Y 页”外加一串审批流信息,我会在 Node 侧拼一个footerText出来,模板里就一个<%= footerText %>。这样文本怎么变都在 JS 里可控,模板永远是死的。
技巧 8:多 Sheet 共享同一份上下文,按业务模块组织数据。
一个 xlsx 里有多个 sheet 很常见,比如“汇总”“明细”“说明”。ejsExcel 渲染时上下文是共享的,所有 sheet 都能访问同一份 data,所以你不需要给每个 sheet 单独准备一份数据对象,只要把 data 按模块组织好即可。
const data = { meta: { title: '月度结算单', period: '2024-05' }, summary: { total: '2540.50', count: 3 }, detailList: [ /* 明细行 */ ], notes: '本单据仅作对账使用,不作为付款凭证。' };模板里“汇总”sheet 用summary.xxx,“明细”sheet 用detailList循环,“说明”sheet 用notes。结构清清楚楚,出问题也容易定位是哪个模块的数据没给到。
技巧 9:静态图片放模板里,动态图片走两段式。
公司 logo、印章底图这类固定图片,直接在 Excel 模板里插好就行,渲染时不会动它,这部分 ejsExcel 处理得很好。
动态图片(比如二维码、手写签名、商品缩略图)就别指望一步到位了。我的做法是两段式:先用 ejsExcel 生成主体内容并落盘,再用另一个能操作 drawing 部件的库去补图。多一步,但稳定。硬要在模板渲染阶段塞动态图片,版本兼容性和占位符语法都容易出问题,投入产出不划算。
技巧 10:渲染结果先落临时目录,做二次校验再交付。
这是工程化层面的技巧,但我觉得比前面九条都实在。生成了 buffer 不代表文件是好的——可能是 0 字节、可能是 XML 损坏打不开、可能是行数不对。我加了一层校验:
function validateOutput(buf, expectedRows) { if (!buf || buf.length < 1024) throw new Error('输出体积异常,疑似渲染失败'); // xlsx 是 zip,文件头固定是 PK if (buf.slice(0, 2).toString() !== 'PK') throw new Error('输出不是合法 xlsx'); return true; }体积校验挡住过好几次“模板被意外改坏导致输出空文件”的事故。成本几乎为零,收益是避免把坏文件发给客户。
4. 行循环:ejsExcel 最容易翻车的地方
4.1 行复制到底是怎么发生的
理解行复制机制,能省掉你 80% 的调试时间。ejsExcel 扫描 sheet XML 里的<row>节点,对每一行判断:这一行的单元格里有没有循环开始标记?有的话,它会把这一整行(包括所有的列宽、样式索引、单元格属性)当成一个模板片段,按循环次数复制出 N 份,然后把循环变量代入每一份再渲染。
这意味着两件非常重要的事。第一,行内的样式是被完整继承的,你在模板里给这一行设的字体、边框、背景色、数字格式,复制出来的每一行都带。这是 ejsExcel 相比代码直写最大的优势。第二,行外的属性不会跟着走,比如行高如果不是显式设置在<row>节点上,而是靠 Excel 自动撑开,复制后可能出现行高不一致。
我遇到过一次比较典型的问题:模板里那一行是手动拖拽调过行高的,渲染出来行高全变成默认值。排查发现是那次模板被同事重新另存了一遍,行高属性写在了别的地方。解决方案很简单,选中整行,右键“行高”显式填一个数值,别用自动。
4.2 合并单元格与样式继承的实测结论
关于合并,我在 3.1 的技巧 4 里讲了原则,这里补充实测细节。
横向合并(同一行内跨列)在循环行里是安全的,因为合并区间的行号在复制后依然对应正确。纵向合并(跨行)一定有风险,尤其是当循环次数变化时,<mergeCells>里的区间不会自动调整,结果就是要么合并区域盖住了数据,要么数据行之间出现空隙。
如果你非要做纵向合并,唯一的可行路径是渲染完之后用另一个库做二次处理,根据实际行数重新计算合并区间。这个工作量大,我一般直接劝业务方放弃这个样式需求,改用“重复字段置空 + 分组底色”的方式让视觉聚焦,效果差不太多,成本低一个数量级。
4.3 空数组、单行、尾行处理的边界
三个边界场景,每个都值得专门测一次。
空数组。list长度为 0 时,循环体不执行,那一行会完全消失。这时候如果紧接着是合计行,合计行会自动上移,视觉上没问题,但你在模板里画的那些分隔线、留白就全乱了。所以必须配合技巧 6 的空态行。
单行。只有一条数据时,循环只执行一次,这一行会保留它自己的样式,没有额外问题。但如果你在模板里给这一行设了“斑马纹”交替底色(用条件判断),单行的情况下只会命中第一种颜色,看起来跟多行时的不太一样,需要接受这个现实或者改成按序号判断。
尾行。循环结束后的那一行(通常是合计行),它不属于循环体,样式是独立的。这里容易犯的错是把循环结束标记<% } %>放在了尾行的格子里,导致尾行被吃进循环。我一般在模板里用颜色标注一下:循环体那一行整行涂个浅黄底,提醒自己和同事“这行会被复制,别乱动”。
5. 多 Sheet、图片与公式的高级玩法
5.1 多 Sheet 的数据组织方式
多个 sheet 的模板制作其实比单 sheet 更省心,因为你不用在一个页面里塞下所有内容。真正的难点在数据组织上。
我的做法是在数据层按“谁生产、谁消费”来切分。汇总数据通常来自一个聚合查询,明细数据来自一个分页查询,说明文本往往是配置中心的常量。三者在 Node 侧各自组装成独立的子对象,最后拼成一个大 data 传进去。
有一点要留意:模板里引用不存在的变量不会报错,只会渲染成空。所以多 sheet 项目一定要建立一套“变量清单”的校验机制。我的做法是在数据层的出口加一层断言,用正则从模板里把所有<%= xxx %>的变量名扫出来,逐个检查 data 里有没有对应的值。模板变多了之后,这套校验能救你无数次。
function extractVars(tplString) { const re = /<%=\s*([a-zA-Z_$][\w.$]*)\s*%>/g; const set = new Set(); let m; while ((m = re.exec(tplString))) set.add(m[1]); return [...set]; }单纯取值类的变量用这个能覆盖大半。循环变量和局部变量会混进来,加个白名单过滤即可。
5.2 动态图片的稳妥落地方式
前面提过两段式,这里展开讲清楚为什么。
xlsx 里图片的处理涉及xl/drawings/、xl/media/和关系文件三处联动,还要在 sheet XML 里挂对应关系。ejsExcel 的主战场是单元格文本,让它去处理这套关系链,等于让它跨界干活,出问题的概率自然高。
我的实际路径是:ejsExcel 负责所有内容和样式,输出一个完整的 xlsx;然后读取这个文件,找到预留的图片占位区域(比如某个合并单元格里写了[LOGO]),用支持图片操作能力的库把图片塞进去,同时删掉占位文本。整个流程串在一个函数里调用,业务代码看起来还是一步。
这里有个细节要提醒:图片尺寸和单元格锚点要提前算好。我一般按“1 像素 ≈ 0.75 磅”去折算,再留 10% 的余量,避免图片撑破单元格导致后面的内容被挤压。
5.3 公式与缓存值这个隐蔽的坑
这一条我觉得很多人没意识到。xlsx 里的公式单元格存了两份东西:一份是公式本身<f>,一份是上次计算的结果缓存<v>。用 Excel 打开时,它可能会重新计算;但如果你生成的文件被程序直接读取(比如拿去再做一次数据校验或导入其他系统),读到的往往是那份旧缓存。
模板里如果放了=SUM(D5:D20)这类公式,而循环生成的行数变了,缓存值就是错的。程序读到的合计可能是上一次的数,甚至直接是 0。
两条应对路径。稳妥的一条是:合计值在 Node 侧算好,直接写值,不放公式。反正数据你都有,算一遍不费事,还能避免不同阅读器行为不一致。另一条是如果业务方坚持要看到公式(他们想手工改数据看结果变化),那就在生成后做一次重算,用无界面的表格处理工具打开再保存一遍,把缓存刷新掉。
我现在的默认选择是前者。打印和归档场景下,用户根本点不进单元格看公式,写死值反而更可靠。
6. 生产环境的问题排查与性能优化
6.1 常见问题速查表
这张表是我从实际报障记录里整理出来的,按出现频率排序。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 标记被原样输出成文本 | 单元格被当成了公式或转义了 | 整列设文本格式,重新输入标记 |
| 循环行只渲染出最后一条 | 开始/结束标记不在同一行,或标记被拆散 | 检查两个标记是否各占独立单元格且同行 |
| 输出文件打不开 | 模板本身损坏,或渲染中途异常 | 用原始模板直接打开验证,加 buffer 头校验 |
| 中文变问号 | Node 版本过低或读文件没指定编码 | 统一用 Buffer 读写,升级 Node |
| 合并单元格错位 | 循环体内存在纵向合并 | 改为横向合并,或数据层置空代替合并 |
| 行高不一致 | 行高未显式设置在行节点上 | 选中行右键设置固定行高 |
| 合计行跟着循环复制了 | 结束标记放在了合计行所在的格子 | 把结束标记挪回循环体最后一个格子 |
| 程序读到的合计是旧值 | 公式缓存未刷新 | 数据层算好直接写值 |
| 图片没显示 | 走了模板渲染阶段插动态图 | 改为两段式,渲染后再补图 |
| 输出体积异常小 | 数据为空或模板变量名拼错 | 加空态判断和变量清单校验 |
6.2 大数据量下的内存与耗时
ejsExcel 的渲染过程是把整个 xlsx 读进内存、解压、渲染、再压缩,所以内存占用和文件大小、行数基本是线性关系。我实测过,一万行左右、十几列、样式不算复杂的报表,单次渲染耗时在几百毫秒到一秒出头,内存峰值几十兆,完全扛得住。
到了十万行级别就是另一回事了。内存峰值会飙到几百兆甚至更高,而且是那种“单个请求就能把进程顶住”的飙法。这时候如果并发几个请求同时进来,很容易触发内存告警。
三条优化路径,按性价比排序。第一,拆分输出。十万行的报表本身就不该一次性给用户,按月份、按部门拆成多个文件,或者分 sheet 承载,是最符合业务直觉的做法。第二,限制并发生成。用一个简单的队列,同一时刻只允许 N 个渲染任务在跑,其余排队。这个改动只有几十行代码,但能把内存曲线压得非常平。第三,模板瘦身。删掉模板里用不到的条件格式、数据验证、隐藏定义名称,这些都会增大 XML 体积,而它们对最终报表的贡献可能为零。
超大数据量(几十万行以上)就别用模板引擎了,直接走流式的数据导出方案,边写边吐,牺牲一点样式保住内存。这是个明确的取舍点,不要硬扛。
6.3 封装成内部模块的几个要点
用了半年之后我把这套东西收拢成了一个内部模块,主要做了四件事,这里列出来供参考。
模板缓存。模板文件读盘和解压是重复开销,尤其模板较大的时候。我的做法是启动时把模板 Buffer 读进内存,按文件路径 + 修改时间做 key,开发环境监听文件变化自动失效,生产环境只在启动时加载一次。
统一异常出口。渲染失败不能把原始堆栈直接抛给业务层,也不能让前端只看到一个 500。我把它包成带错误码的结果对象,记录模板名、数据摘要、失败阶段(读模板 / 渲染 / 写文件),方便快速定位。
输出目录规范。文件名里带上业务标识和时间戳,避免并发覆盖;定期清理超过一定时间的临时文件,否则磁盘会被慢慢吃光。这个小问题我在线上遇到过,某次磁盘告警追下去发现是导出目录堆了两百多个 G 的旧文件。
渲染耗时监控。在封装层埋一个计时,超过阈值的记录下来。哪些报表在变慢、哪个模板特别臃肿,数据一目了然。这套监控上线之后,我们主动优化了三个模板,平均耗时降了四成。
最后分享一个我自己觉得挺值的小习惯:维护一份模板变更记录。每次业务方发来新模板,我在仓库里存一份带版本号的文件,同时在记录里写清楚“谁在什么时候改了什么样式、对应哪个需求”。这玩意儿看着像流程负担,但真出了“上周还好好的这周怎么变样了”的问题,翻一眼就知道是哪次改动引入的,比在聊天记录里大海捞针强太多。