如何實現一個 esbuild 插件?從入門到上手,沒有比這更簡單的了!

前言

隨着 Vite 2.0 的發佈,其底層的設計也不斷地被大家所認知。其中,大家十分津津樂道的就是採用 esbuild 來做 Dev 環境下的代碼轉換(快到飛起 😲)。

與此同時,這也給 esbuild 帶來了很多曝光。並且,esbuild 生態也陸續出現了一些插件(Plugin),例如 esbuild-plugin-alias、esbuild-plugin-webpack-bridge 等。

那麼,回到正題,今天我將和大家一起從 esbuild 插件基礎知識出發,手把手教學如何實現一個 esbuild 插件 🚀!

1 認識 esbuild 插件基礎

在 esbuild 中,插件被設計爲一個函數,該函數需要返回一個對象(Object),對象中包含 namesetup 等 2 個屬性:

const myPlugin = options ={
  return {
    name: "my",
    setup(build) {
      // ....
    }
  }
}

其中,name 的值是一個字符串,它表示你的插件名稱 。setup 的值是一個函數,它會被傳入一個參數 build(對象)。

build 對象上會暴露整個構建過程中非常重要的 2 個函數:onResolveonLoad,它們都需要傳入 Options(選項)和 CallBack(回調)等 2 個參數。

其中,Options 是一個對象,它包含 filter(必須)和 namespace 等 2 個屬性:

interface OnResolveOptions {
  filter: RegExp;
  namespace?: string;
}

而 CallBack 是一個函數,即回調函數。插件實現的關鍵則是在 onResolveonLoad 中定義的回調函數內部做一些特殊的處理。

那麼,接下來我們先來認識一下 Options 的 2 個屬性:namespacefilter(劃重點,它們非常重要 😲)

1.1 namespace

默認情況下,esbuild 是在文件系統上的文件(File Modules)相對應的 namespace 中運行的,即此時 namespace 的值爲 file

esbuild 的插件可以創建 Virtual Modules,而 Virtual Modules 則會使用 namespace 來和 File Modules 做區分。

注意,每個 namespace 都是特定於該插件的。

並且,這個時候,我想可能有同學會問:什麼是 Virtual Modules 😲?

簡單地理解,Virtual Modules 是指在文件系統中不存在的模塊,往往需要我們構造出 Virtual Modules 對應的模塊內容。

1.2 filter

filter 作爲 Options 上必須的屬性,它的值是一個正則。

它主要用於匹配指定規則的導入(import)路徑的模塊,避免執行不需要的回調,從而提高整體打包性能。

那麼,在認識完 namespacefilter 後。下面我們來分別認識一下 onResolveonLoad 中的回調函數。

1.3 onResolve 的回調函數

onResolve 函數的回調函數會在 esbuild 構建每個模塊的導入路徑(可匹配的)時執行。

onResolve 函數的回調函數需要返回一個對象,其中會包含 pathnamespaceexternal 等屬性。

通常,該回調函數會用於自定義 esbuild 處理 path 的方式,例如:

1.4 onLoad 的回調函數

onLoad 函數的回調函數會在 esbuild 解析模塊之前調用,主要是用於處理並返回模塊的內容,並告知 esbuild 要如何解析它們。

並且,需要注意的是 onLoad 的回調函數不會處理被標記爲 external 的模塊。

onLoad 函數的回調函數需要返回一個對象,該對象總共有 9 個屬性。這裏我們來認識一下較爲常見 3 個屬性:

那麼,到這裏我們就已經簡單認識完有關 esbuild 插件的基礎知識了 😎。

所以,下面我們從實際應用場景出發,動手實現一個 esbuild 插件。

2 動手實現一個 esbuild 插件

這裏我們來實現一個刪除代碼中 console 語句的 esbuild 插件。

因爲,這個過程需要識別和刪除 console 對應的 AST 節點。所以,需要使用 babel 提供的 3 個工具包:

那麼,首先是創建整個插件的整體結構,如插件名稱、setup 函數:

module.exports = options ={
  return {
    name: "auto-delete-console",
    setup(build) {
    }
  }
}

其次,由於我們這個插件主要是對代碼內容進行操作。所以,需要使用 onLoad 函數,並且要聲明 filter/\.js$/,即只匹配 JavaScript 文件:

module.exports = options ={
  return {
    name: "auto-delete-console",
    setup(build) {
      build.onLoad({ filter: /\.js$/ }(args) ={
      }
    }
  }
}

而在 onLoad 函數的回調函數中,我們需要做這 4 件事:

1. 獲取文件內容

onLoad 函數的回調函數會傳入一個參數 args,它會包含此時模塊的文件路徑,即 args.path

所以,這裏我們使用 fs.promises.readFile 函數來讀取該模塊的內容:

build.onLoad({ filter: /\.js$/ }, async (args) ={
  const source = await fs.promises.readFile(args.path, "utf8")
}

2. 轉化代碼生成 AST

因爲,之後我們需要找到並刪除 console 對應的 AST 節點。所以,需要使用 @babel/parserparse 函數將模塊的內容(代碼)轉爲 AST:

build.onLoad({ filter: /\.js$/ }, async (args) ={
  const ast = parser.parse(source)
}

3. 遍歷 AST 節點,刪除 console 對應的 AST 節點

接着,我們需要使用 @babel/traverse 來遍歷 AST 來找到 console 的 AST 節點。

但是,需要注意的是我們並不能直接就可以找到 console 的 AST 節點。

因爲,console 屬於普通的函數調用,並沒有像 await 一樣有特殊的 AST 節點類型(AwaitExpression)。

不過,我們可以先使用 CallExpression 來直接訪問函數調用的 AST 節點。

然後,判斷 AST 節點的 callee.object.name 是否等於 console,是則調用 path.remove 函數刪除該 AST 節點:

build.onLoad({ filter: /\.js$/ }, async (args) ={
  traverse(ast, {
    CallExpression(path) {
      //...
      const memberExpression = path.node.callee
      if (memberExpression.object && memberExpression.object.name === 'console') {
        path.remove()
      }
    }
  })
}

4. 轉化 AST 生成代碼

最後,我們需要使用 @babel/coretransformFromAst 函數將處理過的 AST 轉爲代碼並返回:

build.onLoad({ filter: /\.js$/ }, async (args) ={
  //...
  const { code } = core.transformFromAst(ast)
  return {
    contents: code,
    loader: "js"
  }
}

那麼,到這裏我們就完成了一個刪除代碼中 console 語句的 esbuild 插件,用一句話概括這個過程:“沒有比這個更簡單的了 😃”。

整個插件實現的全部代碼如下(注意引入上述 babel 工具包):

module.exports = options ={
  return {
    name: "auto-delete-console",
    setup(build) {
      build.onLoad({ filter: /\.js$/ }, async (args) ={
        const source = await fs.promises.readFile(args.path, "utf8")
        const ast = parser.parse(source)

        traverse(ast, {
          CallExpression(path) {
            const memberExpression = path.node.callee
            if (memberExpression.object && memberExpression.object.name === 'console') {
              path.remove()
            }
          }
        })

        const { code } = core.transformFromAst(ast)
        return {
          contents: code,
          loader: "js"
        }
      }
    }
  }
}

結語

總體上來說,esbuild 的插件設計是十分簡約和強大的。這一點,如果寫過 webpack 插件的同學對比一下,我想應該深有體會 😶。

並且,通過閱讀本文,相信大家都可以隨手甩出一個 esbuild 的插件,很可能將來官方提供的插件列表就有你實現的插件 😎。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1_fCkMYUGKlNWOdWNnjQdg