当前位置: 首页 > 工具软件 > esbuild > 使用案例 >

esbuild

孟智志
2023-12-01

esbuild

一个非常快的js打包工具

特性

  • 无需缓存即可达到极速
  • ES6 和 CommonJS 模块
  • ES6 模块的摇树
  • 用于 JavaScript 和 Go 的 API
  • TypeScript 和 JSX 语法
  • source map
  • 缩小
  • 插件

安装

yarn add esbuild
npm install esbuild

浏览器绑定

bundler 默认输出浏览器的代码,所以不需要额外的配置就可以上手.对于开发构建,您可能希望使用 --sourcemap 启用源映射,而对于生产构建,您可能希望使用 --minify 启用缩小。您可能还想为您支持的浏览器配置目标环境。所有这些可能看起来像这样:

require('esbuild').buildSync({
  entryPoints: ['app.jsx'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
  outfile: 'out.js',
})

有时候一个你想使用的包可能会导入另一个只有node才有的包,比如内置的路径包。发生这种情况时,您可以使用 package.json 文件中的浏览器字段,将包替换为浏览器友好的替代方案,如下所示:

{
  "browser": {
    "path": "path-browserify"
  }
}

您要使用的某些 npm 包可能不是设计为在浏览器中运行的。有时您可以使用 esbuild 的配置选项来解决某些问题并成功地捆绑包。未定义的全局变量可以在简单情况下用 define功能替换,或者在更复杂情况下用inject功能替换。

node绑定

尽管在使用 node 时不需要 bundler,但有时在 node 中运行之前使用 esbuild 处理代码仍然是有益的。Bundling 可以自动剥离 TypeScript 类型,将 ECMAScript 模块语法转换为 CommonJS,并将新的 JavaScript 语法转换为特定版本节点的旧语法。并且在发布之前捆绑包可能是有益的,这样下载量就更小,加载时从文件系统读取的时间也更少。
如果您正在捆绑将在 node 中运行的代码,您应该通过将 --platform=node 传递给 esbuild 来配置平台设置。这同时将一些不同的设置更改为节点友好的默认值。例如,所有内置于节点的包(如 fs)都会自动标记为外部包,因此 esbuild 不会尝试捆绑它们。此设置还会禁用 package.json 中浏览器字段的解释。
如果您的代码使用在您的 node 版本中不起作用的较新 JavaScript 语法,您将需要配置 node 的目标版本:

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  platform: 'node',
  target: ['node10.4'],
  outfile: 'out.js',
})

有时,您要使用的包包含由于某种原因无法捆绑的代码。这方面的一个示例是带有本机扩展(如 fsevents)的包。或者,您可能出于其他原因希望从捆绑包中排除一个包。这可以通过将包标记为外部来完成:

require('esbuild').buildSync({
  entryPoints: ['app.jsx'],
  bundle: true,
  platform: 'node',
  external: ['fsevents'],
  outfile: 'out.js',
})

API

esbuild 的 API 中有两个主要的 API 调用:transform 和 build。了解您应该使用哪一个很重要,因为它们的工作方式不同。
形式 --foo 用于启用布尔标志,例如 --minify; 形式 --foo=bar 用于具有单个值且仅指定一次的标志,例如 --platform=;并且形式 --foo:bar 用于具有多个值并且可以多次重新指定的标志,例如 --external:

Transform API

Transform API调用对单个字符串进行操作,不需要访问文件系统。非常适合在没有文件系统的环境中使用或作为另一个工具链的一部分

require('esbuild').transformSync('let x: number = 1', {
  loader: 'ts',
})
{
  code: 'let x = 1;\n',
  map: '',
  warnings: []
}

Build API

Build API调用对文件系统中的一个或多个文件进行操作。这使得文件可以相互引用,并被编译在一起。下面是个简单例子:

require('fs').writeFileSync('in.ts', 'let x: number = 1')
require('esbuild').buildSync({
  entryPoints: ['in.ts'],
  outfile: 'out.js',
})

option

1. bundle:boolean Build支持

捆绑文件意味着将任何导入的依赖项内联到文件本身中.此过程是递归的,因此依赖项(等等)的依赖项也将被内联。默认情况下,esbuild 不会捆绑输入文件。必须像这样显式启用捆绑

require('esbuild').buildSync({
 entryPoints: ['in.js'],
 bundle: true,
 outfile: 'out.js',
})

不可分析的bunble : 与 esbuild 捆绑仅适用于静态定义的导入(即当导入路径是字符串文字时)。在运行时定义的导入(即依赖于运行时代码评估的导入)不会被捆绑,因为捆绑是一个编译时操作.
解决此问题的方法是将包含此问题代码的包标记为外部包,使其不包含在包中。然后,您需要确保外部包的副本在运行时可用于您的捆绑代码。
一些打包器(例如 Webpack)尝试通过将所有可能可访问的文件包含在包中然后在运行时模拟文件系统来支持这一点.但是,运行时文件系统仿真超出了范围,不会在 esbuild 中实现。如果您确实需要捆绑执行此操作的代码,则可能需要使用另一个捆绑器而不是 esbuild。

Define Transform | Build支持

此功能提供了一种用常量表达式替换全局标识符的方法。它可以是一种在构建之间更改某些代码行为而不更改代码本身的方法:

 let js = 'DEBUG && require("hooks")'
