babel工具学习

136 阅读10分钟

工具

在线ast工具 astexplorer.net/

babel转化工具

@babel/parser 可以把源码转换成AST

@babel/traverse 用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点

@babel/generate 可以把AST生成源码,同时生成sourcemap

@babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用

@babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transformparse,并实现了插件功能

步骤

  • 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如下

image-20241115150825818转存失败,建议直接上传图片文件

现有工具

@babel/parser 可以把源码转换成AST

@babel/traverse 用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点

@babel/generate 可以把AST生成源码,同时生成sourcemap

@babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用

@babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transformparse,并实现了插件功能

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的函数进行调用,在其中放入需要对应处理的方法,