前言
esbuild 是近些天来非常火的一个全新的工具,年前霸屏掘金前端板块,被称作第三代构建方案,它最与众不同之处就是它独特的使用 go 语言构建。得益于此,它的打包速度非常的惊人。
构建十次 threejs 副本竟然只需要 0.37s,它被作为 vite 的打包工具的构建底层方案从而一战成名。
令人遗憾的是 esbuild 本身是不支持 babel 插件,所以本次文章,将带大家编写一个插件,该插件对标 babel-plugin-import,将赋予 esbuild 一个 babel 中非常通用的插件功能——动态引入。
why
动态 import 的想法其实非常简单,即通过改变 ast 的方式将代码强行改为仅引入单文件,即:
import { Button, Select } from 'antd'
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
import Button from 'antd/lib/button'
import Select from 'antd/lib/select'
为什么要这样做?
我们知道,引入模块时,如果引入形如 import { Button } from 'antd'
是会将包全部模块都引进来的。
我们在看一个库时,首先要读这个库的 pacakge.json
文件:
其中的 main
和 module
字段标记着用户在使用 import 语句是具体引入的文件是哪个,main
字段代表 commonjs 引入形式默认查找的文件,module
字段为 es 形式默认查找的文件。这是 commonjs 时期遗留下的规定,因此你在使用不带 module
字段的库时,形如 import { Button } from 'antd'
总是会引入 main
所标记的文件,丢失了 esm 的 tree-shaking 特性。
在本例中我们需要查看 main
中指向的文件,即 lib/index.js
。
这是一个 commonjs 形式的文件,你会发现,所有的组件都通过 require
的形式引入,我们知道 commonjs 的 require
是动态依赖的,这意味着只要引入了 import { Button } from 'antd'
(即便你只使用 Button 组件),就会将全量组件引入,导致打包结果增大。
尝试仅引入 antd 的 Button 组件,将 antd 的 package.json 中的 module
字段删除后打包:
然后再用动态引入的方式:
OMG!竟足足少了 80%!
我们通过 import 指定路径的文件,绕过了 lib/index.js
文件,自然也就不会将所有组件打包。社区中一般将此行为称作「动态引入」(dynamic import) 或者「按需引入」(import on demand)。
how
在最流行的 webpack 中我们可以利用 babel-plugin-import 插件来解决这一问题。该插件由 antd 团队开发,周均下载超 30 万次,可谓是前端界的超级网红,为的正是解决这一情形:
// .babelrc
"plugins": [
["import", { "libraryName": "antd" }]
]
如此设置,babel 仅针对 antd 库做按需引入,且引入形式默认是 <libraryName>/lib/<componentName>
。但其他的类库路径不一定如此,比如 lodash,lodash 的函数路径就是 lodash/<functionName>
的,因此它允许你配置 libraryDirectory
更改引入包路径:
// .babelrc
"plugins": [
["import", { "libraryName": "lodash", libraryDirectory: '' }]
]
看来 babel-plugin-import 早有准备。继续翻看文档我们会看到如下配置项,能够覆盖所有的引入情形:
配置项 | 意义 |
---|---|
libraryName | 包名 |
libraryDirectory | 模块引入路径 |
styleLibraryDirectory | 样式引入路径 |
camel2DashComponentName | 是否将 camelCase 转为 kebabCase,默认为 true |
style | 是否默认引入样式,支持函数形式 |
customName | 改变模块引用路径的函数 |
customStyleName | 改变样式引入路径的函数 |
那么,让我们开始 esbuild 插件的开发,首先我们要了解一下 esbuild 的插件机制。
esbuild 插件机制
esbuild 插件的机制相比 webpack 可谓是极简,对程序仅有 onStart、onEnd 两个环节,对文件仅影响 onResolve、onLoad 两个环节。
esbuild 的规范要求插件必须是有 name、setup 的对象,其中 setup 是一个接收 build 流程的函数,你可以在 build 上注册周期钩子,在 build 流程开始时会统一挂载,并在相应环节触发。
const plugin = {
name: "myPlugin",
setup: (build) => {
build.onLoad({}, (args) => {
const contents = fs.readFileSync(args.path, "utf-8");
console.log("content: ", contents);
return {
contents: contents + "\n",
};
});
},
};
具体的周期钩子的接受值和返回值可以参阅 esbuild 官方 using plugins 一栏。
开始开发
我们可以借助最近非常火的一个 AST 分析框架 gogocode 的能力,快速将我们的想法变现。
我们要干的事情事实上就两种情况:
情况一:ImportDefaultSpecifier
类型
import Antd from "antd";
const Button = Antd.Button;
const { Select } = Antd;
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
import Button from "antd/lib/button";
import Select from "antd/lib/select";
ReactDOM.render(
<>
<Button />
<Select />
<Antd.Table />
</>,
document.querySelector("#root")
);
情况二:ImportSepecifier
类型
import { Button, Select } from 'antd'
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
import Button from 'antd/lib/button'
import Select from 'antd/lib/select'
ReactDOM.render(
<>
<Button />
<Select />
</>,
document.querySelector('#root')
)
情况二处理起来相对比较简单,我们分析它所有的 ImportDeclaration
的 ImportSepecifier
,只要分析到了,就向原 ast 下方 push 几个 DefaultImportSepecifier
,并将原 ImportDeclaration
删除。
……:省略号用以省略与本需求无关的 ast 声明
需要注意的是这个声明有一特殊情况:import { Button as MyButton } from 'antd'
,我们需要记录它的「引入名称」(importSepecifier.imported
)和「开发者想要使用的新名称」(importSepecifier.local
),在替换时将其赋值即可。
对于情况一,我们又可以分成三种情况:
- 直接使用:
<Antd.Select />
- 间接引用:
const Button = Antd.Button
- 间接的列表引用:
const { Button: MyButton, Select } = Antd
对于后两者,我们得在脑内将 VariableDeclaration
转变为 ImportDeclaration
。
由于 gogocode 使用的是直接匹配的模式,所以可以选择性地跳过无关环节,非常方便。
上代码!
回过头来看看 getUsedComponents
干了什么:
就是这样喵!
测试用例
对于测试用例,babel-plugin-import
已经帮我们写好了,我们直接使用即可。
esbuild 处理文件使用何种 loader 与 webpack 不同,它是直接根据后缀名来判断的,所以我们要将所有用到 js 的文件后缀名改成 jsx。例如:
然后,让我们写好测试逻辑:
跑下试试:
芜湖!比预计的还要顺利,还就那个全部通过!
这个库已经发布到了 npm,名字叫 esbuild-dynamic-import-plugin,并且已经加入到 awesome esbuild 全家桶。在 awesome esbuild 中还有大量插件没有被覆盖,欢迎大家使用 gogocode 进行开发!
写在后面
我们在《webpack、babel、vite、rollup 中完美融入 gogocode》的那次文章中,对 webpack 的插件系统做了一个小剖析,它的确更可控,但是实在太很重了:webpack 竟然有 28 个生命周期钩子,而 esbuild 只有 4 个;webpack 的 tapable 的规范容易让开发者陷入无所适从的境地,esbuild 就相对来说清爽得很。
其次 gogocode 匹配和替换 ast 的方案非常的高效,能够节省大量的代码,比如刚刚我们要匹配 ImportDeclaration
如果使用 babel 的 visitor 模式,那将至少编写十倍不止的代码,而 gogocode 只有一行简单的字符串就解决了。让开发着把注意力放在对代码转换的思路上,不仅节省时间,还节省了程序猿的掉的头发。真正做到了以「猿」(¿) 为本,赞一个!
为什么会变成这样呢~!第一次写 esbuild 构建框架的插件;第一次用 gogocode 搞出如此复杂的插件。这两件愉快的事情交织在了一起。本应该是双倍的快乐,但为什么会这样呢~!(指变成了四倍快乐)
GoGoCode 相关链接
GoGoCode的Github仓库(新项目求star ^_^) github.com/thx/gogocod…
GoGoCode的官网 gogocode.io/
可以来 playground 快速体验一下 play.gogocode.io/
阿里妈妈出的新工具,给批量修改项目代码减轻了痛苦
「GoGoCode 实战」一口气学会 30 个 AST 代码替换小诀窍
0成本上手AST,用GoGoCode解决Vue2迁移Vue3难题
GoGoCode协助清理代码中的「垃圾」
本文作者:冰块