require('esbuild').transformSync(js, {
  define: { DEBUG: 'true' },
})

替换表达式必须是 JSON 对象(空值、布尔值、数字、字符串、数组或对象)或单个标识符。数组和对象以外的替换表达式是内联替换的,这意味着它们可以参与常量折叠。数组和对象替换表达式存储在一个变量中,然后使用标识符引用而不是内联替换,这避免了替换值的重复副本,但意味着这些值不参与常量折叠。

如果您想用字符串文本替换某些内容,请记住传递给 esbuild 的替换值本身必须包含引号。省略引号意味着替换值是一个标识符
引号在不同操作系统中有不同的作用 用命令行去写命令会存在兼容性问题,一般用js文件执行esbuild去消除这些兼容问题

Entry points Build支持

这是一个文件数组,每个文件都用作捆绑算法的输入。它们被称为“入口点”,因为每个入口点都是被评估的初始脚本,然后加载它所代表的代码的所有其他方面。不是使用

require('esbuild').buildSync({
 entryPoints: ['home.ts', 'settings.ts'],
 bundle: true,
 write: true,
 outdir: 'out',
})

这将生成两个输出文件,out/home.js 和 out/settings.js,分别对应两个入口点 home.ts 和 settings.ts
此外,您还可以使用替代入口点语法为每个单独的入口点指定完全自定义的输出路径:

require('esbuild').buildSync({
 entryPoints: {
   out1: 'home.js',
   out2: 'settings.js',
 },
 bundle: true,
 write: true,
 outdir: 'out',
})

这将生成两个输出文件,out/out1.js 和 out/out2.js 分别对应两个入口点 home.ts 和 settings.ts

External Build支持

您可以将文件或包标记为外部以将其从构建中排除
这有多种用途。首先,它可用于从您的包中修剪不必要的代码,以便您知道永远不会执行的代码路径。例如,一个包可能包含只在 node 中运行的代码,但你只能在浏览器中使用该包。它还可用于在运行时从无法捆绑的包中导入 node 中的代码。例如, fsevents 包包含一个本机扩展,而 esbuild 不支持该扩展。将某些内容标记为外部内容如下所示:

require('fs').writeFileSync('app.js', 'require("fsevents")')
require('esbuild').buildSync({
 entryPoints: ['app.js'],
 outfile: 'out.js',
 bundle: true,
 platform: 'node',
 external: ['fsevents'],
})

您还可以在外部路径中使用 * 通配符将匹配该模式的所有文件标记为外部文件。

Format Transform | Build支持

这将设置生成的 JavaScript 文件的输出格式。 目前可以配置三个可能的值:iife、cjs 和 esm。 如果未指定输出格式,esbuild 会在启用 bundling(如下所述)的情况下为您选择一种输出格式,或者如果禁用 bundling则不进行任何格式转换。

  • iife格式代表“立即调用的函数表达式”,旨在在浏览器中运行。 将代码包装在函数表达式中可确保代码中的任何变量不会意外地与全局范围内的变量发生冲突。 如果您的入口点具有要在浏览器中公开为全局的导出,您可以使用全局名称设置来配置该全局的名称。 当未指定输出格式、启用捆绑并且平台设置为浏览器(默认情况下)时,将自动启用 iife 格式。 指定 iife 格式如下所示:
let js = 'alert("test")'
let out = require('esbuild').transformSync(js, {
  format: 'iife',
})
process.stdout.write(out.code)
  • cjs 格式代表“CommonJS”,旨在在 node.js 中运行。它假设环境包含导出、要求和模块。使用 ECMAScript 模块语法导出的入口点将被转换为一个模块,每个导出名称的导出都有一个 getter。当未指定输出格式、启用捆绑并且平台设置为节点时,将自动启用 cjs 格式。指定 cjs 格式如下所示:
let js = 'export default "test"'
let out = require('esbuild').transformSync(js, {
 format: 'cjs',
})
process.stdout.write(out.code)
  • esm 格式代表“ECMAScript 模块”。它假定环境支持导入和导出语法。 CommonJS 模块语法中带有导出的入口点将被转换为 module.exports 值的单个默认导出。
let js = 'module.exports = "test"'
let out = require('esbuild').transformSync(js, {
 format: 'esm',
})
process.stdout.write(out.code)

esm 格式既可以在浏览器中使用,也可以在node中使用,但是您必须将其作为模块显式加载。如果您从另一个模块导入它,这会自动发生。
在浏览器中,您可以使用 加载模块。
在 node 中,您可以使用 node --experimental-modules file.mjs 加载模块。请注意,节点需要 .mjs 扩展名,除非您在 package.json 文件中配置了 “type”: “module”。您可以使用 esbuild 中的 out 扩展名设置来自定义 esbuild 生成的文件的输出扩展名。

Inject Build支持

此选项允许您使用来自另一个文件的导入自动替换全局变量。这可能是一个有用的工具,可以将您无法控制的代码适应新环境。例如,假设您有一个名为 process-shim.js 的文件,它导出一个名为 process 的变量:

// process-shim.js
export let process = {
  cwd: () => ''
}
// entry.js
console.log(process.cwd())

require('esbuild').buildSync({
  entryPoints: ['entry.js'],
  bundle: true,
  inject: ['./process-shim.js'],
  outfile: 'out.js',
})

