Skip to main content
JavaScript 中的模块解析是一个复杂的话题。 生态系统目前正处于从 CommonJS 模块向原生 ES 模块的多年过渡期。TypeScript 强制执行自己的一套关于导入扩展名的规则,这些规则与 ESM 不兼容。不同的构建工具通过各自不兼容的机制支持路径重映射。 Bun 旨在提供一个一致且可预测的模块解析系统,确保“开箱即用”。但不幸的是,这仍然相当复杂。

语法

考虑以下文件。
import { hello } from "./hello";

hello();
当我们运行 index.ts 时,它会打印 “Hello world!”。
terminal
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 将只检查具有该确切扩展名的文件。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import { hello } from "./hello";
import { hello } from "./hello.ts"; // 这样写是可以的
如果你导入 from "*.js{x}",Bun 会额外检查匹配的 *.ts{x} 文件,以兼容 TypeScript 的ES 模块支持
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
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();
不过,建议新项目避免使用 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 模块模块命名空间对象模块命名空间对象
CommonJSmodule.exportsdefaultmodule.exportsmodule.exports 的键作为命名导出

使用 require()

你可以 require() 任何文件或包,甚至是 .ts.mjs 文件。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
const { foo } = require("./foo"); // 扩展名可选
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");
2016 年,ECMAScript 添加了对ES 模块的支持。ES 模块是 JavaScript 模块的标准。但数以百万计的 npm 包仍采用 CommonJS 模块。CommonJS 模块是使用 module.exports 导出值的模块。通常使用 require 来导入 CommonJS 模块。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/javascript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=e1a9947d6e369be0e97814b29cf9f8cdmy-commonjs.cjs
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 模块时,其代码会立即执行,且程序必须等待它执行完毕后才能继续。就像从头到尾读完一本书,不跳页。ES 模块(ESM): 是另一种引入的模块系统。它的行为与 CommonJS 略有不同。ESM 中,静态导入(使用 import)是同步的,就像 CommonJS 一样。当你用常规的 import 语句导入 ESM 时,其模块代码会立即执行,程序会一步一步执行相应操作。类似于逐页阅读一本书。动态导入: 令人迷惑的是,ES 模块支持通过 import() 函数动态导入模块。这是“动态导入”,是异步的,不会阻塞主程序执行。它会在后台加载模块,同时主程序继续运行。模块准备好后即可使用。就像边读书边获取补充信息,不需要暂停阅读。总结:
  • CommonJS 模块和静态 ES 模块 (import 语句) 工作方式类似,都是同步的,像连贯读完一本书。
  • ES 模块还支持用动态 import() 异步导入模块。这就像在阅读过程中查找额外信息而不中止。

使用 import

你可以 import 任何文件或包,甚至是 .cjs 文件。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import { foo } from "./foo"; // 扩展名可选
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";

同时使用 importrequire()

在 Bun 中,你可以在同一文件内同时使用 importrequire —— 它们都随时有效。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
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 中导入包。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import { stuff } from "foo";
该算法的完整规范在 Node.js 文档 中有官方说明,这里不再赘述。简要地说:如果你导入 from "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 字段并检查以下条件:
package.json
{
  "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"
package.json
{
  "name": "foo",
  "exports": {
    ".": "./index.js"
  }
}
子路径导入和条件导入可以共同使用。
package.json
{
  "name": "foo",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.js"
    }
  }
}
与 Node.js 一样,若在 "exports" 映射中指定了任何子路径,则禁止导入未明确导出的其他子路径;只能导入明确导出的文件。以上述 package.json 为例:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import stuff from "foo"; // 这样可行
import stuff from "foo/index.mjs"; // 这样不行
发布 TypeScript — 请注意 Bun 支持特殊的 "bun" 导出条件。如果你的库是用 TypeScript 编写的,可以直接将未转译的 TypeScript 文件发布到 npm。只需在包的 "bun" 条件中指定你的 *.ts 入口,Bun 将直接导入并执行你的 TypeScript 源文件。
如果未定义 exports,Bun 会回退到先使用 "module"(仅用于 ESM 导入),然后是 "main"
package.json
{
  "name": "foo",
  "module": "./index.js",
  "main": "./index.js"
}

自定义条件

--conditions 标志允许你指定一个条件列表,用于解析 package.json"exports" 此标志既支持 bun build,也支持 Bun 的运行时。
terminal
# 使用 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
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79build.ts
await Bun.build({
  conditions: ["react-server"],
  target: "bun",
  entryPoints: ["./app/foo/route.js"],
});

路径重映射

Bun 支持通过 TypeScript 的 tsconfig.json 中的 compilerOptions.paths 进行导入路径重映射,这对编辑器支持良好。如果你不用 TypeScript,可以使用项目根目录下的 jsconfig.json 实现相同功能。
tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "config": ["./config.ts"],     // 映射标识符到文件
      "components/*": ["components/*"] // 通配符匹配
    }
  }
}
Bun 也支持 Node.js 风格的子路径导入,可在 package.json 中配置;映射路径必须以 # 开头。这种方法对编辑器支持不如前者,但两种方案可以同时使用。
package.json
{
  "imports": {
    "#config": "./config.ts",       // 映射标识符到文件
    "#components/*": "./components/*" // 通配符匹配
  }
}
Bun 的 JavaScript 运行时对 CommonJS 有原生支持。当 Bun 的 JavaScript 转译器检测到 module.exports 的用法时,会将文件视作 CommonJS。模块加载器会把转译后的模块包裹进如下形式的函数:
(function (module, exports, require) {
  // 转译后的模块代码
})(module, exports, require);
moduleexportsrequire 十分类似于 Node.js 中的同名变量。它们通过 C++ 中的 with 作用域 赋值。一个内部的 Map 用来存储 exports 对象,以处理模块未完全加载完前的循环 require 调用。当 CommonJS 模块成功评估后,会创建一个合成的模块记录(Synthetic Module Record),其 default ES 模块导出被设置为 module.exports,而 module.exports 对象中的键(如果是对象)会被重新导出为命名导出。使用 Bun 的打包器时,上述机制有所不同。打包器会将 CommonJS 模块包裹进 require_${moduleName} 函数,该函数返回 module.exports 对象。

import.meta

import.meta 对象允许模块访问有关自身的信息。它是 JavaScript 语言的一部分,但其内容未标准化。每个宿主环境(浏览器、运行时等)都可以自由实现各自的 import.meta 属性。 Bun 实现了如下属性。
/path/to/project/file.ts
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.dirnameimport.meta.dir 的别名,兼容 Node.js
import.meta.envprocess.env 的别名
import.meta.file当前文件名,例如 index.tsx
import.meta.path当前文件的绝对路径,例如 /path/to/project/index.ts。等价于 CommonJS 模块(及 Node.js)中的 __filename
import.meta.filenameimport.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当前文件的字符串 URL,例如 file:///path/to/project/index.ts。等效于浏览器中的import.meta.url