1.什么是抽象语法树(Abstract Syntax Tree) #

2.抽象语法树用途 #

3.抽象语法树组成 #

ast

4. JavaScript Parser #

4.1 常用的 JavaScript Parser #

4.2 AST节点 #

4.3 AST遍历 #

npm i esprima estraverse escodegen -S
let esprima = require('esprima');//把JS源代码转成AST语法树
let estraverse = require('estraverse');///遍历语法树,修改树上的节点
let escodegen = require('escodegen');//把AST语法树重新转换成代码
let code = `function ast(){}`;
let ast = esprima.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
estraverse.traverse(ast,{
    enter(node){
        console.log(padding()+node.type+'进入');
        if(node.type === 'FunctionDeclaration'){
            node.id.name = 'newAst';
        }
        indent+=2;
    },
    leave(node){
        indent-=2;
        console.log(padding()+node.type+'离开');
    }
});
Program进入
  FunctionDeclaration进入
    Identifier进入
    Identifier离开
    BlockStatement进入
    BlockStatement离开
  FunctionDeclaration离开
Program离开

5.babel #

ast-compiler-flow.jpg

5.2 babel 插件 #

5.3 Visitor #

5.3.1 path #

5.3.2 scope #

5.4 转换箭头函数 #

转换前

const sum = (a,b)=>{
    console.log(this);
    return a+b;
}

转换后

var _this = this;

const sum = function (a, b) {
  console.log(_this);
  return a + b;
};
npm i @babel/core @babel/types -D

实现

// Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
const babelCore = require('@babel/core');
//用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法
const types = require('@babel/types');
//const arrowFunctions = require('babel-plugin-transform-es2015-arrow-functions');
const arrowFunctions2 = {
    visitor: {
        //当遍历语法遇到箭头函数的时候,执行此函数,参数是箭头函数的节点路径对象
        ArrowFunctionExpression(path) {
            const { node } = path;
            hoistFunctionEnvironment(path);
            node.type = 'FunctionExpression';
            let body = node.body;
            //如果body不是一个块级语句的话 isXX用来判断某个AST语法树节点是不是某种类型
            if (!types.isBlockStatement(body)) {
                //https://babeljs.io/docs/babel-types.html
                node.body = types.blockStatement([
                    types.returnStatement(body)
                ]);
            }
        }
    }
}
function getThisBindingIdentifier(scope) {
    for (const bindingName in scope.bindings) {
        const binding = scope.bindings[bindingName];
        if (binding.kind === 'const' || binding.kind === 'let' || binding.kind === 'var') {
            const initValue = binding.path.node.init;
            if (types.isThisExpression(initValue)) {
                return binding.identifier;
            }
        }
    }
    return null;
}
/**
 * 1.在函数的外部声明一个变量_this,值是this
 * 2.在函数体内把所有的this变成_this
 * @param {*} path 
 */
function hoistFunctionEnvironment(path) {
    //indParent(callback)从当前节点一直向上找到根节点(不包括自己)
    const thisEnv = path.findParent(parent => {
        //如果这个父节点是一个普通函数,或者是一个根节点的话返回此节点
        return (parent.isFunction() && !parent.isArrowFunctionExpression()) || parent.isProgram()
    });
    //1.需要确定在当前的作用域内是否使用到了this
    let thisPaths = getThisPaths(path);
    let thisBinding = getThisBindingIdentifier(thisEnv.scope);
    if (!thisBinding) {
        thisBinding = types.identifier(thisEnv.scope.generateUid('this'));
        thisEnv.scope.push({
            id: thisBinding,
            init: types.thisExpression()
        });
    }
    if (thisPaths.length > 0) {
        thisPaths.forEach(thisPath => {
            thisPath.replaceWith(thisBinding);
        });
    }
}
function getThisPaths(path) {
    let thisPaths = [];
    //判断path的子节点
    path.traverse({
        FunctionDeclaration(path) {
            path.skip();
        },
        ThisExpression(thisPath) {
            thisPaths.push(thisPath);
        }
    });
    return thisPaths;
}
let sourceCode = `
const sum = (a,b)=>{
    console.log(this);
        function multiply(){
             console.log(this);
        }
    return a+b;
}
const minus = (a,b)=>{
    console.log(this);
        const divide = (a,b)=>{
      console.log(this);
      return a/b;
        }
    return a-b;
}
`;
let targetSource = babelCore.transform(sourceCode, {
    plugins: [
        arrowFunctions2
    ]
});
console.log(targetSource.code);