// out.js
let process = {cwd: () => ""};
console.log(process.cwd());

define vs inject

// process-shim.js
export function dummy_process_cwd() {
  return ''
}
// entry.js
console.log(process.cwd())
require('esbuild').buildSync({
  entryPoints: ['entry.js'],
  bundle: true,
  inject: ['./process-shim.js'],
  outfile: 'out.js',
})
// out.js
let process = {cwd: () => ""};
console.log(process.cwd());

JSX 的自动导入:您可以使用注入功能自动提供 JSX 表达式的实现。比如你可以自动导入react包来提供React.createElement等功能。

  • JSX 的自动导入: 您可以使用注入功能自动提供 JSX 表达式的实现。比如你可以自动导入react包来提供React.createElement等功能。有关详细信息,请参阅 JSX 文档。
  • 在没有导入的情况下注入文件: 您还可以将此功能用于没有导出的文件。在这种情况下,注入的文件只是在输出的其余部分之前出现,就好像每个输入文件都包含导入“./file.js”。由于 ECMAScript 模块的工作方式,这种注入仍然是“卫生的”,因为不同文件中具有相同名称的符号被重命名,因此它们不会相互冲突。
  • 有条件地注入文件: 如果您只想在实际使用导出时有条件地导入文件,您应该将注入的文件标记为没有副作用,将其放入包中并在该包的 package.json 文件中添加 “sideEffects”: false 。此设置是来自 Webpack 的约定,esbuild 尊重任何导入的文件,而不仅仅是用于注入的文件。

Loader Transform | Build 支持

此选项更改给定输入文件的解释方式。例如,js 加载器将文件解释为 JavaScript,而 css 加载器将文件解释为 CSS。有关所有内置加载器的完整列表,请参阅内容类型页面。
为给定的文件类型配置加载器允许您使用导入语句或 require 调用加载该文件类型。例如,将 .png 文件扩展名配置为使用数据 URL 加载程序意味着导入 .png 文件会为您提供一个包含该图像内容的数据 URL:

import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)

import svg from './example.svg'
let doc = new DOMParser().parseFromString(svg, 'application/xml')
let node = document.importNode(doc.documentElement, true)
document.body.appendChild(node)

上面的代码可以像这样使用构建 API 调用进行捆绑

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: {
    '.png': 'dataurl',
    '.svg': 'text',
  },
  outfile: 'out.js',
})

如果您将构建 API 与来自 stdin 的输入一起使用,则此选项的指定方式不同,因为 stdin 没有文件扩展名。使用构建 API 为 stdin 配置加载器如下所示

require('esbuild').buildSync({
  stdin: {
    contents: 'import pkg = require("./pkg")',
    loader: 'ts',
    resolveDir: __dirname,
  },
  bundle: true,
  outfile: 'out.js',
})

转换 API 调用只需要一个加载器,因为它不涉及与文件系统的交互,因此不处理文件扩展名。为转换 API 配置加载器(在本例中为 ts 加载器)如下所示:

let ts = 'let x: number = 1'
require('esbuild').transformSync(ts, {
  loader: 'ts',
})
{
  code: 'let x = 1;\n',
  map: '',
  warnings: []
}
Minify

Minify Transform | Build支持

启用后,生成的代码将被缩小而不是漂亮的打印。压缩代码通常等同于非压缩代码,但更小,这意味着它下载速度更快但更难调试。通常你会在生产中缩小代码而不是在开发中。
在 esbuild 中启用缩小如下所示:

var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {
  minify: true,
})
{
  code: 'fn=n=>n.x;\n',
  map: '',
  warnings: []
}

此选项组合执行三个独立的操作:删除空格,将语法重写为更紧凑,并将局部变量重命名为更短。通常你想要做所有这些事情,但如果需要,也可以单独启用这些选项:

var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {
  minifyWhitespace: true,
})
{
  code: 'fn=obj=>{return obj.x};\n',
  map: '',
  warnings: []
}
require('esbuild').transformSync(js, {
  minifyIdentifiers: true,
})
{
  code: 'fn = (n) => {\n  return n.x;\n};\n',
  map: '',
  warnings: []
}
require('esbuild').transformSync(js, {
  minifySyntax: true,
})
{
  code: 'fn = (obj) => obj.x;\n',
  map: '',
  warnings: []
}

这些相同的概念也适用于 CSS,而不仅仅是 JavaScript:

var css = 'div { color: yellow }'
require('esbuild').transformSync(css, {
  loader: 'css',
  minify: true,
})
{
  code: 'div{color:#ff0}\n',
  map: '',
  warnings: []
}

