博客 数栈干货放送!babel-plugin-import最全源码详解(二)

数栈干货放送!babel-plugin-import最全源码详解(二)

   小美   发表于 2023-01-19 11:25  233  0

三、了如指掌

在 step3 中会进行按需加载转换最后的两个步骤:

  1. 引入 import 绑定的引用肯定不止 JSX 语法,还有其他诸如,三元表达式,类的继承,运算,判断语句,返回语法等等类型,我们都得对他们进行处理,确保所有的引用都绑定到最新的 import,这也会导致importMethod 函数被重新调用,但我们肯定不希望 import 函数被引用了 n 次,生成 n 个新的 import 语句,因此才会有先前的判断语句。
  2. 一开始进入 ImportDeclaration 收集信息的时候我们只是对其进行了依赖收集工作,并没有删除节点。并且我们尚未补充 Program 节点 exit 所做的 action

接下来将以此列举需要处理的所有 AST 节点,并且会给每一个节点对应的接口(Interface)与例子(不关注语义):

MemberExpression

MemberExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
const pluginState = this.getPluginState(state);
if (!node.object || !node.object.name) return;
if (pluginState.libraryObjs[node.object.name]) {
// antd.Button -> _Button
path.replaceWith(this.importMethod(node.property.name, file, pluginState));
} else if (pluginState.specified[node.object.name] && path.scope.hasBinding(node.object.name)) {
const { scope } = path.scope.getBinding(node.object.name);
// 全局变量处理
if (scope.path.parent.type === 'File') {
node.object = this.importMethod(pluginState.specified[node.object.name], file, pluginState);
}
}
}

MemberExpression(属性成员表达式),接口如下

interface MemberExpression {
type: 'MemberExpression';
computed: boolean;
object: Expression;
property: Expression;
}
/**
* 处理类似:
* console.log(lodash.fill())
* antd.Button
*/

如果插件的选项中没有关闭 transformToDefaultImport ,这里会调用 importMethod 方法并返回@babel/helper-module-imports 给予的新节点值。否则会判断当前值是否是收集到 import 信息中的一部分以及是否是文件作用域下的全局变量,通过获取作用域查看其父节点的类型是否是 File,即可避免错误的替换其他同名变量,比如闭包场景。

VariableDeclarator

VariableDeclarator(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, 'init', path, state);
}

VariableDeclarator(变量声明),非常方便理解处理场景,主要处理 const/let/var 声明语句

interface VariableDeclaration : Declaration {
type: "VariableDeclaration";
declarations: [ VariableDeclarator ];
kind: "var" | "let" | "const";
}
/**
* 处理类似:
* const foo = antd
*/

本例中出现 buildDeclaratorHandler 方法,主要确保传递的属性是基础的 Identifier 类型且是 import 绑定的引用后便进入 importMethod 进行转换后返回新节点覆盖原属性。

buildDeclaratorHandler(node, prop, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
if (!types.isIdentifier(node[prop])) return;
if (
pluginState.specified[node[prop].name] &&
path.scope.hasBinding(node[prop].name) &&
path.scope.getBinding(node[prop].name).path.type === 'ImportSpecifier'
) {
node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState);
}
}

ArrayExpression

ArrayExpression(path, state) {
const { node } = path;
const props = node.elements.map((_, index) => index);
this.buildExpressionHandler(node.elements, props, path, state);
}

ArrayExpression(数组表达式),接口如下所示

interface ArrayExpression {
type: 'ArrayExpression';
elements: ArrayExpressionElement[];
}
/**
* 处理类似:
* [Button, Select, Input]
*/

本例的处理和刚才的其他节点不太一样,因为数组的 Element 本身就是一个数组形式,并且我们需要转换的引用都是数组元素,因此这里传递的 props 就是类似 [0, 1, 2, 3] 的纯数组,方便后续从 elements 中进行取数据。这里进行具体转换的方法是 buildExpressionHandler,在后续的 AST 节点处理中将会频繁出现

buildExpressionHandler(node, props, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
props.forEach(prop => {
if (!types.isIdentifier(node[prop])) return;
if (
pluginState.specified[node[prop].name] &&
types.isImportSpecifier(path.scope.getBinding(node[prop].name).path)
) {
node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState);
}
});
}

首先对 props 进行遍历,同样确保传递的属性是基础的 Identifier 类型且是 import 绑定的引用后便进入 importMethod 进行转换,和之前的 buildDeclaratorHandler 方法差不多,只是 props 是数组形式

LogicalExpression

LogicalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['left', 'right'], path, state);
}

LogicalExpression(逻辑运算符表达式)

interface LogicalExpression {
type: 'LogicalExpression';
operator: '||' | '&&';
left: Expression;
right: Expression;
}
/**
* 处理类似:
* antd && 1
*/

主要取出逻辑运算符表达式的左右两边的变量,并使用 buildExpressionHandler 方法进行转换

ConditionalExpression

ConditionalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test', 'consequent', 'alternate'], path, state);
}

ConditionalExpression(条件运算符)

interface ConditionalExpression {
type: 'ConditionalExpression';
test: Expression;
consequent: Expression;
alternate: Expression;
}
/**
* 处理类似:
* antd ? antd.Button : antd.Select;
*/

主要取出类似三元表达式的元素,同用 buildExpressionHandler 方法进行转换。

IfStatement

IfStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test'], path, state);
this.buildExpressionHandler(node.test, ['left', 'right'], path, state);
}

