esbuild相关
esbuild使用
先初始化一个项目
pnpm init
安装依赖
pnpm install esbuild -D
pnpm install react react-dom
创建src/index.jsx
import * as React from 'react'
import * as Server from 'react-dom/server'
let Greet = () => <h1>Hello, world!</h1>
console.log(Server.renderToString(<Greet />))
使用esbuild有2种方式,分别是命令行调用和代码调用
命令行调用
命令行中cd到项目跟目录,执行下面打包命令
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=dist/index.js

如上图,已经成功打包了,不过这种方式不够灵活,通常情况下还是会使用代码调用
代码调用
esbuild暴露了一系列API,主要包括两类: Build API和Transform API,可以调用这些API来使用esbuild
Build API
Build API主要用于项目打包,提供了build、buildSync方法来对项目打包,提供serve方法来启动开发服务器
TIP
build方法是异步的,buildSync是同步的,但是一般使用build方法,使用buildSync方法有以下限制
由于插件是
异步的,所以使用buildSync同步方法不能使用插件使用
buildSync同步方法会阻塞当前线程使用
buildSync同步方法会阻碍esbuild API并行调用
详见sync
build方法常用参数
build方法常见的配置
bundle
Type: boolean,表示是否将引入(import)的依赖的代码打包到自身文件中,默认为false
例如:
src/index.js
import * as React from 'react'
import * as Server from 'react-dom/server'
let Greet = () => <h1>Hello, world!</h1>
console.log(Server.renderToString(<Greet />))
设置
bundle: false的情况
scripts/build.js
import { build } from 'esbuild'
const runBuild = async () => {
await build({
// bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ["src/index.jsx"],
outdir: "dist"
})
}
runBuild();
dist/index.js

设置
bundle: true的情况
scripts/build.js
import { build } from 'esbuild'
const runBuild = async () => {
await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ["src/index.jsx"],
outdir: "dist"
})
}
runBuild();
dist/index.js

可以看到设置bundle: true后,会将react、react-dom的代码打包到index.js中,设置为bundle: false后,只打包src/index.jsx文件的内容
splitting
Type: boolean,表示是否开启代码分割
WARNING
代码拆分仍在不断改进中,目前,它仅与
esm输出格式兼容,所以设置此选项为splitting: true时,也要同时设置format: true,此外,存在与代码拆分块之间的导入语句排序问题当设置
splitting: true时,必须要设置outdir选项配置输出目录
outfile
Type: string,设置打包输出文件的名称
import { build } from 'esbuild'
const runBuild = async () => {
await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ["src/index.jsx"],
// outdir: "dist",
outfile: 'dist/aaa.js'
})
}
runBuild();
WARNING
outfile字段和outdir字段不能同时使用outfile字段只适用于单入口场景,如果是多入口,不能使用该字段,要使用outdir字段
metafile
Type: boolean,这个选项告诉esbuild是否以JSON格式生成一些关于构建的元数据信息,可通过打包结果的metafile字段查看
示例:
vite.config.js
import { build } from 'esbuild'
const runBuild = async () => {
const result = await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ["src/index.jsx"],
// outdir: "dist",
outfile: 'dist/aaa.js',
metafile: true
});
console.log(result)
}
runBuild();
设置metafile: true后再次打包,打印出result如下:

设置metafile: false后再次打包,打印出result如下:

outdir
Type: string,表示打包结果要输出的目录
TIP
如果输出目录如果不存在会自动创建该目录
如果输出目录已创建,并且里面有文件,重新
build时不会清空原目录内的文件如果有同名文件,新打包生成的文件会覆盖老的文件
outbase
Type: string,该参数适用于多入口打包,会将打包结果复制到相对于outbase目录的输出目录中
例如,项目目录结构如下
.
├── package.json
├── pnpm-lock.yaml
├── scripts
│ └── build.js
└── src
├── index.jsx
└── pages
├── foo
│ └── index.js
└── home
└── index.js
vite.cofig.js,设置多入口,并且将outbase设置为src
import { build } from 'esbuild'
const runBuild = async () => {
const result = await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: [
'src/pages/home/index.js',
'src/pages/foo/index.js',
],
outdir: "dist",
// outfile: 'dist/aaa.js',
metafile: true,
outbase: 'src',
});
console.log(result)
}
runBuild();
打包结果元数据如下
metafile: {
inputs: {
'src/pages/home/index.js': [Object],
'src/pages/foo/index.js': [Object]
},
outputs: {
'dist/pages/home/index.js': [Object],
'dist/pages/foo/index.js': [Object]
}
},
打包后dist目录结构如下:
dist
└── pages
├── foo
│ └── index.js
└── home
└── index.js
如果outbase胡乱设置为一个不存在的目录,打包后dist目录结构如下
dist
└── _.._
└── src
└── pages
├── foo
│ └── index.js
└── home
└── index.js
external
Type: string[],标记一个文件/依赖包为外部的,在打包时不对其进行打包
alias
Type: Record<string, string>,用于在构建时将一个包替换为另一个包
import { build } from 'esbuild'
const runBuild = async () => {
const result = await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ['src/index.jsx'],
outdir: "dist",
metafile: true,
alias: {
'oldPkg': 'newPkg'
}
});
console.log(result)
}
runBuild();
TIP
这些替换首先发生在
esbuild的其他路径解析逻辑之前此功能的一个使用场景是使用
浏览器兼容包替换仅Node环境可使用的包,从而替换那些无法控制的第三方代码当使用
Alias替换导入路径时,生成的导入路径将在工作目录中解析,而不是在包含具有导入路径的源文件的目录中解析。如果需要,可以使用Working directory设置esbuild所使用的工作目录。
loader
Type: { [ext: string]: Loader }type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx'
esbuild内置了一系列的loader,包括base64、binary、css、dataurl、file、js(x)、ts(x)、text,针对一些特殊类型的文件,调用不同的loader进行加载,查看完整类型列表
resolveExtensions
Type: string[],设置文件的隐式扩展名的顺序,默认为.tsx,.ts,.jsx,.js,.css,.json
write
Type: boolean
是否将构建后的产物写入磁盘
minify
Type: boolean
是否进行代码压缩
watch
Type: boolean
是否开启watch模式,在watch模式下代码变动则会触发重新打包
publicPath
Type: string,设置加载loader的跟路径
chunkNames
Type: string,当启用代码分割splitting时,设置生成的共享代码块的文件名,例如chunks/[name].[hash].[ext]
字符串中有三个占位符可用
name:分割的chunk文件名称,第三方库名或者
chunkhash:文件hash
ext: 文件后缀
assetNames
Type: string,静态资源输出的文件名称,有以下几个占位符可用dir:相对于
outbase目录的相对路径name:文件原始名称(不包含扩展名)
hash:
ext:
例如
import { build } from 'esbuild'
const runBuild = async () => {
const result = await build({
bundle: true,
absWorkingDir: process.cwd(),
entryPoints: ['src/index.jsx'],
// entryPoints: [
// 'src/pages/home/index.js',
// 'src/pages/foo/index.js',
// ],
outdir: "dist",
// outfile: 'dist/aaa.js',
metafile: true,
// outbase: 'aaa',
// alias: {
// 'oldPkg': 'newPkg'
// }
splitting: true,
chunkNames: '[name]/[name].[hash].[ext]',
assetNames: 'assets/[name].[hash].[ext]',
format: 'esm',
loader: { '.webp': 'file', '.JPG': "file" },
});
console.log(result)
}
runBuild();
plugins
plugins?: Plugin[],插件API允许在构建的不同步骤期间注入一些代码
export interface Plugin {
name: string
setup: (build: PluginBuild) => (void | Promise<void>)
}
banner
Type: { [type: string]: string },使用它可以在生成的JavaScript和CSS文件的开头插入任意字符串
import { build } from 'esbuild'
const runBuild = async () => {
const result = await build({
banner: {
js: '/* comment */',
css: '// css comment'
}
});
console.log(result)
}
runBuild();
footer
Type: { [type: string]: string },使用它可以在生成的JavaScript和CSS文件的末尾插入任意字符串
absWorkingDir
Type: string,设置当前项目打包的工作目录
format
Tpye: 'iife' | 'cjs' | 'esm',设置生成的JavaScript文件的输出格式
sourcemap
Type: booleab,是否生成SourceMap文件
buildSync
buildSync方法的使用和build几乎相同,如下代码所示:
function runBuildSync() {
// 同步方法
const result = buildSync({
// 省略一系列的配置
})
console.log(result);
}
runBuildSync()
serve
开启serve模式后,将在指定的端口和目录上搭建一个
静态文件服务,这个服务器 用原生Go语言实现,性能比Nodejs更高该服务类似
webpack-dev-server,所有的产物文件都默认不会写到磁盘,而是放在内存中,通过请求服务来访问每次
请求到来时,都会进行重新构建(rebuild),永远返回新的产物
import * as Esbuild from 'esbuild'
const runServer = async () => {
try {
const ctx = await Esbuild.context({
entryPoints: ['src/index.jsx'],
outdir: 'dist',
bundle: true,
loader: {
'.webp': "file",
'.JPG': 'file'
},
})
const serveRes = await ctx.serve({
servedir: 'dist',
port: 9527,
})
console.log(`HTTP Server starts at port ${serveRes.port}`)
}catch (e) {
console.log(e, 'runServer')
}
}
runServer()
serve方法可传参数如下:
interface ServeOptions {
port?: number // 服务端口
host?: string // 服务host
servedir?: string // 静态服务目录
keyfile?: string // https用
certfile?: string // https用
fallback?: string // 类似于404页面路径,当传入请求与生成的输出文件路径不匹配时,返回这个文件
onRequest?: (args: ServeOnRequestArgs) => void // 对于每个传入的带有请求相关信息的请求,都会调用该函数
}
interface ServeOnRequestArgs {
remoteAddress: string
method: string
path: string
status: number
timeInMS: number
}
TIP
Serve API 只适合在开发阶段使用,不适用于生产环境。
Transform API
esbuild还专门提供了单文件编译的能力,即Transform API,它也包含了同步和异步的两个方法transformSync和transform,和build一样,推荐使用异步方法transform
import { transform } from "esbuild";
const runTransform = async () => {
const content = await transform(
`const delay = (ms: number) => new Promise((resolve) => {
setTimeout(resolve, ms)
})`,
{
loader: 'ts',
sourcemap: true
}
)
console.log(content)
}
runTransform()
transform函数接受两个参数,第一个参数是需要转换的源代码,第二个参数为编译配置
esbuild插件
插件的作用就是在构建过程中对资源进行解析和处理,esbuild支持我们传入自己的插件,在构建过程中完成自己想要的操作
插件只能和build API一起使用,不能和transform API一起使用,现有的一些esbuild 插件
esbuild插件被设计为一个对象,里面有name和setup两个选项
name:当前插件的名称
setup:一个函数,参数是一个
build对象,这个对象上挂载了一些钩子可供我们自定义一些钩子函数逻辑,build的详细字段如下

概念
Namespaces
每个模块都会有一个关联的命名空间(namespace)。默认情况下,esbuild是在文件系统上的文件所对应的namespace中运行的,此时namespace的值为file
esbuild的插件可以创建虚拟模块(virtual modules),虚拟模块(virtual modules)是指在文件系统中不存在的模块,可以自己定义虚拟模块的内容
Filters
每个回调函数必须提供一个filter,filter的值为一个正则表达式,主要用于匹配指定规则的导入(import)路径的模块,当路径不匹配filter时,回调函数不会执行,这是一种优化,加速运行速度
TIP
- filter的正则表达式和
JavaScript中的是区别的,这个正则表达式是Go语言中的正则实现的,不支持前瞻(? <=)、后顾(?=)和反向引用(\1)这三种规则
钩子函数
onResolve和onLoad函数是最常用的函数,接受的第一个参数都是{ filter: RegExp; namespace?: string }
onResolve
onResolve函数用来控制路径解析,该函数会在esbuild解析每个模块的导入路径时执行,前提是该路径符合filter正则规则,并返回一些字段
export interface OnResolveResult {
pluginName?: string // 插件名称
errors?: PartialMessage[] // 错误信息
warnings?: PartialMessage[] // 警告信息
path?: string // 模块路径
external?: boolean // 是否需要 external
sideEffects?: boolean // 设置为 false,如果模块没有被用到,模块代码将会在产物中会删除。否则不会这么做
namespace?: string // namespace 标识
suffix?: string // 添加一些路径后缀,如`?xxx`
pluginData?: any // 额外绑定的插件数据
/**
* 仅仅在 Esbuild 开启 watch 模式下生效
* 告诉 Esbuild 需要额外监听哪些文件/目录的变化
*/
watchFiles?: string[]
watchDirs?: string[]
}
export interface PartialMessage {
id?: string
pluginName?: string
text?: string
location?: Partial<Location> | null
notes?: PartialNote[]
detail?: any
}
onLoad
onLoad钩子函数用来控制模块内容加载,该函数会在esbuild解析模块之前调用,主要用来处理模块的内容并返回自己想要的内容,并且需要告知esbuild要如何解析该内容
返回值详情如下
export interface OnLoadResult {
pluginName?: string // 插件名称
errors?: PartialMessage[] // 错误信息
warnings?: PartialMessage[] // 警告信息
contents?: string | Uint8Array // 模块返回的具体内容
resolveDir?: string // 基准路径(将导入路径解析为文件系统上实际路径时,要使用的文件系统目录)
loader?: Loader // 指定解析 loader,如`js`、`ts`、`jsx`、`tsx`、`json`等等
pluginData?: any // 额外的插件数据
// 和onResolve中的一样
watchFiles?: string[]
watchDirs?: string[]
}
onStart
- 该函数的执行时机是在每次
build的时候,包括触发watch或者serve模式下的重新构建
onEnd
- 构建结束时执行
插件示例
创建虚拟模块并指定虚拟模块返回的内容
pugins/envPlugin
const envPlugin = {
name: 'env-plugin',
setup(build) {
build.onResolve({ filter: /^my-env$/ }, args => {
return {
path: args.path,
namespace: 'my-env-namespace'
}
})
// 这里也可以去读取文件/请求文件 然后将获取到的文件内容返回
build.onLoad({ filter: /.*/, namespace: 'my-env-namespace' }, args => {
return {
contents: JSON.stringify({ "name": "222222" }),
loader: 'json',
}
})
}
}
export default envPlugin;
scripts/serve.js中添加自己写的插件
import * as Esbuild from 'esbuild'
import envPlugin from "../plugins/envPlugin.js";
const runServer = async () => {
try {
const ctx = await Esbuild.context({
entryPoints: ['src/index.jsx'],
outdir: 'dist',
bundle: true,
loader: {
'.webp': "file",
'.JPG': 'file'
},
plugins: [envPlugin]
})
const serveRes = await ctx.serve({
servedir: 'dist',
port: 9527,
})
console.log(`HTTP Server starts at port ${serveRes.port}`)
}catch (e) {
console.log(e, 'runServer')
}
}
runServer()
业务代码中打印
import myEnv from 'my-env'
console.log({ myEnv }) // myEnv: {name: '222222'}
打包完成后自动创建html文件
plugins/autoGenerateHtmlPlugin.js
import path from 'node:path'
import fs from "fs/promises";
const createScript = src => `<script type="module" src="${src}"></script>`;
const createLink = href => `<link rel="stylesheet" href="${href}" />`;
const generateHtml = ({ scripts, links }) => {
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
${links?.join('\n')}
</head>
<body>
<div class="root"></div>
${scripts?.join('\n')}
</body>
</html>
`
}
/**
* 打包结束后自动创建html文件
* @param options
* @returns {{name: string, setup(*): void}}
*/
const autoGenerateHtmlPlugin = (options = { outdir: 'dist' }) => {
return {
name: 'auto-generate-plugin-html',
setup(build) {
build.onEnd(async buildResult => {
if (buildResult.errors.length) {
return;
}
const {metafile} = buildResult; // 想获取 metafile,打包配置中必须配置 { metafile: true }
if (!metafile) return;
const { outputs} = metafile;
const { outdir } = options;
// 输出目录要提取出来
const outputKeys = Object.keys(outputs).map(i => i.replace(outdir, ''));
const scripts = [], links = [];
outputKeys.forEach(item => {
if(item.endsWith('.js')) {
scripts.push(createScript(item))
return;
}
if(item.endsWith('.css')) {
links.push(createLink(item))
}
})
const htmlContent = generateHtml({ scripts, links });
const htmlFilePath = path.join(process.cwd(), outdir, 'index.html')
await fs.writeFile(htmlFilePath, htmlContent)
})
}
}
}
export default autoGenerateHtmlPlugin
在plugins中添加该插件
plugins: [envPlugin, autoGenerateHtmlPlugin()]
其他
- 相关代码可查看usage-esbuild