esbuild 中的 JavaScript 缩小算法通常生成的输出非常接近行业标准 JavaScript 缩小工具的缩小输出大小。该基准测试对不同缩小器之间的输出大小进行了示例比较。虽然 esbuild 不是所有情况下的最佳 JavaScript 缩小器(并且不会尝试成为),但它努力在大多数代码的专用缩小工具大小的百分之几内生成缩小的输出,当然,这样做比其他工具快得多。
注意事项:

  • 您可能还应该在启用缩小时设置目标选项。默认情况下,esbuild 利用现代 JavaScript 功能使您的代码更小。例如,a === undefined ||一个 === 空? 1 : a 可以缩小为 a ?? 1. 如果您不希望 esbuild 在缩小时利用现代 JavaScript 功能,您应该使用较旧的语言目标,例如 --target=es6。
  • 缩小对于 100% 的 JavaScript 代码都是不安全的。这对于 esbuild 以及其他流行的 JavaScript 压缩器(如 terser)都是如此。特别是,esbuild 并非旨在保留对函数调用 .toString() 的值。这样做的原因是因为如果所有函数中的所有代码都必须逐字保存,那么缩小几乎不会做任何事情并且几乎毫无用处。然而,这意味着依赖 .toString() 返回值的 JavaScript 代码在缩小时可能会中断。例如,当代码被缩小时,AngularJS 框架中的一些模式会中断,因为 AngularJS 使用 .toString() 来读取函数的参数名称。一种解决方法是改用显式注释。
  • 默认情况下,esbuild 不会在函数和类对象上保留 .name 的值。这是因为大多数代码不依赖此属性,并且使用较短的名称是重要的大小优化。但是,某些代码确实依赖 .name 属性进行注册和绑定。如果您需要依赖它,您应该启用保留名称选项。
  • 使用某些 JavaScript 功能可以禁用许多 esbuild 的优化,包括缩小。具体来说,使用直接 eval 和/或 with 语句可防止 esbuild 将标识符重命名为较小的名称,因为这些功能会导致标识符绑定发生在运行时而不是编译时。这几乎总是无意的,并且只会发生因为人们不知道直接 eval 是什么以及它为什么不好。
    如果您正在考虑编写一些这样的代码:
// Direct eval (will disable minification for the whole file)
let result = eval(something)

您可能应该像这样编写代码,以便可以缩小代码:

// Indirect eval (has no effect on the surrounding code)
let result = (0, eval)(something)

此处提供了有关直接评估的后果和可用替代方案的更多信息。

esbuild 中的缩小算法尚未进行高级代码优化。特别是,以下代码优化对于 JavaScript 代码是可能的,但不是由 esbuild 完成的(不是详尽的列表)

  • 函数体内的死代码消除
  • 函数内联
  • 跨语句常量传播
  • 物体形状建模
  • 分配下沉
  • 方法去虚拟化
  • 符号执行
  • JSX 表达式提升
  • TypeScript 枚举检测和内联
  • 如果您的代码使用的模式要求其中一些代码优化形式紧凑,或者如果您正在为您的用例搜索最佳 JavaScript 缩小算法,您应该考虑使用其他工具。实现其中一些高级代码优化的工具的一些示例包括 Terser 和 Google Closure Compiler。

Outdir Build支持

此选项设置构建操作的输出目录。例如,此命令将生成一个名为out的目录:

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  outdir: 'out',
})

如果输出目录尚不存在,则会生成输出目录,但如果已包含某些文件,则不会清除该目录。任何生成的文件都会以静默方式覆盖同名的现有文件。如果您希望输出目录仅包含来自当前 esbuild 运行的文件,您应该在运行 esbuild 之前自己清除输出目录。
如果您的构建在不同的目录中包含多个入口点,则目录结构将从所有输入入口点路径中最低的公共祖先目录开始复制到输出目录中。例如,如果有两个入口点 src/home/index.ts 和 src/about/index.ts,则输出目录将包含 home/index.js 和 about/index.js。如果要自定义此行为,则应更改 outbase 目录。

Outfile Build支持

此选项设置构建操作的输出文件名。这仅适用于有单个入口点的情况。如果有多个入口点,则必须改用 outdir 选项来指定输出目录。使用 outfile 看起来像这样:

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
})

Platform Build支持

默认情况下,esbuild 的 bundler 被配置为生成用于浏览器的代码。如果您的捆绑代码打算在 node 中运行,您应该将平台设置为 node:

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  platform: 'node',
  outfile: 'out.js',
})

当平台设置为浏览器时(默认值):

  • 启用捆绑时,默认输出格式设置为 iife,它将生成的 JavaScript 代码包装在立即调用的函数表达式中,以防止变量泄漏到全局范围内。
  • 如果包在其 package.json 文件中为 browser字段指定了映射,esbuild 将使用该映射将特定文件或模块替换为其浏览器友好版本。例如,一个包可能包含用 path-browserify 替换路径。
  • 主要字段设置设置为 browser,module,main 但有一些额外的特殊行为。如果包支持 module 和 main 但不支持浏览器,那么如果使用 require() 导入该包,则使用 main 而不是 module。此行为通过将函数分配给 module.exports 来提高与导出函数的 CommonJS 模块的兼容性。
  • 条件设置自动包含浏览器条件。这改变了 package.json 文件中的导出字段被解释为更喜欢浏览器特定代码的方式。
  • 使用构建 API 时,如果启用了所有缩小选项,则所有 process.env.NODE_ENV 表达式都会自动定义为“生产”,否则会自动定义为“开发”。仅当 process、process.env 和 process.env.NODE_ENV 尚未定义时才会发生这种情况。这种替换对于避免基于 React 的代码立即崩溃是必要的(因为进程是一个nodeAPI,而不是一个 Web API)。
    当平台设置为节点时:
  • 启用捆绑后,默认输出格式设置为 cjs,代表 CommonJS(节点使用的模块格式)。使用 export 语句的 ES6 样式导出将转换为 CommonJS 导出对象上的 getter。
  • 所有内置节点模块(例如 fs)都会自动标记为外部模块,因此当捆绑器尝试捆绑它们时,它们不会导致错误。
  • 主要字段设置设置为 main,module。这意味着同时提供 module 和 main 的包可能不会发生摇树,因为摇树适用于 ECMAScript 模块,但不适用于 CommonJS 模块。
    不幸的是,一些包错误地将模块视为“浏览器代码”而不是“ECMAScript 模块代码”,因此为了兼容性需要这种默认行为。如果您想启用摇树并且知道这样做是安全的,您可以手动将主要字段设置配置为 module,main。
  • 条件设置自动包含node条件。这改变了 package.json 文件中的导出字段被解释为更喜欢node特定代码的方式。
    当平台设置为neutral时:
  • 启用捆绑后,默认输出格式设置为 esm,它使用 ECMAScript 2015(即 ES6)引入的导出语法。如果此默认值不合适,您可以更改输出格式。
  • 默认情况下,主要字段设置为空。如果您想使用 npm 风格的包,您可能必须将其配置为其他内容,例如 node 使用的标准 main 字段的 main 。
  • 条件设置不会自动包含任何特定于平台的值
    另请参阅浏览器的捆绑和node的捆绑。

