執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

XML

通過執行環境 API 和打包器整合使用 Bun 內建的 XML 支援

在 Bun 中,XML 與 JSON、TOML、YAML 和 JSON5 一樣是一等公民。你可以:

  • 使用 Bun.XML.parse 和 Bun.XML.stringify 解析和序列化 XML
  • 在執行環境將 XML 檔案 import 和 require 為模組(包括熱過載和 watch 模式支援)
  • 在使用 Bun 打包器的前端應用中 import 和 require XML 檔案

執行環境 API

Bun.XML.parse()

將 XML 文件解析為普通 JavaScript 物件。

TypeScript
import { XML } from "bun";

const data = XML.parse(`
  <order id="A1" currency="USD">
    <customer>Ada</customer>
    <item sku="tea" qty="2">Green tea</item>
    <item sku="mug" qty="1">Mug</item>
    <paid/>
  </order>
`);

console.log(data);
// {
//   order: {
//     "@id": "A1",
//     "@currency": "USD",
//     customer: "Ada",
//     item: [
//       { "@sku": "tea", "@qty": "2", "#text": "Green tea" },
//       { "@sku": "mug", "@qty": "1", "#text": "Mug" },
//     ],
//     paid: "",
//   },
// }

預設情況下,結果是一個以元素名稱為鍵的緊湊物件——這是大多數 XML 轉物件庫採用的 @attr/#text 約定。其工作方式如下:

  • 結果只有一個鍵,即根元素的名稱。
  • 沒有屬性和子元素的元素會變成其字元資料:一個字串;如果為空,則為 ""。在 XML 中,<paid/> 和 <paid></paid> 是同一回事。
  • 其他元素都會變成物件。每個屬性對應一個 "@name" 鍵,然後按照各個子元素名稱首次出現的順序,為每個不同的子元素名稱新增一個鍵,併為元素自身的文本新增一個 "#text" 鍵。
  • 如果某個子元素名稱在元素中出現多次,其鍵對應的值就是按文件順序排列的陣列;否則對應單個值。請參閱一個或多個。
  • compact 用於選擇結構,不會改變值。Bun 會按原樣傳回文本:包括前導、尾隨和內部空白字元,展開 CDATA 段和實體引用,並將行尾規範化為 \n——這與該元素對應的樹結構所給出的文本相同。由於一個元素只有一個 "#text",Bun 會串聯該元素的文本片段,並忽略位於子元素之間的純空白片段(即文件的排版空白)。如果文件經過手動排版(<name>\n value\n</name>),請在讀取時進行修剪。
  • 所有值都是字串。不會將任何值轉換為數字、布林值或 null。
  • 名稱保持原樣,包括名稱空間字首("soap:Body");xmlns 宣告是普通屬性。
  • 註釋、處理指令、<?xml …?> 宣告和 <!DOCTYPE …> 都不會被表示。

@ 和 # 不能作為 XML 名稱的開頭,因此屬性鍵和文本鍵不會與子元素鍵衝突。

緊湊結構用於 資料。它不會保留名稱不同的兄弟元素之間的相對順序,也不會保留文本相對於子元素的位置:

TypeScript
XML.parse(`<p>Hello <b>world</b>!</p>`);
// { p: { "#text": "Hello !", b: "world" } }

如果這些資訊很重要——例如處理文件而非資料時——請傳入 { compact: false },以獲取根元素的樹結構,並保留元素內容在文件中的順序:

TypeScript
const p = XML.parse(`<p class="lead">Hello <b>world</b>!<!-- draft --></p>`, { compact: false });

console.log(p);
// {
//   name: "p",
//   attributes: { class: "lead" },
//   children: [
//     "Hello ",
//     { name: "b", attributes: {}, children: ["world"] },
//     "!",
//     { comment: " draft " },
//   ],
// }

每個元素都是 { name, attributes, children };即使為空,這兩個鍵也都會存在。children 按順序儲存元素內容:文本以字串形式儲存(保持原樣,包括純空白片段,相鄰文本會合並)、子元素、以 { comment } 表示的註釋,以及以 { target, data } 表示的處理指令。可以根據物件所包含的鍵來區分物件子項。與緊湊結構一樣,樹結構不會表示宣告、DOCTYPE,或根元素之前和之後的任何內容。

一個或多個

在緊湊結構中,一個元素的列表和兩個元素的列表具有不同的型別(entry: {…} 與 entry: [{…}, {…}])。通常是字串的元素,如果帶有屬性,也會變成物件(<title> 與 <title type="html">)。對於需要遍歷或可能帶有屬性的值,請採取穩妥的處理方式:

TypeScript
const entries = [feed.entry ?? []].flat(); // an array either way
const title = typeof e.title === "string" ? e.title : (e.title["#text"] ?? "");

樹結構沒有這類歧義:children 是陣列,每個元素都是一個物件。

輸入型別和編碼

XML.parse 接受字串,或以 Buffer、TypedArray、DataView、ArrayBuffer 或 Blob 表示的位元組資料。

字串已經是解碼後的文本,因此會檢查其中 encoding 宣告的語法,但除此之外會忽略它。位元組資料會按照 XML 規則解碼:位元組順序標記或 <?xml version="1.0" encoding="..."?> 中的 encoding 會指定使用 UTF-8(預設值)、UTF-16(任一位元組序)或 ISO-8859-1。其他編碼會丟擲錯誤。

TypeScript
XML.parse(await Bun.file("feed.xml").bytes());

錯誤處理

當文件格式不正確時,Bun.XML.parse() 會丟擲 SyntaxError(沒有寬鬆模式);對於深度異常的巢狀,會丟擲 RangeError:

TypeScript
try {
  XML.parse("<a><b></a>");
} catch (error) {
  console.error(error.message); // "XML Parse error: Expected closing tag </b> but found </a>"
}

Bun.XML.stringify()

將任一結構中的一個元素序列化為 XML。

TypeScript
import { XML } from "bun";

XML.stringify({
  order: {
    "@id": "A1",
    customer: "Ada",
    item: [{ "@sku": "tea", "#text": "Green tea" }, { "@sku": "mug" }],
    paid: null,
  },
});
// '<order id="A1"><customer>Ada</customer><item sku="tea">Green tea</item><item sku="mug"/><paid/></order>'

XML.stringify({
  name: "p",
  attributes: { class: "lead" },
  children: ["Hello ", { name: "b", children: ["world"] }, "!", { comment: " draft " }],
});
// '<p class="lead">Hello <b>world</b>!<!-- draft --></p>'

當一個值具有字串型別的 name,並且具有 children 或 attributes 屬性時,Bun 會將其作為樹節點寫入。在 children 中,包含 name 的物件是元素,包含 comment 的物件是註釋,包含 target 的物件是處理指令。其他所有值都會被視為緊湊物件,其中一個鍵用於命名根元素。Bun 會按順序寫入鍵,並將 @ 鍵作為屬性。字串、數字、布林值和 bigint 會通過 String() 轉換為文本;Date 會轉換為其 ISO 字串。null 會變成空元素。與 JSON.stringify 一樣,Bun 會跳過 undefined、函式和符號。陣列會為每個項生成一個元素。

輸出是格式正確的 XML,否則 stringify 會丟擲錯誤。Bun 會轉義 &、< 和 >。屬性值中的 "、製表符和換行符,以及任意位置的回車符,都會以字元引用的形式寫入,以便解析後保持不變。XML 無法容納的值會導致錯誤,而不是生成損壞的文件:不是 XML 名稱的元素和屬性名稱("first name"、"0")、XML 字元集之外的字元(U+0000 和其他控制字元、未配對的代理項——XML 1.0 沒有可用於這些字元的轉義形式)、註釋中的 --、處理指令中的 ?>、根節點處的陣列或其他陣列中的陣列,以及迴圈結構。

結果只有元素本身,不包含 <?xml …?> 宣告和 DOCTYPE,因此你可以將結果串聯到外層元素中。要寫入檔案,請自行新增序言:

TypeScript
await Bun.write(
  "Info.plist",
  `<?xml version="1.0" encoding="UTF-8"?>\n` +
    `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n` +
    XML.stringify(plist, null, "\t"),
);

美化列印

傳入 space 引數(空格數或縮排字串,與 JSON.stringify 一樣),即可縮排僅包含元素的內容。對於包含文本的元素,Bun 會將其寫在同一行,因此縮排不會改變字元資料:

TypeScript
console.log(XML.stringify(data, null, 2));
// <order id="A1" currency="USD">
//   <customer>Ada</customer>
//   <item sku="tea" qty="2">Green tea</item>
//   <item sku="mug" qty="1">Mug</item>
//   <paid/>
// </order>

第二個引數已保留,請傳入 null 或 undefined。

對於 XML.parse 生成的值,無論採用哪種結構,XML.parse(XML.stringify(value)) 都會傳回相等的值。


模組匯入

ES Modules

