JavaScript 中的模塊解析是一個復雜的話題。
生態系統目前正處於從 CommonJS 模塊向原生 ES 模塊的多年過渡之中。TypeScript 強制執行自己的一套導入擴展規則,這些規則與 ESM 不兼容。不同的構建工具通過不同的不兼容機制支持路徑重映射。
Bun 旨在提供一個一致且可預測的模塊解析系統,開箱即用。不幸的是,它仍然相當復雜。
語法
考慮以下文件。
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}當我們運行 index.ts 時,它打印 "Hello world!"。
bun index.ts
Hello world!在這種情況下,我們從 ./hello 導入,這是一個沒有擴展名的相對路徑。帶擴展名的導入是可選的但受支持。 為了解析此導入,Bun 將按順序檢查以下文件:
./hello.tsx./hello.jsx./hello.ts./hello.mjs./hello.js./hello.cjs./hello.json./hello/index.tsx./hello/index.jsx./hello/index.ts./hello/index.mjs./hello/index.js./hello/index.cjs./hello/index.json
導入路徑可以可選地包含擴展名。如果存在擴展名,Bun 將僅檢查具有該確切擴展名的文件。
import { hello } from "./hello";
import { hello } from "./hello.ts"; // 這有效如果你從 "*.js{x}" 導入,Bun 還將檢查匹配的 *.ts{x} 文件,以與 TypeScript 的 ES 模塊支持 兼容。
import { hello } from "./hello";
import { hello } from "./hello.ts"; // 這有效
import { hello } from "./hello.js"; // 這也有效Bun 支持 ES 模塊(import/export 語法)和 CommonJS 模塊(require()/module.exports)。以下 CommonJS 版本在 Bun 中也能工作。
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;也就是說,在新項目中不鼓勵使用 CommonJS。
模塊系統
Bun 具有 CommonJS 和 ES 模塊的原生支持。ES 模塊是新項目的推薦模塊格式,但 CommonJS 模塊仍在 Node.js 生態系統中廣泛使用。
在 Bun 的 JavaScript 運行時中,require 可以被 ES 模塊和 CommonJS 模塊使用。如果目標模塊是 ES 模塊,require 返回模塊命名空間對象(等同於 import * as)。如果目標模塊是 CommonJS 模塊,require 返回 module.exports 對象(如在 Node.js 中)。
| 模塊類型 | require() | import * as |
|---|---|---|
| ES 模塊 | 模塊命名空間 | 模塊命名空間 |
| CommonJS | module.exports | default 是 module.exports,module.exports 的鍵是命名導出 |
使用 require()
你可以 require() 任何文件或包,甚至是 .ts 或 .mjs 文件。
const { foo } = require("./foo"); // 擴展名是可選的
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");什麼是 CommonJS 模塊?
在 2016 年,ECMAScript 添加了對 ES 模塊 的支持。ES 模塊是 JavaScript 模塊的標准。然而,數百萬個 npm 包仍使用 CommonJS 模塊。
CommonJS 模塊是使用 module.exports 導出值的模塊。通常,require 用於導入 CommonJS 模塊。
const stuff = require("./stuff");
module.exports = { stuff };CommonJS 和 ES 模塊之間最大的區別在於 CommonJS 模塊是同步的,而 ES 模塊是異步的。還有其他區別。
- ES 模塊支持頂層
await,CommonJS 模塊不支持。 - ES 模塊始終處於 嚴格模式,而 CommonJS 模塊不是。
- 瀏覽器沒有對 CommonJS 模塊的原生支持,但它們通過
<script type="module">對 ES 模塊有原生支持。 - CommonJS 模塊不是靜態可分析的,而 ES 模塊只允許靜態導入和導出。
CommonJS 模塊: 這些是 JavaScript 中使用的一種模塊系統。CommonJS 模塊的一個關鍵特性是它們同步加載和執行。這意味著當你導入 CommonJS 模塊時,該模塊中的代碼立即運行,你的程序等待它完成後再繼續下一個任務。這類似於從頭到尾閱讀一本書而不跳過頁面。
ES 模塊(ESM): 這些是 JavaScript 中引入的另一種模塊系統。與 CommonJS 相比,它們的行為略有不同。在 ESM 中,靜態導入(使用 import 語句進行的導入)是同步的,就像 CommonJS 一樣。這意味著當你使用常規 import 語句導入 ESM 時,該模塊中的代碼立即運行,你的程序逐步進行。把它想象成一頁一頁地讀書。
動態導入: 現在,這裡可能會令人困惑。ES 模塊還支持通過 import() 函數即時導入模塊。這稱為"動態導入",它是異步的,因此不會阻塞主程序執行。相反,它在後台獲取和加載模塊,而你的程序繼續運行。一旦模塊准備好,你就可以使用它。這就像在閱讀書籍時獲取額外信息,而不必暫停閱讀。
總結:
- CommonJS 模塊和靜態 ES 模塊(
import語句)以類似的同步方式工作,就像從頭到尾閱讀一本書。 - ES 模塊還提供使用
import()函數異步導入模塊的選項。這就像在閱讀書籍中間查找額外信息而不中斷。
使用 import
你可以 import 任何文件或包,甚至是 .cjs 文件。
import { foo } from "./foo"; // 擴展名是可選的
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";一起使用 import 和 require()
在 Bun 中,你可以在同一文件中使用 import 或 require——它們始終有效。
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");頂層 await
此規則的唯一例外是頂層 await。你不能 require() 使用頂層 await 的文件,因為 require() 函數本質上是同步的。
幸運的是,很少有庫使用頂層 await,所以這很少成為問題。但是,如果你在自己的應用程序代碼中使用頂層 await,請確保該文件沒有被應用程序中其他地方的 require()。相反,你應該使用 import 或 動態 import()。
導入包
Bun 實現了 Node.js 模塊解析算法,因此你可以使用裸說明符從 node_modules 導入包。
import { stuff } from "foo";此算法的完整規范在 Node.js 文檔 中有官方記錄;我們這裡不再贅述。簡而言之:如果你從 "foo" 導入,Bun 會在文件系統中向上掃描包含包 foo 的 node_modules 目錄。
NODE_PATH
Bun 支持 NODE_PATH 用於額外的模塊解析目錄:
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";多個路徑使用平台的分隔符(Unix 上為 :,Windows 上為 ;):
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # Windows一旦找到 foo 包,Bun 會讀取 package.json 來確定應如何導入該包。要確定包的入口點,Bun 首先讀取 exports 字段並檢查以下條件。
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // 如果導入者是 CommonJS
"import": "./index.mjs", // 如果導入者是 ES 模塊
"default": "./index.js"
}
}在 package.json 中 首先 出現的這些條件中的任何一個都用於確定包的入口點。
Bun 尊重子路徑 "exports" 和 "imports"。
{
"name": "foo",
"exports": {
".": "./index.js"
}
}子路徑導入和條件導入一起使用。
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}與 Node.js 一樣,在 "exports" 映射中指定任何子路徑將阻止其他子路徑可導入;你只能導入顯式導出的文件。給定上面的 package.json:
import stuff from "foo"; // 這有效
import stuff from "foo/index.mjs"; // 這無效NOTE
**發布 TypeScript** — 請注意,Bun 支持特殊的 `"bun"` 導出條件。如果你的庫是用 TypeScript 編寫的,你可以直接將(未轉譯的!)TypeScript 文件發布到 `npm`。如果你在 `"bun"` 條件中指定包的 `*.ts` 入口點,Bun 將直接導入和執行你的 TypeScript 源文件。如果未定義 exports,Bun 會回退到 "module"(僅限 ESM 導入),然後是 "main"。
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}自定義條件
--conditions 標志允許你指定在從 package.json "exports" 解析包時使用的條件列表。
此標志在 bun build 和 Bun 的運行時中都受支持。
# 與 bun build 一起使用:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# 與 Bun 的運行時一起使用:
bun --conditions="react-server" ./app/foo/route.js你也可以在編程上與 Bun.build 一起使用 conditions:
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});路徑重映射
Bun 通過 TypeScript 的 compilerOptions.paths 在 tsconfig.json 中支持導入路徑重映射,這與編輯器配合良好。如果你不是 TypeScript 用戶,你可以通過在項目根目錄使用 jsconfig.json 實現相同的行為。
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // 將說明符映射到文件
"components/*": ["components/*"] // 通配符匹配
}
}
}Bun 還支持 package.json 中的 Node.js 風格子路徑導入,其中映射的路徑必須以 # 開頭。這種方法與編輯器配合不佳,但兩種選項可以一起使用。
{
"imports": {
"#config": "./config.ts", // 將說明符映射到文件
"#components/*": "./components/*" // 通配符匹配
}
}Bun 中 CommonJS 互操作性的底層細節
Bun 的 JavaScript 運行時具有 CommonJS 的原生支持。當 Bun 的 JavaScript 轉譯器檢測到 module.exports 的使用時,它將文件視為 CommonJS。然後模塊加載器將轉譯後的模塊包裝在如下形狀的函數中:
(function (module, exports, require) {
// 轉譯的模塊
})(module, exports, require);module、exports 和 require 非常像 Node.js 中的 module、exports 和 require。這些通過 C++ 中的 with scope 分配。內部 Map 存儲 exports 對象以處理在模塊完全加載之前的循環 require 調用。
一旦 CommonJS 模塊成功評估,就會創建一個 Synthetic Module Record,其 default ES 模塊 導出設置為 module.exports,module.exports 對象的鍵重新導出為命名導出(如果 module.exports 對象是對象)。
當使用 Bun 的打包器時,這工作方式不同。打包器將 CommonJS 模塊包裝在 require_${moduleName} 函數中,該函數返回 module.exports 對象。
import.meta
import.meta 對象是模塊訪問有關自身信息的一種方式。它是 JavaScript 語言的一部分,但其內容未標准化。每個"宿主"(瀏覽器、運行時等)可以自由地在 import.meta 對象上實現任何它希望的屬性。
Bun 實現了以下屬性。
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"
import.meta.main; // 如果此文件由 `bun run` 直接執行則為 `true`
// 否則為 `false`
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| 屬性 | 描述 |
|---|---|
import.meta.dir | 包含當前文件的目錄的絕對路徑,例如 /path/to/project。等同於 CommonJS 模塊(和 Node.js)中的 __dirname |
import.meta.dirname | import.meta.dir 的別名,用於 Node.js 兼容性 |
import.meta.env | process.env 的別名。 |
import.meta.file | 當前文件的名稱,例如 index.tsx |
import.meta.path | 當前文件的絕對路徑,例如 /path/to/project/index.ts。等同於 CommonJS 模塊(和 Node.js)中的 __filename |
import.meta.filename | import.meta.path 的別名,用於 Node.js 兼容性 |
import.meta.main | 指示當前文件是否是當前 bun 進程的入口點。該文件是否由 bun run 直接執行,還是被導入? |
import.meta.resolve | 將模塊說明符(例如 "zod" 或 "./file.tsx")解析為 url。等同於 瀏覽器中的 import.meta.resolve。示例:import.meta.resolve("zod") 返回 "file:///path/to/project/node_modules/zod/index.ts" |
import.meta.url | 當前文件的 string url,例如 file:///path/to/project/index.ts。等同於 瀏覽器中的 import.meta.url |