Serve Building支持

在开发过程中,进行更改时经常在文本编辑器和浏览器之间来回切换。在浏览器中重新加载代码之前手动重新运行 esbuild 很不方便。有几种方法可以自动执行此操作:

  • 使用监视模式在文件更改时重新运行 esbuild
  • 配置您的文本编辑器以在每次保存时运行 esbuild
  • 使用根据每个请求重建的 Web 服务器为您的代码提供服务
    这个 API 调用实现了最后一个方法.服务 API 类似于构建 API 调用,但它不是将生成的文件写入文件系统,而是启动一个长期存在的本地 HTTP Web 服务器,为最新构建的生成文件提供服务。每一批新的请求都会导致 esbuild 在响应请求之前重新运行构建命令,以便您的文件始终是最新的。
    与其他方法相比,这种方法的优势在于 Web 服务器可以延迟浏览器的请求,直到构建完成。这样,在最新构建完成之前在浏览器中重新加载代码将永远不会运行先前构建中的代码。这些文件是从内存中提供的,不会写入文件系统,以确保无法观察到过时的文件。
    请注意,这仅用于开发。不要在生产中使用它。在生产中,您应该在不使用 esbuild 作为 Web 服务器的情况下提供静态文件。
    使用服务 API 有两种不同的方法:
  • 方法一:用 esbuild 服务一切
    使用这种方法,除了 esbuild 生成的文件之外,您还为 esbuild 提供了一个名为 serveir 的目录,其中包含要提供的额外内容。这适用于创建一些静态 HTML 页面并希望使用 esbuild 捆绑 JavaScript 和/或 CSS 的简单情况。您可以将您的 HTML 文件放在 servedir 中,并将您的其他源代码放在 servedir 之外,然后将 outdir 设置在 servedir 内的某个位置: 命令行界面 JS 去
require('esbuild').serve({
  servedir: 'www',
}, {
  entryPoints: ['src/app.js'],
  outdir: 'www/js',
  bundle: true,
}).then(server => {
  // Call "stop" on the web server to stop serving
  server.stop()
})

在上面的例子中,你的 www/index.html 页面可以像这样引用 src/app.js 中的编译代码:

<script src="js/app.js"></script>

当您这样做时,每个 HTTP 请求都会导致 esbuild 重建您的代码并为您提供最新版本。因此,每次您重新加载页面时,js/app.js 将始终是最新的。请注意,尽管生成的代码似乎在 outdir 目录中,但它实际上从未使用 serve API 写入文件系统。相反,生成的代码阴影的路径(即优先于)servedir 和生成的文件中的其他路径直接从内存中提供。
这样做的好处是您可以在开发和生产中使用完全相同的 HTML 页面。在开发中,您可以使用 --servedir= 运行 esbuild,esbuild 将直接提供生成的输出文件。对于生产,您可以省略该标志,esbuild 会将生成的文件写入文件系统。在这两种情况下,您应该在开发和生产中使用完全相同的代码在浏览器中获得完全相同的结果。
默认情况下,端口会自动选择为第一个等于或大于 8000 的开放端口。端口号从 API 调用返回(或打印到 CLI 的终端),因此您可以知道要访问哪个 URL。端口号从 API 调用返回(或打印到 CLI 的终端),因此您可以知道要访问哪个 URL。如有必要,可以将端口设置为特定的内容(下面将进一步描述)。

  • 方法 2:仅使用 esbuild 提供生成的文件
    使用这种方法,您只需告诉 esbuild 提供 outdir 的内容,而无需为其提供任何额外的内容。这适用于更复杂的开发设置。例如,您可能希望使用 NGINX 作为反向代理,在开发过程中将不同路径路由到单独的后端服务(例如 /static/ 到 NGINX、/api/ 到节点、/js/ 到 esbuild 等)。通过这种方法使用 esbuild 看起来像这样:
require('esbuild').serve({
  port: 8000,
}, {
  entryPoints: ['src/app.js'],
  bundle: true,
  outfile: 'out.js',
}).then(server => {
  // Call "stop" on the web server to stop serving
  server.stop()
})

上面示例中的 API 调用将在 http://localhost:8000/out.js 提供 src/app.js 的编译内容.就像第一种方法一样,每个 HTTP 请求都会导致 esbuild 重建您的代码并为您提供最新版本,因此 out.js 将始终是最新的。然后,您的 HTML 文件(由另一个端口上的另一个 Web 服务器提供服务)可以像这样从您的 HTML 引用编译后的文件:

<script src="http://localhost:8000/out.js"></script>

在未启用 Web 服务器的情况下使用普通构建命令时,Web 服务器的 URL 结构与输出目录的 URL 结构完全相同。例如,如果输出目录通常包含一个名为 ./pages/about.js 的文件,则 Web 服务器将具有相应的 /pages/about.js 路径。
如果您想浏览 Web 服务器以查看哪些 URL 可用,您可以通过访问目录名而不是文件名来使用内置目录列表.例如,如果您在端口 8000 上运行 esbuild 的 Web 服务器,则可以在浏览器中访问 http://localhost:8000/ 以查看 Web 服务器的根目录。从那里您可以单击链接以浏览 Web 服务器上的不同文件和目录。

参数

请注意,服务 API 是与构建 API 不同的 API 调用。这是因为启动一个长时间运行的 Web 服务器是不同的,足以保证不同的参数和返回值。服务 API 调用的第一个参数是带有服务特定选项的选项对象:

interface ServeOptions {
  port?: number;
  host?: string;
  servedir?: string;
  onRequest?: (args: ServeOnRequestArgs) => void;
}

interface ServeOnRequestArgs {
  remoteAddress: string;
  method: string;
  path: string;
  status: number;
  timeInMS: number;
}
  • port 可以选择在此处配置 HTTP 端口。如果省略,它将默认为开放端口,优先选择端口 8000。您可以在命令行上使用 --serve=8000 而不是 --serve 设置端口。
  • host 默认情况下,esbuild 使 Web 服务器可用于所有 IPv4 网络接口。这对应于 0.0.0.0 的主机地址。如果您想配置不同的主机(例如,仅在 127.0.0.1 环回接口上提供服务而不向网络公开任何内容),您可以使用此参数指定主机。您可以使用 --serve=127.0.0.1:8000 而不仅仅是 --serve 在命令行上设置主机。
    如果您需要使用 IPv6 而不是 IPv4,您只需要指定一个 IPv6 主机地址。等效于 IPv6 中的 127.0.0.1 环回接口是 ::1,等效于 IPv6 中的 0.0.0.0 通用接口是 ::。如果在命令行中将主机设置为 IPv6 地址,则需要用方括号将 IPv6 地址括起来,以区分地址中的冒号和分隔主机和端口的冒号,如下所示:–serve=[:: ]:8000。
  • servedir 当传入请求与任何生成的输出文件路径不匹配时,这是 esbuild 的 HTTP 服务器提供的额外内容目录,而不是 404。这使您可以将 esbuild 用作通用本地 Web 服务器。例如,使用 esbuild --servedir=。为本地主机上的当前目录提供服务。在前面关于不同方法的部分中更详细地描述了使用 servedir。
  • onRequest 每个传入请求都会调用一次,并提供有关请求的一些信息。 CLI 使用此回调为每个请求打印日志消息。时间字段是为请求生成数据的时间,但不包括将请求流式传输到客户端的时间。
    请注意,这是在请求完成后调用的。无法使用此回调以任何方式修改请求。如果你想这样做,你应该在 esbuild 前面放置一个代理。
    服务 API 调用的第二个参数是在每个请求上调用的底层构建 API 的正常选项集。有关这些选项的更多信息,请参阅 build API的文档。

Return values

interface ServeResult {
  port: number;
  host: string;
  wait: Promise<void>;
  stop: () => void;
}
  • port 这是 Web 服务器最终使用的端口。如果您没有指定端口,您将需要使用它,因为 esbuild 最终会选择一个任意的开放端口,并且您需要知道它选择了哪个端口才能连接到它。
  • host 这是最终被 Web 服务器使用的主机。除非配置了自定义主机,否则它将是 0.0.0.0(即在所有可用的网络接口上提供服务)
  • wait 只要可以打开套接字,服务 API 调用就会立即返回。等待返回值提供了一种在 Web 服务器终止时通知的方法,无论是由于网络错误还是由于在将来的某个时间点停止调用。
  • stop 调用这个回调来停止 web 服务器,当你不再需要它来清理资源时你应该这样做。这将立即终止所有打开的连接并唤醒任何等待等待返回值的代码。

自定义服务器行为

无法连接到 esbuild 的本地服务器来自定义服务器本身的行为。相反,应该通过在 esbuild 前面放置一个代理来自定义行为。这是一个简单的代理服务器示例,可帮助您入门。它添加了一个自定义的 404 页面,而不是 esbuild 的默认 404 页面:

const esbuild = require('esbuild');
const http = require('http');