IfStatement(if 语句)

interface IfStatement {
type: 'IfStatement';
test: Expression;
consequent: Statement;
alternate?: Statement;
}
/**
* 处理类似:
* if(antd){ }
*/

这个节点相对比较特殊,但笔者不明白为什么要调用两次 buildExpressionHandler ,因为笔者所想到的可能性,都有其他的 AST 入口可以处理。望知晓的读者可进行科普。

ExpressionStatement

ExpressionStatement(path, state) {
const { node } = path;
const { types } = this;
if (types.isAssignmentExpression(node.expression)) {
this.buildExpressionHandler(node.expression, ['right'], path, state);
}
}

ExpressionStatement(表达式语句)

interface ExpressionStatement {
type: 'ExpressionStatement';
expression: Expression;
directive?: string;
}
/**
* 处理类似:
* module.export = antd
*/

ReturnStatement

ReturnStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['argument'], path, state);
}

ReturnStatement(return 语句)

interface ReturnStatement {
type: 'ReturnStatement';
argument: Expression | null;
}
/**
* 处理类似:
* return lodash
*/

ExportDefaultDeclaration

ExportDefaultDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['declaration'], path, state);
}

ExportDefaultDeclaration(导出默认模块)

interface ExportDefaultDeclaration {
type: 'ExportDefaultDeclaration';
declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
}
/**
* 处理类似:
* return lodash
*/

BinaryExpression

BinaryExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['left', 'right'], path, state);
}

BinaryExpression(二元操作符表达式)

interface BinaryExpression {
type: 'BinaryExpression';
operator: BinaryOperator;
left: Expression;
right: Expression;
}
/**
* 处理类似:
* antd > 1
*/

NewExpression

NewExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['callee', 'arguments'], path, state);
}

NewExpression(new 表达式)

interface NewExpression {
type: 'NewExpression';
callee: Expression;
arguments: ArgumentListElement[];
}
/**
* 处理类似:
* new Antd()
*/

ClassDeclaration

ClassDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['superClass'], path, state);
}

ClassDeclaration(类声明)

interface ClassDeclaration {
type: 'ClassDeclaration';
id: Identifier | null;
superClass: Identifier | null;
body: ClassBody;
}
/**
* 处理类似:
* class emaple extends Antd {...}
*/

Property

Property(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, ['value'], path, state);
}

Property(对象的属性值)

/**
* 处理类似:
*
const a={
* button:antd.Button
*
}
*/

处理完 AST 节点后,删除掉原本的 import 导入,由于我们已经把旧 import 的 path 保存在 pluginState.pathsToRemove 中,最佳的删除的时机便是 ProgramExit ,使用 path.remove() 删除。

ProgramExit(path, state) {
this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
}

恭喜各位坚持看到现在的读者,已经到最后一步啦,把我们所处理的所有 AST 节点类型注册到观察者中

export default function({ types }) {
let plugins = null;
function applyInstance(method, args, context) { ... }
const Program = { ... }

// 补充注册 AST type 的数组
const methods = [
'ImportDeclaration'
'CallExpression',
'MemberExpression',
'Property',
'VariableDeclarator',
'ArrayExpression',
'LogicalExpression',
'ConditionalExpression',
'IfStatement',
'ExpressionStatement',
'ReturnStatement',
'ExportDefaultDeclaration',
'BinaryExpression',
'NewExpression',
'ClassDeclaration',
]

const ret = {
visitor: { Program },
};

for (const method of methods) { ... }

}

到此已经完整分析完 babel-plugin-import 的整个流程,读者可以重新捋一捋处理按需加载的整个处理思路,其实抛去细节,主体逻辑还是比较简单明了的。

四、一些思考

笔者在进行源码与单元测试的阅读后,发现插件并没有对 Switch 节点进行转换,遂向官方仓库提了 PR,目前已经被合入 master 分支,读者有任何想法,欢迎在评论区畅所欲言。 笔者主要补了 SwitchStatement ,SwitchCase 与两个 AST 节点处理。

SwitchStatement

SwitchStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['discriminant'], path, state);
}

SwitchCase

SwitchCase(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test'], path, state);
}

五、小小总结

这是笔者第一次写源码解析的文章,也因笔者能力有限,如果有些逻辑阐述的不够清晰,或者在解读过程中有错误的,欢迎读者在评论区给出建议或进行纠错。

现在 babel 其实也出了一些 API 可以更加简化 babel-plugin-import 的代码或者逻辑,例如:path.replaceWithMultiple ,但源码中一些看似多余的逻辑一定是有对应的场景,所以才会被加以保留。

此插件经受住了时间的考验,同时对有需要开发 babel-plugin 的读者来说,也是一个非常好的事例。不仅如此,对于功能的边缘化处理以及操作系统的兼容等细节都有做完善的处理。

如果仅仅需要使用babel-plugin-import ,此文展示了一些在 babel-plugin-import 文档中未暴露的API,也可以帮助插件使用者实现更多扩展功能,因此笔者推出了此文,希望能帮助到各位同学。


想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=bbs

同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:https://github.com/DTStack

0条评论
社区公告
  • 大数据领域最专业的产品&技术交流社区,专注于探讨与分享大数据领域有趣又火热的信息,专业又专注的数据人园地

最新活动更多
微信扫码获取数字化转型资料
钉钉扫码加入技术交流群