5.5 把类编译为 Function #

es6

class Person {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

classast

es5

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function () {
  return this.name;
};

es5class1 es5class2

实现

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let transformClassesPlugin = require('@babel/plugin-transform-classes');
let transformClassesPlugin = {
    visitor: {
        //如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
        //path代表路径,node代表路径上的节点
        ClassDeclaration(path) {
            let node = path.node;
            let id = node.id;//Identifier name:Person
            let methods = node.body.body;//Array<MethodDefinition>
            let nodes = [];
            methods.forEach(method => {
                if (method.kind === 'constructor') {
                    let constructorFunction = types.functionDeclaration(
                        id,
                        method.params,
                        method.body
                    );
                    nodes.push(constructorFunction);
                } else {
                    let memberExpression = types.memberExpression(
                        types.memberExpression(
                            id, types.identifier('prototype')
                        ), method.key
                    )
                    let functionExpression = types.functionExpression(
                        null,
                        method.params,
                        method.body
                    )
                    let assignmentExpression = types.assignmentExpression(
                        '=',
                        memberExpression,
                        functionExpression
                    );
                    nodes.push(assignmentExpression);
                }
            })
            if (nodes.length === 1) {
                //单节点用replaceWith
                //path代表路径,用nodes[0]这个新节点替换旧path上现有老节点node ClassDeclaration
                path.replaceWith(nodes[0]);
            } else {
                //多节点用replaceWithMultiple
                path.replaceWithMultiple(nodes);
            }
        }
    }
}
let sourceCode = `
class Person{
    constructor(name){
        this.name = name;
    }
    sayName(){
        console.log(this.name);
    }
}
`;
let targetSource = core.transform(sourceCode, {
    plugins: [transformClassesPlugin]
});

console.log(targetSource.code);

5.6 实现日志插件 #

5.6.1 logger.js #

const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const sourceCode = `
console.log("hello");
`;
const visitor = {
    CallExpression(nodePath,state){
        const {node} = nodePath;
        if(types.isMemberExpression(node.callee)){
            if(node.callee.object.name === 'console'){
                if(['log','warn','info','error','debug'].includes(node.callee.property.name)){
                    const {line,column} = node.loc.start;
                    // 获取相对于当前文件的文件名并将反斜杠替换为正斜杠
                    const relativeFileName = path.relative(__dirname, state.file.opts.filename).replace(/\\/g, '/');
                    // 将文件名和位置信息插入到参数列表的开头
                    node.arguments.unshift(types.stringLiteral(`${relativeFileName} ${line}:${column}`));
                }
            }
        }
    }
}

function logParamPlugin(){
    return {
        visitor
    }
}
const {code} = transformSync(sourceCode,{
    filename:'any.js',
    plugins:[logParamPlugin()]
});
console.log(code);

5.7 自动日志插件 #

5.7.1 use.js #

const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const autoLoggerPlugin = require('./autoLoggerPlugin');
const sourceCode = `
let _logger2 = 'xxx';
function sum(a,b){
    return a+b;
}
const multiply = function(a,b){
    return a*b;
}
const minis = (a,b)=>a-b;
class Math{
    divide(a,b){
        return a/b;
    }
}
`;
const {code} = transformSync(sourceCode,{
    filename:'some.js',
    plugins:[autoLoggerPlugin({
        fnNames:['sum'],
        libName:'logger',//把获取业务数据的逻辑写在logger里
        params:['a','b','c']
    })]
});
console.log(code);

5.7.2 autoLoggerPlugin.js #

const types = require('@babel/types');
const pathLib = require('path');
const importModuleHelper = require('@babel/helper-module-imports');
const template = require('@babel/template');

function autoLoggerPlugin(options){
    return {
        visitor:{
            Program:{
                //state 可以在遍历过程保存和传递状态
                enter(path,state){
                    let loggerId;
                    path.traverse({
                        ImportDeclaration(path){
                            debugger
                            //获取导入库的名称
                            //const libName = path.node.source.value;
                            //jquery.find 在path的下层属性中寻找属性名为source的路径path,
                            const libName = path.get('source').node.value;
                            //如果此导入语句导入的第三方模块和配置的日志第三方库名称一样
                            if(options.libName === libName){
                                const specifierPath = path.get('specifiers.0');
                                if(specifierPath.isImportDefaultSpecifier()
                                || specifierPath.isImportSpecifier()
                                ||specifierPath.isImportNamespaceSpecifier()){
                                    loggerId=specifierPath.node.local;
                                }
                                path.stop();//停止遍历查找
                            }
                        }
                    });
                    //如果遍历完Program,loggerId还是空的,那说明在源码中尚未导入logger模块
                    if(!loggerId){
                        loggerId = importModuleHelper.addDefault(path,options.libName,{
                            //在Program作用域内生成一个不会与当前作用域内变量重复的变量名
                            nameHint:path.scope.generateUid(options.libName)
                        });
                    }
                    //使用template模块生成一个ast语法树节点,把一个字符串变成节点
                    state.loggerNode = template.statement(`LOGGER_PLACE();`)({
                        LOGGER_PLACE:loggerId.name
                    })
                    //state.loggerNode = types.expressionStatement(types.callExpression(loggerId,[]));
                }
            },
            "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod"(path,state){
                const {node} = path;
                let fnName;
                if(node.type === 'FunctionDeclaration'){
                    fnName=node.id.name;
                }
                if(options.fnNames.includes(fnName)){
                    if(types.isBlockStatement(node.body)){
                        node.body.body.unshift(state.loggerNode);
                    }else {
                        const newNode = types.blockStatement([
                            state.loggerNode,
                            types.returnStatement(node.body)
                        ]);
                        path.get('body').replaceWith(newNode);
                    }
                }
            }
        }
    }
}
module.exports = autoLoggerPlugin;

5.8 eslint #

5.8.1 use.js #

const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const noConsolePlugin = require('./noConsolePlugin');
const sourceCode = `
var a = 1;
console.log(a);
var b = 2;
`;
const {code} = transformSync(sourceCode,{
    filename:'./some.js',
    plugins:[noConsolePlugin({
       fix:true
    })]
});
console.log(code);

5.8.2 eslintPlugin.js #

eslintPlugin.js

function noConsolePlugin(options){
    return {
       pre(file){
        file.set('errors',[]);
       },
       visitor:{
        CallExpression(path,state){
            const {node} = path;
            const errors = state.file.get('errors');
            if(node.callee.object && node.callee.object.name === 'console'){
                const stackTraceLimit = Error.stackTraceLimit;
                Error.stackTraceLimit = 0;
                errors.push(path.buildCodeFrameError(`代码中不能出现console语句`,Error));
                Error.stackTraceLimit = stackTraceLimit;
                if(options.fix){//如果需要自动修复,就删除此语句
                    path.parentPath.remove();
                }
            }
        }
       },
       post(file){
        console.log(...file.get('errors'));
       }
    }
}
module.exports = noConsolePlugin;

5.9 uglify #

5.9.1 use.js #

const {transformSync} = require('@babel/core');
const uglifyPlugin = require('./uglifyPlugin');
const sourceCode = `
var age = 12;
console.log(age);
var name = 'zhufeng';
console.log(name)
`;
const {code} = transformSync(sourceCode,{
    filename:'./some.js',
    plugins:[uglifyPlugin()]
});
console.log(code);

5.9.2 uglifyPlugin.js #

uglifyPlugin.js

function uglifyPlugin(options){
    return {
       visitor:{
        //捕获所有的作用域的节点
        Scopable(path){
            //遍历作用域内所有的绑定,也就是变量
            Object.entries(path.scope.bindings).forEach(([key,binding])=>{
               const newName =  path.scope.generateUid('_');
               binding.path.scope.rename(key,newName);
            });
        }
       }
    }
}
module.exports = uglifyPlugin;

5.10 tsc #

5.10.1 use.js #

const {transformSync} = require('@babel/core');
const tscPlugin = require('./tscPlugin');
const sourceCode = `
var age:number = "aaa";
`;
const {code} = transformSync(sourceCode,{
    parserOpts:{plugins:["typescript"]},
    filename:'./some.js',
    plugins:[tscPlugin()]
});
console.log(code);

5.10.2 tscPlugin.js #

tscPlugin.js

const typeAnnotationMap= {
    TSNumberKeyword:'NumberLiteral',
    TSStringKeyword:'StringLiteral'
}
function tscPlugin(){
    return {
        pre(file){
         file.set('errors',[]);
        },
        visitor:{
            VariableDeclarator(path,state){
                const errors = state.file.get('errors');
                const {node} = path;
                const idType = typeAnnotationMap[node.id.typeAnnotation.typeAnnotation.type];
                const initType = node.init.type;
                if(idType !== initType){
                    Error.stackTraceLimit = 0;
                    errors.push(path.buildCodeFrameError(
                        `无法把${initType}赋值给${idType}`,Error
                    ));
                }
            }
        },
        post(file){
         console.log(...file.get('errors'));
        }
     }
}
module.exports = tscPlugin;

6. webpack中使用babel插件 #

6.1 实现按需加载 #

import { flatten, concat } from "lodash";

treeshakingleft

转换为

import flatten from "lodash/flatten";
import concat from "lodash/flatten";

treeshakingright

6.1.1 webpack 配置 #

npm i webpack webpack-cli babel-plugin-import -D
const path = require("path");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve("dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        {
            loader:'babel-loader',
            options:{
                "plugins": [[
                    path.resolve('./plugins/babel-plugin-import.js')
                    , {
                    "libraryDirectory": "",
                    "libraryName": "lodash"
                  }]]
            }
        },
      },
    ],
  },
};

