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 |