Esbuild

为什么选择esbuild?

简而言之就是:esbuild是go语言写的,编译速度快,支持的环境多。
究竟有多快:它的编译速度是普通编译插件的 100多倍
它的API可以通过三种方式访问:命令行、JavaScript和Go,而且文档还是独一份

安装

npm install esbuild

基础命令

version

esbuild --version

构建

esbuild app.jsx --bundle --outfile=out.js

快速转化

echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;

API

Transform API

转换API调用只对单个字符串进行操作,而不访问文件系统。这使得它非常适合在没有文件系统(如浏览器)的环境中使用,或者作为另一个工具链的一部分。下面是一个简单的变换:

echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;

基础选项:

高级选项:

Build API

构建API调用对文件系统中的一个或多个文件进行操作。这允许文件相互引用并捆绑在一起。下面是一个简单的构建:

echo 'let x: number = 1' > in.ts
esbuild in.ts --outfile=out.js
cat out.js
# let x = 1;

基础选项:

高级选项:

基础选项
Bundle

启用后将你的代会打包成一个包(文件)
举个例子
你有以下文件
/src/index.js

import { doSomething } from './utils'
doSomething()

/src/utils.js

export function doSomething() {}

执行命令

npx esbuild ./src/index.js --bundle --outfile=bundle.js

打包后是一个bundle.js

function doSomething() {}
doSomething()

如果不启用 bundle 那么打包出来bundle.js长这样

import { doSomething } from "./utils";
doSomething();
Define

定义一个全局可以访问的变量
举个例子
index.js

console.log(**DEFINE**)

执行命令

npx esbuild ./src/index.js --bundle --outfile=./bundle.js "--define:__DEFINE__=\"define\"

打包结果
bundle.js

console.log("define")
Entry points

就是入口文件列表,esbuild 后边跟的参数
举个例子
index.js

console.log('index')

utils.js

console.log('utils')

执行脚本