编译顺序为首先plugins从左往右,然后presets从右往左

6.1.2 babel 插件 #

plugins\babel-plugin-import.js

const types = require('@babel/types');
const template = require('@babel/template');
function babelPluginImport(){
    return {
       visitor:{
        ImportDeclaration(path,state){
            const {node} = path;
            const {specifiers} = node;
            const {libraryName,libraryDirectory='lib'} = state.opts;
            if(node.source.value === libraryName
                &&(!types.isImportDefaultSpecifier(specifiers[0]))){
                const  newImportDeclarations = specifiers.map(specifier=>{
                    return template.statement(
                    `import ${specifier.local.name} from '${libraryName}/${specifier.imported.name}';`
                    )();
                    /* return types.importDeclaration(
                        [types.importDefaultSpecifier(specifier.local)],
                        types.stringLiteral(libraryDirectory?
                            `${libraryName}/${libraryDirectory}/${specifier.imported.name}`
                            :`${libraryName}/${specifier.imported.name}`)
                    ); */
                })
                path.replaceWithMultiple(newImportDeclarations);
            }
        }
       }
    }
}
module.exports = babelPluginImport;

7. 参考 #

5.9 tsc #

5.9.1 use.js #

const { transformSync } = require('@babel/core');
const tscCheckPlugin = require('./tscCheckPlugin');
const sourceCode = `
var age:number="12";
`;

