Open WJCHumble opened 3 years ago
随着 Vite 2.0 的发布,其底层的设计也不断地被大家所认知。其中,大家十分津津乐道的就是采用 esbuild 来做 Dev 环境下的代码转换(快到飞起 😲)。
与此同时,这也给 esbuild 带来了很多曝光。并且,esbuild 生态也陆续出现了一些新的插件(Plugin),例如 esbuild-plugin-alias、esbuild-plugin-webpack-bridge 等。
那么,回到正题,今天我将和大家一起从 esbuild 插件基础知识出发,手把手教学如何实现一个 esbuild 插件 🚀!
在 esbuild 中,插件被设计为一个函数,该函数需要返回一个对象(Object),对象中包含 name 和 setup 等 2 个属性:
Object
name
setup
const myPlugin = options => { return { name: "my", setup(build) { // .... } } }
其中,name 的值是一个字符串,它表示你的插件名称 。 setup 的值是一个函数,它会被传入一个参数 build(对象)。
build
build 对象上会暴露整个构建过程中非常重要的 2 个函数:onResolve 和 onLoad,它们都需要传入 Options(选项)和 CallBack(回调)等 2 个参数。
onResolve
onLoad
其中,Options 是一个对象,它包含 filter(必须)和 namespace 等 2 个属性:
filter
namespace
interface OnResolveOptions { filter: RegExp; namespace?: string; }
而 CallBack 是一个函数,即回调函数。插件实现的关键则是在 onResolve 和 onLoad 中定义的回调函数内部做一些特殊的处理。
那么,接下来我们先来认识一下 Options 的 2 个属性:namespace 和 filter(划重点,它们非常重要 😲)
默认情况下,esbuild 是在文件系统上的文件(File Modules)相对应的 namespace 中运行的,即此时 namespace 的值为 file。
file
esbuild 的插件可以创建 Virtual Modules,而 Virtual Modules 则会使用 namespace 来和 File Modules 做区分。
注意,每个 namespace 都是特定于该插件的。
并且,这个时候,我想可能有同学会问:什么是 Virtual Modules 😲?
简单地理解,Virtual Modules 是指在文件系统中不存在的模块,往往需要我们构造出 Virtual Modules 对应的模块内容。
filter 作为 Options 上必须的属性,它的值是一个正则。它主要用于匹配指定规则的导入(import)路径的模块,避免执行不需要的回调,从而提高整体打包性能。
import
那么,在认识完 namespace 和 filter 后。下面我们来分别认识一下 onResolve 和 onLoad 中的回调函数。
onResolve 函数的回调函数会在 esbuild 构建每个模块的导入路径(可匹配的)时执行。
onResolve 函数的回调函数需要返回一个对象,其中会包含 path、namespace、external 等属性。
path
external
通常,该回调函数会用于自定义 esbuild 处理 path 的方式,例如:
重写原本的路径,例如重定向到其他路径
将该路径所对应的模块标记为 external,即不会对改文件进行构建操作(原样输出)
onLoad 函数的回调函数会在 esbuild 解析模块之前调用,主要是用于处理并返回模块的内容,并告知 esbuild 要如何解析它们。并且,需要注意的是 onLoad 的回调函数不会处理被标记为 external 的模块。
onLoad 函数的回调函数需要返回一个对象,该对象总共有 9 个属性。这里我们来认识一下较为常见 3 个属性:
contents
loader
js
css
resolveDir
那么,到这里我们就已经简单认识完有关 esbuild 插件的基础知识了 😎。 所以,下面我们从实际应用场景出发,动手实现一个 esbuild 插件。
这里我们来实现一个删除代码中 console 语句的 esbuild 插件。因为,这个过程需要识别和删除 console 对应的 AST 节点。所以,需要使用 babel 提供的 3 个工具包:
console
babel
@babel/parser
parse
@babel/traverse
@babel/core
transformFromAst
那么,首先是创建整个插件的整体结构,如插件名称、setup 函数:
module.exports = options => { return { name: "auto-delete-console", setup(build) { } } }
其次,由于我们这个插件主要是对代码内容进行操作。所以,需要使用 onLoad 函数,并且要声明 filter 为 /\.js$/,即只匹配 JavaScript 文件:
/\.js$/
module.exports = options => { return { name: "auto-delete-console", setup(build) { build.onLoad({ filter: /\.js$/ }, (args) => { } } } }
而在 onLoad 函数的回调函数中,我们需要做这 4 件事:
1.获取文件内容
onLoad 函数的回调函数会传入一个参数 args,它会包含此时模块的文件路径,即 args.path。
args
args.path
所以,这里我们使用 fs.promises.readFile 函数来读取该模块的内容:
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)。
await
AwaitExpression
不过,我们可以先使用 CallExpression 来直接访问函数调用的 AST 节点。然后,判断 AST 节点的 callee.object.name 是否等于 console,是则调用 path.remove 函数删除该 AST 节点:
CallExpression
callee.object.name
path.remove
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 插件,用一句话概括这个过程:“没有比这更简单的了 😃”。
整个插件实现的全部代码如下:
const parser = require("@babel/parser") const traverse = require("@babel/traverse").default const core = require("@babel/core") const esbuild = require("esbuild") const fs = require("fs") 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 的插件,很可能将来官方提供的插件列表就有你实现的插件 😎。
前言
随着 Vite 2.0 的发布,其底层的设计也不断地被大家所认知。其中,大家十分津津乐道的就是采用 esbuild 来做 Dev 环境下的代码转换(快到飞起 😲)。
与此同时,这也给 esbuild 带来了很多曝光。并且,esbuild 生态也陆续出现了一些新的插件(Plugin),例如 esbuild-plugin-alias、esbuild-plugin-webpack-bridge 等。
那么,回到正题,今天我将和大家一起从 esbuild 插件基础知识出发,手把手教学如何实现一个 esbuild 插件 🚀!
1 认识 esbuild 插件基础
在 esbuild 中,插件被设计为一个函数,该函数需要返回一个对象(
Object
),对象中包含name
和setup
等 2 个属性:其中,
name
的值是一个字符串,它表示你的插件名称 。setup
的值是一个函数,它会被传入一个参数build
(对象)。build
对象上会暴露整个构建过程中非常重要的 2 个函数:onResolve
和onLoad
,它们都需要传入 Options(选项)和 CallBack(回调)等 2 个参数。其中,Options 是一个对象,它包含
filter
(必须)和namespace
等 2 个属性:而 CallBack 是一个函数,即回调函数。插件实现的关键则是在
onResolve
和onLoad
中定义的回调函数内部做一些特殊的处理。那么,接下来我们先来认识一下 Options 的 2 个属性:
namespace
和filter
(划重点,它们非常重要 😲)1.1 namespace
默认情况下,esbuild 是在文件系统上的文件(File Modules)相对应的
namespace
中运行的,即此时namespace
的值为file
。esbuild 的插件可以创建 Virtual Modules,而 Virtual Modules 则会使用
namespace
来和 File Modules 做区分。并且,这个时候,我想可能有同学会问:什么是 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
函数:其次,由于我们这个插件主要是对代码内容进行操作。所以,需要使用
onLoad
函数,并且要声明filter
为/\.js$/
,即只匹配 JavaScript 文件:而在
onLoad
函数的回调函数中,我们需要做这 4 件事:1.获取文件内容
onLoad
函数的回调函数会传入一个参数args
,它会包含此时模块的文件路径,即args.path
。所以,这里我们使用
fs.promises.readFile
函数来读取该模块的内容:2.转化代码生成 AST
因为,之后我们需要找到并删除
console
对应的 AST 节点。所以,需要使用@babel/parser
的parse
函数将模块的内容(代码)转为 AST: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 节点:4.转化 AST 生成代码
最后,我们需要使用
@babel/core
的transformFromAst
函数将处理过的 AST 转为代码并返回:那么,到这里我们就完成了一个删除代码中
console
语句的 esbuild 插件,用一句话概括这个过程:“没有比这更简单的了 😃”。整个插件实现的全部代码如下:
结语
总体上来说,esbuild 的插件设计是十分简约和强大的。这一点,如果写过 webpack 插件的同学对比一下,我想应该深有体会 😶。
并且,通过阅读本文,相信大家都可以随手甩出一个 esbuild 的插件,很可能将来官方提供的插件列表就有你实现的插件 😎。