Identifier square Entered!
Identifier square Exited!
Identifier n Entered!
Identifier n Exited!
Identifier n Entered!
Identifier n Exited!
Identifier n Entered!
Identifier n Exited!
var hasBarProperty = foo.hasOwnProperty("bar");
var isPrototypeOfBar = foo.isPrototypeOf(bar);
var barIsEnumerable = foo.propertyIsEnumerable("bar");
转换成这种写法:
var hasBarProperty = Object.prototype.hasOwnProperty.call(foo, "bar");
var isPrototypeOfBar = Object.prototype.isPrototypeOf.call(foo, bar);
var barIsEnumerable = Object.prototype.propertyIsEnumerable.call(foo, "bar");
var hasBarProperty = Object.prototype.hasOwnProperty.call(foo, "bar");
var isPrototypeOfBar = foo.isPrototypeOf(bar);
var barIsEnumerable = Object.prototype.propertyIsEnumerable.call(foo, "bar");
if (Object.prototype.hasOwnProperty.call(foo, "bar")) {}
if (foo.isPrototypeOf(bar)) {}
if (Object.prototype.propertyIsEnumerable.call(foo, "bar")) {}
Babel的三个主要处理步骤分别是:解析(parse)、转换(transform)、生成(generate)。
解析
解析步骤主要是接受源代码并输出抽象语法树(AST)。此步骤主要由
@babel/parser
(原Babylon
)负责解析和理解js代码,输出对应的AST。转换
转换步骤主要是接受AST,并对其进行遍历,在此过程中会进行分析和修改AST,这也是Babel插件主要工作的地方。此步骤主要用到
@babel/traverse
和@babel/types
两个包。生成
生成步骤主要是将(经过一系列转换之后的)AST再转换为正常的字符串代码。此步骤主要由
@babel/generator
深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。抽象语法树(AST)
学过《编译原理》的童鞋应该都知道AST,即使不知道也没关系,我们可以通过
astexplorer
在线查看。如上所示。
这段代码可以表示成如下所示的一棵树:
这个AST中的每一层结构叫做
节点(node)
,一个AST可以由单一的节点或是成百上千个节点构成。 它们组合在一起可以描述用于静态分析的程序语法。每一个节点都有如下所示的接口(Interface):
字符串形式的 type 字段表示节点的类型(如: "FunctionDeclaration","Identifier",或 "BinaryExpression")。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。
Babel插件就是对这些节点进行添加、更新和删除。
路径(Path)
AST能够表示语法的结构,但是我们对节点进行操作时,更多的是希望获得节点之间的联系。
Path是表示两个节点之间连接的对象。
例如,如果有下面这样一个节点及其子节点︰
将子节点
Identifier
表示为一个路径(Path)的话,看起来是这样的:同时它还包含关于该路径的其他元数据:
在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式
Reactive
表示。路径对象还包含添加、更新、移动和删除节点有关的其他很多方法,当你调用一个修改树的方法后,路径信息也会被更新。@babel/traverse
这个独立的包对AST进行遍历,解析出整个树的path
,并更新节点。访问者(visitor)
@babel/traverse
遍历AST时,会依次进入每个节点。假设有如下AST结构:
则遍历过程如下:
FunctionDeclaration
Identifier (id)
dentifier (id)
Identifier (params[0])
Identifier (params[0])
BlockStatement (body)
ReturnStatement (body)
BinaryExpression (argument)
Identifier (left)
Identifier (left)
Identifier (right)
Identifier (right)
BinaryExpression (argument)
ReturnStatement (body)
BlockStatement (body)
FunctionDeclaration
当我们说进入某一个节点,实际上是说我们在访问他们。
访问者简单的说就是一个对象,它定义了在树状机构的遍历中,如何获取节点的方法。
这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个
Identifier
的时候会调用Identifier()
方法。所以在下面的代码中
Identifier()
方法会被调用四次(包括square
在内,总共有四个Identifier
)。这些调用都发生在进入节点时,不过有时候我们也可以在退出时调用访问者方法。
因此,对于一个具体的节点我们有两次访问的机会。
当你有一个
Identifier()
成员方法的访问者时,你实际上是在访问路径而非节点。通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。输出结果如下:
初窥插件
从上面的示例我们已经知道如何访问节点,现在我们可以操作节点。
输出结果:
这便是Babel插件的基本运行原理。
@Babel/types
工具库@Babel/Types
模块是一个用于 AST 节点的Lodash
式工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。这里使用
isIdentifier(path.node, { name: "n" })
验证值为n
的Identifier
节点。然后使用identifier('x')
创建一个值为x
的节点进行替换。输出结果一致。
@Babel/Types
还提供了多种节点类型的构造、验证方法(eg:binaryExpression
、returnStatement
、classDeclaration
),详细请查阅文档。判断节点类型时,在类型名称前加
is
,然后将类型名称第一个字母变成大写,例如isIdentifier
,该方法还有另外一个版本assetIdentifier
(抛出异常,而不是返回true 与 false)。创建一个类型节点时(用于插入AST中,或者替换AST中的节点),直接调用节点类型函数,类型名称第一个字母小写,例如
identifier('x')
。转换操作函数
Path
对象提供了添加、更新、移动和删除等一系列节点操作方法。get
:获取子节点属性isReferencedIdentifier
:检查标示符(Identifier)是否被引用findParent
:向父路径搜索节点find
:向父节点搜索节点,并且搜索本节点getFunctionParent
:查找最接近的父函数或程序getStatementParent
:向上遍历语法树,直到找到在列表中的父节点路径inList
:判断路径是否有同级节点getSibling
:获得同级路径key
:获取路径所在容器的索引container
:获取路径的容器(包含所有同级节点的数组)skip
:停止子节点的遍历stop
:停止整个路径的遍历replaceWith
:替换节点replaceWithMultiple
:用多节点替换单节点replaceWithSourceString
:用字符串源码替换节点insertBefore
:在当前节点之前插入兄弟节点insertAfter
:在当前节点之前插入兄弟节点unshiftContainer
:在节点容器的开始插入节点pushContainer
:在节点容器的结尾插入节点remove
:删除节点(自身)...
更多函数与使用方法参考
babel-handbook
。第一个Babel插件
插件分析
假设我想把所有的如下代码:
转换成这种写法:
这里主要目的是将
Object.prototype
上的方法调用,改成Object.prototype.propertyIsEnumerable.call()
形式。这里以转换
hasOwnProperty
为例,先在astexplorer
观察待处理AST(foo.hasOwnProperty("bar")
)与目标AST(Object.prototype.hasOwnProperty.call(foo, "bar")
)结构。待处理的
foo.hasOwnProperty("bar")
的AST大致如下:期望转换为
Object.prototype.hasOwnProperty.call(foo, "bar")
的目标AST大致如下:根据如上两个AST,转换的大致思路为:
CallExpression
访问者CallExpression.callee
,该节点为MemberExpression
类型,代表属性调用表达式(即foo.hasOwnProperty
部分)。CallExpression.arguments
,该节点是一个数组,代表参数部分(即"bar"
部分)。callee.object
为Identifier
类型节点,并且callee.property
为值为hasOwnProperty
的Identifier
节点时,则进行转换。Object.prototype.hasOwnProperty.call
形式的MemberExpression
嵌套节点。MemberExpression
节点,即Object.prototype
部分,以此类推,构造完整的Object.prototype.hasOwnProperty.call
。CallExpression
节点,其中新的参数为原CallExpression.callee.object
的值,与原参数组成的数组。CallExpression
节点替换原来的节点。编写插件
新建如下结构项目:
分别有如下代码:
在使用插件之前需要先安装依赖:
执行如下命令,使用
babel-plugin-transform-object-prototype-methods
插件转换代码:查看输出文件
output.js
:源代码已经正确转换。
从上可知:
babel
对象作为参数。babel.types
可直接使用@babel/types
模块,在插件中不用单独引入。.babelrc
配置文件,或者命令行--plugins
参数,或者babel.transform
的plugins
选项加载。完善插件功能
下面来完善插件功能,让该插件可以处理
hasOwnProperty
、isPrototypeOf
、propertyIsEnumerable
三种类型。现在以下代码均可正确转换:
插件选项
下面我们让该插件可以接受插件选项,并根据选项开启或禁用转换:
修改插件:
现在插件选项已经可以工作了,转换结果如下:
恭喜你,你已经开始你的大佬(装逼)之路了。