首页 / Bun 入门教程 / Bun 打包器入门

Bun 入门教程

Bun 打包器入门

本教程共 34 篇 · 第 16 篇 · 更新于 2026-08-06

Bun打包器Bundler构建前端工具链

本节目标:

  • 认识 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 是绝大多数新项目的推荐选择。cjsiife 默认启用实验性支持,如果你的场景确实需要它们,记得在文档和团队内对齐预期,避免依赖尚未稳定的行为。

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 buildbun runbun test 共享同一套模块解析与转译逻辑。

结果就是不会出现”运行时能跑、打包却报错”的解析差异。新手少配一堆工具,团队则天然拿到一致的开发、构建、测试环境。

打包器本身也可以单独用。把 target 设成 node,产物交给 Node.js 执行就行,并不强制你把运行时也换成 Bun。想清楚这点,在”全用 Bun”和”只借打包器”之间就好取舍了。

骨架搭完,下一章进加载器与资源处理,看看 Bun 怎么识别并转换 CSS、图片、JSON、TOML 这些文件。