前言
关注「Vite」底层实现的同学,我想应该清楚它使用「esbuild」来实现对 .ts
、jsx
、.js
代码的转化。当然,在「Vite」之前更早使用「esbuild」的就是「Snowpack」。不过,相比较「Vite」拥有的巨大社区,显然「Snowpack」的关注度较小。
「Vite」的核心是基于浏览器原生的 ES Module
。但是,相比较传统的打包工具和开发工具而言,它做出了很多改变,采用「esbuild」来支持 .ts
、jsx
、.js
代码的转化就是其中之一。
那么,接下来我们就步入今天的正题,What is esbuild, and how to use it?
1 什么是 esbuild
「esbuild」官方的介绍:它是一个「JavaScript」Bundler
打包和压缩工具,它可以将「JavaScript」和「TypeScript」代码打包分发在网页上运行。
目前「esbuild」支持的功能:
- 加载器
- 压缩
- 打包
- Tree shaking
- Source map 生成
- 将 JSX 和较新的 JS 语法移植到 ES6
- ...
这里,我们列出了几点常关注的,至于其他,有兴趣的同学可以移步官方文档自行了解。
目前对于「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
、Service
。下面,我们从源码定义的角度来认识一下它们。
3.1 transform
transform
可以用于转化 .js
、.tsx
、ts
等文件,然后输出为旧的语法的 .js
文件,它提供了两个参数:
- 第一个参数(必填,字符串),指需要转化的代码(模块内容)。
- 第二个参数(可选),指转化需要的选项,如源文件路径
sourcefile
、需要加载的loader
,其中loader
的定义:
type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary';
transform
会返回一个 Promise
,对应的 TransformResult
为一个对象,它会包含转化后的旧的 js
代码、sourceMap
映射、警告信息:
interface TransformResult {
js: string;
jsSourceMap: string;
warnings: Message[];
}
3.2 build
build
实现了 transform
的能力,即代码转化,并且它还会将转换后的代码压缩并生成 .js
文件到指定 output
目录。build
只提供了一个参数(对象),来指定需要转化的入口文件、输出文件、loader
等选项:
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
:
interface BuildResult {
warnings: Message[];
outputFiles?: OutputFile[];
}
但是,需要注意的是outputFiles
只有在write
为false
的情况下才会输出,它是一个Uint8Array
。
3.3 buildSync
buidSync
顾名思义,相比较 build
而言,它是同步的构建方式,即如果使用 build
我们需要借助 async await
来实现同步调用,而使用 buildSync
可以直接实现同步调用。
3.4 Service
Service
的出现是为了解决调用上述 API 时都会创建一个子进行来完成的问题,如果存在多次调用 API 的情况出现,那么就会出现性能上的浪费,这一点在文档中也有讲解。
所以,使用了 Service
来实现代码的转化或打包,则会创建一个长期的用于共享的子进程,避免了性能上的浪费。而在「Vite」中也正是使用 Service
的方式来进行 .ts
、.js
、.jsx
代码的转化工作。
Service
定义:
interface Service {
build(options: BuildOptions): Promise<BuildResult>;
transform(input: string, options?: TransformOptions): Promise<TransformResult>;
stop(): void;
}
可以看到,Service
的本质封装了 build
、transform
、stop
函数,只是不同于单独调用它们,Service
底层的实现是一个长期存在可供共享的子进程。
但是,在实际使用上,我们并不是直接使用 Service
创建实例,而是通过 startService
来创建一个 Service
实例:
const {
startService,
build,
} = require("esbuild")
const service = await startService()
try {
const res = await service.build({
entryPoints: ["./src/main.js"],
write: false
})
console.log(res)
} finally {
service.stop()
}
并且,在使用 stop
的时候需要注意,它会结束这个子进程,这也意味着任何在此时处于 pending
的 Promise
也会被终止。
4 实现一个小而美的 Bundler 打包
在简单地认识「esbuild」,我们就来实现一个小而美的 Bunder
打包:
1.初始化项目和安装「esbuild」:
mkdir esbuild-bundler & npm init -y & npm i esbuild
2.目录结构:
|——— src
|—— main.js #项目入口文件
|——— index.js #bundler实现核心文件
3.index.js
:
(async () => {
const {
startService,
build,
} = require("esbuild")
const service = await startService()
try {
const res = await service.build({
entryPoints: ["./src/main.js"],
outfile: './dist/main.js',
minify: true,
bundle: true,
})
} finally {
service.stop()
}
})()
4.运行一下 node index
即可体验一下闪电般的 bundler
打包!
写在最后
想必看完这篇文章,大家对「esbuild」应该建立起一个基础的认知。并且,文中的源码只是基于「Go」实现的底层能力上的,而真正的底层实现还是得看「Go」是如何实现的,由于脱离了大家熟知的前端,所以就不做介绍。那么,在一下篇文章中,我将会讲解在「Vite」的源码设计中是怎么使用 esbuild
来实现 .ts
、jsx
、.js
语法解析,以及我们如何自定义 plugin
来实现一些代码转化。最后,文章中如果存在表述不当的地方,欢迎各位同学提 Issue。
❤️爱心三连击
通过阅读,如果你觉得有收获的话,可以爱心三连击!!!
前端问路人 —— 五柳(微信公众号: Code center)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。