5

前言

image
关注「Vite」底层实现的同学,我想应该清楚它使用「esbuild」来实现对 .tsjsx.js 代码的转化。当然,在「Vite」之前更早使用「esbuild」的就是「Snowpack」。不过,相比较「Vite」拥有的巨大社区,显然「Snowpack」的关注度较小。

「Vite」的核心是基于浏览器原生的 ES Module。但是,相比较传统的打包工具和开发工具而言,它做出了很多改变,采用「esbuild」来支持 .tsjsx.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」总共提供了四个函数:transformbuildbuildSyncService。下面,我们从源码定义的角度来认识一下它们。

3.1 transform

transform 可以用于转化 .js.tsxts 等文件,然后输出为旧的语法的 .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 只有在 writefalse 的情况下才会输出,它是一个 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 的本质封装了 buildtransformstop 函数,只是不同于单独调用它们,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 的时候需要注意,它会结束这个子进程,这也意味着任何在此时处于 pendingPromise 也会被终止。

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 来实现 .tsjsx.js 语法解析,以及我们如何自定义 plugin 来实现一些代码转化。最后,文章中如果存在表述不当的地方,欢迎各位同学提 Issue。

❤️爱心三连击

通过阅读,如果你觉得有收获的话,可以爱心三连击!!!

前端问路人 —— 五柳(微信公众号: Code center)

五柳
1.1k 声望1.4k 粉丝

你好,我是五柳,希望能带给大家一些别样的知识和生活感悟,春华秋实,年年长茂。