npx esbuild index.js utils.js --outdir=out
# 或者
npx esbuild ./** --outdir=out
# 或者
npx esbuild out1=index.js out2=utils.js --outdir=out
External

将文件或包标记为外部文件, 构建时会自动排除
举个例子
index.js

require("fsevents")

执行脚本

npx esbuild index.js --bundle --external:fsevents --platform=node --outfile=bundle.js

说明

这个api一般是给 commenJs 的包用的
比如打包的 bundle.js 是给 node环境的代码使用,那么我们打包出来的内容不应该把 node_modules 包的内容打包到 bundle.js 里,而是应该在使用 bundle.js,就应该在本地的 node_modules 里包含它的依赖

Format

决定生成的JavaScript文件的输出格式
目前支持三个参数

  • iife
    自调用函数
    !(function() { // ...do something })();
  • cjs
    commenJs
    const xx = require("xx")
  • esm
    esModule
    import xx from "xx"
Inject

允许你用另一个文件的导入替换全局变量
process-shim.js

export let process = {
  cwd: () => ''
}

entry.js

console.log(process.cwd())

执行命令

npx esbuild entry.js --bundle --inject:./process-shim.js --outfile=bundle.js

bundle.js

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

说明

你可以通过该 api 注入 jsx 语法的解析函数
例如,您可以自动导入 react 提供React.createElement之类的函数
详情请看 JSX DOC

Loader

此选项更改给定输入文件的解释方式。例如,js加载器将文件解释为JavaScript,而css加载器将文件解释为css

npx esbuild index.js --bundle --loader:.png=dataurl --loader:.svg=text
# 或者
echo 'import index = require("./index")' | npx esbuild --loader=ts --bundle
# 或者
echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;

注意

esbuild 仅仅是将 ts 语法转化为 js 语法,并不做类型校验

Minify

生成的代码将被最小化

echo 'fn = obj => { return obj.x }' | npx esbuild --minify
# fn=n=>n.x;
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-whitespace
# fn=obj=>{return obj.x};
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-identifiers
#fn = (n) => {
#  return n.x;
#};
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-syntax
#fn = (obj) => obj.x;

注意

  • 当启用缩小功能时,您可能还应该设置目标选项
    默认情况下,esbuild利用了现代JavaScript的特性使你的代码更小
    例如: a === undefined || a === = null ? 1 : a 可以被压缩为 a??1
    如果你不想让esbuild在缩小时利用现代JavaScript特性,你应该使用一个比较老的语言,比如: --target=es6
  • 在JavaScript模板文本中,字符转义序列 \n 将被替换为换行符
    如果目标支持字符串字面量,并且这样做会导致更小的输出,那么字符串字面量也会被转换成模板字面量
    这不是一个bug,缩小意味着你要求更小的输出,转义序列 \n 需要两个字节,而换行字符需要一个字节
  • 默认情况下,esbuild不会缩小顶级声明的名称
    这是因为esbuild不知道你将如何处理输出
    您可能会将缩减后的代码注入到其他一些代码中,在这种情况下,缩减顶级声明名称将是不安全的
    设置输出格式(或者启用绑定,如果你还没有设置,它会为你选择一个输出格式)告诉esbuild输出将在它自己的范围内运行,这意味着它可以安全地减少顶级声明名称。
  • 对JavaScript代码来说,缩小不是100%安全的
    对于esbuild以及其他流行的JavaScript迷你器(如terser)来说都是如此
    特别是,esbuild并不是为了保留函数调用 .tostring() 的值而设计的
    这样做的原因是,如果所有函数中的所有代码都必须逐字保存,那么最小化几乎什么都做不了,而且实际上是无用的
    然而,这意味着依赖于.tostring() 返回值的JavaScript代码在最小化时可能会中断
    例如,AngularJS框架中的一些模式在
  • 默认情况下,esbuild不会在函数和类对象上保留 .name 的值
    这是因为大多数代码不依赖于这个属性,使用更短的名称是一个重要的大小优化
    但是,有些代码确实依赖于 .name 属性进行注册和绑定
    如果你需要依赖这个选项,你应该启用keep names选项。
  • 某些 JavaScript 特性可以禁用 esbuild 的优化,包括缩小
    具体来说,使用直接 eval 或 with 语句可以防止esbuild将标识符重命名为更小的名称,因为这些特性会导致标识符绑定发生在运行时,而不是编译时
    这可能是无关紧要的,因为大部分人不会这样做
Outdir

构建操作的输出目录

npx esbuild index.js --bundle --outdir=out
Outfile

构建操作的输出文件

npx esbuild index.js --bundle --outfile=bundle.js
Platform

默认情况下,esbuild的绑定器被配置为生成用于浏览器的代码。
如果你打包的代码打算在node中运行,你应该将 platform 设置为 node。

# 自调用函数 (默认)
npx esbuild index.js --bundle --platform=browser
# 或者 commenJs
npx esbuild index.js --bundle --platform=node
# 或者 Es module
npx esbuild index.js --bundle --platform=neutral
Serve

类似 webpack-server 的一个 http 服务器,意思就是让你改了代码不用一直执行 esbuild
执行命令

npx esbuild src/index.js --servedir=www --outdir=www/js --bundle

然后创建 www 目录,创建 index.html

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

或者您只需要服务你的 js

npx esbuild src/index.js --outfile=out.js --bundle --serve=8000

index.html

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

参数

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

代理服务器示例

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

// 在一个随机的本地端口上启动esbuild服务器
esbuild.serve(
  {
    servedir: __dirname,
  }, 
  {
    // ... 构建选项 ...
  }
).then(result => {
  // result 告诉我们 esbuild 的本地服务器在哪里
  const {host, port} = result

  // 然后在端口上启动代理服务器 3000
  http.createServer((req, res) => {
    const options = {
      hostname: host,
      port: port,
      path: req.url,
      method: req.method,
      headers: req.headers,
    }

    // 将每个传入的请求转发给esbuild
    const proxyReq = http.request(options, proxyRes => {
      // 如果esbuild返回“未找到”,发送一个自定义404页面
      if (proxyRes.statusCode === 404) {
        res.writeHead(404, { 'Content-Type': 'text/html' });
        res.end('<h1>A custom 404 page</h1>');
        return;
      }

      // 否则,将响应从esbuild转发到客户端
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(res, { end: true });
    });

    // 将请求体转发给esbuild
    req.pipe(proxyReq, { end: true });
  }).listen(3000);
});
Sourcemap

让打包后的代码有一个源代码得映射文件,就是说在浏览器调试的时候可以看源代码

# linked (默认)
# 生成为一个单独的 .js.map 源映射, 输出文件 bundle.js 包含一个特殊的 //# sourceMappingURL= 注释,该注释指向 .js.map。这样,当您打开调试器时,浏览器就知道在哪里找到给定文件的源映射
npx esbuild index.js --sourcemap --outfile=bundle.js

# external
# 生成为一个单独的 .js.map 源映射, 但与 linked 模式不同的是, 输出文件 bundle.js 不包含 //# sourceMappingURL=
npx esbuild index.js --sourcemap=external --outfile=bundle.js

# inline
# 不生成 .js.map 文件,注解 //# sourceMappingURL= 后边跟着一串映射的 base64 数据
npx esbuild index.js --sourcemap=inline --outfile=bundle.js

# both
# inline 和 external 的结合
npx esbuild index.js --sourcemap=both --outfile=bundle.js

注意

在浏览器中,需要打开设置里的Enable source maps的按钮
在nodejs中, v12.12.0 已经内置支持了原映射
node --enable-source-maps index.js

Splitting

代码分割,将相同的代码打包到一个公共的 js 中
注意:这个 api 还在开发中, 目前只适用于esm输出格式

npx esbuild index.js utils.js --bundle --splitting --outdir=out --format=esm
Target

打包的js需要支持的环境,默认是 esnext,也就是最新的 javascript 和 css 语法和特性,意味着默认打包出来的代码天然不支持 ie(that’s good)

npx esbuild index.js --target=es2020,chrome58,firefox57,safari11,edge16,node12
Watch

esbuild 监听文件系统上的更改,并在文件更改可能导致构建无效时重新构建

npx esbuild index.js --outfile=bundle.js --bundle --watch

当你在 js 中使用它的时候,watch 可以是一个对象,例如:

require('esbuild').build({
  entryPoints: ['index.js'],
  outfile: 'bundle.js',
  bundle: true,
  watch: {
    onRebuild(error, result) {
      if (error) console.error('监听失败:', error)
      else console.log('监听成功:', result)
      // result.stop() 可以停止监听
    },
  },
}).then(result => {
  console.log('监听中...')
})
Write

默认情况下,build api 会将构建的内容自动写入系统文件,write: false 可以阻止这种行为
举个例子

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)
}
高级选项
Allow overwrite

允许输出文件覆盖输入文件(我猜没人会这样干)
举个例子

npx esbuild index.js --outdir=. --allow-overwrite
# 这样你的 index.js 源代码会消失
Analyze

生成一个关于bundle内容的易于阅读的报告

npx esbuild --bundle index.js --outfile=bundle.js --minify --analyze
Asset names

loader选项为file时,给打包后的资源增加额外的信息

# [name], [hash], [dir], [ext] 这四个顾名思义,任意组合
npx esbuild index.js --asset-names=assets/[name]-[hash] --loader:.png=file --bundle --outdir=out
Banner

在生成的JavaScript和CSS文件的开头插入任意字符串,通常用于插入注释
在 git bash 上运行可能会有一些问题:https://github.com/evanw/esbuild/issues/2150

npx esbuild index.js --banner:js=//comment --banner:css=/*comment*/ --outfile=bundle.js
Charset

