天下武功,唯快不破~
前言
关注 Vite
底层实现的同学,我想应该清楚它使用esbuild来实现对 .ts
、jsx
、.js
代码的转化。当然,在 Vite
之前更早使用 esbuild
的就是Snowpack。不过,相比较 Vite
拥有的巨大社区,显然Snowpack
的关注度较小。
Vite
的核心是基于浏览器原生的 ES Module
。但是,相比较传统的打包工具和开发工具而言,它做出了很多改变,采用 esbuild
来支持 .ts
、jsx
、.js
代码的转化就是其中之一。
1 什么是 esbuild
esbuild官方的介绍:它是一个JavaScript Bundler
打包和压缩工具,它可以将JavaScript
和TypeScript
代码打包分发在网页上运行。
目前 esbuild
支持的功能:
- 支持 js、ts、jsx、css、json、文本、图片等资源
- 增量更新
Source map
生成- 开发服务器支持
- 代码打包和压缩
- Code split
- Tree shaking
- 插件支持
- ...
这里,我们列出了几点常关注的,至于其他,有兴趣的同学可以移步官方文档自行了解。
目前对于JavaScript
语法转化不支持的特性有:
Top-level await
async await
BigInt
Hashbang
语法
需要注意的是对于不支持转化的语法会原样输出。
2 对比现有的打包工具
esbuild
的作者对比目前现阶段类似的工具做了基准测试。最后的结果是:
对于这些基准测试,esbuild 比其他
JavaScript
打包程序 快至少100
倍。
100 倍,可以说快到飞起了...而 esbuild
快的原因,这里我分两个层面解释:
2.1 官方解释
- 它是用
Go
语言编写的,该语言可以编译为本地代码。 - 解析,生成最终文件和生成 source maps 全部完全并行化。
- 无需昂贵的数据转换,只需很少的几步即可完成所有操作。
- 该库以提高编译速度为编写代码时的第一原则,并尽量避免不必要的内存分配。
2.2 语言层面解释
- 现阶段的类似工具,底层的实现都是基于
JavaScript
,其受限于本身是一门动态解释型的语言,每次执行都需要先由解释器一边将源码翻译成机器语言,一边穿插调度执行,并不能充分利用 CPU。 - 「Chrome V8」引擎虽然对
JavaScript
的运行做了优化,引进「JIT」(即时编译)的机制,但是部分代码实现机器码与esbuild
全部实现机器码的形式,性能上的差距不可弥补。
当然,语言层面仅仅是官方解释中的一点的展开,其他解释有时间等后续分析其源码实现后讲解。
3 esbuild API 详解
虽然, esbuild
早已开源和使用,但是官方文档只是简单介绍了如何使用,而对于 API 介绍部分是欠缺的,建议读者自己去阅读源码中的定义。
esbuild
总共提供了八个函数:transform
、build
、buildSync
、formatMessages
、formatMessagesSync
、initialize
、serve
、transform
、transformSync
。
我们可以打印出来看看
js
const esbuild = require("esbuild");
console.log("build esbuild", esbuild);
下面挑几个主要的方法,其他方案的定义可以自行前往官方文档了解,从源码定义的角度来认识一下它们。
3.1 transform
transform
可以用于转化 .js
、.tsx
、ts
等文件,然后输出为旧的语法的 .js
文件,它提供了两个参数:
- 第一个参数(必填,字符串),指需要转化的代码(模块内容)。
- 第二个参数(可选),指转化需要的选项,如源文件路径
sourcefile
、需要加载的loader
,其中loader
的定义:
ini
type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary';
transform
会返回一个 Promise
,对应的 TransformResult
为一个对象,它会包含转化后的旧的 js
代码、sourceMap
映射、警告信息:
ini
interface TransformResult {
js: string;
jsSourceMap: string;
warnings: Message[];
}
3.2 build
build
实现了 transform
的能力,即代码转化,并且它还会将转换后的代码压缩并生成 .js
文件到指定 output
目录。build
只提供了一个参数(对象),来指定需要转化的入口文件、输出文件、loader
等选项:
js
interface BuildOptions extends CommonOptions {
bundle?: boolean;
splitting?: boolean;
outfile?: string;
metafile?: string;
outdir?: string;
platform?: Platform;
color?: boolean;
external?: string[];
loader?: { [ext: string]: Loader };
resolveExtensions?: string[];
mainFields?: string[];
write?: boolean;
tsconfig?: string;
outExtension?: { [ext: string]: string };
entryPoints?: string[];
stdin?: StdinOptions;
}
build
函数调用会输出 BuildResult
,它包含了生成的文件 outputFiles
和提示信息 warnings
:
js
interface BuildResult {
warnings: Message[];
outputFiles?: OutputFile[];
}
但是,需要注意的是
outputFiles
只有在write
为false
的情况下才会输出,它是一个Uint8Array
。
3.3 buildSync
buidSync
顾名思义,相比较 build
而言,它是同步的构建方式,即如果使用 build
我们需要借助 async await
来实现同步调用,而使用 buildSync
可以直接实现同步调用。
4 实现一个小而美的 Bundler 打包
在简单地认识 esbuild
,我们就来实现一个小而美的 Bunder
打包:
4.1 初始化项目和安装 esbuild
:
bash
mkdir esbuild-bundler && cd ./esbuild-bundler && npm init -y && npm i esbuild lodash-es
4.2 目录结构:
bash
|--------- src
|------ main.js #项目入口文件
|--------- index.js #bundler实现核心文件
|--------- package.json #npm包配置
4.3 index.js
:
js
const buildHandler = async () => {
const esbuild = require("esbuild");
try {
const result = await esbuild.build({
entryPoints: ["./src/main.js"],
outfile: "./dist/main.js",
minify: true, // 是否压缩代码
bundle: true, // 是否打包成代码块
});
console.log("build result", result);
} catch (error) {
console.error(`esbuild build error`, error);
} finally {
console.log('esbuild finally');
}
};
buildHandler();
4.4 ./src/main.js
:
js
import { cloneDeep } from 'lodash-es';
const obj = {
name: 'james',
like: {
drink: 'pure water',
game: 'wangzhe'
},
age: 18
};
const obj2 = cloneDeep(obj);
obj.age = 20;
obj.like.drink = 'Cola';
console.log('obj', obj);
console.log('obj2', obj2);
4.5 package.json
:
json
{
"name": "esbuild-bundler",
"version": "1.0.0",
"description": "",
"main": "./src/main.js",
"scripts": {
"build": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"esbuild": "^0.12.15",
"lodash-es": "^4.17.21"
}
}
4.6 运行看结果
执行一下 npm run build
, 看下 闪电般的 bundler
打包结果!
打包后的代码如下:
执行看下输出结果:
总结
现在大家对 esbuild
应该可以建立起一个基础的认知。不过按照目前的社区使用情况来看,esbuild
暂时不可能替代 webpack、parcel
等构建工具,因为他们做的事情是不太一样,插件体系没有前面几个老大哥那么丰富,它更适合作为一种偏底层的模块打包工具,然后需要在它的基础上进行二次封装,扩展出一套既能兼顾性能,又有完善工程化能力的工具链,例如 Snowpack
, Vite
, SvelteKit
, Remix Run
等,而且,不过基于编译性能层面来说,esbuild
还是遥遥领先的,可以说是完全为性能而定制的。