接上一篇《深入了解babel(一)》Babel的处理步骤Babel的三个主要处理步骤是:parse、transform、generate。分别对应babel-core源码中使用的babylon、babel-traverse、babel-generator。(1)BabylonBabylon是Babel的解析器。最初是从Acorn项目派生出来的。Acorn非常快速且易于使用。import*asbabylonfrom"babylon";constcode=`functionsquare(n){returnn*n;}`;babylon.parse(code);//节点{//type:"File",//start:0,//end:38,//loc:SourceLocation{...},//program:Node{...},//comments:[],//tokens:[...]//}(2)babel-traverseBabelTraverse(遍历)模块维护着整棵树的状态,负责替换、移除、添加节点。我们可以将它与Babylon一起使用来迭代和更新节点。从“巴比伦”导入*作为巴比伦;从“babel-traverse”导入遍历;constcode=`functionsquare(n){returnn*n;}`;constast=babylon.parse(code);traverse(ast,{enter(path){if(path.node.type==="标识符"&&path.node.name==="n"){path.node.name="x";}}});(3)babel-generatorBabelGenerator模块是Babel的代码生成器,它读取AST并将其转换为代码和源码映射import*asbabylonfrom"babylon";importgeneratefrom"babel-generator";constcode=`functionsquare(n){returnn*n;}`;constast=babylon.parse(code);generate(ast,{},code);//{//代码:"...",//map:"..."//}抽象语法树(AST)ast抽象语法树在上面三个神器中都出现过,所以ast对于编译器来说非常重要。下面列出一些AST应用:浏览器会将js源代码通过解析器转化为抽象语法树,然后进一步转化为字节码或者直接生成机器码JSLint和JSHint来检查代码错误或样式,发现一些潜在的错误的IDE错误提示、格式化、高亮、自动补全等UglifyJS代码打包工具webpack、rollupCoffeeScript、TypeScript、JSX等转为原生Javascript自行编写插件presetspresets是一系列插件的集合-ins,presets减少了babelrc配置文件的大小,不会看到一大堆插件,保证每个用户配置的插件列表完全一样,所以插件对于babel来说非常重要。前端开发者如何开发自定义插件?决定了以后对代码编译的控制程度,而babel插件就像一把手术刀,准确可靠的修改js源码。我在写习题和写插件的过程中主要使用了以下两种方法:astexplorer在IDE中基于babel-core编写代码,引用babel-core模块进行编码,如下:const{transform,generate}=require('babel-core');constmyPlugin=require('./myPlugin');constcode=`d=a+b+c`;vares5Code=transform(code,{plugins:[myPlugin]})console.log(es5Code.code);astexplorer个人比较喜欢babel插件的在线写法。可以实时看到编译结果和对应的AST部分。结合babel-types,可以快速编写出手术刀式的插件。下图是astexplorer解析的json:写插件的第一站——知乎pathexport默认函数(babel){const{types:t}=babel;return{name:"可选的插件名称",visitor:{VariableDeclaration(path,state){console.log(path);}},};}每一个插件都必须返回一个带有visitor字段的对象,visitor对象存储你的遍历方法。我得出的结论是相当于上面astexplorer截图中的type属性(例如:VariableDeclaration),遍历方式是指插件让ast中的节点按照你写的遍历方式函数进入遍历法。遍历方式就像js中的addeventlistener,可以重复写多个监听函数,所以当多个插件叠在一起的时候,会发生一些意想不到的事情。这就是考验你的插件编写是否安全可靠。这也是最难的部分。举个简单的例子,如何删除代码中的所有控制台?leta=33;console.log(12121212);varb;console.warn(12121212);aaaa,ccccconsole.error(12121212);dd=0;letc;导出默认函数({types:t}){return{name:"删除所有控制台",visitor:{CallExpression(path,state){if(path.get('callee').isMemberExpression()){if(path.get('callee').get('object').isIdentifier()){if(path.get('callee').get('object').get('name').node=='console')path.remove()}}}},};CallExpression遍历方法是console.log(...)对应的AST类型属性。进入CallExpression函数后,我们可以得到path和state这两个参数。前端思想可以理解为一个dom节点,可以向上也可以向下查找。当前节点路径包含了很多信息,方便我们写插件,状态包含了插件的选项和数据。options是当babelrc中的插件被引入到plug-in时,状态可以接收添加的options。刚开始写插件的时候,直接把node里面的信息作为DOM节点获取是非常危险的(看了babel的多个插件也知道了),每次取一条信息,只好判断类型是否和我们的ast树一样,这样其他情况就可以去掉了。例如,其他CallExpressions也转到此函数,但它可能没有被调用者或对象。这里执行代码会出现错误或者误伤。严格控制节点的获取过程会帮我们省去很多不必要的麻烦。代码中获取callee节点的方式有两种,一种是path.node.callee,另一种是path.get('callee')。我个人更喜欢后者,因为可以直接调用方法(比如isMemberExpression),否则你必须这样判断t.isMemberExpression(path.node.callee),不够优雅。当我们有条件判断当前节点是console时,可以直接使用remove方法删除ast节点。编译后的代码:leta=33;varb;aaaa,ccccdd=0;letc;babel官方发布了一个delete控制台插件,可以对比一下,发现思路和步骤基本一致,和babel官方开发的比较全面,综合考虑了另外两种情况。插件编写的第二站——作用域函数的影响a(n){n*n}letn=1考虑如何将函数中的n重写为_n?导出默认函数({types:t}){letparamsName='';return{name:"underlinetheparametersinfunction",visitor:{FunctionDeclaration(path){if(!path.get('params').length||!path.get('params')[0])return;paramsName=path.get('params')[0].get('name').node;path.traverse({Identifier(path){if(path.get('name').node==paramsName)path.replaceWith(t.Identifier('_'+paramsName));}});},}};}按照第一个例子的思路,我们可以很方便的把n改成_n,但是此时letn=1function外面也会被改写,所以我们在FunctionDeclaration方法中调用path.traverse,而method将需要遍历的标识符包裹在里面,这样可以保护外部代码的安全。这种方式保证了插件编写的安全性。插件编写第三站——bindingsconstaaaa=1;constbb=4;functionb(){letaaaa=2;aaaa=3;}aaaa=34;我们再举一个例子,如何把const改成var,对const声明的值进行只读保护呢?exportdefaultfunction(babel,options){return{name:"constpolyfill",visitor:{变量声明(路径){如果(path.get('kind').node!='const')返回;path.node.kind='var';},ExpressionStatement(path){if(!path.get('expression').isAssignmentExpression())return;让nodeleft=path.get('expression').get('left');如果(!nodeleft.isIdentifier())返回;if(path.scope.bindings[nodeleft.get('name').node].kind=='const')console.error('Assignmenttoconstantvariable');}},};}在VariableDeclaration方法中将const改为let,通过ExpressionStatement方法观察const变量是否被修改,因为function有自己的作用域,所以aaaa可以重新声明和修改。这里使用了bindings属性。您可以检查节点的变量声明类型。当发现kind是const时,会发出错误警告。此示例适用于绑定的一个应用程序插件编写第四站——创建节点当我们替换一个节点或者将一个节点插入到容器中时,我们需要根据节点的构造规则来创建它。下面的例子是将n*n修改为n+100functionsquare(n){returnn*n;}先给出答案,代码如下:exportdefaultfunction({types:t}){return{name:“将n*n更改为n+100”,访问者:{BinaryExpression(path){path.replaceWith(t.binaryExpression('+',path.node.left,t.Identifier('100')));路径.停止();}},};}现在我们要给BinaryExpression类型的节点来替换它,必须按照BinaryExpression节点的规则创建。可以参考babel-types网站的文档:我们需要构建三种类型的节点:operator、left、right,然后查看ast中对这三种节点的描述就OK了,left和right都是Identifier类型,运算符是一个字符串。string可以直接写“+”来替换,Identifier类型节点的创建依赖babel-types给的文档:我们只需要给输出string类型的名字就可以了,这样就可以成功创建我们自己的节点。总结一下,astexplorer确实是一个不错的网站,而且可以在插件中写console,在console中可以实时看到console结果,对我们了解astnode很有帮助。另外,上面的插件例子还是太多了,写插件要注意的远不止这些,只是没时间拿出那么多例子好好介绍一下,所以你可以直接阅读这篇文档来获得更深入的理解。
