万字长文带你学习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 项目。

相关推荐
清灵xmf几秒前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据7 分钟前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_3901617715 分钟前
防抖函数--应用场景及示例
前端·javascript
3345543243 分钟前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
测试19982 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js