HTMLRewriter 让你可以使用 CSS 选择器转换 HTML 文档。它适用于 Request、Response 以及 string。Bun 的实现基于 Cloudflare 的 lol-html。
用法
一个常见的用例是重写 HTML 内容中的 URL。以下示例将图像源和链接 URL 重写为使用 CDN 域名:
// 将所有图像替换为 rickroll
const rewriter = new HTMLRewriter().on("img", {
element(img) {
// 著名的 rickroll 视频缩略图
img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");
// 将图像包装在指向视频的链接中
img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
html: true,
});
img.after("</a>", { html: true });
// 添加一些有趣的替代文本
img.setAttribute("alt", "Definitely not a rickroll");
},
});
// 示例 HTML 文档
const html = `
<html>
<body>
<img src="/cat.jpg">
<img src="dog.png">
<img src="https://example.com/bird.webp">
</body>
</html>
`;
const result = rewriter.transform(html);
console.log(result);这将把所有图像替换为 Rick Astley 的缩略图,并将每个 <img> 包装在链接中,产生如下差异:
<html>
<body>
<img src="/cat.jpg" />
<img src="dog.png" />
<img src="https://example.com/bird.webp" />
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" />
</a>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" />
</a>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" />
</a>
</body>
</html>现在页面上的每个图像都将被替换为 Rick Astley 的缩略图,点击任何图像都会跳转到 一个非常著名的视频。
输入类型
HTMLRewriter 可以转换来自各种来源的 HTML。输入根据其类型自动处理:
// 来自 Response
rewriter.transform(new Response("<div>content</div>"));
// 来自字符串
rewriter.transform("<div>content</div>");
// 来自 ArrayBuffer
rewriter.transform(new TextEncoder().encode("<div>content</div>").buffer);
// 来自 Blob
rewriter.transform(new Blob(["<div>content</div>"]));
// 来自 File
rewriter.transform(Bun.file("index.html"));请注意,Cloudflare Workers 的 HTMLRewriter 实现仅支持 Response 对象。
元素处理器
on(selector, handlers) 方法允许你为匹配 CSS 选择器的 HTML 元素注册处理器。在解析期间,每个匹配的元素都会调用这些处理器:
rewriter.on("div.content", {
// 处理元素
element(element) {
element.setAttribute("class", "new-content");
element.append("<p>New content</p>", { html: true });
},
// 处理文本节点
text(text) {
text.replace("new text");
},
// 处理注释
comments(comment) {
comment.remove();
},
});处理器可以是异步的并返回 Promise。请注意,异步操作将阻塞转换直到完成:
rewriter.on("div", {
async element(element) {
await Bun.sleep(1000);
element.setInnerContent("<span>replace</span>", { html: true });
},
});CSS 选择器支持
on() 方法支持广泛的 CSS 选择器:
// 标签选择器
rewriter.on("p", handler);
// 类选择器
rewriter.on("p.red", handler);
// ID 选择器
rewriter.on("h1#header", handler);
// 属性选择器
rewriter.on("p[data-test]", handler); // 具有属性
rewriter.on('p[data-test="one"]', handler); // 精确匹配
rewriter.on('p[data-test="one" i]', handler); // 不区分大小写
rewriter.on('p[data-test="one" s]', handler); // 区分大小写
rewriter.on('p[data-test~="two"]', handler); // 单词匹配
rewriter.on('p[data-test^="a"]', handler); // 以...开头
rewriter.on('p[data-test$="1"]', handler); // 以...结尾
rewriter.on('p[data-test*="b"]', handler); // 包含
rewriter.on('p[data-test|="a"]', handler); // 短横线分隔
// 组合器
rewriter.on("div span", handler); // 后代
rewriter.on("div > span", handler); // 直接子级
// 伪类
rewriter.on("p:nth-child(2)", handler);
rewriter.on("p:first-child", handler);
rewriter.on("p:nth-of-type(2)", handler);
rewriter.on("p:first-of-type", handler);
rewriter.on("p:not(:first-child)", handler);
// 通用选择器
rewriter.on("*", handler);元素操作
元素提供多种操作方法。所有修改方法都返回元素实例以便链式调用:
rewriter.on("div", {
element(el) {
// 属性
el.setAttribute("class", "new-class").setAttribute("data-id", "123");
const classAttr = el.getAttribute("class"); // "new-class"
const hasId = el.hasAttribute("id"); // boolean
el.removeAttribute("class");
// 内容操作
el.setInnerContent("New content"); // 默认转义 HTML
el.setInnerContent("<p>HTML content</p>", { html: true }); // 解析 HTML
el.setInnerContent(""); // 清除内容
// 位置操作
el.before("Content before").after("Content after").prepend("First child").append("Last child");
// HTML 内容插入
el.before("<span>before</span>", { html: true })
.after("<span>after</span>", { html: true })
.prepend("<span>first</span>", { html: true })
.append("<span>last</span>", { html: true });
// 删除
el.remove(); // 删除元素及其内容
el.removeAndKeepContent(); // 仅删除元素标签
// 属性
console.log(el.tagName); // 小写标签名
console.log(el.namespaceURI); // 元素的命名空间 URI
console.log(el.selfClosing); // 元素是否自关闭(例如 <div />)
console.log(el.canHaveContent); // 元素是否可以包含内容(对于 <br> 等空元素为 false)
console.log(el.removed); // 元素是否被删除
// 属性迭代
for (const [name, value] of el.attributes) {
console.log(name, value);
}
// 结束标签处理
el.onEndTag(endTag => {
endTag.before("Before end tag");
endTag.after("After end tag");
endTag.remove(); // 删除结束标签
console.log(endTag.name); // 小写标签名
});
},
});文本操作
文本处理器提供文本操作方法。文本块表示文本内容的部分,并提供有关其在文本节点中位置的信息:
rewriter.on("p", {
text(text) {
// 内容
console.log(text.text); // 文本内容
console.log(text.lastInTextNode); // 是否是文本节点的最后一块
console.log(text.removed); // 文本是否被删除
// 操作
text.before("Before text").after("After text").replace("New text").remove();
// HTML 内容插入
text
.before("<span>before</span>", { html: true })
.after("<span>after</span>", { html: true })
.replace("<span>replace</span>", { html: true });
},
});注释操作
注释处理器允许注释操作,方法与文本节点类似:
rewriter.on("*", {
comments(comment) {
// 内容
console.log(comment.text); // 注释文本
comment.text = "New comment text"; // 设置注释文本
console.log(comment.removed); // 注释是否被删除
// 操作
comment.before("Before comment").after("After comment").replace("New comment").remove();
// HTML 内容插入
comment
.before("<span>before</span>", { html: true })
.after("<span>after</span>", { html: true })
.replace("<span>replace</span>", { html: true });
},
});文档处理器
onDocument(handlers) 方法允许你处理文档级事件。这些处理器在文档级别发生的事件上调用,而不是在特定元素内:
rewriter.onDocument({
// 处理 doctype
doctype(doctype) {
console.log(doctype.name); // "html"
console.log(doctype.publicId); // 如果存在则为公共标识符
console.log(doctype.systemId); // 如果存在则为系统标识符
},
// 处理文本节点
text(text) {
console.log(text.text);
},
// 处理注释
comments(comment) {
console.log(comment.text);
},
// 处理文档结束
end(end) {
end.append("<!-- Footer -->", { html: true });
},
});Response 处理
转换 Response 时:
- 状态码、头信息和其他响应属性会被保留
- 主体会被转换,同时保持流式传输功能
- 内容编码(如 gzip)会自动处理
- 原始响应主体在转换后会被标记为已使用
- 头信息会被克隆到新响应中
错误处理
HTMLRewriter 操作在以下几种情况下可能会抛出错误:
on()方法中的选择器语法无效- 转换方法中的 HTML 内容无效
- 处理 Response 主体时的流错误
- 内存分配失败
- 无效的输入类型(例如传递 Symbol)
- 主体已使用的错误
应捕获并适当处理错误:
try {
const result = rewriter.transform(input);
// 处理结果
} catch (error) {
console.error("HTMLRewriter error:", error);
}另请参阅
你还可以阅读 Cloudflare 文档,此 API 旨在与其兼容。