顾名思义,设置字符编码

echo 'let a = 你好' | npx esbuild
# let a = \u4F60\u597D;
echo 'let a = 你好' | npx esbuild --charset=utf8
# let a = 你好;
Chunk names

启用代码分割时自动生成的共享代码块的文件名

# [name], [hash], [ext]
esbuild index.js --chunk-names=chunks/[name]-[hash] --bundle --outdir=out --splitting --format=esm
Color

构建时候是否开启颜色

echo 'typeof x == "null"' | npx esbuild --color=true
Conditions

说实话不太懂这个api的含义和用处,所以这里解释比较简单
conditions 允许你将相同的导入路径重定向到不同的文件位置

npx esbuild index.js --bundle --conditions=custom1,custom2

举个例子
当我们 import “pkg/foo” 的时候,它指向 ./imported.mjs
当我们 require(“pkg/foo”) 的时候,它指向 ./required.cjs

{
  "name": "pkg",
  "exports": {
    "./foo": {
      "import": "./imported.mjs",
      "require": "./required.cjs",
      "default": "./fallback.js"
    }
  }
}
Drop

在构建之前需要删除的内容,比如页面里的 console

# 删除 debugger
npx esbuild index.js --drop:debugger
# 删除 console
npx esbuild index.js --drop:console
Entry names

入口文件名修改,一般情况下修改文件名,可以让我们的用户用到最新的功能

# [name], [hash], [dir], [ext]
npx esbuild index.js --entry-names=[dir]/[name]-[hash] --bundle --outdir=out
Footer

和 Banner API 有相同的问题
在生成的 JavaScript 和 CSS 文件的末尾插入任意字符串,通常用于插入注释

npx esbuild index.js --footer:js=//comment --footer:css=/*comment*/ --outfile=bundle.js
Global name

只在 formatiife 时起作用,设置全局变量的名称,也就是将 iife 自调用的函数赋值给该变量

echo 'module.exports = "test"' | npx esbuild --format=iife --global-name=vue
# 或者
echo 'module.exports = "test"' | npx esbuild --format=iife --global-name='vue.test["xx"]'
Ignore annotations

由于JavaScript是一种动态语言,对于编译器来说,识别未使用的代码有时非常困难,因此社区开发了一些注释,以帮助告诉编译器哪些代码应该被认为是没有副作用的,并且可以删除。目前esbuild支持两种形式的副作用注解

  • 行内 /* @PURE */ 函数调用前的注释告诉 esbuild,如果结果值没有被使用,函数调用可以被删除。
  • package.json 的 sideEffects 字段用来告诉esbuild,如果你的包中的所有导入文件最终都没有被使用,那么该包中的哪些文件可以被删除。这是Webpack 的约定,很多发布到 npm 的库都已经在包定义中包含了这个字段。你可以在 Webpack 的文档中了解更多关于这个字段的信息。
npx esbuild index.js --bundle --ignore-annotations
Incremental

用相同的选项反复调用 esbuild 的 build API,就打开这个选项

JSX

处理JSX语法,可以将 JSX 转换为 JS(默认),也可以在输出中保留 JSX 语法

echo '<div/>' | npx esbuild --jsx=preserve --loader=jsx
# <div />;

echo '<div/>' | npx esbuild --loader=jsx
# /* @__PURE__ */ React.createElement("div", null);
JSX factory

设置 JSX元素 调用的函数

echo '<div/>' | npx esbuild --jsx-factory=h --loader=jsx
# /* @__PURE__ */ h("div", null);
JSX fragment

设置 JSX片段调用的函数

echo '<>content</>' | npx esbuild --jsx-fragment=Fragment --loader=jsx
# /* @__PURE__ */ React.createElement(Fragment, null, "content");
Keep names

将函数的name属性设置为"fn",这个api也不懂它的含义在哪

npx esbuild index.js --minify --keep-names
Legal comments
  • none
    不要保留任何 legal comment
  • inline
    保留所有 legal comment
  • eof
    legal comment 移到文件末尾.
  • linked
    legal comment 移动到 .LEGAL.txt 文件中,并用注释链接它.
  • external
    legal comment 移动到 .LEGAL.txt 文件中,但不链接它
npx esbuild index.js --legal-comments=eof
Log level

日志级别

  • silent
    不要显示任何日志输出
  • error
    只显示错误
  • warning
    只显示警告和错误
  • info
    显示警告、错误和输出文件摘要
  • debug
    记录所有的信息和一些额外的消息.
  • verbose
    生成大量日志消息
echo 'typeof x == "null"' | npx esbuild --log-level=error
Log limit

控制打印日志的数量,否则可能控制台会很卡,默认是10条

npx esbuild index.js --log-limit=10
Main fields

当在node中导入包时,定义导入的是 package.json 的哪个字段

  • main
  • module
  • browser
npx esbuild index.js --bundle --main-fields=module,main
Mangle props

传递一个正则表达式给esbuild,告诉esbuild自动重写所有匹配这个正则表达式的属性(破坏性很强的 api,少用为妙)
举个例子
index.js

let x = { xx_: 'x' };
let y = { xx_: "y" };
# 它会将所有的以 _ 结尾的属性全部改掉
npx esbuild index.js --mangle-props=_$
# let x = { a: "x" };
# let y = { b: "y" };
Metafile

以JSON格式生成一些关于构建的元数据

npx esbuild index.js --bundle --metafile=meta.json --outfile=out.js
Node paths

全局目录列表。除了查找所有父目录中的node_modules目录之外,还会搜索这些路径

NODE_PATH=someDir npx esbuild index.js --bundle --outfile=bundle.js
Out extension

自定义esbuild生成的文件的扩展名

npx esbuild index.js --bundle --outdir=dist --out-extension:.js=.mjs
Outbase

基础路径

npx esbuild src/pages/home/index.ts src/pages/about/index.ts --bundle --outdir=out --outbase=src
Preserve symlinks

This setting mirrors the --preserve-symlinks setting in node. If you use that setting (or the similar resolve.symlinks setting in Webpack), you will likely need to enable this setting in esbuild too. It can be enabled like this:

npx esbuild index.js --bundle --preserve-symlinks --outfile=bundle.js
Public path

为每个被这个 loader 加载的文件的导出字符串添加一个基本路径

npx esbuild index.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
Pure

各种JavaScript工具都有一个惯例,在new或call表达式之前有一个包含/* @PURE /或/ #PURE */的特殊注释,这意味着如果结果值未被使用,该表达式可以被删除。它看起来是这样的:

echo 'console.log("foo:", foo())' | npx esbuild --pure:console.log --minify
Resolve extensions

设置后引入文件可以省略文件扩展名

