JavaScript抽象语法树使用之编译时修改源码

2018-01-11  本文已影响0人  藿香正气五花儿

写在开始前:如有不准确的地方希望大家提出,文章可以改知识不能错。

相关概念

抽象语法树(AST),nodejs,UglifyJS,gulp,through2搜索引擎输入相关关键字会有很多文章这里就不一一阐述。
UglifyJS http://lisperator.net/uglifyjs/
gulp https://www.gulpjs.com.cn/
through https://cnodejs.org/topic/59f220b928137001719a8270
抽象语法树(AST) http://www.iteye.com/news/30731

待解决的问题

编码过程中难免会写一些console.log...输出语句用于测试,如果未及时删除发布后会出现莫名其妙的输出打印在控制台,这里叫它们幽灵输出。

解决方案

1 封装输出方法

造个轮子?
定义一个统一的日志输出方法,通过设置开关变量来控制输出打印

...
function printLog(src){
  if(dev){
    console.log(src);
  }
}
...
缺陷:
  1. 需要对源码进行统一更改,新加入人员需要学习相关api如果出现遗漏也是会出现幽灵输出问题。
  2. 通过if语句配合开关变量进行控制,在发布前需要更改开关变量值,代码中会出现大量的判断语句影响代码效率[虽说可以忽略但总是存在的]。

2 重构console

轮子不圆改一改嘛
为避免统一定义打印方法使用遗漏问题,决定重新定义系统console

let oldInfo=console.info;
console.info=function(){
  if(dev){
   oldInfo.apply(console,arguments);
  }
console.log=...
  ...
}
缺陷
  1. 与解决方案1存在相同问题,if语句判断,开关变量。
  2. 我重写了info你却用了log。

这里来了一个新需求,希望在开发状态打印方法参数值,方法执行顺序,发布状态不打印并清理无用输出。
这里需要一个全局拦截器,重构,造轮子已然无望。

3 写一个编译脚本做一点儿不可描述的事

源码-->编译-->可执行文件
在编译时向源码中注入一段代码实现拦截器,或者删除无用代码。

3.1 需要做什么

以下面这段代码为例说一下需要做什么

function test(id, name) {
    console.log('This is a demo ' + id + ' ' + name);
    return;
}
function test111(id, name) {
    console.log('This is a demo ' + id + ' ' + name);
    return;
}

function doTest() {
    test111(4, 5);
    test(6, 7);
    test111(4, 5);
}

doTest();
3.1.1 开发模式

在每一个方法体内部增加日志打印语句打印方法名称,方法参数,方法调用编号(累加,标示方法运行顺序);

function test(id, name) {
 //[TODO 这里需要增加输出代码]
    console.log('This is a demo ' + id + ' ' + name);
    return;
}
function test111(id, name) {
 //[TODO 这里需要增加输出代码]
    console.log('This is a demo ' + id + ' ' + name);
    return;
}

function doTest() {
 //[TODO 这里需要增加输出代码]
    test111(4, 5);
    test(6, 7);
    test111(4, 5);
}

doTest();
3.1.2 发布模式

找到输出语句删除;

function test(id, name) {
    console.log('This is a demo ' + id + ' ' + name);// 删除
    return;
}
function test111(id, name) {
    console.log('This is a demo ' + id + ' ' + name);// 删除
    return;
}

function doTest() {
    test111(4, 5);
    test(6, 7);
    test111(4, 5);
}

doTest();
3.2 开始

创建一个项目来实现这个需求

3.2.1 项目结构
image.png
libs 资源库
dev 开发模式编译输出
dist 发布模式编译输出
src 源码目录
build.js 编译主调度文件
global.js 全局资源定义文件
3.2.2 遍历文件策略

对于语法遍历总是需要一定的规则的,如果真的有人和你说无脑遍历文件来个正则过滤这样的的方案请对他好一些。

1) 古人教导我们先种一棵树

需要理解一个概念抽象语法树
使用UglifyJS来对代码进行转换与遍历。部分如下:

global.js文件

global.U2 = require('uglify-js');
global.gulp = require('gulp');
global.through = require('through2');
global.del = require('del');
global.util = require("./libs/util");

util.js

module.exports = {
    splice_string: function (str, begin, end, replacement) {
        if (replacement.length > 0) {
            return str.substr(0, begin) + replacement + str.substr(end);
        } else {
            return str.substr(0, begin) + replacement + str.substr(end + 1);
        }
    }
}

