使用UglifyJS批量修改JavaScript源代码
接到主管的任务安排:统计每个接口的端到端耗时,及接口请求成功的回调函数执行耗时。只要求先在测试环境中收集一些数据,然后决定是否部署上线。
好在框架在接口这一层做了封装,每一个远程接口都是RemoteInterface类的实例。所以只要在这个文件中的发送请求前,接收到响应后,和调用回调函数完成这三处进行埋点计时即可,之后把这三个点的时间戳相减,然后发送一条pingback log到服务器即可完成数据初步收集。
唯一不好处理的是,希望在发送的log中同时能携带接口的名字。但接口的名字存储在调用RemoteInterface实例的那一层,上层在创建RemoteInterface实例和调用send方法时都没有传入接口名字。remoteInterface对象内部拿不到上层的接口名字。现在要想办法能给remoteInterface对象加属性_name_
当然可以手动改上层代码,但是涉及到的文件太多,手动改的话,纯粹体力耐力劳动!而且,如果以后停止统计了,那可能还要在手动一个一个的删除。所以就想到用UglifyJS进行批量修改(包括RemoteInterface类的那三处埋点计时和发送代码)。
任务变成:找到所有的new RemoteInterface语句,在这条语句的后面,加一条语句,给new操作返回的对象加一个属性name,值为文件名。
先贴代码,再描述方案:
define(function (require, exports, module) { var RemoteInterface = require('../kit/remoteInterface'); module.exports = Q.Class('partnerVideoListInterface', { construct:function (param) { this._remoteInterface = new RemoteInterface({ partnerVideoListData: {} }); this._remoteInterface_pps = new RemoteInterface({ partnerVideoListData_pps: {} }); } }); });
现在希望给每个new RemoteInterface({})
返回的对象加一个属性`_name_
,其值为文件名,以便在统计RemoteInterface性能时能从实例内部拿到文件名。期待结果(第8行和第12行)如下:
define(function (require, exports, module) { var RemoteInterface = require('../kit/remoteInterface'); module.exports = Q.Class('partnerVideoListInterface', { construct:function (param) { this._remoteInterface = new RemoteInterface({ partnerVideoListData: {} }); this._remoteInterface._name_ = "PartnerVideoListInterface" this._remoteInterface_pps = new RemoteInterface({ partnerVideoListData_pps: {} }); this._remoteInterface_pps._name_ = "PartnerVideoListInterface" } }); });
解决思路就是通过UglifyJS提供的语法树API来定位到new RemoteInterface()
,然后在这条语句的后面加一条语句(第8行和第12行,手动加代码也行,但是文件数量太多,能在部署系统中搞定最佳)。
UglifyJS主要有两种语法树的操作API:一种是TreeWalker,用于遍历读取节点信息;一种是TreeTransformer,继承TreeWalker,用于修改语法树。
细化一下方案:
从define函数调用
AST_Call
为入口(因为define
是全局变量),找到实参列表args
的最后一个参数factory函数的第一个形参argnames[0]
,从而定位到require
形参变量的声明(类型为AST_SymbolFunarg
)。找到对require的函数调用,且参数(文件路径)的文件名部分为"remoteInterface",并定位到所赋给的变量名"RemoteInterface"。
找到所有以new形式调用第2步找到的"RemoteInterface"的
AST_New
节点,并向上定位该行语句,new调用结果所赋給的变量名,该行语句所在的语句块。新建一条赋值语句,右边为字符串常量文件名,左边为
.
属性访问操作,.
右边属性为name,左边为上面一步所记录的new调用结果所赋给的变量名的拷贝。语句块的body是一个数组,数组元素为每一条语句。可以通过indexOf获取语句在body数组的位置,然后在这个位置之后插入地4步创建的语句。
批处理代码如下:
var Fs = require("fs"); var Path = require("path"); var UglifyJS = require("uglify-js"); var hooker = process.hooker hooker.on("before-art-template-preprocess", function(path){ var code = Fs.readFileSync(path, "utf-8"); if(code && code.indexOf("remoteInterface") == -1){ return; } var astToplevel; try{ astToplevel = UglifyJS.parse(code); }catch(e){ console.log("in " + path); throw e; } astToplevel.figure_out_scope(); var remoteInterfaceWalker = new UglifyJS.TreeWalker(function(node, descend){ //找到define if(node instanceof UglifyJS.AST_Call && node.expression.name == "define"){ requireNode = remoteInterfaceVarDef = remoteInterfaceSymbolVar = remoteInterfaceNew = null; var factoryNode = node.args && node.args.length > 0 && node.args[node.args.length -1]; //找到define的最后一个参数factory函数的第一个参数require的变量名 及作用域 if(factoryNode instanceof UglifyJS.AST_Function && factoryNode.argnames && factoryNode.argnames.length > 0){ requireNode = factoryNode.argnames[0]; } } //找到require "remoteInterface"赋值语句,并跟踪所赋给的变量名 及作用域 else if(node instanceof UglifyJS.AST_VarDef && node.value instanceof UglifyJS.AST_Call && node.value.expression instanceof UglifyJS.AST_SymbolRef && node.value.expression.name == requireNode.name && node.value.expression.scope == requireNode.scope){ var args = node.value.args; if(args && args.length > 0 && args[0] instanceof UglifyJS.AST_String && args[0].value && args[0].value.indexOf("/remoteInterface") !== -1){ remoteInterfaceVarDef = node; remoteInterfaceSymbolVar = node.name; } } //找到 this.remoteInterface = new RemoteInterface(...)语句,在之后再加一句 // this.remoteInterface._name_ = filename语句 } });
未完待续...
blog comments powered by Disqus