如何實現一個 esbuild 插件?從入門到上手,沒有比這更簡單的了!
前言
隨着 Vite 2.0 的發佈,其底層的設計也不斷地被大家所認知。其中,大家十分津津樂道的就是採用 esbuild 來做 Dev 環境下的代碼轉換(快到飛起 😲)。
與此同時,這也給 esbuild 帶來了很多曝光。並且,esbuild 生態也陸續出現了一些插件(Plugin),例如 esbuild-plugin-alias、esbuild-plugin-webpack-bridge 等。
那麼,回到正題,今天我將和大家一起從 esbuild 插件基礎知識出發,手把手教學如何實現一個 esbuild 插件 🚀!
1 認識 esbuild 插件基礎
在 esbuild 中,插件被設計爲一個函數,該函數需要返回一個對象(Object
),對象中包含 name
和 setup
等 2 個屬性:
const myPlugin = options => {
return {
name: "my",
setup(build) {
// ....
}
}
}
其中,name
的值是一個字符串,它表示你的插件名稱 。setup
的值是一個函數,它會被傳入一個參數 build
(對象)。
build
對象上會暴露整個構建過程中非常重要的 2 個函數:onResolve
和 onLoad
,它們都需要傳入 Options(選項)和 CallBack(回調)等 2 個參數。
其中,Options 是一個對象,它包含 filter
(必須)和 namespace
等 2 個屬性:
interface OnResolveOptions {
filter: RegExp;
namespace?: string;
}
而 CallBack 是一個函數,即回調函數。插件實現的關鍵則是在 onResolve
和 onLoad
中定義的回調函數內部做一些特殊的處理。
那麼,接下來我們先來認識一下 Options 的 2 個屬性:namespace
和 filter
(劃重點,它們非常重要 😲)
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
)路徑的模塊,避免執行不需要的回調,從而提高整體打包性能。
那麼,在認識完 namespace
和 filter
後。下面我們來分別認識一下 onResolve
和 onLoad
中的回調函數。
1.3 onResolve 的回調函數
onResolve
函數的回調函數會在 esbuild 構建每個模塊的導入路徑(可匹配的)時執行。
onResolve
函數的回調函數需要返回一個對象,其中會包含 path
、namespace
、external
等屬性。
通常,該回調函數會用於自定義 esbuild 處理 path
的方式,例如:
-
重寫原本的路徑,例如重定向到其他路徑
-
將該路徑所對應的模塊標記爲
external
,即不會對改文件進行構建操作(原樣輸出)
1.4 onLoad 的回調函數
onLoad
函數的回調函數會在 esbuild 解析模塊之前調用,主要是用於處理並返回模塊的內容,並告知 esbuild 要如何解析它們。
並且,需要注意的是 onLoad
的回調函數不會處理被標記爲 external
的模塊。
onLoad
函數的回調函數需要返回一個對象,該對象總共有 9 個屬性。這裏我們來認識一下較爲常見 3 個屬性:
-
contents
處理過的模塊內容 -
loader
告知 esbuild 要如何解釋該內容(默認爲js
)。例如,返回的模塊內容是 CSS,則聲明loader
爲css
-
resolveDir
是在將導入路徑解析爲文件系統上實際路徑時,要使用的文件系統目錄
那麼,到這裏我們就已經簡單認識完有關 esbuild 插件的基礎知識了 😎。
所以,下面我們從實際應用場景出發,動手實現一個 esbuild 插件。
2 動手實現一個 esbuild 插件
這裏我們來實現一個刪除代碼中 console
語句的 esbuild 插件。
因爲,這個過程需要識別和刪除 console
對應的 AST 節點。所以,需要使用 babel
提供的 3 個工具包:
-
@babel/parser
的parse
函數解析代碼生成 AST(抽象語法樹) -
@babel/traverse
遍歷 AST,訪問需要進行操作的 AST 節點 -
@babel/core
的transformFromAst
函數將 AST 轉化爲代碼
那麼,首先是創建整個插件的整體結構,如插件名稱、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/parser
的 parse
函數將模塊的內容(代碼)轉爲 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/core
的 transformFromAst
函數將處理過的 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