AST

263 阅读4分钟

JavaScript Parser

JavaScript Parser是把JavaScript源码转化为抽象语法树的解析器

  • esprima
  • traceur
  • acorn
  • shift

可以在这些Parser提供的一系列回调中添加一些操作,来改变js源码:

//它可以把源代转成抽象语法树
let esprima = require('esprima');
//它可遍历语法权树,修改树上的语法节点
let estraverse = require('estraverse');
let escodegen = require('escodegen');
let sourceCode = 'function ast(){}';
let ast = esprima.parse(sourceCode);
let indent = 0;
const padding = () => ` `.repeat(indent);
let visitor = {
    enter(node, parent) {
        console.log(padding() + node.type);
        if (node.type === 'FunctionDeclaration') {
            node.id.name = 'newFunction';
        }
        indent++;
    },
    leave(node, parent) {
        indent--;
        console.log(padding() + node.type);
    }
}
estraverse.traverse(ast,visitor)
//重新生成源代码
let newSourceCode = escodegen.generate(ast);
console.log(newSourceCode);

ast在webpack中的应用

babel就可以通过上述ast转换parser来实现

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
    • IDE 的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2 等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具 webpack、rollup 等等
    • CommonJS、AMD、CMD、UMD 等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX 等转化为原生 Javascript

babel相关插件及功能

  • @babel/parser 可以把源码转换成AST
  • @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
  • @babel/generate 可以把AST生成源码,同时生成sourcemap
  • @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
  • @babel/template可以简化AST的创建逻辑
  • @babel/code-frame可以打印代码位置
  • @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
    • babel-core包括三部分

1.把源代码转成AST语法

2.遍历AST语法树,遍历的时候 会把语法树给插件进行处理。插件可以关注自己感兴趣的类型,进行处理

3.新的AST语法树重新生成源代码

使用案例(以箭头函数转换为例):

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let arrowFunctionPlugin = require('babel-plugin-transform-es2015-arrow-functions');
let arrowFunctionPlugin = {
    visitor: {
        //如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
        ArrowFunctionExpression(path) {
            let node = path.node;
            hostFunctionEnvironment(path);
            node.type = 'FunctionExpression';
        }
    }
}
/**
 * 1.要在函数的外面声明一个_this变量,值是this
 * 2.在函数的内容,换this 变成_this
 * @param {*} path 
 */
function hostFunctionEnvironment(path) {
    //确定我的this变量在哪个环境里生成,向上查找 是普通函数或者是根节点 Program
    const thisEnvFn = path.findParent(parent => {
        return (parent.isFunction() && !path.isArrowFunctionExpression()) || parent.isProgram();
    });
    let thisBindings = '_this';
    //var _this = this;
    if (!thisEnvFn.scope.hasBinding(thisBindings)) {
        thisEnvFn.scope.push({
            id: types.identifier(thisBindings),//_this
            init: types.thisExpression()//this
        });
    }
    //替换this
    let thisPaths = getScopeInfo(path);
    thisPaths.forEach(thisPath => {
        //把this替换成_this
        thisPath.replaceWith(types.identifier(thisBindings));
    })
}
function getScopeInfo(path) {
    let thisPaths = [];
    path.traverse({
        ThisExpression(path) {
            thisPaths.push(path);
        }
    })
    return thisPaths;
}
let sourceCode = `
const sum = (a, b) => {
    console.log(this);
    const minus = (c,d)=>{
          console.log(this);
        return c-d;
    }
    return a + b;
}
`;
let targetSource = core.transform(sourceCode, {
    plugins: [arrowFunctionPlugin]
});

console.log(targetSource.code);

上述代码中也实现了一个小型箭头函数的插件,其原理为遍历整个AST,当遇到ArrowFunctionExpression这类节点时将其转换成FunctionExpression类节点

如果要替换this,在外面存一个_this,可以继续拦截ThisExpression钩子

babel插件举例(babel-plugin-import)

babel-plugin-import的作用是做按需加载,例如某个js文件引用了lodash里的两个方法:

import { flatten, concat } from 'lodash';
console.log(flatten, concat);

但打包后整个lodash被引了进来,所以文件很大,babel-plugin-import可以将上述代码转换为

import flatten from 'lodash/flatten';
import concat from 'lodash/concat';
console.log(flatten, concat);

即只引入单个方法

类似的场景还有antd在使用时会直接引antd整个包,我们也可以通过这个插件将其优化成只引用到的组件

const path = require('path');
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    devtool: false,
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        plugins: [
                            ['import', {
                              	//指定要按需加载的模块
                            		libraryName: 'lodash',
                              	//按需加载的目录,默认是lib
                              	libraryDirectory: ''
                            }]
                        ]
                    }
                }
            }
        ]
    }
}

如果想要自己写一个babel插件,可以按如下方式使用:

    module: {
        rules: [
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        plugins: [
                            [path.resolve(__dirname, 'import.js'), {
                                //指定要按需加载的模块
                                "libraryName": "lodash",
                                //按需加载的目录,默认是lib
                                "libraryDirectory": ""
                            }]
                        ]
                    }
                }
            }
        ]
    }

可以看到,在原来写插件名字的地方改成了path.resolve(__dirname, 'import.js'),作为自定义插件的目录

在对应的import.js中,插件定义格式如下:

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");

const 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 declarations = specifiers.map(specifier => {
                //返回一个importDeclaration节点
                return types.importDeclaration(
                    //导入声明importDefaultSpecifier flatten
                    [types.importDefaultSpecifier(specifier.local)],
                    //导入模块source lodash/flatten
                    types.stringLiteral(libraryDirectory ? `${libraryName}/${libraryDirectory}/${specifier.imported.name}` : `${libraryName}/${specifier.imported.name}`)
                );
            })
            path.replaceWithMultiple(declarations);//替换当前节点
        }
    }
}


module.exports = function () {
    return {
        visitor
    }
}