这次重构目标很小:调用方能够直接拿到 HTML 字符串做检查,不必先写磁盘。输出内容、编码和已有写文件入口保持一致。它不包含改页面样式、改 SEO 字段或加依赖。
重构前:渲染与写入放在一起
在新建的 refactor-demo 目录使用 Node.js 22 或之后版本,保存 before.mjs:
import { writeFileSync } from 'node:fs';
const escape = text => String(text).replace(/[&<>"']/g,
ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
export function buildArticle(article, destination) {
const schema = JSON.stringify({ '@context': 'https://schema.org',
'@type': 'Article', headline: article.title }).replace(/</g, '\\u003c');
const html = '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">'
+ '<title>' + escape(article.title) + '</title></head><body><h1>'
+ escape(article.title) + '</h1><script type="application/ld+json">'
+ schema + '</script></body></html>';
writeFileSync(destination, html, 'utf8');
}
这是教学用微型渲染器,只有标题和 JSON-LD。真实项目已有的页脚、导航、canonical、错误处理等都应另外纳入基线,不能用本例替换整个生产渲染器。
把不变条件写进任务
请先读before.mjs。只把纯HTML生成提取为renderArticle(article),
新模块保存为after.mjs,保留buildArticle(article,destination)的调用方式。
before.mjs保留为本次对照,不修改。输出字节、HTML转义、JSON-LD应一致。
不新增依赖,不调整HTML格式,不修改其他文件。
完成后运行本文的refactor.test.mjs,报告实际结果。
参考的 after.mjs:
import { writeFileSync } from 'node:fs';
const escape = text => String(text).replace(/[&<>"']/g,
ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
export function renderArticle(article) {
const schema = JSON.stringify({ '@context': 'https://schema.org',
'@type': 'Article', headline: article.title }).replace(/</g, '\\u003c');
return '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">'
+ '<title>' + escape(article.title) + '</title></head><body><h1>'
+ escape(article.title) + '</h1><script type="application/ld+json">'
+ schema + '</script></body></html>';
}
export function buildArticle(article, destination) {
writeFileSync(destination, renderArticle(article), 'utf8');
}
用四种输入检查,而不是只看退出码
保存 refactor.test.mjs,与两个模块放同一目录:
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, existsSync, unlinkSync, rmdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildArticle as before } from './before.mjs';
import { buildArticle as after, renderArticle } from './after.mjs';
const titles = ['普通标题', 'A & B', '带"引号"的标题', '</script><p>边界</p>'];
for (const title of titles) {
test('保留输出:' + title, () => {
const dir = mkdtempSync(join(tmpdir(), 'render-demo-'));
try {
const oldFile = join(dir, 'old.html');
const newFile = join(dir, 'new.html');
before({ title }, oldFile);
after({ title }, newFile);
const oldHtml = readFileSync(oldFile, 'utf8');
const newHtml = readFileSync(newFile, 'utf8');
assert.equal(newHtml, oldHtml);
assert.equal(renderArticle({ title }), oldHtml);
const json = newHtml.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
assert.ok(json, '必须保留JSON-LD');
assert.equal(JSON.parse(json[1]).headline, title);
if (title === 'A & B') assert.ok(newHtml.includes('<h1>A & B</h1>'));
if (title.startsWith('</script>')) assert.ok(newHtml.includes('<h1></script>'));
} finally {
for (const name of ['old.html', 'new.html']) {
const file = join(dir, name);
if (existsSync(file)) unlinkSync(file);
}
rmdirSync(dir);
}
});
}
运行 node --test refactor.test.mjs。预期四项通过;临时 HTML 只在该测试创建的系统临时子目录写入,结束后清理该目录。
如果删掉 after 模块中的 JSON-LD 拼接,即使页面仍显示标题,字节对比和结构化数据检查也应该失败。反过来,若测试仍绿,先检查实际运行的测试路径和导入文件,不要宣布重构成功。
怎样判断这次重构有价值
现在可以直接调用 renderArticle({title}) 检查输出,不需要构造磁盘路径;写文件入口仍兼容。这是可观察的结构改善。四个输入通过只证明本工件覆盖的行为,没有检查浏览器布局、网站其他模板、生产发布或搜索引擎展示。
2026-10-11,在 Windows、Node.js 24.15.0 下实际运行了上述四项测试,全部通过;再用故意移除JSON-LD内容的独立故障副本运行同组测试,四项均失败,退出码为1。没有修改正常参考模块。本文未调用 Claude Code 执行重构,读者获得模型生成版本后仍需重新比较。
资料来源
- Claude Code常用工作流
核对代码修改与验证工作方式;工件和测试由本文提供。
下一步
继续阅读与你当前问题相关的教程:


