Skip to content
On this page

esbuild相关

esbuild使用

先初始化一个项目

shell
pnpm init

安装依赖

shell
pnpm install esbuild -D
pnpm install react react-dom

创建src/index.jsx

jsx
import * as React from 'react'
import * as Server from 'react-dom/server'
let Greet = () => <h1>Hello, world!</h1>
console.log(Server.renderToString(<Greet />))

使用esbuild2种方式,分别是命令行调用代码调用

命令行调用

命令行中cd到项目跟目录,执行下面打包命令

shell
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=dist/index.js

如上图,已经成功打包了,不过这种方式不够灵活,通常情况下还是会使用代码调用

代码调用

esbuild暴露了一系列API,主要包括两类: Build APITransform API,可以调用这些API来使用esbuild

Build API

Build API主要用于项目打包,提供了buildbuildSync方法来对项目打包,提供serve方法来启动开发服务器

TIP

build方法是异步的,buildSync同步的,但是一般使用build方法,使用buildSync方法有以下限制

  • 由于插件是异步的,所以使用buildSync同步方法不能使用插件

  • 使用buildSync同步方法会阻塞当前线程

  • 使用buildSync同步方法会阻碍esbuild API并行调用

详见sync

build方法常用参数

build方法常见的配置

bundle
  • Type: boolean,表示是否将引入(import)的依赖的代码打包到自身文件中,默认为false

例如:

src/index.js

jsx
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

javascript
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

javascript
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后,会将reactreact-dom的代码打包到index.js中,设置为bundle: false后,只打包src/index.jsx文件的内容

splitting
  • Type: boolean,表示是否开启代码分割

WARNING

  • 代码拆分仍在不断改进中,目前,它仅与esm输出格式兼容,所以设置此选项为splitting: true时,也要同时设置format: true,此外,存在与代码拆分块之间的导入语句排序问题

  • 当设置splitting: true时,必须要设置outdir选项配置输出目录

outfile
  • Type: string,设置打包输出文件的名称
javascript
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

javascript
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目录的输出目录中

例如,项目目录结构如下

shell
.
├── package.json
├── pnpm-lock.yaml
├── scripts
   └── build.js
└── src
    ├── index.jsx
    └── pages
        ├── foo
           └── index.js
        └── home
            └── index.js

vite.cofig.js,设置多入口,并且将outbase设置为src

javascript
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();

打包结果元数据如下

json
  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目录结构如下:

shell
dist
└── pages
    ├── foo
       └── index.js
    └── home
        └── index.js

如果outbase胡乱设置为一个不存在的目录,打包后dist目录结构如下

shell
dist
└── _.._
    └── src
        └── pages
            ├── foo
               └── index.js
            └── home
                └── index.js
external
  • Type: string[],标记一个文件/依赖包为外部的,在打包时不对其进行打包
alias
  • Type: Record<string, string>,用于在构建时将一个包替换为另一个包
javascript
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文件名称,第三方库名或者chunk

  • hash:文件hash

  • ext: 文件后缀

assetNames
  • Type: string,静态资源输出的文件名称,有以下几个占位符可用

  • dir:相对于outbase目录的相对路径

  • name:文件原始名称(不包含扩展名)

  • hash:

  • ext:

例如

javascript
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允许在构建的不同步骤期间注入一些代码
typescript
export interface Plugin {
    name: string
    setup: (build: PluginBuild) => (void | Promise<void>)
}
  • Type: { [type: string]: string },使用它可以在生成的JavaScriptCSS文件的开头插入任意字符串
javascript
import { build } from 'esbuild'

const runBuild = async () => {
    const result = await build({
        banner: {
            js: '/* comment */',
            css: '// css comment'
        }
    });
    console.log(result)
}

runBuild();
  • Type: { [type: string]: string },使用它可以在生成的JavaScriptCSS文件的末尾插入任意字符串
absWorkingDir
  • Type: string,设置当前项目打包的工作目录
format
  • Tpye: 'iife' | 'cjs' | 'esm',设置生成的JavaScript文件的输出格式
sourcemap
  • Type: booleab,是否生成SourceMap文件

buildSync

buildSync方法的使用和build几乎相同,如下代码所示:

javascript
function runBuildSync() {
    // 同步方法
    const result = buildSync({
        // 省略一系列的配置
    })
    console.log(result);
}
runBuildSync()

serve

  • 开启serve模式后,将在指定的端口和目录上搭建一个静态文件服务,这个服务器 用原生Go语言实现,性能比Nodejs更高

  • 该服务类似webpack-dev-server,所有的产物文件都默认不会写到磁盘,而是放在内存中,通过请求服务来访问

  • 每次请求到来时,都会进行重新构建(rebuild),永远返回新的产物

javascript
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方法可传参数如下:

typescript
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,它也包含了同步和异步的两个方法transformSynctransform,和build一样,推荐使用异步方法transform

typescript
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插件被设计为一个对象,里面有namesetup两个选项

  • 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)这三种规则

钩子函数


onResolveonLoad函数是最常用的函数,接受的第一个参数都是{ filter: RegExp; namespace?: string }

onResolve
  • onResolve函数用来控制路径解析,该函数会在esbuild解析每个模块的导入路径时执行,前提是该路径符合filter正则规则,并返回一些字段
typescript
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要如何解析该内容

返回值详情如下

typescript
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

javascript
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中添加自己写的插件

javascript
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()

业务代码中打印

javascript
import myEnv from 'my-env'

console.log({ myEnv }) // myEnv: {name: '222222'}

打包完成后自动创建html文件

plugins/autoGenerateHtmlPlugin.js

javascript
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中添加该插件

javascript
plugins: [envPlugin, autoGenerateHtmlPlugin()]

其他