// Start esbuild's server on a random local port
esbuild.serve({
  servedir: __dirname,
}, {
  // ... your build options go here ...
}).then(result => {
  // The result tells us where esbuild's local server is
  const {host, port} = result

  // Then start a proxy server on port 3000
  http.createServer((req, res) => {
    const options = {
      hostname: host,
      port: port,
      path: req.url,
      method: req.method,
      headers: req.headers,
    }

    // Forward each incoming request to esbuild
    const proxyReq = http.request(options, proxyRes => {
      // If esbuild returns "not found", send a custom 404 page
      if (proxyRes.statusCode === 404) {
        res.writeHead(404, { 'Content-Type': 'text/html' });
        res.end('<h1>A custom 404 page</h1>');
        return;
      }

      // Otherwise, forward the response from esbuild to the client
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(res, { end: true });
    });

    // Forward the body of the request to esbuild
    req.pipe(proxyReq, { end: true });
  }).listen(3000);
});

此代码在随机本地端口上启动 esbuild 的服务器,然后在端口 3000 上启动代理服务器。在开发过程中,您将在浏览器中加载 http://localhost:3000,它与代理通信。此示例演示在 esbuild 处理请求后修改响应,但您也可以在 esbuild 处理请求之前修改或替换请求。

你可以用这样的代理做很多事情

  • 注入你自己的 404 页面(上面的例子)
  • 自定义路由到文件系统上文件的映射
  • 将一些路由重定向到 API 服务器而不是 esbuild
  • 使用您自己的自签名证书添加对 HTTPS 的支持
    如果您有更高级的需求,也可以使用真正的代理,例如 NGINX。

Sourcemap Transform | Build 支持

源映射可以更轻松地调试您的代码。它们对将生成的输出文件中的行/列偏移转换回相应原始输入文件中的行/列偏移所需的信息进行编码。如果您生成的代码与原始代码完全不同(例如,您的原始代码是 TypeScript 或您启用了缩小),这将非常有用。如果您更喜欢在浏览器的开发工具中查看单个文件而不是一个大的捆绑文件,这也很有用。
请注意,JavaScript 和 CSS 都支持源地图输出,并且相同的选项适用于两者。下面讨论 .js 文件的所有内容也同样适用于 .css 文件。
源映射生成有四种不同的模式:

  1. linked 这种模式意味着源映射与 .js 输出文件一起生成到一个单独的 .js.map 输出文件中,并且 .js 输出文件包含一个特殊的 //# sourceMappingURL= 注释,指向 .js.map 输出文件。这样,当您打开调试器时,浏览器就知道在哪里可以找到给定文件的源映射。像这样使用链接源映射模式:
require('esbuild').buildSync({
  entryPoints: ['app.ts'],
  sourcemap: true,
  outfile: 'out.js',
})
  1. external 这种模式意味着源映射与 .js 输出文件一起生成到单独的 .js.map 输出文件中,但与链接模式不同,.js 输出文件不包含 //# sourceMappingURL= 注释。像这样使用外部源映射模式:
require('esbuild').buildSync({
  entryPoints: ['app.ts'],
  sourcemap: 'external',
  outfile: 'out.js',
})
  1. inline 这种模式意味着源映射被附加到 .js 输出文件的末尾,作为 //# sourceMappingURL= 注释内的 base64 有效负载。不会生成额外的 .js.map 输出文件。请记住,源映射通常非常大,因为它们包含您所有的原始源代码,因此您通常不希望发布包含内联源映射的代码。要从源映射中删除源代码(仅保留文件名和行/列映射),请使用源内容选项。像这样使用内联源映射模式:
require('esbuild').buildSync({
  entryPoints: ['app.ts'],
  sourcemap: 'inline',
  outfile: 'out.js',
})
  1. both 这种模式是内联和外联的结合。源映射内联附加到 .js 输出文件的末尾,同一源映射的另一个副本与 .js 输出文件一起写入单独的 .js.map 输出文件。像这样使用两种源映射模式:
require('esbuild').buildSync({
  entryPoints: ['app.ts'],
  sourcemap: 'both',
  outfile: 'out.js',
})

构建 API 支持上面列出的所有四种源映射模式,但转换 API 不支持链接模式。这是因为从转换 API 返回的输出没有关联的文件名。如果您希望转换 API 的输出具有源映射注释,您可以自己附加一个。另外,transform API 的 CLI 形式只支持 inline 模式,因为输出是写到 stdout 的,所以生成多个输出文件是不可能的。

使用源映射

在浏览器中,只要启用了源映射设置,浏览器的开发人员工具就会自动获取源映射。请注意,浏览器仅使用源映射在记录到控制台时更改堆栈跟踪的显示。堆栈跟踪本身不会被修改,因此在您的代码中检查 error.stack 仍然会提供包含已编译代码的未映射堆栈跟踪。以下是在浏览器的开发人员工具中启用此设置的方法:
Chrome:⚙ → 启用 JavaScript 源映射 Safari: ⚙ → Sources → Enable source maps 火狐:···→ 启用源地图
在 node 中,从 v12.12.0 版本开始原生支持源映射。默认情况下禁用此功能,但可以使用标志启用。与浏览器不同,实际的堆栈跟踪也在 node 中修改,因此检查代码中的 error.stack 将提供包含原始源代码的映射堆栈跟踪.以下是在节点中启用此设置的方法(–enable-source-maps 标志必须位于脚本文件名之前)node --enable-source-maps app.js

Splitting Build支持