build_dev.js

//开发编译策略
module.exports = {
    doBuild: function (code) {
        console.log("[Dev] Building Code ");
        return through.obj(function (file, enc, cb) {
            let addLog_nodes = [];
            //获得文件内容
            let code = file
                .contents
                .toString();
            //将源码转换成语法树
            let ast = U2.parse(code);
            //遍历树
            ast.walk(new U2.TreeWalker(function (node) {
                if (node.body && node.name) {
                    //找到方法定义并放到等更改数组中
                    addLog_nodes.push(node);
                }
            }));
            let runindexstr = `\n\tif (global.runIndex) { global.runIndex++; } else {global.runIndex = 1; }\t`;
            //遍历待修改数组注入代码
            for (var j = addLog_nodes.length; --j >= 0;) {
                var conStr = `\n\tconsole.log('['+global.runIndex+']\t方法名称:${addLog_nodes[j].name.print_to_string()} 参数列表:'+`;
                var node = addLog_nodes[j];
                var start_pos = node.start.pos;
                var end_pos = node.end.endpos;
                if (node.argnames.length > 0) {
                    for (var k = node.argnames.length; --k >= 0;) {
                        conStr += `'${node.argnames[k].print_to_string()} = '+${node.argnames[k].print_to_string()}+','+`;
                    }
                } else {
                    conStr += `'无',`;
                }
                conStr = runindexstr + conStr.substring(0, conStr.length - 1) + ");\n\t";
                code = util.splice_string(code, start_pos + (node.print_to_string().indexOf("{")) + 3, start_pos + (node.print_to_string().indexOf("{")) + 3, conStr);
            }
            file.contents = new Buffer(code);
            //写入转换后文件
            this.push(file);
            cb();
        });
    }
}

build_dist.js

//发布编译策略
module.exports = {
    doBuild: function () {
        console.log("[Dist] Building Code ");
        return through.obj(function (file, enc, cb) {
            let console_nodes = [];
            //读取文件内容
            let code = file
                .contents
                .toString();
            //将代码转换成语法树
            let ast = U2.parse(code);
            //语法树遍历
            ast.walk(new U2.TreeWalker(function (node) {
                //找到console语句放到待处理数组中
                if (node &&node.expression) {
                    if (node.expression.print_to_string().indexOf('console') > -1) {
                        console_nodes.push(node);
                    }
                }
            }));
            //遍历待处理数组进行代码删除
            for (var i = console_nodes.length; --i >= 0;) {
                var node = console_nodes[i];
                var start_pos = node.start.pos;
                var end_pos = node.end.endpos;
                var replacement = "";
                // var replacement = "console.log('add code');\n\t" + node.print_to_string();
                code = util.splice_string(code, start_pos, end_pos, replacement);
            }
            //压缩代码
            code = U2
                .minify(code)
                .code;
            file.contents = new Buffer(code);
            //将文件装入实体
            this.push(file);
            cb();
        });
    }
}

build.js

require("./global");
const build_dev = require("./libs/build_dev");
const build_dist = require("./libs/build_dist");
var buildType = "dev";
if (process.argv.length < 3) {
    buildType = "dev";
} else {
    buildType = process.argv[2];
}
/**
 * 清理输出任务
 */
gulp
    .task("clean:dist", function (cb) {
        if (buildType == "dev") {
            del([process.cwd() + "/dev/*"], cb);
        } else {
            del([process.cwd() + "/dist/*"], cb);
        }
        cb();
    });

/**
 * 代码更改任务
 */
gulp.task("modifyCode", function (cb) {
    if (buildType == "dev") {
        gulp
            .src('./src/*.js')
            .pipe(build_dev.doBuild())
            .pipe(gulp.dest(process.cwd() + "/dev"));
    } else {
        gulp
            .src('./src/*.js')
            .pipe(build_dist.doBuild())
            .pipe(gulp.dest(process.cwd() + "/dist"));
    }
    cb();
});

/**
 * 入口任务
 */
gulp.start("modifyCode", ["clean:dist"], function (error, msg) {
    console.info("successfull");
    if (error) {
        console.info(error);
    }
});

写在最后

关于拦截器部分实现,只是实现了一个通用的拦截器,考虑升级实现类似spring 注解形式的定向注入模式,后续会发布相关解决方案。

上一篇下一篇

猜你喜欢

热点阅读