npx esbuild index.js --bundle --resolve-extensions=.ts,.js
Source root

设置映射源代码目录

npx esbuild index.js --sourcemap --source-root=https://raw.githubusercontent.com/some/repo/v1.2.3/
Sourcefile

设置映射源代码文件

cat index.js | npx esbuild --sourcefile=example.js --sourcemap
Sources content

设置映射源代码内容,不知道如何使用

npx esbuild --bundle app.js --sourcemap --sources-content=false
Stdin

通常用于没有入口文件的时候,比如对应于在命令行上将一个文件管道接到stdin

echo 'export * from "./another-file"' | npx esbuild --bundle --sourcefile=imaginary-file.js --loader=ts --format=cjs
Tree shaking

死代码删除

npx esbuild app.js --tree-shaking=true
Tsconfig

顾名思义,设置tsconfig

npx esbuild index.ts --bundle --tsconfig=custom-tsconfig.json
Tsconfig raw
echo 'class Foo { foo }' | npx esbuild --loader=ts --tsconfig-raw='{"compilerOptions":{"useDefineForClassFields":true}}'
Working directory

指定要用于构建的工作目录

require('esbuild').buildSync({
  entryPoints: ['file.js'],
  absWorkingDir: process.cwd(),
  outfile: 'out.js',
})
Content Types

内容类型,每种内容类型都有一个关联的默认 loader,你可以覆盖它

  • JavaScript : js 文件后缀: .js, .cjs, .mjs

你可以配置target属性来控制编译出来的代码版本

Syntax transform

Transformed when --target is below

Example

Exponentiation operator

es2016

a ** b

Async functions

es2017

async () => {}

Spread properties

es2018

let x = {…y}

Rest properties

es2018

let {…x} = y

Optional catch binding

es2019

try {} catch {}

Optional chaining

es2020

a?.b

Nullish coalescing

es2020

a ?? b

import.meta

es2020

import.meta

Logical assignment operators

es2021

a ??= b

Class instance fields

esnext

class { x }

Static class fields

esnext

class { static x }

Private instance methods

esnext

class { #x() {} }

Private instance fields

esnext

class { #x }

Private static methods

esnext

class { static #x() {} }

Private static fields

esnext

class { static #x }

Ergonomic brand checks

esnext

#x in y

Import assertions

esnext

import “x” assert {}

Class static blocks

esnext

class { static {} }

还有一些语法它不会转换

Syntax transform

Unsupported when --target is below

Example

Asynchronous iteration

es2018

for await (let x of y) {}

Async generators

es2018

async function* foo() {}

BigInt

es2020

123n

Hashbang grammar

esnext

#!/usr/bin/env node

Top-level await

esnext

await import(x)

Arbitrary module namespace identifiers

esnext

export {foo as ‘f o o’}

  • TypeScript: ts 或者 tsx 文件后缀: .ts, .tsx, .mts, .cts

TypeScript类型声明会被解析并被忽略

Syntax feature

Example

Interface declarations

interface Foo {}

Type declarations

type Foo = number

Function declarations

function foo(): void;

Ambient declarations

declare module ‘foo’ {}

Type-only imports

import type {Type} from ‘foo’

Type-only exports

export type {Type} from ‘foo’

Type-only import specifiers

import {type Type} from ‘foo’

Type-only export specifiers

export {type Type} from ‘foo’

只支持typescript语法扩展,并且总是被转换成JavaScript

Syntax feature

Example

Notes

Namespaces

namespace Foo {}

Enums

enum Foo { A, B }

Const enums

const enum Foo { A, B }

Generic type parameters

(a: T): T => a

Not available with the tsx loader

JSX with types

<Element/>

Type casts

a as B and a

Type imports

import {Type} from ‘foo’

Handled by removing all unused imports

Type exports

export {Type} from ‘foo’

Handled by ignoring missing exports in TypeScript files

Experimental decorators

@sealed class Foo {}

The emitDecoratorMetadata flag is not supported

  • JSX: jsxtsx
  • JSON: json
  • CSS: css
  • Text: text
  • Binary: binary
  • Base64: base64
  • Data URL: dataurl
  • External file: file
Plugins

`Plugins`是新的,仍然处于实验阶段。在esbuild 1.0.0版本之前,它可能会随着新用例的出现而改变。您可以根据跟踪问题获得关于此特性的更新

Plugins 允许你将代码注入到构建过程的各个部分

寻找插件

https://github.com/esbuild/community-plugins

使用插件

一个esbuild插件是一个有名字和setup函数的对象。它们以数组的形式传递给构建API调用。每次构建API调用都运行一次setup函数

其他详情请查看Plugin