XML
通過執行環境 API 和打包器整合使用 Bun 內建的 XML 支援
在 Bun 中,XML 與 JSON、TOML、YAML 和 JSON5 一樣是一等公民。你可以:
- 使用
Bun.XML.parse和Bun.XML.stringify解析和序列化 XML - 在執行環境將 XML 檔案
import和require為模組(包括熱過載和 watch 模式支援) - 在使用 Bun 打包器的前端應用中
import和requireXML 檔案
執行環境 API
Bun.XML.parse()
將 XML 文件解析為普通 JavaScript 物件。
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 名稱的開頭,因此屬性鍵和文本鍵不會與子元素鍵衝突。
緊湊結構用於 資料。它不會保留名稱不同的兄弟元素之間的相對順序,也不會保留文本相對於子元素的位置:
XML.parse(`<p>Hello <b>world</b>!</p>`);
// { p: { "#text": "Hello !", b: "world" } }
如果這些資訊很重要——例如處理文件而非資料時——請傳入 { compact: false },以獲取根元素的樹結構,並保留元素內容在文件中的順序:
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">)。對於需要遍歷或可能帶有屬性的值,請採取穩妥的處理方式:
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。其他編碼會丟擲錯誤。
XML.parse(await Bun.file("feed.xml").bytes());
錯誤處理
當文件格式不正確時,Bun.XML.parse() 會丟擲 SyntaxError(沒有寬鬆模式);對於深度異常的巢狀,會丟擲 RangeError:
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。
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,因此你可以將結果串聯到外層元素中。要寫入檔案,請自行新增序言:
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 會將其寫在同一行,因此縮排不會改變字元資料:
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)。模組的值是上文所述的緊湊物件:
<?xml version="1.0" encoding="UTF-8"?>
<config env="production">
<database host="localhost" port="5432" name="myapp"/>
<feature name="auth"/>
<feature name="rateLimit"/>
</config>
預設匯入
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"]
命名匯入
根元素也可以作為命名匯入使用:
import { config } from "./config.xml";
console.log(config.database["@port"]); // "5432"
CommonJS
const { config } = require("./config.xml");
console.log(config.database["@name"]); // "myapp"
匯入屬性
使用 with { type: "xml" } 將其他副檔名的檔案作為 XML 解析:
import feed from "./export.rss" with { type: "xml" };
XML 熱過載
使用 bun --hot 執行應用程式時,Bun 會在 XML 檔案發生變化時重新載入它們:
import { config } from "./config.xml";
Bun.serve({
port: 3000,
fetch(req) {
return new Response(`Running in ${config["@env"]} against ${config.database["@host"]}`);
},
});
bun --hot server.ts
打包器整合
使用 Bun 打包時,打包器會在建置時解析匯入的 XML 檔案,並將它們內聯為 JavaScript 物件:
bun build app.ts --outdir=dist
在建置時解析意味著:
- 生產環境中零執行環境 XML 解析開銷
- 更小的打包體積
- 對未使用的屬性進行 Tree Shaking
動態匯入
你可以動態匯入 XML 檔案:
const { default: doc } = await import("./config.xml");
一致性
Bun 的 XML 解析器使用 Rust 編寫,並作為非驗證處理器實現了 XML 1.0(第五版),不會讀取外部實體:
- 整個文件(包括內部 DTD 子集)都必須格式正確,否則會丟擲
SyntaxError。 - 解析器會展開文件中宣告的內部實體,並設定展開限制,因此“十億笑聲”攻擊載荷會失敗,而不是耗盡記憶體。它會規範化屬性值,並應用內部子集中宣告的屬性預設值。
- 解析器不會獲取或讀取外部 DTD 或外部實體,因此不存在 XXE 攻擊面。對於沒有 DTD 的文件,引用未宣告的實體會導致錯誤。如果 DOCTYPE 指向外部子集(或使用引數實體),而該子集可能聲明瞭此實體,解析器就會保留原樣的引用(
仍為 ),除非文件指定了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.parse | txml | fast-xml-parser | @xmldom/xmldom | xml2js |
|---|---|---|---|---|---|
S3 ListObjectsV2 響應,231 KB | 1.1 ms | 4.0 ms | 23 ms | 31 ms | 19 ms |
| Atom feed,193 KB | 1.1 ms | 3.7 ms | 19 ms | 23 ms | 16 ms |
| libphonenumber 後設資料,960 KB | 5.3 ms | 9.6 ms | 56 ms | 53 ms | — |
Chromium enums.xml,1.4 MB | 16 ms | 41 ms | 150 ms | 103 ms | — |
| freedesktop MIME 資料庫,2.2 MB | 27 ms | 56 ms | 299 ms | 280 ms | — |