Skip to content

HTMLRewriter 讓你可以使用 CSS 選擇器轉換 HTML 文檔。它適用於 RequestResponse 以及 string。Bun 的實現基於 Cloudflare 的 lol-html


用法

一個常見的用例是重寫 HTML 內容中的 URL。以下示例將圖像源和鏈接 URL 重寫為使用 CDN 域名:

ts
// 將所有圖像替換為 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
<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。輸入根據其類型自動處理:

ts
// 來自 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 元素注冊處理器。在解析期間,每個匹配的元素都會調用這些處理器:

ts
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。請注意,異步操作將阻塞轉換直到完成:

ts
rewriter.on("div", {
  async element(element) {
    await Bun.sleep(1000);
    element.setInnerContent("<span>replace</span>", { html: true });
  },
});

CSS 選擇器支持

on() 方法支持廣泛的 CSS 選擇器:

ts
// 標簽選擇器
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);

元素操作

元素提供多種操作方法。所有修改方法都返回元素實例以便鏈式調用:

ts
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); // 小寫標簽名
    });
  },
});

文本操作

文本處理器提供文本操作方法。文本塊表示文本內容的部分,並提供有關其在文本節點中位置的信息:

ts
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 });
  },
});

注釋操作

注釋處理器允許注釋操作,方法與文本節點類似:

ts
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) 方法允許你處理文檔級事件。這些處理器在文檔級別發生的事件上調用,而不是在特定元素內:

ts
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)
  • 主體已使用的錯誤

應捕獲並適當處理錯誤:

ts
try {
  const result = rewriter.transform(input);
  // 處理結果
} catch (error) {
  console.error("HTMLRewriter error:", error);
}

另請參閱

你還可以閱讀 Cloudflare 文檔,此 API 旨在與其兼容。

Bun學習網由www.bunjs.com.cn整理維護