引言
“代码剖析转换”原本在前端开发中是一个比拟小众的技能树,我所在的阿里妈妈前端技术团队(MUX)也是在大量业务的迁徙架构的过程中遇到了须要批量转换代码的问题,所以对原理和工具进行了一些钻研,最近发现社区里不少对此的探讨的文章也失去了大家的关注,所以也打算在此多分享一些咱们的教训。
其实AST剖析的过程与每一位开发同学的工作都密不可分,小到一次eslint语法查看,大到框架的降级,都波及于此。简略、个别的转换能够通过人眼分别、手动批改,批量的简略转换能够通过正则匹配、字符串替换,但更简单的转换,基于AST是最无效的计划。
基于AST的代码剖析转换
AST简要介绍
形象语法树(Abstract Syntax Tree)简称 AST ,是以树状模式体现编程语言的语法结构,树上的每个节点都示意源代码中的一种构造。JavaScript引擎工作工作的第一步就是将代码解析为AST,Babel、eslint、prettier等工具都基于AST。
将源代码解析为AST分为两步,词法剖析和语法分析
1.词法剖析,将字符序列转为单词(Token)序列的过程。
2.语法分析,将Token序列组合成各类语法短语,如Program,Statement,Expression等
基于AST进行剖析转换的劣势
AST会疏忽代码格调,将代码解析为最纯正的语法树,因而基于AST进行的转换是更加精确谨严的,而应用正则表达式来剖析转换代码,无奈无效的剖析一段代码的上下文,即便对简略规定的匹配都须要思考太多边界状况,兼容各种代码格调。
举个简略的例子,将所有由var定义的变量转为let定义,基于AST能够很容易的实现,而应用正则须要思考各种状况,书写的表达式也难以调试、读懂。
基于AST转换的过程
- parse 源代码通过词法剖析、语法分析解析为AST
- transform 对AST进行剖析变换
- generate 将最终的AST输入为代码
其中1、3在社区都有成熟的工具,能够拿来即用。而第2步须要开发者本人操作AST,社区上有风行的工具,但存在肯定问题。
社区风行计划现状及问题
社区风行的计划有Babel、jscodeshift以及esprima、recast、acorn、estraverse等,本文抉择最具代表性的Babel和jscodeshift来剖析。
Babel插件计划的分析
没有Babel就没有JS社区明天在语言标准上的高度凋敝,babel/parser也是十分优良的解析器。很多开发者进行代码剖析转换时都离不开Babel插件,然而我集体认为Babel插件目前的编写计划上存在几个问题 1.上手难度高,学习老本高 2.匹配、生成节点的逻辑简单,代码量大 3.代码可读性差,不利于保护。具体而言:
1.上手难度、学习老本较高
上手Babel插件开发之前须要深刻理解AST标准,AST节点的类型和属性。参考 babel-types和babel node type,200多个节点类型。babelrc的配置、babel plugin的编写形式是根底,除此之外还要理解visitor、scope、state、excit、enter等概念、babel-types、babel-traverse、builder等工具。
2.匹配、结构节点的逻辑简单,代码量大
匹配节点,须要层层、一一比照节点类型和属性,如果须要确定上下文信息会更加简单。结构节点同样须要严格依照类型和构造来进行。须要在AST的操作上消耗大量工夫,无奈专一于剖析与转换的外围逻辑。
- 应用Babel匹配 self.doEdit('price')(this, '100') ,写法如下
MemberExpression(path) { if (path.node.object.name == 'self' && path.node.property.name == 'doEdit') { const firstCallExpression = path.findParent(path => path.isCallExpression()); if (!firstCallExpression) { return; } if (!firstCallExpression.node.arguments[0]) { return; } let secondCallExpression = null if (firstCallExpression.node.arguments[0].type == 'StringLiteral' && firstCallExpression.node.arguments[0].value == 'price') { secondCallExpression = firstCallExpression.findParent( path => path.isCallExpression() ) } if (!secondCallExpression) { return; } if (secondCallExpression.node.arguments.length != 2 || secondCallExpression.node.arguments[0].type != 'ThisExpression') { return; } const pId = secondCallExpression.node.arguments[0].value; }}复制代码
- 应用Babel结构'var varName = require("moduleName")',写法如下
types.variableDeclaration('var', [ types.variableDeclarator( //t.variableDeclarator(id, init) //id就是identifier //此处的init必须是一个Expression types.identifier('varName'), //t.callExpression(callee, arguments) types.callExpression( types.identifier('require'), [types.stringLiteral('moduleName')] ) ),]);复制代码
3.代码可读性差,不利于保护
看了下面两段例子,能够发现不仅代码量大,可读性也不够好,即便对AST和Babel十分相熟,也须要认真逐句进行了解。
jscodeshift的分析
相比于Babel而言,jscodeshift的劣势是匹配节点更简便一些,链式操作用起来更加棘手。
匹配self.doEdit('price')(this, '100'),写法如下
const callExpressions = root.find(j.CallExpression, { callee: { callee: { object: { name: 'self' }, property: { name: 'doEdit' } }, arguments: [{ value: 'price' }] }, arguments: [{ type: 'ThisExpression' }, { value: '100' }]})复制代码
转换和结构节点的形式与Babel写法相似,不再赘述。能够看出jscodeshift也没有很好的解决上文提到的三个问题。
于是在社区贵重的教训之上,咱们开发了新的工具GoGoCode。目标就是让开发者可能最高效率最低老本的实现代码剖析转换。
另一种计划 GoGoCode
概述
GoGoCode是一个操作AST的工具,能够升高应用AST的门槛,帮忙开发者从繁琐的AST操作中解放出来,更专一于代码剖析转换逻辑的开发。简略的替换甚至不必学习AST,而初步学习了AST节点构造(可参考AST查看器)后就能够实现更简单的剖析转换。
思维
GoGoCode借鉴了JQuery的思维,咱们的使命也是让代码转换像应用JQuery一样简略。JQuery在原生js的根底上大大便当了DOM操作的效率,没有简单的配置流程,能够拿来即用,而且有很多优良的设计思维值得借鉴:比方$()实例化、选择器思维、链式操作等。除此之外,咱们将简略的replace的思维利用在AST中,成果也很不错。
$()实例化办法
应用$(),源代码和AST节点都能够被实例化为AST对象,能够链式调用实例上挂载的任意函数
$(code: string)$('var a = 1')$(node: ASTNode)$({ type: 'Identifier', name: 'a' }).generate()复制代码
代码选择器
DOM树和AST树都是树结构,JQuery能够用各种选择器匹配节点,AST是不是也能够通过简略的选择器来匹配实在的节点呢?于是咱们定义了“代码选择器”
无论你想找什么样的代码,都能够通过代码选择器间接匹配到
$(code).find('import a from "./a"')$(code).find('function a(b, c) {}')$(code).find('if (a && sth) { }')复制代码
如果你想匹配的代码蕴含不确定局部
那就把不确定局部由通配符替换,通配符用$_$示意 。祝大家万事如意,恭喜发财 o(*≧▽≦)ツ
$(code).find('import $_$ from "./a"')$(code).find('function $_$(b, c) {}')$(code).find('if ($_$ && sth) { }')复制代码
链式操作
GoGoCode提供的api大部分都能链式调用,让代码变得更加简洁,优雅。更加不便咱们对整段代码进行多个转换规则的利用
$(sourceCode) .replace('const $_$1 = require($_$2)', 'import $_$1 from $_$2') .find('console.log()') .remove() .root() .generate()复制代码
办法重载:.attr()
既能够获取也能够批改节点属性,比手动遍历,层层判断来操作属性、节点敌对很多
$(code).attr('id.name') // 返回该节点id属性中的name属性值$(code).attr('declarations.0.id.name', 'c') // 批改name属性值复制代码
简略的replace
比通过正则进行replace更简略、更弱小、更好用。$_$n相似于正则中的捕捉组,$$$相似于rest参数
$(code).replace('{ text: $_$1, value: $_$2, $$$ }', '{ name: $_$1, id: $_$2, $$$ }')$(code).replace(`import { $$$ } from "@alifd/next"`, `import { $$$ } from "antd"`)$(code).replace(`<View $$$1>$$$2</View>`,`<div $$$1>$$$2</div>`)$(code).replace(`Page({ $$$1 })`, `Page({ init() { this.data = {} }, $$$1 })`)复制代码
外围API
根底api
获取节点api
操作节点
$()
.find()
.attr()
$.loadFile
.parent()
.replace()
.generate()
.parents()
.replaceBy()
.siblings()
.after()
.next()
.before()
.nextAll()
.append()
.prev()
.prepend()
.prevAll()
.empty()
.root()
.remove()
.eq()
.clone()
.each()
与社区风行计划比照
前文的例子中,匹配 self.doEdit('price')(this, '100')
语句 ,应用GoGoCode写法如下
$(code).find(`self.doEdit('price')(this, '100')`)复制代码
结构'var varName = require("moduleName")'
,应用GoGoCode写法如下
$('var varName = require("moduleName")')复制代码
以一个残缺的例子将GoGoCode和Babel插件进行比照:
对于以下这段代码,咱们心愿对不同的 console.log
做不同的解决
- 将
console.log
的调用删除 console.log()
作为变量初始值时转换为void 0
console.log
作为变量初始值时转换为空办法
代码经转换的后果如下:
应用GoGoCode实现的代码如下:
$(code) .replace(`var $_$ = console.log()`, `var $_$ = void 0`) .replace(`var $_$ = console.log`, `var $_$ = function(){}`) .find(`console.log()`) .remove() .generate();复制代码
应用Babel实现的外围代码如下:
// 代码起源:https://zhuanlan.zhihu.com/p/32189701module.exports = function({ types: t }) {return { name: "transform-remove-console", visitor: { CallExpression(path, state) { const callee = path.get("callee"); if (!callee.isMemberExpression()) return; if (isIncludedConsole(callee, state.opts.exclude)) { // console.log() if (path.parentPath.isExpressionStatement()) { path.remove(); } else { //var a = console.log() path.replaceWith(createVoid0()); } } else if (isIncludedConsoleBind(callee, state.opts.exclude)) { // console.log.bind() path.replaceWith(createNoop()); } }, MemberExpression: { exit(path, state) { if ( isIncludedConsole(path, state.opts.exclude) && !path.parentPath.isMemberExpression() ) { //console.log = func if ( path.parentPath.isAssignmentExpression() && path.parentKey === "left" ) { path.parentPath.get("right").replaceWith(createNoop()); } else { //var a = console.log path.replaceWith(createNoop()); } } } } }};复制代码
其中 isIncludedConsole、isIncludedConsoleBind、createNoop 等办法还需额定开发引入
能够看出,与社区工具比照,GoGoCode的劣势是:
- 上手难度低:不须要对所有AST节点标准一目了然,不须要晓得遍历、拜访AST的各种阶段,不须要额定的工具,只需浏览简略的GoGoCode文档。GoGoCode是惟一面向开发者而不是面向AST构造的AST解决工具。
- 代码量非常少:让你专一剖析与转换的外围逻辑,不在AST的操作上消耗大量工夫。无论是匹配、批改还是结构节点都很简略,几行代码就能搞定。
- 可读性很强:比照能够看出,基于GoGoCode写进去的代码很直观,很容易看懂,也更便于短暂的保护。
- 灵活性强:应用Babel插件和jscodeshift能做到的,GoGoCode都能够更便捷的做到。除了js之外,GoGoCode同时反对对html的解决,对vue的解决,这是社区其余风行工具所不具备的。
应用成果
基于GoGoCode初版本咱们开发了妈妈自研框架Magix的降级套件,蕴含78个简略规定、30个简单规定的转换,主动将Magix1代码(左)转换为Magix3代码(右),晋升了框架降级效率
其中一个20行左右的转换逻辑咱们曾尝试用Babel写,近200行代码才实现。
俗话说,磨刀不误砍柴工,在这里编写自动化转换规则是磨刀,施行转换是砍柴。如果磨刀的工夫靠近间接砍柴的工夫,那大家会抉择放弃磨刀。代码转换常常是解决咱们团队、零碎内的特定问题,少数状况下甚至是一次性的,(不能像ES6转ES5那样通过大规模的利用一套通用规定来摊派掉插件开发的老本)这就要求咱们磨刀的效率必须高。
近期咱们在进行支付宝小程序代码转PC框架代码的尝试,团队内对AST理解不多的同学经一小时就能够疾速上手,不到200行代码就实现了80%js逻辑的转换。可见无论是上手难度升高、效率晋升还是代码量减少都是很显著的。
总结
GoGoCode在代码量、可读性、灵活性方面都具备劣势,咱们会持续打磨,增强工具健壮性和易用性。心愿通过GoGoCode人人都能了解并操纵形象语法树,从而实现代码剖析转换逻辑,更好的掌控代码,实现一码多端、更顺畅的框架降级......同时心愿在相干畛域让更多同学可能最低老本的参加进来奉献本人的力量,给业界生态提供更好的解决方案。
除了前文提到的语法查看、一码多端、框架降级之外,还有很多场景须要剖析和转换代码
- 剖析页面或者视图与异步申请的关联
- 剖析模块复杂度
- 剖析模块依赖
- 清理无用代码
- 埋点代码主动生成
- 单测插桩文件生成
- 主动修改代码问题
- ......
如果你须要剖析、转换代码,如果你想疾速实现Babel现有插件不能满足的需要,欢送应用和共建GoGoCode。
如果你用 GoGoCode 不不便解决或者出了错,心愿你能提给咱们
QQ群:735216094 钉钉群:34266233
Github:https://github.com/thx/gogocode 新我的项目求 star 反对 o(_////▽////_)q
官网:gogocode.io
playground:play.gogocode.io/
相干文章:
阿里妈妈出的新工具,给批量批改我的项目代码加重了苦楚
「GoGoCode 实战」一口气学会 30 个 AST 代码替换小窍门
作者:阿里妈妈前端快爆
链接:https://juejin.cn/post/694566...
起源:掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。