天天看點

下一個時代的打包工具 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)

繼續閱讀