-
Notifications
You must be signed in to change notification settings - Fork 2
Description
思路:我们知道Uglifyjs可以混淆代码,那么它必然有一个js的解析器,并能对解析后的AST树进行调整,再用js生成器输出为js代码。同样的Babel作为一个强大的js预处理工具,它也必然有一个js的解析器,并能对解析后的AST树进行调整,再用js生成器输出为js代码。
在Uglifyjs的官网上示意了Uglifyjs的解析器的使用,
JS解析器为UglifyJS.parseJS代码生成器为UglifyJS.parse("").print_to_string()或UglifyJS.parse("").print(UglifyJS.OutputStream())
写测试代码如下
var UglifyJS = require('uglify-js')
var code = 'function foo() {\n\
function x(){}\n\
function y(){}\n\
}\n\
function bar() {}'
var ast = UglifyJS.parse(code)
var walker = new UglifyJS.TreeWalker(function (node) {
console.log(node.print_to_string());
if (node instanceof UglifyJS.AST_Defun) {
// string_template is a cute little function that UglifyJS uses for warnings
console.log(
UglifyJS.string_template('Found function {name} at {line},{col}', {
name: node.name.name,
line: node.start.line,
col: node.start.col
})
)
}
})
ast.walk(walker)感觉得到的AST树结构不那么好理解和操作,是否有更好理解和操作的AST树,看看Babel吧
在Babel的项目 https://github.com/babel/babel 中发现它使用的解析器是 babylon,生成器是babel-generator
进入到Babylon项目 https://github.com/babel/babylon 发现它是基于 acorn 扩展的,并提到它的 AST 又扩展了 ESTree 标准。
进入 Acorn 项目 https://github.com/ternjs/acorn 发现它的示例代码中用到的生成器是 Escodegen 。
进入 Escodegen 项目 https://github.com/estools/escodegen 知道可以把 Mozilla's Parser API 格式的AST 生成js代码,在它的示例中用的js解析器是 esprima
进入到 esprima 项目 https://github.com/jquery/esprima 没有新的发现
在网上搜索 这几个关键词,找到一篇文章
babel/babel#3921
又知道了另外一个解析器及生成器,
综上,我们可以得到几种js解析为ast及ast生成js的几组工具。
1、UglifyJS 自带解析器、遍历器、生成器
2、babylon + babel-traverse + babel-generator
3、babylon +babylon-to-espree+escodegen
4、acorn + escodegen
5、esprima +estraverse+ escodegen
6、shift-parser + shift-codegen
可能还有
代码生成器 patrisika 没有再细究了。
看来 Uglifyjs 是较少被使用的,可能因为它不是ESTree标准
解析器中
acorn 是最快的
babylon 是最慢的
生成器中
shift-codegen 是最快的
babel-generator 是最慢的
https://zhuanlan.zhihu.com/p/21620242
SiZapPaaiGwat/inhuman-cpc.github.io#106
http://wwsun.github.io/posts/javascript-ast-tutorial.html
Parser建议从Esprima开始学习,相比较于其它Parser文档和示例更加丰富和形象。
另外还可以使用 Estraverse https://github.com/estools/estraverse 来作节点的遍历和转换
esprima在线工具
http://esprima.org/demo/parse.html
从这篇文章中提到的 https://astexplorer.net/ 网站中发现好多种JS解析器
acorn acorn-to-esprima babylon babylon6 esformatter-parser espree esprima flow recast shift traceur typescript uglify-js
从文章 http://tech.meituan.com/abstract-syntax-tree.html
和
https://github.com/sinolz/amd2cmd/blob/master/src/AMD2CMDTransformer.js
还有
http://www.cnblogs.com/ziyunfei/p/3183325.html
会发现,代替更改的一般方式是对最终代码的替换
var U2 = require("uglify-js");
var parseint_nodes = [];
if (node instanceof U2.AST_Call
&& node.expression.print_to_string() === 'parseInt'
&& node.args.length === 1) {
parseint_nodes.push(node);
}
for (var i = parseint_nodes.length; --i >= 0;) {
var node = parseint_nodes[i];
var start_pos = node.start.pos;
var end_pos = node.end.endpos;
node.args.push(new U2.AST_Number({
value: 10
}));
var replacement = node.print_to_string({ beautify: true });
var start_pos = node.start.pos;
var end_pos = node.end.endpos;
code = code.substr(0, start_pos) + replacement + code.substr(end_pos);
}var parse = require("acorn").parse;
var result = new StringEditor(this.content);//注意,这个StringEditor类的replace不是即时替换,调用toString后才替换
var defineExps=[]
if (expression.type === 'CallExpression' &&
expression.callee.name === 'define') {
defineExps.push(exps);
}
for (const exp of defineExps) {
result.replace(exp.start, exp.end, content.substring(exp.start, exp.end)
.replace(/return/, 'module.exports =')));
}
result.toString();var collectedDatas = [];
if (comment.range[0] == commentRange[0] && comment.range[1] == commentRange[1]) {
var commentSourceRange = [commentRange[0] + 2, commentRange[1] - 2];
var commentSource = source.slice(commentSourceRange[0], commentSourceRange[1]);
var escapedCommentSource = ("'" + commentSource.replace(/(?=\\|')/g, "\\") + "'").replace(/\s*^\s*/mg, "");
collectedDatas.push({
range: heredocCallExpression.range,
replaceString: escapedCommentSource
});
}
for (var i = collectedDatas.length - 1; i >= 0; i--) { //从后往前修改输入源码,就可以不用考虑偏移量的问题了
var range = collectedDatas[i].range;
var replaceString = collectedDatas[i].replaceString;
source = source.slice(0, range[0]) + replaceString + source.slice(range[1]);
}acorn acorn-to-esprima babylon babylon6 esformatter-parser espree esprima flow recast shift traceur typescript uglify-js
对这几个解析器解析出的AST再作观察,
TodoView=BackboneView()又以acorn和esprima可以配置让得到的AST数据比较简洁
其他解析器位置都存在range属性中,acorn可以设置为存在range属性同时存在start和end属性上,
acorn解析出的AST
{
"type": "Program",
"start": 0,
"end": 23,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 23,
"expression": {
"type": "AssignmentExpression",
"start": 0,
"end": 23,
"operator": "=",
"left": {
"type": "Identifier",
"start": 0,
"end": 8,
"name": "TodoView"
},
"right": {
"type": "CallExpression",
"start": 9,
"end": 23,
"callee": {
"type": "Identifier",
"start": 9,
"end": 21,
"name": "BackboneView"
},
"arguments": []
}
}
}
],
"sourceType": "module"
}esprima解析出的AST
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "TodoView",
"range": [
0,
8
]
},
"right": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "BackboneView",
"range": [
9,
21
]
},
"arguments": [],
"range": [
9,
23
]
},
"range": [
0,
23
]
},
"range": [
0,
23
]
}
],
"sourceType": "module",
"range": [
0,
23
]
}到时此我们可以定下要使用到的库
即只用 acorn 遍历有它自己的 acorn.walk 方法,因为使用字符替换,也就不需要用到遍历器了。
先转换作代码转换测试,找一两个典型性的,功能类似的的,分别用Backbone和React实现过的代码
http://todomvc.com/examples/backbone/
http://todomvc.com/examples/react/
下载里面涉及的源码
以下将把backbone写的view类todo-view.js转为React写的todoItem.js
/*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */
var app = app || {};
(function ($) {
'use strict';
// Todo Item View
// --------------
// The DOM element for a todo item...
app.TodoView = Backbone.View.extend({
//... is a list tag.
tagName: 'li',
// Cache the template function for a single item.
template: _.template($('#item-template').html()),
// The DOM events specific to an item.
events: {
'click .toggle': 'toggleCompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'keydown .edit': 'revertOnEscape',
'blur .edit': 'close'
},
// The TodoView listens for changes to its model, re-rendering. Since
// there's a one-to-one correspondence between a **Todo** and a
// **TodoView** in this app, we set a direct reference on the model for
// convenience.
initialize: function () {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'visible', this.toggleVisible);
},
// Re-render the titles of the todo item.
render: function () {
// Backbone LocalStorage is adding `id` attribute instantly after
// creating a model. This causes our TodoView to render twice. Once
// after creating a model and once on `id` change. We want to
// filter out the second redundant render, which is caused by this
// `id` change. It's known Backbone LocalStorage bug, therefore
// we've to create a workaround.
// https://github.com/tastejs/todomvc/issues/469
if (this.model.changed.id !== undefined) {
return;
}
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('completed', this.model.get('completed'));
this.toggleVisible();
this.$input = this.$('.edit');
return this;
},
toggleVisible: function () {
this.$el.toggleClass('hidden', this.isHidden());
},
isHidden: function () {
return this.model.get('completed') ?
app.TodoFilter === 'active' :
app.TodoFilter === 'completed';
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function () {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function () {
this.$el.addClass('editing');
this.$input.focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function () {
var value = this.$input.val();
var trimmedValue = value.trim();
// We don't want to handle blur events from an item that is no
// longer being edited. Relying on the CSS class here has the
// benefit of us not having to maintain state in the DOM and the
// JavaScript logic.
if (!this.$el.hasClass('editing')) {
return;
}
if (trimmedValue) {
this.model.save({ title: trimmedValue });
} else {
this.clear();
}
this.$el.removeClass('editing');
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function (e) {
if (e.which === ENTER_KEY) {
this.close();
}
},
// If you're pressing `escape` we revert your change by simply leaving
// the `editing` state.
revertOnEscape: function (e) {
if (e.which === ESC_KEY) {
this.$el.removeClass('editing');
// Also reset the hidden input back to the original value.
this.$input.val(this.model.get('title'));
}
},
// Remove the item, destroy the model from *localStorage* and delete its view.
clear: function () {
this.model.destroy();
}
});
})(jQuery);/*jshint quotmark: false */
/*jshint white: false */
/*jshint trailing: false */
/*jshint newcap: false */
/*global React */
var app = app || {};
(function () {
'use strict';
var ESCAPE_KEY = 27;
var ENTER_KEY = 13;
app.TodoItem = React.createClass({
handleSubmit: function (event) {
var val = this.state.editText.trim();
if (val) {
this.props.onSave(val);
this.setState({editText: val});
} else {
this.props.onDestroy();
}
},
handleEdit: function () {
this.props.onEdit();
this.setState({editText: this.props.todo.title});
},
handleKeyDown: function (event) {
if (event.which === ESCAPE_KEY) {
this.setState({editText: this.props.todo.title});
this.props.onCancel(event);
} else if (event.which === ENTER_KEY) {
this.handleSubmit(event);
}
},
handleChange: function (event) {
if (this.props.editing) {
this.setState({editText: event.target.value});
}
},
getInitialState: function () {
return {editText: this.props.todo.title};
},
/**
* This is a completely optional performance enhancement that you can
* implement on any React component. If you were to delete this method
* the app would still work correctly (and still be very performant!), we
* just use it as an example of how little code it takes to get an order
* of magnitude performance improvement.
*/
shouldComponentUpdate: function (nextProps, nextState) {
return (
nextProps.todo !== this.props.todo ||
nextProps.editing !== this.props.editing ||
nextState.editText !== this.state.editText
);
},
/**
* Safely manipulate the DOM after updating the state when invoking
* `this.props.onEdit()` in the `handleEdit` method above.
* For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
* and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
*/
componentDidUpdate: function (prevProps) {
if (!prevProps.editing && this.props.editing) {
var node = React.findDOMNode(this.refs.editField);
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}
},
render: function () {
return (
<li className={classNames({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.title}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
className="edit"
value={this.state.editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
);
}
});
})();打开 https://astexplorer.net/
和 https://github.com/ternjs/acorn
备查,开始写代码
第一个目标
Backbone.View.extend(
替换为
React.createClass(
var acorn = require('acorn')
var walk = require('acorn/dist/walk')
var fs = require('fs')
var file = 'backbone/todo-view.js'
var source = fs.readFileSync(file)
var ast = acorn.parse(source)
var collectedDatas = []
walk.simple(ast, {
CallExpression: function (node) {
// console.log(node)
if (
node.callee.property &&
node.callee.property.name === 'extend' &&
node.callee.object.property.name === 'View' &&
node.callee.object.object.name === 'Backbone'
) {
collectedDatas.push({
start: node.callee.start,
end: node.callee.end,
newCode: 'React.createClass'
})
}
}
})
for (var i = collectedDatas.length - 1; i >= 0; i--) {
// 从后往前修改输入源码,就可以不用考虑偏移量的问题了
var start = collectedDatas[i].start
var end = collectedDatas[i].end
var newCode = collectedDatas[i].newCode
source = source.slice(0, start) + newCode + source.slice(end)
}
console.log(source)替换成功,可以进行更多的处理了。
在最后作字符的替换时要求collectedDatas中要处理的位置是严格按照先后顺序的,这一点处理复杂情况时显然无法保证,只所以封装了一个字符串处理类。
/**
* 字符串操作类,在调用replace时不会真正的替换,而只是记录要替换的位置,在调用toString时才进行替换,并返回替换后的新的字符串
* @param {String} content
* @param {Number} [start=0]
* @param {Number} [end=content.length]
*/
var StringEditor = function (content, start, end) {
this.content = content
this.start = start || 0
this.end = end || content.length
this.replaceActions = []
}
StringEditor.prototype = {
/**
* 记录要替换的字符的位置和新字符串
* @param {Number} start
* @param {Number} end
* @param {String|StringEditor} newContent
*/
replace: function (start, end, newContent) {
this.replaceActions.push({
range: [start, end],
newContent
})
},
/**
* 同String.prototype.slice
* @param {Number} start
* @param {Number} end
* @param {String} content
*/
slice: function (start, end) {
return this.content.slice(start, end)
},
/**
* 完成字符串替换并返回结果
* @returns {String} newContent
*/
toString: function () {
var value = ''
var start = this.start
this.replaceActions = this.replaceActions.sort(function (a, b) {
return a.range[0] - b.range[0]
})
for (var action of this.replaceActions) {
value += this.content.substring(start, action.range[0])
value += action.newContent.toString()
start = action.range[1]
}
value += this.content.substring(start, this.end)
return value
}
}涉及到lodash模板转为jsx,需要引入一个不依赖bom的html操作工具,
网上搜索一下,找到一个12034个star的项目 https://github.com/cheeriojs/cheerio
在转换lodash到jsx中发现cheerio有一个问题,期望得到
<li class={classNames({completed:this.props.model.get('completed')})}>实际得到
<li class="{classNames({completed:this.props.model.get('completed')})}">经翻源代码发现拼接html的方法位于node_modules\dom-serializer\index.js,
对个js文件中的formatAttrs()作调整,
if (!value && booleanAttributes[key]) {
output += key;
} else {
output += key + '="' + (opts.decodeEntities ? entities.encodeXML(value) : value) + '"';
}改为
if (!value && booleanAttributes[key]) {
output += key;
} else if(opts.jsx && value.startsWith('{') && value.endsWith('}')) {
output += key + '=' + value;
} else {
output += key + '="' + (opts.decodeEntities ? entities.encodeXML(value) : value) + '"';
}对个js文件中的renderText()作调整,
if (opts.decodeEntities && !(elem.parent && elem.parent.name in unencodedElements)) {
data = entities.encodeXML(data);
}改为
if (opts.decodeEntities && !(elem.parent && elem.parent.name in unencodedElements) && !opts.jsx) {
data = entities.encodeXML(data);
}加配置项{jsx:true}时,以{开始、以}结束的属性,不用双引号引起来,并具不对其中的特殊符进行编码。
终于成功将一个BackboneView类转为React类,但其中写了很多特例处理,代码并不通用,据此得出的结论是,Backbone View类自动转React类可以有限地减少手工修改的工作量(30%~60%)但无法代替手工修改