babel 简介
Babel 是一个 JavaScript 编译器,它能将 es2015,react 等低端浏览器无法识别的语言,进行编译。上图的左边代码中有箭头函数,Babel 将进行了源码转换,下面我们来看 Babel 的运行原理。
Babel 运行原理
Babel 的三个主要处理步骤分别是:
解析(parse),转换(transform),生成(generate)。
其过程分解用语言描述的话,就是下面这样:
解析
使用 babylon 解析器对输入的源代码字符串进行解析并生成初始 AST(File.prototype.parse)
利用 babel-traverse 这个独立的包对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程
转换
transform 过程:遍历 AST 树并应用各 transformers(plugin) 生成变换后的 AST 树,babel 中最核心的是 babel-core,它向外暴露出 babel.transform 接口。
let result = babel.transform(code, { plugins: [ arrayPlugin ]})
生成
利用 babel-generator 将 AST 树输出为转码后的代码字符串
AST 解析
AST 解析会把拿到的语法,进行树形遍历,对语法的每个节点进行响应的变化和改造再生产新的代码字符串
节点(node)
AST将开头提到的箭头函数转根据节点换为节点树
ES2015 箭头函数
codes.map(code=>{ return code.toUpperCase()})
AST 树形遍历转换后的结构
{ type:"ExpressionStatement", expression:{ type:"CallExpression" callee:{ type:"MemberExpression", computed:false object:{ type:"Identifier", name:"codes" } property:{ type:"Identifier", name:"map" } range:[] } arguments:{ { type:"ArrowFunctionExpression", id:null, params:{ type:"Identifier", name:"code", range:[] } body:{ type:"BlockStatement" body:{ type:"ReturnStatement", argument:{ type:"CallExpression", callee:{ type:"MemberExpression" computed:false object:{ type:"Identifier" name:"code" range:[] } property:{ type:"Identifier" name:"toUpperCase" } range:[] } range:[] } } range:[] } generator:false expression:false async:false range:[] } } }}
我们从 ExpressionStatement 开始往树形结构里面走,看到它的内部属性有 callee、type、arguments,所以我们再依次访问每一个属性及它们的子节点。
于是就有了如下的顺序
进入 ExpressionStatement进入 CallExpression进入 MemberExpression进入 Identifier离开 Identifier进入 Identifier离开 Identifier离开 MemberExpression进入 ArrowFunctionExpression进入 Identifier离开 Identifier进入 BlockStatement进入 ReturnStatement进入 CallExpression进入 MemberExpression进入 Identifier离开 Identifier进入 Identifier离开 Identifier离开 MemberExpression离开 CallExpression离开 ReturnStatement离开 BlockStatement离开 ArrowFunctionExpression离开 CallExpression离开 ExpressionStatement离开 Program
Babel 的转换步骤全都是这样的遍历过程。有点像 koa 的洋葱模型?
AST转换
解析好树结构后,我们手动对箭头函数进行转换。发现不一样的地方就是两个函数的 arguments.type
let babel = require('babel-core');//babel核心库let types = require('babel-types');let code = `codes.map(code=>{return code.toUpperCase()})`;// 转换语句
let visitor = { ArrowFunctionExpression(path) {//定义需要转换的节点 let params = path.node.params let blockStatement = path.node.body let func = types.functionExpression(null, params, blockStatement, false, false) path.replaceWith(func) // } }let arrayPlugin = { visitor } let result = babel.transform(code, { plugins: [ arrayPlugin ] }) console.log(result.code)
注意: ArrowFunctionExpression() { ... }
是 ArrowFunctionExpression: { enter() { ... } }
的简写形式。
Path 是一个对象,它表示两个节点之间的连接。
解析步骤
定义需要转换的节点
ArrowFunctionExpression(path) { ...... }
创建用来替换的节点
types.functionExpression(null, params, blockStatement, false, false)
babel-types 文档链接
- 在 node 节点上找到需要的参数
- replaceWith(替换)
手写一个babel插件
作用是给async的方法批量增加try catch
const template = require('@babel/template');
// 定义try语句模板 catch要打印的信息 合并选项 判断执行的file文
const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util')
module.exports = function (babel) {
// 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
let types = babel.types;
// visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
const visitor = {
AwaitExpression(path) {
// 通过this.opts 获取用户的配置
if (this.opts && !typeof this.opts === 'object') {
return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
}
// 判断父路径中是否已存在try语句,若存在直接返回
if (path.findParent((p) => p.isTryStatement())) {
return false;
}
// 合并插件的选项
const options = mergeOptions(this.opts);
// 获取编译目标文件的路径,如:D:\myapp\src\App.vue
const filePath = this.filename || this.file.opts.filename || 'unknown';
// 对排除列表的文件不编译
if (matchesFile(options.exclude, filePath)) {
return;
}
// 如果设置了include,只编译include中的文件
if (options.include.length && !matchesFile(options.include, filePath)) {
return;
}
// 获取当前的await节点
let node = path.node;
// 在父路径节点中查找声明 async 函数的节点
// async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
// 获取async的方法名
let asyncName = '';
let type = asyncPath.node.type;
switch (type) {
// 1️⃣函数表达式
// 情况1:普通函数,如const func = async function () {}
// 情况2:箭头函数,如const func = async () => {}
case 'FunctionExpression':
case 'ArrowFunctionExpression':
// 使用path.getSibling(index)来获得同级的id路径
let identifier = asyncPath.getSibling('id');
// 获取func方法名
asyncName = identifier && identifier.node ? identifier.node.name : '';
break;
// 2️⃣函数声明,如async function fn2() {}
case 'FunctionDeclaration':
asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
break;
// 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
case 'ObjectMethod':
asyncName = asyncPath.node.key.name || '';
break;
}
// 若asyncName不存在,通过argument.callee获取当前执行函数的name
let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
const temp = template(tryTemplate);
// 给模版增加key,添加console.log打印信息
let tempArgumentObj = {
// 通过types.stringLiteral创建字符串字面量
CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
};
// 通过temp创建try语句
let tryNode = temp(tempArgumentObj);
// 获取async节点(父节点)的函数体
let info = asyncPath.node.body;
// 将父节点原来的函数体放到try语句中
tryNode.block.body.push(...info.body);
// 将父节点的内容替换成新创建的try语句
info.body = [tryNode];
}
};
return {
name: 'babel-plugin-await-add-trycatch',
visitor
};
}