天天看点

下一个时代的打包工具 esbuild

前言

关注「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

    的定义:

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)

继续阅读