在带 sourcemaps 和 minification 的情况下,从零开始捆绑 10 个 three.js 副本
通过内置的 Bun.build() 函数或者 bun build CLI 命令用这款 bundler 构建前端应用。
Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './build',
minify: true,
// additional config
});
JavaScript 最初只负责自动填充表单字段,如今却在多年发展之后成了众多关键场景内的开发手段,甚至负责驱动火箭发射项目的地面控制台。
相应的,JavaScript 生态系统的复杂性也呈现出爆发式增长。我们该如何运行 TypeScript 文件?如何构建 / 捆绑生产代码?软件包是否适用于 ESM?如何加载仅限本地的配置?需不需要安装相应的依赖项?如何让 sourcemaps 顺利起效?需要烦恼的问题实在太多。
复杂性是效率的大敌,导致我们得花很长时间把工具组合起来,或者坐等处理完成。npm 包的安装速度很慢,测试消耗的时间也远比预想的长。既然 2003 年的时候只需要几毫秒就能把文件上传到 FTP 服务器,2023 年部署软件反倒还需要好几分钟?
多年以来,我一直对 JavaScript 缓慢的运行速度感到沮丧。如果测试变更甚至是保存文件所消耗的时间都足够我们看几眼 Hacker News,那肯定是哪里出了大问题。
倒不是说复杂性就不该存在。Bundlers 和 minifiers 让网站的加载速度圩快,TypeScript 的编辑器内交互文档则提升了开发人员的工作效率。类型安全有助于在实际报错前就提出警告,而作为版本化包形式的依赖项也要比以往的复制文件更易于维护。
但 Unix 的基本理念就是“集中精力做好一件事”,但这“一件事”被多种孤立工具所割裂时,理念也将随之崩溃。
正因为如此,我们决定打造 Bun,并在今天高兴地向大家介绍刚刚推出的 Bun bundler。
使用这款新的 bundler,Bun 生态系统将捆绑雕琢成了一流元素,具体涵盖 bun build CLI 命令、新的顶级 Bun.build 函数,还有稳定的插件系统。
之所以决定打造 Bun 的专有 bundler,主要是考虑到以下几个原因。
Bundlers 是一种元工具,能够协调并启用所有其他工具,包括 JSX、TypeScript、CSS 模块和服务器组件等——所有这一切都需要 bundler 的集成才能工作。
现如今,bundlers 已经成为 JavaScript 生态系统中巨大复杂性的来源。通过向 JavaScript 运行时内引入新捆绑机制,我们相信能够让前端和全栈代码的交付变得更简单、更快捷。
快速插件。插件将在快速启动的轻量化 Bun 进程内执行。
无冗余转译。使用 target:”bun”,新 bundler 就能生成面向 Bun 运行时进行优化的预转译文件,从而提高运行性能并避免不必要的重新转译。
统一插件 API。Bun 提供统一的插件 API,能够与 bundler 和运行时配合使用。任何扩展 Bun 捆绑功能的插件亦可用于扩展 Bun 的运行时功能。
运行时集成。构建会返回一个 BuildArtifact 对象数组,这些对象将实现 Blob 接口,并可被直接传递至 HTTP API,例如 new Resonpse( )。运行时还为 BiuildArtifact 实现了非常精美的输出效果。
独立的可执行文件。此 bundler 可以通过—compile 标志在 TypeScript 和 JavaScript 脚本中生成独立的可执行文件。这些可执行文件是完全独立的,包括 Bun 运行时副本。
bundler 还将很快与 Bun 的 HTTP 服务器 API(Bun.serve)相集成,从而利用简单的声明性 API 表达当前复杂的构建管线。这一点将在后文中具体介绍。
性能当然有所提高。作为运行时,Bun 的代码库已经包含了快速解析和转换源代码的基础成果(在 Zig 中实现)。但问题是其很难与现有原生 bundler 相集成,而且进程间通信所引发的额外开销还会损耗性能。
而新 bundler 没有让我们失望。在基准测试中(esbuild 的 three.js 基准测试),Bun 比 esbuild 快 1.75 倍,比 Parcel 2 快 150 倍,比 Rollup + Terser 快 180 倍,比 Webpack 快 220 倍。
回顾原有 bundlers 的 API,我们发现其中仍有巨大的改进空间。没人喜欢被 bundler 的配置工作消耗宝贵精力,因此 Bun bundler 的 API 设计中着重强调了清晰易用。
API 目前遵循设计最小化的理念。我们的初始版本希望能在不牺牲性能的情况下,提供一个速度快、稳定性佳、可适应大多数现代用例的最小功能集。
下面来看目前的 API:
interface Bun {
build(options: BuildOptions): Promise<BuildOutput>;
}
interface BuildOptions {
entrypoints: string[]; // required
outdir?: string; // default: no write (in-memory only)
target?: "browser" | "bun" | "node"; // "browser"
format?: "esm"; // later: "cjs" | "iife"
splitting?: boolean; // default false
plugins?: BunPlugin[]; // [] // see https://bun.sh/docs/bundler/plugins
loader?: { [k in string]: string }; // see https://bun.sh/docs/bundler/loaders
external?: string[]; // default []
sourcemap?: "none" | "inline" | "external"; // default "none"
root?: string; // default: computed from entrypoints
publicPath?: string; // e.g. http://mydomain.com/
naming?:
| string // equivalent to naming.entry
| { entry?: string; chunk?: string; asset?: string };
minify?:
| boolean // default false
| { identifiers?: boolean; whitespace?: boolean; syntax?: boolean };
}
其他很多 bundler 为了追求功能完备性,而做出了很多糟糕的架构决策,最终导致整体性能下降。而我们正努力避免重蹈覆辙。
目前仅支持 format: "esm"格式。我们打算后续逐步添加对其他模块系统和目标(如 iife)的支持。如果大家确有需求,我们也会考虑添加 cjs otuput 支持(支持 CommonJS 输入,但不支持输出)。
我们目前支持三个“targets”目标,分别为“browser”(默认)、“bun”和“node”。
TypeScript 和 JSX 将被自动转译为原始 JavaScript。
在有可用模块时,通过 "browser" package.json "exports"执行模块解析。
Bun 在浏览器中导入时会自动填充某些 Node.js API,例如 node:crypto,这种行为与 Webpack 4 类似。Bun 自己的 API 目前禁止导入,但我们未来可能会重新考虑这个问题。
Bun 和 Node.js APIs 仍受支持且将保持不变。
通过 Bun 运行时所使用的默认解析算法进行模块解析。
生成的捆绑包使用特殊的 // @bun 标注,表明它们是由 Bun 所生成。通过这种方式,Bun 运行时就能意识到该文件在执行前无需重新编译。
目前,node 的处理方式等同于 target: "bun"。未来,我们计划自动填充 Bun API,例如 Bun global 和 bun:* 内置模块。
新 bundler 支持以下文件类型:
.js .jsx .ts .tsx – 各种 JavaScript 和 TypeScript 文件;
.txt — 纯文本文件,将被内联为字符串;
.json .toml — 这些格式将在编译时被解析并内联为 JSON。
其他一切都将被视为资产,资产会按原本被复制到 outdir,并使用该文件的相对路径或 URL(/images/logo.png)替换导出。
import logo from "./images/logo.png";
console.log(logo);
与运行时本身一样,新 bundler 在设计上可通过插件实现扩展。实际上,运行时插件和 bundler 插件间没有任何区别。
import YamlPlugin from "bun-plugin-yaml";
const plugin = YamlPlugin();
// register a runtime plugin
Bun.plugin(plugin);
// register a bundler plugin
Bun.build({
entrypoints: ["./src/index.ts"],
plugins: [plugin],
});
Bun.build 函数会返回一个 Promise
interface BuildOutput {
outputs: BuildArtifact[];
success: boolean;
logs: Array<object>; // see docs for details
}
interface BuildArtifact extends Blob {
kind: "entry-point" | "chunk" | "asset" | "sourcemap";
path: string;
loader: Loader;
hash: string | null;
sourcemap: BuildArtifact | null;
}
其中 outputs 数组中包含构建所生成的所有文件。每个 artifact 均实现 Blob 接口。
const build = await Bun.build({
/* */
});
for (const output of build.outputs) {
output.size; // file size in bytes
output.type; // MIME type of file
await output.arrayBuffer(); // => ArrayBuffer
await output.text(); // string
}
Artifacts 还包含以下附加属性:
与 BunFile 类似,BuildArtifact 对象也可以被直接传递至 new Response( ) 当中。
const build = Bun.build({
/* */
});
const artifact = build.outputs[0];
// Content-Type is set automatically
return new Response(artifact);
Bun 运行时在记录 BuildArtifact 对象时会提供更精美的输出效果,让调试过程更加轻松。
// build.ts
const build = Bun.build({/* */});
const artifact = build.outputs[0];
console.log(artifact);
Bun 的 bundler 目前通过—server-components 标志对 React 服务器组件提供实验性支持。我们将在本周之内发布额外的说明文档和示例项目。
Bun bundler 支持对未使用代码进行摇树,且在捆绑过程中始终执行摇树精简。
Bun 在 package.json 当中支持"sideEffects": false,相当于提示 bundler 当前包没有副作用,可以积极消除其中的无用代码。
Bun 支持 PURE 注释:
function foo() {
return 123;
}
/** #__PURE__ */ foo();
由于 foo 没有副作用,因此这会产生一个空文件:output.js。
请参阅 Webpack 说明文档,了解更多细节信息。
https://webpack.js.org/guides/tree-shaking/#mark-a-function-call-as-side-effect-free
Bun 支持 NODE_ENV 环境变量和—define CLI 标志。这些一般用于按特定条件在生产构建中包含代码。
如果 process.env.NODE_ENV 被设置为"production",则 Bun 会自动删除包含在 if (process.env.NODE_ENV !== "production") { ... }中的代码。
if (process.env.NODE_ENV !== "production") {
module.exports = require("./cjs/react.development.js");
} else {
module.exports = require("./cjs/react.production.min.js");
}
ESM 和 CommonJS 输入文件均支持 ESM 摇树。Bun bundler 将在安全的情况下,自动删除 ESM 文件中未使用的导出。
import { foo } from "./foo.js";
console.log(foo);
未使用的 bar 导出被删除,因此:
// foo.js
var $foo = 456;
console.log($foo);
在某些受限情况下,Bun bundler 会自动以零运行时开销自动将 CommonJS 转换为 ESM。考虑以下简单示例:
import { foo } from "./foo.js";
console.log(foo);
Bun 会自动将 foo.js 转换为 ESM,并摇树精简未使用的 exports 对象。
// foo.js
var $foo = 123;
// entry.js
console.log($foo);
注意,在大多数情况下,CommonJS 的动态特性会令摇树难以实现。例如考虑以下三个文件:entry.js、foo.js、bar.js。
// entry.js
export default require("./foo");
Bun 无法在不执行 foo.js 的情况下静态确定其导出(Object.assign 可能被覆盖掉,基本导致无法进行静态分析)。在这种情况下,Bun 不会对 exports 对象执行摇树;相反,它会注入 CommonJS 运行时代码以确保其按预期工作。
CommonJS打包器
新 bundler 同时支持内联与外部 sourcemaps。
const build = await Bun.build({
entrypoints: ["./src/index.ts"],
// generates a *.js.map file alongside each output
sourcemap: "external",
// adds a base64-encoded `sourceMappingURL` to the end of each output file
sourcemap: "inline",
});
console.log(await build.outputs[0].sourcemap.json()); // => { version: 3, ... }
没有 minifier 的 JavaScript bundler 显然是不完整的。新版本引入了 Bun 中内置的全新 JavaScript minifier。使用 minify: true 即可启用 minification 功能,通过以下选项具体配置 minification 行为:
{
minify?: boolean | {
identifiers?: boolean; // default: false
whitespace?: boolean; // default: false
syntax?: boolean; // default: false
}
}
Minifier 能够删除无用代码、重命名标识符、删除空格并智能压缩和内联常量值。
// This comment will be removed!
console.log("this" + " " + "text" + " will" + " be " + "merged");
我们已经更新了以下 React bun create 模板,可在后台使用 Bun.build。运行下列命令,即可构建一个由 Bun bundler 支持的简单 React 项目。
# a React single-page app
bun create react ./myapp
# a Next.js-like app with a /pages directory
# with SSR and client-side hydration
bun create react-ssr ./myapp
Bundler 只是我们更多开发计划的前提和基础。在未来几个月中,我们还将发布 Bun.App——一个“超级 API”同,能够将 Bun 的原生高速 bundler、HTTP 服务器和文件系统路由程序融合为统一的整体。
其目标是通过几行代码,轻松使用 Bun 表达任何类型的应用程序:
new Bun.App({
bundlers: [
{
name: "static-server",
outdir: "./out",
},
],
routers: [
{
mode: "static",
dir: "./public",
build: "static-server",
},
],
});
app.serve();
app.build();
此 API 仍在积极讨论当中,后续可能随时有所更改。
参考链接:https://bun.sh/blog/bun-bundler
声明:本文为 InfoQ 翻译,未经许可禁止转载。
文章引用微信公众号"前端之巅",如有侵权,请联系管理员删除!