万字长文带你学习esbuild在项目中的实际应用🚀🚀🚀

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 控制 esbuildBundle 过程。这比将选项传递给 CLI API 更易于管理,因为后者可能变得难以处理。一个 npm bundle 脚本运行 init,然后运行 node ./esbuild.config.js

json 复制代码
"bundle": "npm run init && node ./esbuild.config.js",

最后两个 npm 脚本通过将 productiondevelopment 参数传递给 ./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.jstime.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.csselements.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 代码。以下是本文的主要内容:

  1. esbuild 的工作原理: esbuild 通过捆绑和优化代码,解析模块,报告语法问题,执行树摇以删除未使用的函数,擦除日志和调试语句,代码缩小以及生成源映射来完成工作。
  2. 为什么进行代码捆绑: 将代码捆绑成单一文件可以带来多种优势,如更易于维护的小型自包含源文件、在捆绑过程中进行代码检查和美化、树摇以删除未使用的函数、为不同目标创建不同版本的代码以及提高加载速度等。
  3. 为什么使用 esbuild: esbuild 是一个编译后的 Go 可执行文件,支持并行处理,速度比 Rollup、Parcel 或 Webpack 快数十倍。它提供了内置的打包和编译功能,支持 ES 模块和 CommonJS,拥有本地开发服务器和热重载功能,支持插件扩展,以及详细的文档和在线实验工具。
  4. 如何安装和使用 esbuild: 可以通过 npm 安装 esbuild,并使用命令行工具或 JavaScript 配置文件进行打包。通过配置文件可以定义入口点、输出文件、捆绑选项、监视模式等。
  5. CSS 捆绑和配置: esbuild 不仅可以捆绑 JavaScript 代码,还可以捆绑 CSS 代码。配置文件中可以定义 CSS 的输入和输出文件,使用外部选项排除文件,以及配置加载器来处理导入的文件。
  6. 开发构建和生产构建: 在开发模式下,esbuild 可以通过监视文件更改并自动重新构建来提供快速的开发体验。在生产模式下,esbuild 会执行更强的优化以减小输出文件大小。
  7. 配置选项和目标浏览器: esbuild 提供了许多配置选项,可以定制捆绑过程和输出结果。通过设置目标浏览器,可以影响捆绑的输出和语法支持。

总的来说,esbuild 是一个快速、强大且易于使用的打包工具,适用于构建现代的 JavaScript 和 CSS 项目。

相关推荐
早點睡39031 分钟前
ReactNative项目OpenHarmony三方库集成实战:react-native-dropdown-picker
javascript·react native·react.js
RickeyBoy5 小时前
SwiftUI 如何实现 Infinite Scroll?
ios·面试
跟着珅聪学java8 小时前
js编写中文转unicode 教程
前端·javascript·数据库
英俊潇洒美少年8 小时前
Vue3 深入响应式系统
前端·javascript·vue.js
颜酱8 小时前
回溯算法实战练习(3)
javascript·后端·算法
前端摸鱼匠8 小时前
【AI大模型春招面试题12】Scaling Laws揭示了模型性能、数据量、计算量之间的什么关系?
人工智能·ai·语言模型·面试·大模型
我命由我123459 小时前
React Router 6 - 概述、基础路由、重定向、NavLink、路由表
前端·javascript·react.js·前端框架·ecmascript·html5·js
yaaakaaang10 小时前
(四)前端,如此简单!---Promise
前端·javascript
aini_lovee10 小时前
C# 实现邮件发送源码(支持附件)
开发语言·javascript·c#
英俊潇洒美少年11 小时前
js 进程与线程的讲解
javascript