esbuild 是一个快速的捆绑工具,可以优化 JavaScript、TypeScript、JSX 和 CSS 代码。本文将帮助你快速了解 esbuild,并向你展示如何在不依赖其他库的情况下创建自己的构建系统。
esbuild 是如何工作的呢?
例如Vite
等框架已经采用了 esbuild,你可以在自己的项目中将 esbuild 作为独立工具使用。
-
esbuild 将 JavaScript 代码bundle成单个文件,类似于 Rollup 等bundle工具。这是 esbuild 的主要功能,它解析模块,报告语法问题,进行"tree-shakes"以删除未使用的函数,擦除日志和调试器语句,代码缩小,并提供源映射。
-
esbuild 将 CSS 代码bundle成单个文件。它并不能完全替代像 Sass 或 PostCSS 这样的预处理器,但 esbuild 可以处理部分代码、语法问题、嵌套、内联资源编码、源映射、自动前缀和代码缩小。这可能已经满足您的需求。
-
esbuild 还提供了一个带有自动bundle和热重新加载的本地开发服务器,因此无需刷新。虽然它不具备 Browsersync 提供的所有功能,但对于大多数情况来说已经足够了。
为什么要进行Bundle?
将代码bundle
成单个文件具有各种优势。以下是其中一些:
- 可以开发更小、自包含的源文件,更易于维护。
- 可以在捆绑过程中执行代码的 lint、格式化和语法检查。
- 捆绑工具可以删除未使用的函数,也称为摇树(tree-shaking)。
- 可以捆绑相同代码的不同版本,并为较旧的浏览器、Node.js、Deno 等创建目标。
- 单个文件的加载速度比多个文件更快,且浏览器不需要对 ES 模块的支持。
- 生产级别的捆绑可以通过缩小代码、删除日志和调试语句来提高性能。
为什么要使用 esbuild 呢?
与 JavaScript 捆绑工具不同,esbuild 是一个编译后的 Go 可执行文件,实现了强大的并行处理。它速度快,比 Rollup、Parcel 或 Webpack 快上百倍。它可以在项目的整个生命周期内节省数周的开发时间。
此外,esbuild 还提供了:
- 内置的 JavaScript、TypeScript、JSX 和 CSS 捆绑和编译
- 命令行、JavaScript 和 Go 配置 API
- 对 ES 模块和 CommonJS 的支持
- 带有监视模式和实时重新加载的本地开发服务器
- 插件以添加更多功能
- 全面的文档和在线实验工具
为什么要避免使用 esbuild 呢?
当前esbuild 已经达到了版本 0.18。它是可靠的,但仍然是一个测试版产品。
esbuild 经常会进行更新,不同版本之间的选项可能会发生变化。文档建议使用特定的版本。可以进行更新,但可能需要迁移配置文件,并深入研究新文档以了解破坏性变更。
此外,请注意 esbuild 不执行 TypeScript 类型检查,因此仍然需要运行 tsc -noEmit。
快速入门
使用 npm init 创建一个新的 Node.js 项目,然后将 esbuild 作为开发依赖本地安装:
bash
npm install esbuild --save-dev --save-exact
安装大约需要 9MB。通过运行以下命令来检查是否安装成功并查看已安装的版本:
bash
./node_modules/.bin/esbuild --version
或者运行以下命令来查看命令行帮助:
bash
./node_modules/.bin/esbuild --help
使用命令行 API 来将入口脚本(myapp.js)及其导入的所有模块捆绑到一个名为 bundle.js 的单个文件中。esbuild 将使用默认的、面向浏览器的、立即调用的函数表达式(IIFE)格式输出一个文件:
bash
./node_modules/.bin/esbuild myapp.js --bundle --outfile=bundle.js
如果您不使用 Node.js,还可以通过其他方式安装 esbuild。
示例项目
这是一个 Node.js 项目可下载到本地,使用以下命令安装单个 esbuild 依赖:
js
npm install
将 src
中的源文件构建到 build
目录,并启动一个开发服务器,可以使用以下命令:
bash
npm start
现在浏览器中访问 localhost:8000,即可查看显示实时时钟的网页。当更新 src/css/
或 src/css/partials
中的任何 CSS 文件时,esbuild 将重新编译代码并实时重新加载样式。
如果打包可以使用以下命令:
bash
npm run build
实现原理
实时时钟页面是在 build 目录中使用来自 src
的源文件构建的。
package.json
文件定义了五个 npm
脚本。第一个脚本用于删除 build
目录:
json
"clean": "rm -rf ./build",
在进行任何Bundle之前,一个初始化脚本会运行 clean
,创建一个新的 build
目录,并复制:
- 从
src/html/index.html
复制一个静态 HTML 文件到build/index.html
。 - 将
src/images/
中的静态图像复制到build/images/
。
这可以通过在命令行中运行以下脚本来完成:
bash
"init": "npm run clean && mkdir ./build && cp ./src/html/* ./build/ && cp -r ./src/images ./build",
一个 esbuild.config.js
文件使用 JavaScript API 控制 esbuild
的 Bundle
过程。这比将选项传递给 CLI API
更易于管理,因为后者可能变得难以处理。一个 npm bundle
脚本运行 init
,然后运行 node ./esbuild.config.js
:
json
"bundle": "npm run init && node ./esbuild.config.js",
最后两个 npm 脚本通过将 production
或 development
参数传递给 ./esbuild.config.js
来控制构建:
json
"build": "npm run bundle -- production",
"start": "npm run bundle -- development"
配置 esbuild
package.json 的 "type" 属性为 "module",因此所有的 .js 文件都可以使用 ES 模块。esbuild.config.js 脚本导入 esbuild,并在进行生产环境捆绑时将 productionMode 设置为 true,进行开发环境捆绑时设置为 false:
js
import { argv } from 'node:process';
import * as esbuild from 'esbuild';
const
productionMode = ('development' !== (argv[2] || process.env.NODE_ENV)),
target = 'chrome100,firefox100,safari15'.split(',');
console.log(`${ productionMode ? 'production' : 'development' } build`);
Bundle target
target 变量定义了一个包含浏览器和版本号的数组,用于配置中。这会影响Bundle后的输出,并更改语法以支持特定平台。例如,esbuild 可以:
- 将原生的 CSS 嵌套扩展成完整的选择器(如果 "Chrome115" 是唯一的目标,嵌套将保留)
- 在必要时添加 CSS 厂商前缀的属性
- 为 ?? 空值合并操作符提供 polyfill
- 移除私有类字段中的 #
除了浏览器,您还可以将目标设置为 node 和 es 版本,例如 es2020 和 esnext(最新的 JavaScript 和 CSS 特性)。
JavaScript Bundling
创建Bundle的最简单 API:
js
await esbuild.build({
entryPoints: ['myapp.js'],
bundle: true
outfile: 'bundle.js'
});
示例项目使用了更高级的选项,比如文件监视。这需要一个长时间运行的构建上下文,其中设置了配置:
js
// bundle JS
const buildJS = await esbuild.context({
entryPoints: [ './src/js/main.js' ],
format: 'esm',
bundle: true,
target,
drop: productionMode ? ['debugger', 'console'] : [],
logLevel: productionMode ? 'error' : 'info',
minify: productionMode,
sourcemap: !productionMode && 'linked',
outdir: './build/js'
});
esbuild 提供了数十种配置选项。以下是这里使用的一些选项的介绍:
entryPoints
定义了捆绑的文件入口点数组。示例项目中有一个脚本位于 ./src/js/main.js。format
设置输出格式。示例使用 esm,但您还可以选择为旧浏览器设置 iife,或者为 Node.js 设置 commonjs。bundle
设置为 true,将导入的模块内联到输出文件中。target
是上面定义的目标浏览器的数组。drop
是要移除的 console 和/或 debugger 语句的数组。在这种情况下,生产构建会移除这两者,开发构建会保留它们。logLevel
定义日志的详细程度。上面的示例在生产构建期间显示错误,而在开发构建期间显示更详细的信息消息。minify
通过删除注释和空白,并在可能的情况下重命名变量和函数来减小代码大小。示例项目在生产构建期间进行代码缩小,但在开发构建期间对代码进行美化。sourcemap
设置为 linked(仅在开发模式下)会生成一个链接的源映射文件,以便在浏览器开发工具中可用于查看原始源文件和行。您还可以设置为 inline,将源映射包含在捆绑文件中,或同时创建两者,或设置为 external,生成一个不在捆绑的 JavaScript 中链接的 .map 文件。outdir
定义了捆绑文件的输出目录。
要运行一次构建,可以调用上下文对象的 rebuild() 方法,通常用于生产构建。
js
await buildJS.rebuild();
buildJS.dispose(); // free up resources
要保持运行并在监视的文件更改时自动重新构建,请调用上下文对象的 watch() 方法:
js
await buildJS.watch();
JavaSctipt导入导出文件
入口文件 src/js/main.js
导入了 lib 子文件夹中的 dom.js
和 time.js
模块。它会查找所有具有 class 为 clock 的元素,并在每秒更新一次它们的文本内容为当前时间:
js
import * as dom from './lib/dom.js';
import { formatHMS } from './lib/time.js';
// get clock element
const clock = dom.getAll('.clock');
if (clock.length) {
console.log('initializing clock');
setInterval(() => {
clock.forEach(c => c.textContent = formatHMS());
}, 1000);
}
dom.js 导出了两个函数。main.js 导入了这两个函数,但只使用了 getAll():
js
// DOM libary
// fetch first node from selector
export function get(selector, doc = document) {
return doc.querySelector(selector);
}
// fetch all nodes from selector
export function getAll(selector, doc = document) {
return Array.from(doc.querySelectorAll(selector));
}
time.js
导出了两个函数。main.js
导入了 formatHMS()
,但它使用了模块中的其他函数:
javascript
// time library
// return 2-digit value
function timePad(n) {
return String(n).padStart(2, '0');
}
// return time in HH:MM format
export function formatHM(d = new Date()) {
return timePad(d.getHours()) + ':' + timePad(d.getMinutes());
}
// return time in HH:MM:SS format
export function formatHMS(d = new Date()) {
return formatHM(d) + ':' + timePad(d.getSeconds());
}
生成的开发捆绑包会从 dom.js 中删除(树摇)get() 函数,但包含了所有的 time.js 函数。同时还生成了源映射:
js
// src/js/lib/dom.js
function getAll(selector, doc = document) {
return Array.from(doc.querySelectorAll(selector));
}
// src/js/lib/time.js
function timePad(n) {
return String(n).padStart(2, "0");
}
function formatHM(d = new Date()) {
return timePad(d.getHours()) + ":" + timePad(d.getMinutes());
}
function formatHMS(d = new Date()) {
return formatHM(d) + ":" + timePad(d.getSeconds());
}
// src/js/main.js
var clock = getAll(".clock");
if (clock.length) {
console.log("initializing clock");
setInterval(() => {
clock.forEach((c) => c.textContent = formatHMS());
}, 1e3);
}
//# sourceMappingURL=main.js.map
CSS Bundling
在示例项目中,CSS 的Bundle使用了类似上面 JavaScript 的上下文对象:
js
// bundle CSS
const buildCSS = await esbuild.context({
entryPoints: [ './src/css/main.css' ],
bundle: true,
target,
external: ['/images/*'],
loader: {
'.png': 'file',
'.jpg': 'file',
'.svg': 'dataurl'
},
logLevel: productionMode ? 'error' : 'info',
minify: productionMode,
sourcemap: !productionMode && 'linked',
outdir: './build/css'
});
它将 external
选项定义为一个文件和路径的数组,用于排除在构建中。在示例项目中,src/images/ 目录中的文件被复制到 build 目录,以便 HTML、CSS 或 JavaScript 可以直接引用它们。如果不设置这个选项,当在 background-image 或类似的属性中使用它们时,esbuild 将把文件复制到输出的 build/css/ 目录中。
loader
选项会改变 esbuild 处理导入的文件的方式,这些文件不被视为外部资源。在这个示例中:
- SVG 图像会以数据 URI 内联
- PNG 和 JPG 图像会被复制到 build/css/ 目录,并作为文件引用
CSS 的输入和输出文件
入口文件 src/css/main.css
导入了 partials 子文件夹中的 variables.css
和 elements.css
:
css
/* import */
@import './partials/variables.css';
@import './partials/elements.css';
variables.css
定义了默认的自定义属性(CSS 变量):
css
/* primary variables */
:root {
--font-body: sans-serif;
--color-fore: #fff;
--color-back: #112;
}
elements.css
定义了所有的样式。注意:
- body 元素有一个从外部图像目录加载的背景图像。
- h1 元素嵌套在 header 元素内部。
- h1 元素有一个背景 SVG 图像,将会被内联。
- 目标浏览器不需要厂商前缀。
以下是 lements.css
的一部分内容:
css
/* element styling */
*, *::before, ::after {
box-sizing: border-box;
font-weight: normal;
padding: 0;
margin: 0;
}
body {
font-family: var(--font-body);
color: var(--color-fore);
background: var(--color-back) url(/images/web.png) repeat;
margin: 1em;
}
/* nested elements with inline icon */
header {
& h1 {
font-size: 2em;
padding-left: 1.5em;
margin: 0.5em 0;
background: url(../../icons/clock.svg) no-repeat;
}
}
.clock {
display: block;
font-size: 5em;
text-align: center;
font-variant-numeric: tabular-nums;
}
生成的开发Bundle包会展开嵌套的语法,内联 SVG
css
/* src/css/partials/variables.css */
:root {
--font-body: sans-serif;
--color-fore: #fff;
--color-back: #112;
}
/* src/css/partials/elements.css */
*,
*::before,
::after {
box-sizing: border-box;
font-weight: normal;
padding: 0;
margin: 0;
}
body {
font-family: var(--font-body);
color: var(--color-fore);
background: var(--color-back) url(/images/web.png) repeat;
margin: 1em;
}
header h1 {
font-size: 2em;
padding-left: 1.5em;
margin: 0.5em 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>*{fill:none;stroke:%23fff;stroke-width:1.5;stroke-miterlimit:10}</style></defs><circle cx="12" cy="12" r="10.5"></circle><circle cx="12" cy="12" r="0.95"></circle><polyline points="12 4.36 12 12 16.77 16.77"></polyline></svg>') no-repeat;
}
.clock {
display: block;
font-size: 5em;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* src/css/main.css */
/*# sourceMappingURL=main.css.map */
Watching, Rebuilding, and Serving
esbuild.config.js
脚本的其余部分在生产构建时会进行一次Bundle,然后终止:
js
if (productionMode) {
// single production build
await buildCSS.rebuild();
buildCSS.dispose();
await buildJS.rebuild();
buildJS.dispose();
}
在开发构建期间,该脚本会持续运行,监视文件更改,并自动重新进行捆绑。buildCSS
上下文会启动一个带有 build/
作为根目录的开发 Web 服务器:
csharp
else {
// watch for file changes
await buildCSS.watch();
await buildJS.watch();
// development server
await buildCSS.serve({
servedir: './build'
});
}
开发时启动项目:
bash
npm start
小结
本文介绍了 esbuild 这个快速的打包工具,可以优化 JavaScript、TypeScript、JSX 和 CSS 代码。以下是本文的主要内容:
- esbuild 的工作原理: esbuild 通过捆绑和优化代码,解析模块,报告语法问题,执行树摇以删除未使用的函数,擦除日志和调试语句,代码缩小以及生成源映射来完成工作。
- 为什么进行代码捆绑: 将代码捆绑成单一文件可以带来多种优势,如更易于维护的小型自包含源文件、在捆绑过程中进行代码检查和美化、树摇以删除未使用的函数、为不同目标创建不同版本的代码以及提高加载速度等。
- 为什么使用 esbuild: esbuild 是一个编译后的 Go 可执行文件,支持并行处理,速度比 Rollup、Parcel 或 Webpack 快数十倍。它提供了内置的打包和编译功能,支持 ES 模块和 CommonJS,拥有本地开发服务器和热重载功能,支持插件扩展,以及详细的文档和在线实验工具。
- 如何安装和使用 esbuild: 可以通过 npm 安装 esbuild,并使用命令行工具或 JavaScript 配置文件进行打包。通过配置文件可以定义入口点、输出文件、捆绑选项、监视模式等。
- CSS 捆绑和配置: esbuild 不仅可以捆绑 JavaScript 代码,还可以捆绑 CSS 代码。配置文件中可以定义 CSS 的输入和输出文件,使用外部选项排除文件,以及配置加载器来处理导入的文件。
- 开发构建和生产构建: 在开发模式下,esbuild 可以通过监视文件更改并自动重新构建来提供快速的开发体验。在生产模式下,esbuild 会执行更强的优化以减小输出文件大小。
- 配置选项和目标浏览器: esbuild 提供了许多配置选项,可以定制捆绑过程和输出结果。通过设置目标浏览器,可以影响捆绑的输出和语法支持。
总的来说,esbuild 是一个快速、强大且易于使用的打包工具,适用于构建现代的 JavaScript 和 CSS 项目。