const { code } = transformSync(sourceCode, {
  parserOpts: { plugins: ['typescript'] },
  plugins: [tscCheckPlugin()]
});

console.log(code);

5.9.2 tscCheckPlugin.js #

tscCheckPlugin.js

// 定义一个类型注解映射对象
const TypeAnnotationMap = {
  TSNumberKeyword: "NumericLiteral"
};

// 定义 eslintPlugin
const eslintPlugin = () => {
  return {
    // 在遍历开始前执行
    pre(file) {
      // 为当前文件设置一个空的 errors 数组
      file.set('errors', []);
    },
    visitor: {
      // 当访问 VariableDeclarator 节点时触发
      VariableDeclarator(path, state) {
        // 获取之前设置的 errors 数组
        const errors = state.file.get('errors');
        const { node } = path;
        // 获取变量声明的类型注解类型
        const idType = TypeAnnotationMap[node.id.typeAnnotation.typeAnnotation.type];
        // 获取变量初始值的类型
        const initType = node.init.type;
        // 打印变量声明类型和初始值类型
        console.log(idType, initType);
        // 如果变量声明类型与初始值类型不匹配
        if (idType !== initType) {
          // 将错误信息添加到 errors 数组
          errors.push(path.get('init').buildCodeFrameError(`无法把${initType}类型赋值给${idType}类型`, Error));
        }
      }
    },
    // 遍历结束后执行
    post(file) {
      // 在控制台输出 errors 数组的内容
      console.log(...file.get('errors'));
    }
  }
};
// 导出 eslintPlugin
module.exports = eslintPlugin;

5.9.3 赋值 #

const babel = require('@babel/core');
function transformType(type){
    switch(type){
        case 'TSNumberKeyword':
        case 'NumberTypeAnnotation':
            return 'number'
        case 'TSStringKeyword':
        case 'StringTypeAnnotation':
            return 'string'
    }
}
const tscCheckPlugin = () => {
    return {
        pre(file) {
            file.set('errors', []);
        },
        visitor: {
            AssignmentExpression(path,state){
              const errors = state.file.get('errors');
              const variable = path.scope.getBinding(path.get('left'));
              const variableAnnotation = variable.path.get('id').getTypeAnnotation();
              const variableType = transformType(variableAnnotation.typeAnnotation.type);
              const valueType = transformType(path.get('right').getTypeAnnotation().type);
              if (variableType !== valueType){
                  Error.stackTraceLimit = 0;
                  errors.push(
                      path.get('init').buildCodeFrameError(`无法把${valueType}赋值给${variableType}`, Error)
                  );
              }  
            }
        },
        post(file) {
            console.log(...file.get('errors'));
        }
    }
}

let sourceCode = `
  var age:number;
  age = "12";
`;

const result = babel.transform(sourceCode, {
    parserOpts:{plugins:['typescript']},
    plugins: [tscCheckPlugin()]
})
console.log(result.code);

5.9.4 泛型 #

const babel = require('@babel/core');
function transformType(type){
    switch(type){
        case 'TSNumberKeyword':
        case 'NumberTypeAnnotation':
            return 'number'
        case 'TSStringKeyword':
        case 'StringTypeAnnotation':
            return 'string'
    }
}
const tscCheckPlugin = () => {
    return {
        pre(file) {
            file.set('errors', []);
        },
        visitor: {
            CallExpression(path,state){
              const errors = state.file.get('errors');
              const trueTypes = path.node.typeParameters.params.map(param=>transformType(param.type));
              const argumentsTypes = path.get('arguments').map(arg=>transformType(arg.getTypeAnnotation().type));
              const calleePath = path.scope.getBinding(path.get('callee').node.name).path;
              const genericMap=new Map();  
              calleePath.node.typeParameters.params.map((item, index) => {
                genericMap[item.name] = trueTypes[index];
              });
              const paramsTypes =  calleePath.get('params').map(arg=>{
                const typeAnnotation = arg.getTypeAnnotation().typeAnnotation;
                if(typeAnnotation.type === 'TSTypeReference'){
                    return genericMap[typeAnnotation.typeName.name];
                }else{
                    return transformType(type);
                }
              });
              Error.stackTraceLimit = 0;
              paramsTypes.forEach((type,index)=>{
                  console.log(type,argumentsTypes[index]);
                if(type !== argumentsTypes[index]){
                    errors.push(
                        path.get(`arguments.${index}`).buildCodeFrameError(`实参${argumentsTypes[index]}不能匹配形参${type}`, Error)
                    );
                }
              }); 
            }
        },
        post(file) {
            console.log(...file.get('errors'));
        }
    }
}

