Esbuild打包

本文最后更新于:2023年7月28日 凌晨

Esbuild是一个前端打包工具,使用Go编写拥有极快的打包于压缩能力

安装

Esbuild

1
2
pnpm i esbuild -D
pnpm i typescript -D

ts-node

由于使用了TS,后续用ts-node来直接执行TS文件,所以要先安装ts-node

1
pnpm add ts-node -g

tsc --init初始化tsconfig.json,并添加ts-node配置与module配置

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ts-node": {
"esm": true
},
"compilerOptions": {
"target": "es2017",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

CLI 编译

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "module",
"scripts": {
"build": "esbuild src/main.ts --outdir=dist"
},
"devDependencies": {
"@types/node": "^18.11.18",
"esbuild": "^0.17.5",
"typescript": "^4.9.5"
}
}

esbuild自带一些loader,可以直接打包ts, jsx, tsx, css等文件

src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
name: string
age: number
}

const person: Person = {
name: '清山',
age: 20
}

console.log(person)

API Transform

transformAPI接受一个待编译的字符串和一个TransformOptions对象, 用Promise返回编译结果

1
2
3
4
5
6
7
8
9
10
11
import { transform, TransformOptions } from 'esbuild'

async function runTransform() {
const options: TransformOptions = {}

const res = await transform('typeof x == null', options)
console.log(res)
}

runTransform()

API Build

buildAPI接受一个BuildOptions对象, 用Promise返回构建结果

src/cli/build.ts

1
2
3
4
5
6
7
8
9
10
11
import { build, BuildOptions } from 'esbuild'

const options: BuildOptions = {}

runBuild(options)

async function runBuild(options: BuildOptions) {
const res = await build(options)
console.log(res)
}

absWorkingDir

absWorkingDir属性设置项目的根目录,设置后输入输出的地址可以依此来用相对路径,若用绝对路径设置输入输出也可不设置该属性

1
2
3
4
const options: BuildOptions = {
absWorkingDir: process.cwd(),
}

outfile与outdir

outfile选项适用于单入口项目,打包输出单文件的情况.

outdir设置结果输出目录

1
2
3
4
5
6
7
8
9
const options1: BuildOptions = {
absWorkingDir: process.cwd(),
outfile: './dist/index.js'
}

const options2: BuildOptions = {
absWorkingDir: process.cwd(),
outdir: './dist'
}

entryPoints

声明入口文件路径数组,有多个独立模块可以声明多个路径入口,默认输出项目入口名与入口相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const options: BuildOptions = {
absWorkingDir: process.cwd(),
outdir: './dist',
entryPoints: [
'./src/main.ts',
]
}

// 传入对象来改写输出文件名
const options: BuildOptions = {
absWorkingDir: process.cwd(),
outdir: './dist',
entryPoints: [
{out: 'utils/log.js', in: './src/utils/say.ts'}
]
}

先写两个待编译的文件

src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { say } from './utils/say'

interface Person {
name: string
age: number
}

const person: Person = {
name: '清山',
age: 20,
}

say(person)

src/utils/say.ts

1
2
3
4
export function say(words: unknown): void {
console.log(words)
}

src/cli/build.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { build, BuildOptions } from 'esbuild'

const options: BuildOptions = {
absWorkingDir: process.cwd(),
entryPoints: ['./src/main.ts'],
outdir: './dist/esm',
// 设置打包格式, 支持"esm" | "iife" | "cjs"
format: 'esm',
}

async function runBuild() {
const res = await build(options)
console.log(res)
}

runBuild()

使用命令ts-node ./src/cli/build.ts即可打包, 但打包结果没有打包say.ts

1
2
3
4
5
6
7
8
import { say } from "./utils/say";
const a = 1;
const person = {
name: "\u6E05\u5C71",
age: 20
};
say(person);

bundle

默认build只会打包入口文件,不会将导入的文件与包一起打包,需要显示传递bundle: true才能打包所有入口文件即子文件导入的文件

1
2
3
4
5
const options: BuildOptions = {
// ...省略部分
bundle: true,
}

1
2
3
4
5
6
7
8
9
10
11
12
// src/utils/say.ts
function say(words) {
console.log(words);
}

// src/main.ts
var person = {
name: "\u6E05\u5C71",
age: 20
};
say(person);

banner与footer

bannerfooter分别在相应类型的文件开头与结尾插入代码

1
2
3
4
5
6
7
8
9
const options: BuildOptions = {
// ...省略部分
banner: {
js: '#!/usr/bin/env node'
},
footer: {
js: '// @link https://esbuild.github.io/api/#footer'
}
}

编译结果如下

dist/esm/main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env node

// src/utils/say.ts
function say(words) {
console.log(words);
}

// src/main.ts
var person = {
name: "\u6E05\u5C71",
age: 20
};
say(person);
/** @link https://esbuild.github.io/api/#footer */

打包结果设置

  • format: 设置产物格式"iife" | "cjs" | "esm"
  • charset: 设置产物字符编码
  • minify: 压缩产物
  • spitting: 自动拆包
  • treeShaking: 是否使用树摇
  • sourcemap: 是否使用源映射文件

plugins

使用插件以拓展esbuild打包能力或优化打包效果esbuild - Plugins,下面使用插件打包Vue3

1
2
pnpm i vue
pnpm i esbuild-plugin-vue-next -D
1
2
3
4
5
6
7
8
9
10
11
import pluginVue from 'esbuild-plugin-vue-next'

const options: BuildOptions = {
absWorkingDir: process.cwd(),
entryPoints: ['./src/main.ts'],
plugins: [pluginVue()],
outdir: './dist/esm',
format: 'esm',
bundle: true,
minify: true,
}

src/main.ts

1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

src/App.vue

1
2
3
<template>
<h1>Hello World!</h1>
</template>

define

该配置可以对代码中匹配的字符串进行替换,如Vue3打包后浏览器控制台会提示建议替换__VUE_OPTIONS_API__,__VUE_PROD_DEVTOOLS__以提供这两个常量

Feature flags __VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__ are not explicitly defined. You are running the esm-bundler build of Vue, which expects these compile-time feature flags to be globally injected via the bundler config in order to get better tree-shaking in the production bundle.

For more details, see https://link.vuejs.org/feature-flags.

1
2
3
4
5
6
7
const options: BuildOptions = {
// ...省略部分
define: {
__VUE_OPTIONS_API__: 'false',
__VUE_PROD_DEVTOOLS__: 'false',
},
}

API Context

contextAPI接受同buildAPI相同的参数

1
2
3
4
import { context, BuildOptions } from 'esbuild'

const options: BuildOptions = {}
const ctx = await context(options)

自定义插件

esbuild插件为一个对象拥有namesetup属性

1
2
3
4
5
6
7
8
9
10
11
const pluginTmp = {
name: 'tmp',
setup(build: PluginBuild) {
console.log(build)
}
}

const options: BuildOptions: {
// ...
plugins: [pluginTmp]
}

可以将插件用函数返回这样就可以对插件传入一些设置

1
2
3
4
5
6
7
function pluginTmp(lang: 'en-US' | 'zh-CN') {
// ...
return {
name: 'tmp'
setup(build: PluginBuild) {}
}
}

参考文档:

《Esbuild Docs》


Esbuild打包
https://qingshaner.com/Esbuild打包/
作者
清山
发布于
2023年2月5日
许可协议