Bun 打包器入门
本教程共 34 篇 · 第 16 篇 · 更新于 2026-08-06
本节目标:
- 认识 Bun 内置打包器与
bun build命令的基本定位- 掌握入口(entrypoint)、输出(outdir/outfile)的配置方式
- 理解 target(目标环境)与 format(模块格式)的差异与默认行为
- 跑通一个最小可运行的打包示例
加载器、插件、压缩这些进阶主题,得先有个骨架托着才好讲。所以这一章我们只做一件事:把 Bun 打包器的基本盘摸清楚。
Bun 的打包器不是外挂工具,而是和运行时同在一个二进制里的原生模块。解析、转译、产物生成走的是同一套代码路径。你不用装 esbuild 或 Rollup,也不用维护配置胶水,一条命令就能从源码走到产物。
对刚上手的人来说,把下面几个概念理顺,比背一堆参数管用得多。
16.1 一条命令就能打包
Bun 打包器最简单的用法就是 bun build,后面跟上入口文件的路径。所谓”入口”,就是构建的起点文件——打包器会从它出发,递归分析 import / require 的依赖关系,把整棵依赖树合并成最终产物。
bun build ./index.tsx --outdir ./out
上面这条命令会以 ./index.tsx 为入口,把它的全部依赖打包后输出到 ./out 目录。默认情况下,Bun 会把入口名作为输出文件名(例如 index.js)。如果你只想打包单文件、并明确指定输出文件名,可以使用 --outfile:
bun build ./src/cli.ts --outfile ./dist/cli.js
Note
bun build既可以接收单个入口,也可以接收多个入口(例如bun build ./a.ts ./b.ts --outdir ./out),此时每个入口都会生成一个对应的产物。在 JavaScript API 中,入口以数组形式传入entrypoints。
除了命令行,Bun 也提供等价的 JavaScript API Bun.build(),适合写进构建脚本或配合其他工具编排:
const result = await Bun.build({
entrypoints: ["./index.tsx"],
outdir: "./out",
});
// 成功时,result.outputs 里是所有产物,result.logs 里是警告与提示
for (const artifact of result.outputs) {
console.log(artifact.path, artifact.kind);
}
这里有个容易踩的坑:构建失败时 Bun.build() 默认抛错,返回的 Promise 会以一个 AggregateError 拒绝,而不是安静地返回 success: false。所以要接管错误,就得用 try/catch:
try {
await Bun.build({
entrypoints: ["./index.tsx"],
outdir: "./out",
});
} catch (e) {
const error = e as AggregateError;
// error.errors 是一组 BuildMessage / ResolveMessage
console.error(error);
}
如果你更习惯”看返回值判断成败”的写法,把 throw 显式设成 false 即可。这时 Bun.build() 不再抛错,而是返回带 success 字段的对象:
const result = await Bun.build({
entrypoints: ["./index.tsx"],
outdir: "./out",
throw: false,
});
if (!result.success) {
for (const message of result.logs) {
console.error(message);
}
}
Warning
throw的默认值是true。照抄老博客里if (!result.success)的写法而不加throw: false,构建一旦失败就会直接抛异常,那段判断根本走不到。
16.2 入口与输出
入口是构建的”根”。Bun 支持多种入口类型:一个普通的 TypeScript/JavaScript 文件、一个 HTML 文件(见第 20 章)、甚至是字符串形式的虚拟模块(通过 files 选项在内存中打包)。绝大多数场景下,入口就是一个 .ts / .tsx / .js / .jsx 文件。
输出相关的选项主要有三个:
outdir:输出目录。多个入口、或打包过程中生成的代码分割(splitting)产物都会放在这个目录。outfile:单文件输出的完整路径。它和outdir互斥,用于只打包一个入口并固定文件名。naming:自定义产物命名模板,默认./[dir]/[name].[ext],支持[dir]、[name]、[ext]、[hash]四个占位符。
命名模板在 CLI 上不是一个 --naming,而是按产物类型拆成三个标志:--entry-naming(入口)、--chunk-naming(分割出的 chunk)、--asset-naming(复制出的静态资源)。
# 给入口产物加哈希,便于做长效缓存(long-term caching)
bun build ./index.ts --outdir ./dist --entry-naming "[dir]/[name]-[hash].[ext]"
// JS API 里则是一个 naming 对象,三类产物各自配置
await Bun.build({
entrypoints: ["./index.ts"],
outdir: "./dist",
naming: {
entry: "[dir]/[name].[ext]",
chunk: "chunks/[name]-[hash].[ext]",
asset: "assets/[name]-[hash].[ext]",
},
});
Tip当你部署到 CDN 或带强缓存的静态服务器时,给文件名加 hash 是一种通用做法:内容变化 hash 才变,浏览器缓存策略可以设成”永久缓存”,既快又安全。
16.3 目标环境 target
target 决定打包器针对哪种运行环境生成代码,可选值有:
browser:默认值,生成浏览器能直接运行的代码。bun:生成面向 Bun 运行时的代码,可以使用 Bun 专属 API。node:生成面向 Node.js 的代码,保留require等 Node 语义。
# 明确指定目标为 Node.js
bun build ./server.ts --outfile ./dist/server.js --target=node
这里有个自动推断规则要记住。只要任意一个入口文件首行带 Bun shebang(#!/usr/bin/env bun),打包器就会把默认 target 从 browser 改成 bun。
道理很直白:写了 shebang,说明你想做的是一个能直接执行的 Bun 脚本。省一次手动指定,挺实用。
Warning不要把
target和”编译成二进制”混为一谈。target=bun只是告诉打包器按 Bun 运行时语义产出代码;真正生成独立可执行文件要用第 19 章讲的bun build --compile。
16.4 模块格式 format
format 控制产物的模块语法,可选值有:
esm:默认格式,使用import/export,现代浏览器和打包工具都支持。cjs:CommonJS 格式,使用require/module.exports,主要兼容传统 Node.js 生态(在 v1.3.14 中标记为实验性)。选cjs时默认 target 会从browser变成node。iife:立即执行函数表达式,把整段代码包进一个函数作用域,常用于直接丢进<script>标签的老式浏览器脚本(同样为实验性)。
# 打包成 CommonJS 格式,方便旧版 Node 项目 require
bun build ./lib.ts --outfile ./dist/lib.cjs --format=cjs
# 打包成 IIFE 格式,直接当作传统脚本引用
bun build ./widget.tsx --outfile ./dist/widget.js --format=iife
// 在 JS API 中通过 format 字段指定
await Bun.build({
entrypoints: ["./widget.tsx"],
outfile: "./dist/widget.js",
format: "iife",
});
Note
esm是绝大多数新项目的推荐选择。cjs和iife默认启用实验性支持,如果你的场景确实需要它们,记得在文档和团队内对齐预期,避免依赖尚未稳定的行为。
16.5 跑通一个最小示例
下面用一个完整的端到端例子把前面概念串起来。假设项目结构如下:
my-app/
├── src/
│ ├── index.ts
│ └── greet.ts
└── package.json
src/greet.ts 导出一个简单函数:
export function greet(name: string): string {
return `你好,${name}!`;
}
src/index.ts 作为入口引入并使用它:
import { greet } from "./greet";
console.log(greet("Bun"));
在 my-app 目录下执行:
bun build ./src/index.ts --outdir ./dist
构建完成后,./dist/index.js 就是合并了两个文件的可执行产物,直接运行:
bun ./dist/index.js
# 输出:你好,Bun!
你也可以用 JS API 写成一个构建脚本 build.ts:
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "bun",
throw: false, // 让失败以返回值形式呈现,便于自定义退出码
});
if (!result.success) {
console.error(result.logs);
process.exit(1);
}
console.log("构建完成,产物在 ./dist");
然后 bun run build.ts 即可。脚本化的好处在后面几章会越来越明显:loader、plugin、minify 这些选项都直接挂在同一个 Bun.build() 调用上,不用换工具。
16.6 小结
本章我们建立了 Bun 打包器的整体心智模型:
- 用
bun build <入口>或Bun.build()触发构建,二者能力对齐; - 入口是依赖树的根,输出靠
outdir/outfile/naming控制,CLI 上命名模板拆成--entry-naming等三个标志; Bun.build()默认失败即抛AggregateError,想拿success字段要传throw: false;target决定运行环境(browser/bun/node),入口带 Bun shebang 会把默认值改成bun;format决定模块语法(esm 默认,cjs/iife 为实验性);- 一个小例子跑通了”多文件源码 → 单产物 → 直接运行”的完整链路。
再说一点设计上的好处。打包器、运行时、包管理器、测试运行器都在同一个可执行文件里,bun build 和 bun run、bun test 共享同一套模块解析与转译逻辑。
结果就是不会出现”运行时能跑、打包却报错”的解析差异。新手少配一堆工具,团队则天然拿到一致的开发、构建、测试环境。
打包器本身也可以单独用。把 target 设成 node,产物交给 Node.js 执行就行,并不强制你把运行时也换成 Bun。想清楚这点,在”全用 Bun”和”只借打包器”之间就好取舍了。
骨架搭完,下一章进加载器与资源处理,看看 Bun 怎么识别并转换 CSS、图片、JSON、TOML 这些文件。