let sourceCode = `
  function join<T>(a:T,b:T):string{
      return a+b;
  }
  join<number>(1,'2');
`;

const result = babel.transform(sourceCode, {
    parserOpts:{plugins:['typescript']},
    plugins: [tscCheckPlugin()]
})
console.log(result.code);

5.9.5 类型别名 #

const babel = require('@babel/core');
function transformType(type){
    switch(type){
        case 'TSNumberKeyword':
        case 'NumberTypeAnnotation':
            return 'number'
        case 'TSStringKeyword':
        case 'StringTypeAnnotation':
            return 'string'
        case 'TSLiteralType':
            return 'literal';
        default:
        return type;
    }
}
const tscCheckPlugin = () => {
    return {
        pre(file) {
            file.set('errors', []);
        },
        visitor: {
            TSTypeAliasDeclaration(path){
                const typeName  = path.node.id.name;
                const typeInfo = {
                    typeParams:path.node.typeParameters.params.map(item =>item.name),//['K']
                    typeAnnotation:path.getTypeAnnotation()//{checkType,extendsType,trueType,falseType}
                }
                path.scope.setData(typeName,typeInfo)
            },
            CallExpression(path,state){
              const errors = state.file.get('errors');
              const trueTypes = path.node.typeParameters.params.map(param=>{
               //TSTypeReference   typeName=Infer  typeParameters=[]
                if(param.type === 'TSTypeReference'){
                    const name = param.typeName.name;//Infer
                    const {typeParams,typeAnnotation} = path.scope.getData(name);//typeParams=['K']
                    const trueTypeParams = typeParams.reduce((memo, name, index) => {
                        memo[name] = param.typeParameters.params[index].type;//TSLiteralType
                        return memo;
                    },{}); //trueTypeParams={K:'TSLiteralType'}
                    const {checkType,extendsType,trueType,falseType} = typeAnnotation;
                    let check=checkType.type;
                    if(check === 'TSTypeReference'){
                        check = trueTypeParams[checkType.typeName.name]
                    }
                    if (transformType(check) === transformType(extendsType.type)) {
                        return transformType(trueType.type);
                    } else {
                        return transformType(falseType.type);
                    }
                }else{
                    return  transformType(param.type);
                }
              });
              const argumentsTypes = path.get('arguments').map(arg=>transformType(arg.getTypeAnnotation().type));
              const calleePath = path.scope.getBinding(path.get('callee').node.name).path;
              const genericMap=new Map();  
              calleePath.node.typeParameters.params.map((item, index) => {
                genericMap[item.name] = trueTypes[index];
              });
              const paramsTypes =  calleePath.get('params').map(arg=>{
                const typeAnnotation = arg.getTypeAnnotation().typeAnnotation;
                if(typeAnnotation.type === 'TSTypeReference'){
                    return genericMap[typeAnnotation.typeName.name];
                }else{
                    return transformType(type);
                }
              });
              Error.stackTraceLimit = 0;
              paramsTypes.forEach((type,index)=>{
                if(type !== argumentsTypes[index]){
                    errors.push(
                        path.get(`arguments.${index}`).buildCodeFrameError(`实参${argumentsTypes[index]}不能匹配形参${type}`, Error)
                    );
                }
              }); 
            }
        },
        post(file) {
            console.log(...file.get('errors'));
        }
    }
}

let sourceCode = `
    type Infer<K> = K extends 'number' ? number : string;
    function sum<T>(a: T, b: T) {

    }
    sum<Infer<'number'>>(1, 2);
`;

const result = babel.transform(sourceCode, {
    parserOpts:{plugins:['typescript']},
    plugins: [tscCheckPlugin()]
})
console.log(result.code);