工具
在线ast工具 astexplorer.net/
babel转化工具
@babel/parser 可以把源码转换成AST
@babel/traverse 用于对 AST
的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
@babel/generate 可以把AST
生成源码,同时生成sourcemap
@babel/types 用于 AST
节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST
节点的方法,对编写处理 AST
逻辑非常有用
@babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform
、parse
,并实现了插件功能
步骤
- ast(抽象语法树) Parsing
- 转化 transformation
- 生成代码
function compiler(input) {
let tokens = tokenizer(input); //生成tokens
let ast = parser(tokens); //生成ast
let newAst = transformer(ast); //拿到新的ast
let output = codeGenerator(newAst); //生成新代码
return output;
}
ast(抽象语法树) Parsing
解析:词法解析,语法解析
-
词法解析:差分成各个tokens{type:** ,value: **}
-
语法分析:将词法分析生成的token进行转化,产物即为ast
转化 transformation
改写ast,根据当前ast生成一个新的ast
这个过程包括
Traversal(遍历)
visitors(访问器)
用于处理不同的节点,遍历过程中,当进入到该节点时会调用访问器,然后调用这个节点的相关函数(传入该节点与父节点),离开时也能调用访问器
访问器结构如下
const visitor = {
NumberLiteral:{
enter(node, parent) {},
exit(node, parent) {},
}
}
生成代码
将新的ast转回代码
原理
生成token
遍历所有字符串,利用switch,case等方式区分不同类型
function tokenizer(input){
let current = 0;
let tokens = []
while(current < input.length){
if(input[current] === '('){
tokens.push({type:'paren',value:'(' })
current++;
continue
}else if....
// 容错处理,如果我们什么都没有匹配到,说明这个token不在我们的解析范围内
throw new TypeError('I dont know what this character is: ' + char);
}
}
生成ast
function parser (tokens) {
let current = 0; //访问tokens的下标
//walk函数辅助我们遍历整个tokens
function walk () {
let token = tokens[current]
// 现在就是遍历出每一个token,根据其类型生成对应的节点
if (token.type === 'number') {
current++
return {
type: 'NumberLiteral',
value: token.value
}
}
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
//这里处理调用语句
if (token.type === 'paren' && token.value === "(") {
token = tokens[++current]
//这里以一个例子解释(add 2 3) 这样的代码 "(" 就是 paren token ,而接下来的node其实就是那个 name 类型的token "add"
let node = {
type: "CallExpression",
value: token.value,
params: []
}
//获取name后我们需要继续获取接下来调用语句中的参数,直到我们遇到了")",这里会存在嵌套的现象如下
// (add 2 (subtract 4 2))
/*
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
*/
token = tokens[++current];
//这里我们通过递归调用不断的读取参数
while (
(token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')
) {
node.params.push(walk())
token = tokens[current] //因为参数的if判断里会让 current++ 实际上就是持续向后遍历了tokens,然后将参数推入params
}
// 当while中断后就说明参数读取完了,现在下一个应该是")",所以我们++越过
current++
return node // 最终将CallExpression节点返回了
}
//当然这里做了容错处理,如果没有匹配到预计的类型,就说明出现了,parse无法识别的token
throw new TypeError(token.type);
}
// 现在我们创建AST,树的最根层就是Program
let ast = {
type: 'Program',
body: [],
};
//然后我们通过调用walk遍历tokens将tokens内的对象,转化为AST的节点,完成AST的构建
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
转化transformer
遍历traverse
访问器设置
traverse(ast,visitor)
traverse(ast,{
Program:{enter(node,parent){...},exit(node,parent){}},
CallExpression:{enter(node,parent){...},exit(node,parent){...}},
NumberLiteral:{enter(node,parent){...},exit(node,parent){...}}
})
遍历
- 找type
- 判断当前类型在visitor 有无enter并执行
- 遍历子项(Program遍历body,CallExperssion遍历node.params,NumberLiteral&StringLiteral直接break)
- 调用exit
即将期待的转为目标想要的
例如箭头函数转为普通函数等
function transformer(ast){
let newAst = {type:'Program',body:[]}
// 利用引用传递的特质,后续操作_content即操作newAst的body
ast._content = newAst.body;
traverse(ast,{
CallExperssion:{
...
// 在其中改造ast中type为CallExperssion的对象
}
})
}
traverse的实现
function traverse(ast,visitor){
function traversArray(){
array.forEach(child => {
traverseNode(child, parent)
});
}
function traverseNode(node,parent){
let method = visitor[node.type]
if(methods&&methods.enter){
methods.enter(node,parent)
}
switch(node.type){
case:'...':
// 传入children
traversArray(node[...],node)
break:
}
if(methods&&methods.exit){
methods.exit(node,parent)
}
}
traverseNode(ast,null)
}
新代码生成
遍历每一个节点,根据规则生成最终代码
function codeGenerator(node) {
// 我们以节点的种类拆解(语法树)
switch (node.type) {
// 如果是Progame,那么就是AST的最根部了,他的body中的每一项就是一个分支,我们需要将每一个分支都放入代码生成器中
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 如果是声明语句注意看新的AST结构,那么在声明语句中expression,就是声明的标示,我们以他为参数再次调用codeGenerator
case 'ExpressionStatement':
return (
codeGenerator(node.expression) + ';'
);
// 如果是调用语句,我们需要打印出调用者的名字加括号,中间放置参数如生成这样"add(2,2)",
case 'CallExpression':
return (
codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
);
// 如果是识别就直接返回值 如: (add 2 2),在新AST中 add就是那个identifier节点
case 'Identifier':
return node.name;
// 如果是数字就直接返回值
case 'NumberLiteral':
return node.value;
// 如果是文本就给值加个双引号
case 'StringLiteral':
return '"' + node.value + '"';
// 容错处理
default:
throw new TypeError(node.type);
}
}
ast如下
现有工具
@babel/parser 可以把源码转换成AST
@babel/traverse 用于对 AST
的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
@babel/generate 可以把AST
生成源码,同时生成sourcemap
@babel/types 用于 AST
节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST
节点的方法,对编写处理 AST
逻辑非常有用
@babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform
、parse
,并实现了插件功能
babel插件仿写
plugins会按照配置的顺序从前往后执行
箭头函数处理
const core = require("@babel/core"); //babel核心模块
let arrowFunctionPlugin = require("babel-plugin-transform-es2015-arrow-functions"); //转换箭头函数插件
let sourceCode = `
const sum = (a, b) => {
return a + b;
}
`;
let targetSource = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin], //使用插件
});
console.log(targetSource.code);
const core = require("@babel/core"); //babel核心模块
let sourceCode = `
const sum = (a, b) => {
return a + b;
}
`;
const arrowFunctionPlugin = {
visitor: {
//如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
ArrowFunctionExpression(path) {
let { node } = path;
node.type = "FunctionExpression";
},
},
};
let targetSource = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin], //使用插件
});
console.log(targetSource.code);
案例升级 处理无括号return
返回值修改,若按上述插件修改,得到的结果只会是(无return 无大括号)
const sum = function (a,b) a+b
逻辑:若不是块语句,则加上块并将内容return
let sourceCode = `
const sum = (a, b) => a + b
`;
let types = require("@babel/types"); //用来生成或者判断节点的AST语法树的节点
const arrowFunctionPlugin = {
visitor: {
//如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
ArrowFunctionExpression(path) {
let { node } = path;
node.type = "FunctionExpression";
//如果函数体不是块语句
if (!types.isBlockStatement(node.body)) {
node.body = types.blockStatement([types.returnStatement(node.body)]); //生成一个块语句,并将内容return
}
},
},
};
处理this
第一步:找到当前箭头函数要使用哪个作用域内的this
,暂时称为父作用域
第二步:往父作用域中加入_this
变量,也就是添加语句:var _this = this
第三步:找出当前箭头函数内所有用到this
的地方
第四步:将当前箭头函数中的this
,统一替换成_this
function hoistFunctionEnvironment(path) {
//确定当前箭头函数要使用哪个地方的this---应该是返回true就停下了
const thisEnv = path.findParent((parent) => {
return (
(parent.isFunction() && !parent.isArrowFunctionExpression()) ||
parent.isProgram()
); //要求父节点是函数且不是箭头函数,找不到就返回根节点
});
}
const arrowFunctionPlugin = {
visitor: {
//如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
ArrowFunctionExpression(path) {
let { node } = path;
+ hoistFunctionEnvironment(path); //提升函数环境,解决this作用域问题
node.type = "FunctionExpression"; //箭头函数转换为普通函数
//如果函数体不是块语句
if (!types.isBlockStatement(node.body)) {
node.body = types.blockStatement([types.returnStatement(node.body)]);
}
},
},
};
function hoistFunctionEnvironment(path) {
//确定当前箭头函数要使用哪个地方的this
const thisEnv = path.findParent((pare nt) => {
return (
(parent.isFunction() && !parent.isArrowFunctionExpression()) ||
parent.isProgram()
); //要求父节点是函数且不是箭头函数,找不到就返回根节点
});
//向父作用域内放入一个_this变量
+ thisEnv.scope.push({
+ id: types.identifier("_this"), //生成标识符节点,也就是变量名
+ init: types.thisExpression(), //生成this节点 也就是变量值
+ });
+ let thisPaths = []; //获取当前节点this的路径 收集this
+ //遍历当前节点的子节点
+ path.traverse({
+ ThisExpression(thisPath) {
+ thisPaths.push(thisPath);
+ },
+ });
+ //替换
+ thisPaths.forEach((thisPath) => {
+ thisPath.replaceWith(types.identifier("_this")); //this => _this
+ });
}
添加监控上传日志
如下 有四种函数声明都需要添加
//四种声明函数的方式
function sum(a, b) {
return a + b;
}
const multiply = function (a, b) {
return a * b;
};
const minus = (a, b) => a - b;
class Calculator {
divide(a, b) {
return a / b;
}
}
转化后期望如下
import loggerLib from "logger"
function sum(a, b) {
loggerLib()
return a + b;
}
const multiply = function (a, b) {
loggerLib()
return a * b;
};
const minus = (a, b) =>{
loggerLib()
return a - b;
}
class Calculator {
divide(a, b) {
loggerLib()
return a / b;
}
}
思路
- 第一步:先判断源代码中是否引入了
logger
库 - 第二步:如果引入了,就找出导入的变量名,后面直接使用该变量名即可
- 第三步:如果没有引入我们就在源代码的顶部引用一下
- 第四步:在函数中插入引入的函数
三种导入均需要插入
import logger2 from "logger1";
import { logger4 } from "logger3";
import * as logeer6 from "logger5";
const core = require("@babel/core");
const types = require("@babel/types");
const sourceCode = `//四种声明函数的方式
function sum(a, b) {
return a + b;
}
const multiply = function (a, b) {
return a * b;
};
const minus = (a, b) => a - b;
class Calculator {
divide(a, b) {
return a / b;
}
}`;
function autoImportLog() {
return {
visitor: {
Program(path) {
let loggerId;
path.traverse({
ImportDeclaration(path) {
const { node } = path;
if (node.source.value === "logger") {
const specifiers = node.specifiers[0];
loggerId = specifiers.local.name
path.stop()
}
},
});
if (!loggerId) {
// 没有loggerId说明源码没有引入,需要外面手动导入
loggerId = path.scope.generateUid('loggerLib')
path.node.body.unshift(
types.importDeclaration(
[types.importDefaultSpecifier(types.identifier(loggerId))],
types.stringLiteral('logger')
)
)
}
},
},
};
}
const {code}= core.transformSync(sourceCode, { plugins: [autoImportLog()] });
console.log(code);
简易版eslint实现(no-console及fix)
no-console及fix
// noconsole
const { transformSync } = require('@babel/core');
function eslintPlugin({fix}){
return {
pre(file){
file.set('errors',[])
},
visitor:{
CallExpression(path,state){
const errors = state.file.get('errors')
if(path.node?.callee?.object?.name === 'console'){
errors.push(
path.buildCodeFrameError('不能拿存在console',Error)
)
if(fix){
path.parentPath.remove()
}
}
}
},
post(file){
console.log(...file.get('errors'))
}
}
}
const sourceCode = `
let a = 123
console.log(a)
`
let targetSouce = transformSync(sourceCode,{
plugins:[eslintPlugin({fix:true})]
})
console.log(targetSouce.code)
ugly(压缩)
const {transformSync } = require('@babel/core')
const codeSource = `function getAge(){
var age = 12;
console.log(age);
var name = 'name2';
console.log(name);
}`
function uglyPlugins(){
return {
visitor:{
//这是一个别名,用于捕获所有作用域节点:函数、类的函数、函数表达式、语句快、if else 、while、for
Scopable(path){
console.log('2222')
Object.entries(path.scope.bindings).forEach(([key,bindings])=>{
const newName = path.scope.generateUid()//在当前作用域内生成一个新的uid,并且不会和任何本地定义的变量冲突的标识符
bindings.path.scope.rename(key,newName)
})
}
}
}
}
const {code} = transformSync(codeSource,{
plugins:[uglyPlugins]
})
console.log(code)
思考:scopable操作是在语句之前还是之后
如下代码,我们发现在将age改名为ggg之前就运行了scopable的操作,所以此时并不会就行age和ggg的改动
const {transformSync } = require('@babel/core')
const codeSource = `function getAge(){
var age = 12;
console.log(age);
var name = 'name2';
console.log(name);
}`
function uglyPlugins(){
return {
visitor:{
VariableDeclarator(path){
console.log(11111)
if(path.node.id.name ==='age'){
path.node.id.name = 'ggg'
}
},
//这是一个别名,用于捕获所有作用域节点:函数、类的函数、函数表达式、语句快、if else 、while、for
Scopable(path){
console.log('2222')
Object.entries(path.scope.bindings).forEach(([key,bindings])=>{
const newName = path.scope.generateUid()//在当前作用域内生成一个新的uid,并且不会和任何本地定义的变量冲突的标识符
bindings.path.scope.rename(key,newName)
})
}
}
}
}
const {code} = transformSync(codeSource,{
plugins:[uglyPlugins]
})
console.log(code)
打印结果如下
2222
2222
2222
11111
11111
function _temp() {
var _temp4 = 12;
console.log(_temp4);
var _temp5 = 'name2';
console.log(_temp5);
}
总结
通过以下流程实现babel操作js的过程
- ast(抽象语法树) Parsing【词法解析、语法解析】
- 转化 transformation【遍历,访问器】
- 生成代码
接下来使用babel进行实训
主要调用@babell/core中的transform,传入ast,再写入plugins、实现visitor的函数进行调用,在其中放入需要对应处理的方法,