你可以直接匯入 XML 檔案。Bun 會像處理傳遞給 XML.parse 的位元組資料一樣解碼檔案(根據位元組順序標記或宣告使用 UTF-8、UTF-16 或 ISO-8859-1)。模組的值是上文所述的緊湊物件:

config.xml
<?xml version="1.0" encoding="UTF-8"?>
<config env="production">
  <database host="localhost" port="5432" name="myapp"/>
  <feature name="auth"/>
  <feature name="rateLimit"/>
</config>

預設匯入

app.ts
import doc from "./config.xml";

console.log(doc.config["@env"]); // "production"
console.log(doc.config.database["@host"]); // "localhost"
console.log(doc.config.feature.map(f => f["@name"])); // ["auth", "rateLimit"]

命名匯入

根元素也可以作為命名匯入使用:

app.ts
import { config } from "./config.xml";

console.log(config.database["@port"]); // "5432"

CommonJS

app.ts
const { config } = require("./config.xml");
console.log(config.database["@name"]); // "myapp"

匯入屬性

使用 with { type: "xml" } 將其他副檔名的檔案作為 XML 解析:

TypeScript
import feed from "./export.rss" with { type: "xml" };

XML 熱過載

使用 bun --hot 執行應用程式時,Bun 會在 XML 檔案發生變化時重新載入它們:

server.ts
import { config } from "./config.xml";

Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response(`Running in ${config["@env"]} against ${config.database["@host"]}`);
  },
});
terminal
bun --hot server.ts

打包器整合

使用 Bun 打包時,打包器會在建置時解析匯入的 XML 檔案,並將它們內聯為 JavaScript 物件:

terminal
bun build app.ts --outdir=dist

在建置時解析意味著:

  • 生產環境中零執行環境 XML 解析開銷
  • 更小的打包體積
  • 對未使用的屬性進行 Tree Shaking

動態匯入

你可以動態匯入 XML 檔案:

TypeScript
const { default: doc } = await import("./config.xml");

一致性

Bun 的 XML 解析器使用 Rust 編寫,並作為非驗證處理器實現了 XML 1.0(第五版),不會讀取外部實體:

  • 整個文件(包括內部 DTD 子集)都必須格式正確,否則會丟擲 SyntaxError。
  • 解析器會展開文件中宣告的內部實體,並設定展開限制,因此“十億笑聲”攻擊載荷會失敗,而不是耗盡記憶體。它會規範化屬性值,並應用內部子集中宣告的屬性預設值。
  • 解析器不會獲取或讀取外部 DTD 或外部實體,因此不存在 XXE 攻擊面。對於沒有 DTD 的文件,引用未宣告的實體會導致錯誤。如果 DOCTYPE 指向外部子集(或使用引數實體),而該子集可能聲明瞭此實體,解析器就會保留原樣的引用(&nbsp; 仍為 &nbsp;),除非文件指定了 standalone="yes"。
  • 解析器不會根據 DTD 進行驗證。它不會解析名稱空間,並會原樣保留帶字首的名稱。

解析器使用 W3C XML 一致性測試套件進行測試。對於此類處理器有明確預期結果的 1,679 個測試用例,解析器均已通過:拒絕格式不正確的文件,並接受格式正確的文件。對於測試套件提供了規範輸出的用例,格式正確文件的元素樹(包括處理指令)與規範輸出逐位元組匹配。已轉換的測試套件列出了所有用例,包括那些結果合理地取決於是否讀取外部實體的用例。


效能

與 Bun 的 JSON 解析器一樣,解析器分兩個階段工作。SIMD 掃描階段(執行環境分派 AVX2/AVX-512/NEON/SVE 核心)會查詢可能影響解析的位元組,因此解析器無需逐位元組掃描字元資料、屬性值、註釋和 CDATA 段。元素名稱和屬性名稱會像 JSON.parse 一樣複用 JavaScriptCore 的原子字串快取。

bench/xml/xml.mjs 會在相同文件上比較 Bun.XML.parse 與常用的 npm 解析器(越低越好;Linux x64,單核):

文件Bun.XML.parsetxmlfast-xml-parser@xmldom/xmldomxml2js
S3 ListObjectsV2 響應,231 KB1.1 ms4.0 ms23 ms31 ms19 ms
Atom feed,193 KB1.1 ms3.7 ms19 ms23 ms16 ms
libphonenumber 後設資料,960 KB5.3 ms9.6 ms56 ms53 ms—
Chromium enums.xml,1.4 MB16 ms41 ms150 ms103 ms—
freedesktop MIME 資料庫,2.2 MB27 ms56 ms299 ms280 ms—