How Does babel-plugin-import Work
Babel 的处理流程
作为一个transpiler
,Babel
处理的事情比较简单,就是将各种形式的源代码翻译成目标环境的代码,整个过程主要由三个部分组成,如下图所示:
Parse
将源代码转换为Abstract Syntax Tree(AST),Babel 相关的AST Node Types详见spec,这个过程会涉及到@babel/plugin-syntax-*
相关的插件,并且这个过程不支持自定义插件,只能通过配置来增减需要的官方插件。整个 Parse 的过程我们基本上不需要做什么,Babel 会把最终的 AST 返回给我们以便进行后续的遍历和转换操作。
Transform
一般我们处理 AST 的做法就是对 AST 进行遍历,Babel 官方也提供了相关的工具@babel/traverse
,它接受第一步 Parse 产生的 AST,然后通过@babel/plugin-transform-*
和@babel/plugin-proposal-*
相关插件,定义不同AST Node Types的 visitor 来进行 AST 的处理和转换。对于比较复杂的场景,Babel 也提供了相应的@babel/helper-*
来帮助处理和转换。对于AST Node Types的类型判断以及增加和改变,Babel 也专门提供了@babel/types
。这个流程我们可以通过自定义插件来控制。
Generate
将第二步转换之后的 AST 传入@babel/generator
,生成目标环境的代码。
babel-plugin-import
解析
babel-plugin-import
的主要作用是避免antd
等包的无关模块引入的问题,可以减少最终 build 结果的大小,原理是将代码做如下转换:
1 | import { Button } from 'antd'; |
babel-plugin-import
的源码也比较少,我们主要聚焦在Plugin.js
上
ImportDeclaration
babel-plugin-import
主要是对import { Button } from 'antd';
这样的形式进行转换,那么首先我们需要关注的就是对import
的处理,我们先去spec查询到关于ImportDeclaration
的作用和定义:
An import declaration, e.g., import foo from “mod”;.
1 | interface ImportDeclaration <: ModuleDeclaration { |
然后看一下源码对这块的处理:
1 | ImportDeclaration(path, state) { |
那么这里的主要作用是找到和传入的配置项libraryName
相等的值,然后遍历相关node.specifiers
并分别存入 import 的组件到specified
和libraryObjs
以便后续使用,其中specified
存储的是类似于import { Button } from 'antd'
这样的ImportSpecifier
,libraryObjs
存储的是类似于import antd from 'antd
这样的ImportDefaultSpecifier
和类似于import * as antd from 'antd
这样的ImportNamespaceSpecifier
,这两个的导入方式都是将整个包全部导入。且将当前path
存入pathsToRemove
数组,在最终Program.exit()
阶段删除,也就是把示例中import { Button } from 'antd';
这行代码进行删除操作,为什么删除,之后会讲到
CallExpression
将import { Button } from 'antd';
转换为var _button = require('antd/lib/button');
之后对应的变量名称也随之改变,那么我们就需要关心调用这个步骤了,首先在 spec 中查询到关于CallExpression
的作用和定义:
A function or method call expression.
1 | interface CallExpression <: Expression { |
然后看一下源码对这一块的处理:
1 | CallExpression(path, state) { |
这段的主要作用是找到第一步存入specified
的组件名称,并进行相应的替换,替换逻辑在importMethod
中
importMethod
这一步是进行转换的核心步骤,使用了@babel/helper-module-imports
,该 helper 比较重要的方法示例如下:
1 | import { addNamed } from '@babel/helper-module-imports'; |
调用addNamed(path, 'named', 'source');
会在 AST 中增加import { named as _named } from "source
对应的结构,并在最终 generate 阶段生成该代码,并且返回相应的_named
对应的Identifier
,addSideEffect
和addDefault
的作用和addNamed
类似。
知道了这些,我们来看一下importMethod
的内部逻辑:
1 | importMethod(methodName, file, pluginState) { |
主要做了以下几点内容:
- 检测
pluginState.selectedMethods
是否存在methodName
,不存在则在之后的逻辑中生成methodName
对应的结果,存在则直接返回 - 依据传入的
camel2DashComponentName
,style
等配置项,生成相应的 path 等,并调用addNamed(file.path, methodName, path);
等生成对应的import { named as _named } from "source
类似的结构,该过程在整个 traverse 过程只执行一次
MemberExpression
在某些情况下,我们会使用import antd from 'antd
和import * as antd from 'antd
来引入antd
等包来使用,并在后续的使用中通过antd.Button
这样的形式来使用,这种情况babel-plugin-import
也考虑到了,依然是通过 spec 查询:
A member expression. If computed is true, the node corresponds to a computed (a[b]) member expression and property is an Expression. If computed is false, the node corresponds to a static (a.b) member expression and property is an Identifier or a PrivateName
1 | interface MemberExpression <: Expression, Pattern { |
再看一下这块的处理逻辑:
1 | MemberExpression(path, state) { |
主要做了如下两点内容:
- 类似于
import * as antd from 'antd
全包引入的方式,直接调用replaceWith
替换antd.Button
为_Button
- 处理
import { message } from 'antd'
并且调用message
的情况,将message
替换为目标Identifier
buildExpressionHandler & buildDeclaratorHandler
对于逻辑判断和赋值等操作,babel-plugin-import
也进行了处理,主要的逻辑都封装在了buildExpressionHandler
和buildDeclaratorHandler
内,这两个函数的方法基本上都是判断并调用importMethod
改变Identifier
内容,这里不再赘述。
总结
babel 在整体架构上采用微内核的形式,将主要功能封装在@babel/parser
,@babel/traverse
,@babel/generator
几个包内,其它次要功能通过引入各种插件实现,整体架构较为灵活,可以应对不断变化的tc39提案更新。同时基于这样的架构,用户也可以参与并控制相关流程,促进了相关生态。但是架构灵活所带来的问题往往是核心功能往往较为难以理解,同时官方文档对这一块的部分描述语焉不详,这就更导致了开发 babel plugin 的难度增加。希望通过本篇文章,能加深读者对 babel 周边生态的认识。