作者:京东批发 周亮堂
写在后面
这里咱们初步提到了一些根底概念和利用:
- 分析器
- 形象语法树 AST
- AST 在 JS 中的用处
- AST 的利用实际
有了初步的意识,还有惯例的代码革新利用实际,当初咱们来具体说说应用 AST, 如何进行代码革新?
Babel AST 四件套的应用办法
其实在解析 AST 这个工具上,有很多能够应用,上文咱们曾经提到过了。对于 JS 的 AST 大家曾经造成了对立的标准命名,惟一不同的可能是,不同工具提供的具体水平不一样,有的可能会额定提供额定办法或者属性。
所以,在抉择工具上,大家依照各自喜爱抉择即可,这里咱们抉择了babel这个老朋友。
初识 Babel
我置信在这个前端框架频出的时代,应该都晓得babel的存在。 如果你还没听说过babel,那么咱们通过它的相干文档,持续深刻学习一下。
因为,它在任何框架外面,咱们都能看到它的影子。
- Babel JS 官网
- Babel JS Github
作为应用最宽泛的 JS 编译器,他能够用于将采纳 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便可能运行在以后和旧版本的浏览器或其余环境中。
而它可能做到向下兼容或者代码转换,就是基于代码解析和革新。接下来,咱们来说说:如何应用@babel/core外面的外围四件套:@babel/parser、@babel/traverse、@babel/types及@babel/generator。
1. @babel/parser
@babel/parser 外围代码解析器,通过它进行词法剖析及语法分析过程,最终转换为咱们提到的 AST 模式。
假如咱们须要读取React中index.tsx文件中代码内容,咱们能够应用如下代码:
const { parse } = require("@babel/parser")// 读取文件内容const fileBuffer = fs.readFileSync('./code/app/index.tsx', 'utf8');// 转换字节 Bufferconst fileCode = fileBuffer.toString();// 解析内容转换为 AST 对象const codeAST = parse(fileCode, { // parse in strict mode and allow module declarations sourceType: "module", plugins: [ // enable jsx and typescript syntax "jsx", "typescript", ],});
当然我不仅仅只读取React代码,咱们甚至能够读取Vue语法。它也有对应的语法分析器,比方:@vue/compiler-dom。
此外,通过不同的参数传入 options,咱们能够解析各种各样的代码。如果,咱们只是读取一般的.js文件,咱们能够不应用任何插件属性即可。
const codeAST = parse(fileCode, { // parse in strict mode and allow module declarations sourceType: "module"});
通过上述的代码转换,咱们就能够失去一个规范的 AST 对象。在上一篇文章中,已做详细分析,在这里不在开展。比方:
// 原代码const me = "我"function write() { console.log("文章")}// 转换后的 AST 对象const codeAST = { "type": "File", "errors": [], "program": { "type": "Program", "sourceType": "module", "interpreter": null, "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "me" }, "init": { "type": "StringLiteral", "extra": { "rawValue": "我", "raw": "\"我\"" }, "value": "我" } } ], "kind": "const" }, { "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "write" }, "generator": false, "async": false, "params": [], "body": { "type": "BlockStatement", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "computed": false, "property": { "type": "Identifier", "name": "log" } }, "arguments": [ { "type": "StringLiteral", "extra": { "rawValue": "文章", "raw": "\"文章\"" }, "value": "文章" } ] } } } ] } } ] }}
2. @babel/traverse
当咱们拿到一个规范的 AST 对象后,咱们要操作它,那必定是须要进行树结构遍历。这时候,咱们就会用到 @babel/traverse 。
比方咱们失去 AST 后,咱们能够进行遍历操作:
const { default: traverse } = require('@babel/traverse');// 进入结点const onEnter = pt => { // 进入以后结点操作 console.log(pt)}// 退出结点const onExit = pe => { // 退出以后结点操作}traverse(codeAST, { enter: onEnter, exit: onExit })
那么咱们拜访的第一个结点,打印出pt的值,是怎么的呢?
// 已省略局部有效值<ref *1> NodePath { contexts: [ TraversalContext { queue: [Array], priorityQueue: [], ... } ], state: undefined, opts: { enter: [ [Function: onStartVist] ], exit: [ [Function: onEndVist] ], _exploded: true, _verified: true }, _traverseFlags: 0, skipKeys: null, parentPath: null, container: Node { type: 'File', errors: [], program: Node { type: 'Program', sourceType: 'module', interpreter: null, body: [Array], directives: [] }, comments: [] }, listKey: undefined, key: 'program', node: Node { type: 'Program', sourceType: 'module', interpreter: null, body: [ [Node], [Node] ], directives: [] }, type: 'Program', parent: Node { type: 'File', errors: [], program: Node { type: 'Program', sourceType: 'module', interpreter: null, body: [Array], directives: [] }, comments: [] }, hub: undefined, data: null, context: TraversalContext { queue: [ [Circular *1] ], priorityQueue: [], ... }, scope: Scope { uid: 0, path: [Circular *1], block: Node { type: 'Program', sourceType: 'module', interpreter: null, body: [Array], directives: [] }, ... }}
是不是发现,这一个遍历怎么这么多货色?太长了,那么咱们进行省略,只看要害局部:
// 第1次<ref *1> NodePath { listKey: undefined, key: 'program', node: Node { type: 'Program', sourceType: 'module', interpreter: null, body: [ [Node], [Node] ], directives: [] }, type: 'Program',}
咱们能够看出是间接进入到了程序program结点。 对应的 AST 结点信息:
program: { type: 'Program', sourceType: 'module', interpreter: null, body: [ [Node] [Node] ], },
接下来,咱们持续打印输出的结点信息,咱们能够看出它拜访的是program.body结点。
// 第2次<ref *2> NodePath { listKey: 'body', key: 0, node: Node { type: 'VariableDeclaration', declarations: [ [Node] ], kind: 'const' }, type: 'VariableDeclaration',}// 第3次<ref *1> NodePath { listKey: 'declarations', key: 0, node: Node { type: 'VariableDeclarator', id: Node { type: 'Identifier', name: 'me' }, init: Node { type: 'StringLiteral', extra: [Object], value: '我' } }, type: 'VariableDeclarator',}// 第4次<ref *1> NodePath { listKey: undefined, key: 'id', node: Node { type: 'Identifier', name: 'me' }, type: 'Identifier',}// 第5次<ref *1> NodePath { listKey: undefined, key: 'init', node: Node { type: 'StringLiteral', extra: { rawValue: '我', raw: "'我'" }, value: '我' }, type: 'StringLiteral',}
- node以后结点
- parentPath父结点门路
- scope作用域
- parent父结点
- type以后结点类型
当初咱们能够看出这个拜访的法则了,他会始终找以后结点node属性,而后进行层层拜访其内容,直到将 AST 的所有结点遍历实现。
这里肯定要辨别NodePath和Node两种类型,比方下面:pt是属于NodePath类型,pt.node才是Node类型。
其次,咱们看到提供的办法除了进入 [enter]还有退出 [exit]办法,这也就意味着,每次遍历一次结点信息,也会退出以后结点。这样,咱们就有两次机会取得所有的结点信息。
当咱们遍历完结,如果找不到对应的结点信息,咱们还能够进行额定的操作,进行代码结点补充操作。结点残缺拜访流程如下:
进入>Program
进入>node.body[0]
进入>node.declarations[0]
- 进入>node.id
- 退出<node.id
- 进入>node.init
- 退出<node.init
- 退出<node.declarations[0]
- 退出<node.body[0]
进入>node.body[1]
- ...
- ...
- 退出<node.body[1]
- 退出<Program
3. @babel/types
有了后面的铺垫,咱们通过解析,取得了相干的 AST 对象。通过一直遍历,咱们拿到了相干的结点,这时候咱们就能够开始革新了。@babel/types 就提供了一系列的判断办法,以及将一般对象转换为 AST 结点的办法。
比方,咱们想把代码转换为:
// 革新前代码const me = "我"function write() { console.log("文章")}// 革新后的代码let you = "你"function write() { console.log("文章")}
首先,咱们要剖析下,这个代码改了哪些内容?
- 变量申明从const改为let
- 变量名从me改为you
- 变量值从"我"改为"你"
那么咱们有两种替换形式:
- 计划一:整体替换,相当于把program.body[0]整个结点进行替换为新的结点。
- 计划二:部分替换,相当于一一结点替换结点内容,即:program.body.kind,program.body[0].declarations[0].id,program.body[0].declarations[0].init。
借助@babel/types咱们能够这么操作,一起看看区别:
const bbt = require('@babel/types');const { default: traverse } = require('@babel/traverse');// 进入结点const onEnter = p => { // 计划一,全结点替换 if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') { // 间接替换为新的结点 p.replaceWith( bbt.variableDeclaration('let', [ bbt.variableDeclarator(bbt.identifier('you'), bbt.stringLiteral('你')), ]), ); } // 计划二,单结点逐个替换 if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') { // 替换申明变量形式 p.node.kind = 'let'; } if (bbt.isIdentifier(p.node) && p.node.name == 'me') { // 替换变量名 p.node.name = 'you'; } if (bbt.isStringLiteral(p.node) && p.node.value == '我') { // 替换字符串内容 p.node.value = '你'; } };traverse(codeAST, { enter: onEnter });
咱们发现,不仅能够进行整体结点替换,也能够替换属性的值,都能达到预期成果。
当然 咱们不仅仅能够全副遍历,咱们也能够只遍历某些属性,比方VariableDeclaration,咱们就能够这样进行定义:
traverse(codeAST, { VariableDeclaration: function(p) { // 只操作类型为 VariableDeclaration 的结点 p.node.kind = 'let'; }});
@babel/types提供大量的办法供应用,能够通过官网查看。对于@babel/traverse返回的可用办法,能够查看 ts 定义:
babel__traverse/index.d.ts 文件。
罕用的办法:p.stop()能够提前终止内容遍历, 还有其余的增删改查办法,能够本人缓缓摸索应用!它就是一个树结构,咱们能够操作它的兄弟结点,父节点,子结点。
4. @babel/generator
实现革新当前,咱们须要把 AST 再转换回去,这时候咱们就须要用到 @babel/generator 工具。只拆不组装,那是二哈【狗头】。能装能组,才是一个残缺工程师该干的事件。
废话不多说,上代码:
const fs = require('fs-extra');const { default: generate } = require('@babel/generator');// 生成代码实例const codeIns = generate(codeAST, { retainLines: true, jsescOption: { minimal: true } });// 写入文件内容fs.writeFileSync('./code/app/index.js', codeIns.code);
配置项比拟多,大家能够参考具体的阐明,依照理论需要进行配置。
这里特地提一下:jsescOption: { minimal: true }这个属性,次要是用来保留中文内容,避免被转为unicode模式。
Babel AST 实际
嘿嘿~ 都到这里了,大家应该曾经可能上手操作了吧!
什么?还不会,那再把 1 ~ 4 的步骤再看一遍。缓缓尝试,缓缓批改,当你发现其中的乐趣时,这个 AST 的革新也就简略了,并不是什么难事。
留个课后练习:
// 革新前代码const me = "我"function write() { console.log("文章")}// 革新后的代码const you = "你"function write() { console.log("文章")}console.log(you, write())
大家能够去尝试下,怎么操作简略的 AST 实现代码革新!写文章不易,大家记得一键三连哈~
AST 利用是十分宽泛,再来回顾下,这个 AST 能够干嘛?
- 代码转换畛域,如:ES6 转 ES5, typescript 转 js,Taro 转多端编译,CSS预处理器等等。
- 模版编译畛域,如:React JSX 语法,Vue 模版语法 等等。
- 代码预处理畛域,如:代码语法查看(ESLint),代码格式化(Prettier),代码混同/压缩(uglifyjs) 等等
- 低代码搭建平台,拖拽组件,间接通过 AST 革新生成后的代码进行运行。
下一期预报
《带你揭开神秘的Javascript AST面纱之手写一个简略的 Javascript 编译器》