代码拆分仍在进行中。它目前仅适用于 esm 输出格式。跨代码拆分块的导入语句也存在一个已知的排序问题。您可以关注跟踪问题以获取有关此功能的更新。
这启用了“代码拆分”,它有两个目的:

  • 多个入口点之间共享的代码被拆分为两个入口点都导入的单独共享文件。这样,如果用户首先浏览到一个页面,然后再浏览到另一个页面,如果共享部分已经被他们的浏览器下载和缓存,他们就不必从头开始下载第二个页面的所有 JavaScript。
  • 通过异步 import() 表达式引用的代码将被拆分到一个单独的文件中,并且仅在计算该表达式时才加载。这使您可以通过仅在启动时下载所需的代码,然后在以后需要时懒惰地下载其他代码来缩短应用程序的初始下载时间。
    如果没有启用代码拆分,import() 表达式会变成 Promise.resolve().then(() => require()) 。这仍然保留了表达式的异步语义,但这意味着导入的代码包含在同一个包中,而不是被拆分到一个单独的文件中。启用代码拆分时,您还必须使用 outdir 设置配置输出目录:
require('esbuild').buildSync({
  entryPoints: ['home.ts', 'about.ts'],
  bundle: true,
  splitting: true,
  outdir: 'out',
  format: 'esm',
})

Target Transform | Build支持

他为生成的 JavaScript 和/或 CSS 代码设置目标环境。例如,您可以将 esbuild 配置为不生成 Chrome 版本 58 无法处理的任何更新的 JavaScript 或 CSS。目标可以设置为 JavaScript 语言版本,例如 es2020,也可以设置为单个引擎的版本列表(当前为 chrome、firefox、safari、edge 或 node)。默认目标是 esnext,这意味着默认情况下,esbuild 将假定支持所有最新的 JavaScript 和 CSS 功能。
这是一个使用 esbuild 中所有可用目标环境名称的示例。请注意,您不需要指定所有这些;您可以只指定您的项目关心的目标环境的子集。如果您愿意,您还可以更精确地了解版本号(例如 node12.19.0 而不仅仅是 node12):

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'firefox57',
    'safari11',
    'edge16',
    'node12',
  ],
  outfile: 'out.

您可以参考 JavaScript 加载器,详细了解哪些语言版本引入了哪些语法特性。请记住,虽然 es2020 等 JavaScript 语言版本是按年份标识的,但这是规范获得批准的年份。它与所有主要浏览器实现该规范的年份无关,该规范通常早于或晚于该年。
请注意,如果您使用 esbuild 尚不支持转换为当前语言目标的语法功能,则 esbuild 将在使用不受支持的语法时生成错误。例如,当以 es5 语言版本为目标时,经常会出现这种情况,因为 esbuild 仅支持将大多数较新的 JavaScript 语法特性转换为 es6.

Watch Build支持

在构建 API 上启用监视模式会告诉 esbuild 侦听文件系统上的更改,并在可能使构建无效的文件更改时重新构建。使用它看起来像这样:

require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: true,
}).then(result => {
  console.log('watching...')
})

如果您使用的是 JavaScript 或 Go API,您可以选择提供一个回调,每当增量构建完成时都会调用该回调。这可用于在构建完成后执行某些操作(例如,在浏览器中重新加载您的应用程序):

require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: {
    onRebuild(error, result) {
      if (error) console.error('watch build failed:', error)
      else console.log('watch build succeeded:', result)
    },
  },
}).then(result => {
  console.log('watching...')
})

如果您想在将来的某个时刻停止监视模式,您可以对结果对象调用“停止”以终止文件监视程序: JS 去

require('esbuild').build({
  entryPoints: ['app.js'],
  outfile: 'out.js',
  bundle: true,
  watch: true,
}).then(result => {
  console.log('watching...')

  setTimeout(() => {
    result.stop()
    console.log('stopped watching')
  }, 10 * 1000)
})

esbuild 中的监视模式是使用轮询而不是特定于操作系统的文件系统 API 实现的,以实现可移植性。轮询系统被设计为使用相对较少的 CPU,而不是一次扫描整个目录树的更传统的轮询系统。文件系统仍会定期扫描,但每次扫描仅检查文件的随机子集,这意味着文件更改将在更改后不久被发现,但不一定立即生效。
使用当前的启发式方法,大型项目应该每 2 秒左右完全扫描一次,因此在最坏的情况下,可能需要长达 2 秒才能注意到更改。但是,在注意到更改后,更改的路径会出现在最近更改的路径的简短列表中,在每次扫描时都会检查这些路径,因此应该几乎立即注意到对最近更改的文件的进一步更改。
请注意,如果您不想使用基于轮询的方法,仍然可以使用 esbuild 的增量构建 API 和您选择的文件观察器库自己实现观察模式。

Write Build支持

构建 API 调用可以直接写入文件系统,也可以返回本应作为内存缓冲区写入的文件。默认情况下,CLI 和 JavaScript API 会写入文件系统,而 Go API 不会。要使用内存缓冲区:

let result = require('esbuild').buildSync({
  entryPoints: ['app.js'],
  sourcemap: 'external',
  write: false,
  outdir: 'out',
})

for (let out of result.outputFiles) {
  console.log(out.path, out.contents)
}
 类似资料:

相关阅读

相关文章

相关问答