名词解释
AST:Abstract Syntax Tree, 抽象语法树
DI: Dependency Injection, 依赖注入
===============================================================
Babel使用的引擎是babylon,babylon并非由babel团队自己开发的,而是fork的acorn项目,acorn的项目本人在很早之前在兴趣部落1.0在构建中使用,为了是做一些代码的转换,是很不错的一款引擎,不过acorn引擎只提供基本的解析ast的能力,遍历还需要配套的acorn-travesal, 替换节点需要使用acorn-,而这些开发,在Babel的插件体系开发下,变得一体化了
Babel会将源码转换AST之后,通过便利AST树,对树做一些修改,然后再将AST转成code,即成源码。
上面提到Babel是fork acon项目,我们先来看一个来自兴趣部落项目的,简单的ACON示例
解决的问题
将
1 2 3 4 | Model.task('getData', function($scope, dbService){ }); |
转换成
1 2 3 4 | Model.task('getData', ['$scope', 'dbService', function($scope, dbService){ }]); |
熟悉angular的同学都能看到这段代码做的是对DI的自动提取功能,使用ACON手动撸代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | var code = 'let a = 1; // ....'; var acorn = require("acorn"); var traverse = require("ast-traverse"); var alter = require("alter"); var ast = acorn.parse(code); var ctx = []; traverse(ast, { pre: function(node, parent, prop, idx){ if(node.type === "MemberExpression") { var object = node.object; var objectName = object.name; var property = node.property; var propertyName = property.name; // 这里要进行替换 if (objectName === "Model" && (propertyName === "service" || propertyName === "task")) { // 第一个就为serviceName 第二个是function var arg = parent.arguments; var serviceName = arg[0]; var serviceFunc = arg[1]; for (var i = 0; i < arg.length; i++) { if (arg[i].type === "FunctionExpression") { serviceFunc = arg[i]; break; } } if (serviceFunc.type === "FunctionExpression") { var params = serviceFunc.params; var body = serviceFunc.body; var start = serviceFunc.start; var end = serviceFunc.end; var funcStr = source.substring(start, end); //params里是注入的代码 var injectArr = []; for (var j = 0; j < params.length; j++) { injectArr.push(params[j].name); } var injectStr = injectArr.join('","') var replaceString = '["' + injectStr + '", ' + funcStr + ']'; if(params.length){ ctx.push({ start: start, end: end, str: replaceString }) } } } } } }); var distStr = alter(code, ctx); console.log(distStr); |
具体的流程如下
可以从上面的过程看到acorn的特点
Babel正是扩展了acorn的能力,使得转换变得更一体化
Babel转AST树的过程涉及到语法的问题,转AST树一定有对就的语法,如果在解析过程中,出现了不符合Babel语法的代码,就会报错,Babel转AST的解析过程在Babylon中完成
解析成AST树使用babylon.parse方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import babylon from 'babylon'; let code = ` let a = 1, b = 2; function sum(a, b){ return a + b; } sum(a, b); `; let ast = babylon.parse(code); console.log(ast); |
结果如下
AST如下
https://github.com/babel/babylon/blob/master/ast/spec.md
1 2 3 4 5 | interface Node { type: string; loc: SourceLocation | null; } |
ast中的节点都是继承自Node节点,Node节点有type和loc两个属性,分别代表类型和位置,
其中位置定义如下
1 2 3 4 5 6 | interface SourceLocation { source: string | null; start: Position; end: Position; } |
位置节点又是由source(源码string), 开始位置,结束位置组成,start,end又是Position类型
1 2 3 4 5 | interface Position { line: number; // >= 1 column: number; // >= 0 } |
节点又包含行号和列号
再看Program的定义
1 2 3 4 5 6 7 | interface Program <: Node { type: "Program"; sourceType: "script" | "module"; body: [ Statement | ModuleDeclaration ]; directives: [ Directive ]; } |
Program是继承自Node节点,类型是Program, sourceType有两种,一种是script,一种是module,程序体是一个声明体Statement或者模块声明体ModuleDeclaration节点数组
Babel或者说Babylon支持的语法现阶段是不可以第三方扩展的,也就是说我们不可以使用babylon做一些奇奇怪的语法,换句话说
不要希望通过babel的插件体系来转换自己定义的语法规则
那么babylon支持的语法有哪些呢,除了常规的js语法之外,babel暂时只支持如下的语法
如果要真要自定义语法,可以在babylon的plugins目录下自定义语法
https://github.com/babel/babylon/tree/master/src/plugins
上面提到的babel的AST文档中,并没有提到JSX的语法树,那么JSX的语法树在哪里定义呢,同样jsx的AST树也应该在这个文档中指名,然而babel团队还没精力准备出来
实际上,babel-types有扩展AST树,babel-types的definitions就是天然的文档,具体的源码定义在这里
举例一个AST节点如查是JSXElement,那么它的定义可以在jsx.js中找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | defineType("JSXElement", { builder: ["openingElement", "closingElement", "children", "selfClosing"], visitor: ["openingElement", "children", "closingElement"], aliases: ["JSX", "Immutable", "Expression"], fields: { openingElement: { validate: assertNodeType("JSXOpeningElement"), }, closingElement: { optional: true, validate: assertNodeType("JSXClosingElement"), }, children: { validate: chain( assertValueType("array"), assertEach(assertNodeType("JSXText", "JSXExpressionContainer", "JSXSpreadChild", "JSXElement")) ), }, }, }); |
JSXElement的builder字段指明要构造一个这样的节点需要4个参数,这四个参数分别对应在fields字段中,四个参数的定义如下
openingElement: 必须是一个JSXOpeningElement节点
closingElement: 必须是一个JSXClosingElement节点
children: 必须是一个数组,数组元素必须是JSXText、JSXExpressionContainer、JSXSpreadChild中的一种类型
selfClosing: 未指明验证
使用 babel-types.[TYPE]方法就可以构造这样的一个AST节点
1 2 3 4 5 6 7 8 9 | var types = require('babel-types'); var jsxElement = types.JSXElement( types.OpeningElement(...), types.JSXClosingElement(...), [...], true ); |
构造了一个jsxElement类型的节点,这在Babel插件开发中是很重要的
同样验证是否一个JSXElement节点,也可以使用babel-types.isTYPE方法
比如
1 2 3 4 | var types = require('babel-types'); types.isJSXElement(astNode); |
所以用JSXElement语法定义可以直接看该文件,简单做个梳理如下
其中,斜体代表非终结符,粗体为终结符
Babel负责便利工作的是Babel-traverse包,使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 | import traverse from "babel-traverse"; traverse(ast, { enter(path) { if ( path.node.type === "Identifier" && path.node.name === "n" ) { path.node.name = "x"; } } }); |
遍历结点让我们可以获取到我们想要操作的结点的可能,在遍历一个节点时,存在enter和exit两个时刻,一个是进入结点时,这个时候节点的子节点还没触达,遍历子节点完成的时刻,会离开该节点,所以会有exit方法触发
访问节点,可以使用的参数是path参数,path这个参数并不直接等同于节点,path的属性有几个重要的组成,如下
举个栗子,如下的代码会将所有function变成另外的function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import traverse from "babel-traverse"; import types from "babel-types"; traverse(ast, { enter(path) { let node = path.node; if(types.isFunctionDeclaration(node)){ path.replaceWithSourceString(`function add(a, b) { return a + b; }`); } } }); |
结果如下
1 2 3 4 5 6 | - function square(n) { - return n * n; + function add(a, b) { + return a + b; } |
注意这里我们使用babel-types来判别node的类型,使用path的replaceWithSourceString方法来替换节点
但这里在babel的文档中也有提示,尽量少用replaceWithSourceString方法,该方法一定会调用babylon.parse解析代码,在遍历中解析代码,不如将解析代码放到遍历外面去做
其实上面的过程只是定义了如何遍历节点的时候转换节点
babel将上面的便利操作对外开放出去了,这就构成了babel的插件体系
babel的插件就是定义如何转换当前结点,所以从这里可以看出babel的插件能做的事情,只能转换ast树,而不能在作用在前序阶段(语法分析)
这里不得不提下babel的插件体系是怎么样的,babel的插件分为两部分
babel-plugin-xxx
preset: 预设, preset和plugin其实是一个东西,preset定义了一堆plugin list
这里值得一提的是,preset的顺序是倒着的,plugin的顺序是正的,也就是说
preset: [‘es2015’, ‘react’], 其实是先使用react插件再用es2015
plugin: [‘transform-react’, ‘transfrom-async-function’] 的顺序是正的遍历节点的时候先用transform-react再用transfrom-async-function
如果是自定义插件,还在开发阶段,要先在babel的配置文件指明babel插件的路径
1 2 3 4 5 6 7 8 9 10 11 12 | { "extensions": [".jsx", ".js"], "presets": ["react", "es2015"], "plugins": [ [ path.resolve(SERVER_PATH, "pourout/babel-plugin-transform-xxx"), {} ], ] } |
babel的自定义插件写法是多样,上面只是一个例子,可以传入option,具体可以参考babel的配置文档
上面的代码写成babel的插件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | module.exports = function(babel) { var types = babel.types; // plugin contents return { visitor: { FunctionDeclaration: { enter: function(path){ path.replaceWithSourceString(`function add(a, b){ return a + b}`); } } } }; }; |
Babel的插件包return一个function, 包含babel的参数,function运行后返回一个包含visitor的对象,对象的属性是遍历节点匹配到该类型的处理方法,该方法依然包含enter和exit方法
在写插件的过程中,经常要创建一些AST树,常用的方法如下
1 2 3 4 5 6 7 8 9 10 | types.VariableDeclaration( 'var', [ types.VariableDeclarator( types.Identifier('a'), types.NumericLiteral(1) ) ] ) |
如果使用这样创建一个ast节点,肯定要累死了
比如上面的var a = 1可以使用
1 2 3 4 5 6 7 | var gen = babel.template(`var NAME = VALUE;`); var ast = gen({ NAME: t.Identifier('a'), VALUE: t.NumberLiteral(1) }); |
当然也可以简单写
1 2 3 4 5 | var gen = babel.template(`var a = 1;`); var ast = gen({ }); |
接下来就可以用path的增、删、改操作进行转换了
Babel-generator的工作就是将一颗ast树转回来,具体操作如下
1 2 3 4 | import generator from "babel-generator"; let code = generator(ast); |
至此,代码转换就算完成了
通常我们都是使用webpack编译后代码再执行代码的,使用Babel-register允许我们不提前编译代码就可以运行代码,这在node端是非常便利的
在node端,babel-regiser的核心实现是下面这两个代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function loader(m, filename) { m._compile(compile(filename), filename); } function registerExtension(ext) { var old = oldHandlers[ext] || oldHandlers[".js"] || require.extensions[".js"]; require.extensions[ext] = function (m, filename) { if (shouldIgnore(filename)) { old(m, filename); } else { loader(m, filename, old); } }; } |
通过定义require.extensions方法,可以覆盖require方法,这样调用require的时候,就可以走babel的编译,然后使用m._compile方法运行代码
但这个方法在node是不稳定的方法
最后,就像babylon官网感觉acorn一样,babel为前端界做了一件awesome的工作,有了babel,不仅仅可以让我们的新技术的普及提前几年,我们可以通过写插件做更多的事情,比如做自定义规则的验证,做node的直出node端的适配工作等等。
原文来自:AlloyTeam
声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com
通过企业关键词查询企业涉讼详情,如裁判文书、开庭公告、执行公告、失信公告、案件流程等等。
IP反查域名是通过IP查询相关联的域名信息的功能,它提供IP地址历史上绑定过的域名信息。
结合权威身份认证的精准人脸风险查询服务,提升人脸应用及身份认证生态的安全性。人脸风险情报库,覆盖范围广、准确性高,数据权威可靠。
全国城市和站点空气质量查询,污染物浓度及空气质量分指数、空气质量指数、首要污染物及空气质量级别、健康指引及建议采取的措施等。
输入手机号和拦截等级,查看是否是风险号码