1.babel是什么
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
例如:
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
babel预设和插件相关说明参考官方文档。
插件开发的详细教程在https://www.babeljs.cn/docs/plugins 最后 babel-handbook
1.1 babel处理步骤
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。.
解析
解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:**词法分析(Lexical Analysis) **和 语法分析(Syntactic Analysis)。.
需要使用库包含
npm install --save-dev @babel/types
npm install --save-dev @babel/parser;
词法分析
词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。.
你可以把令牌看作是一个扁平的语法片段数组:
n * n;
[
{ type: { … }, value: “n”, start: 0, end: 1, loc: { … } },
{ type: { … }, value: “*”, start: 2, end: 3, loc: { … } },
{ type: { … }, value: “n”, start: 4, end: 5, loc: { … } },
…
]
每一个 type 有一组属性来描述该令牌:
{
type: {
label: ‘name’,
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
…
}
和 AST 节点一样它们也有 start,end,loc 属性。.
语法分析
语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。
转换
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程 同时也是插件将要介入工作的部分,这将是本手册的主要内容, 因此让我们慢慢来。
需要使用库包含
npm install --save-dev @babel/types
npm install --save-dev @babel/traverse;
生成
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。.
代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
需要使用库包含
npm install --save-dev @babel/generator;
2. ast 是什么
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。
这个处理过程中的每一步都涉及到创建或是操作抽象语法树,亦称 AST。
Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在[这里](https://github. com/babel/babel/blob/master/doc/ast/spec. md)找到。.
2.1 分析代码ast结构
通过https://astexplorer.net/可以快速分析代码的ast结构,从而根据分析结构编写插件。
ast类型
想要转换 AST 你需要进行递归的树形遍历。
比如 :var i=1
ast树形结构为:

其中:
- type:VariableDeclarator表示一个变量装饰
- type: Identifier表示变量名称定义
- init 表示初始化一个type:NumeriLiteral的数字常量。
Visitors(访问者)
当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。.
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。
import * as parse from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import fs from 'fs';
import path from 'path'
import * as t from "@babel/types";
//解析步骤接收代码并输出 AST
var ast = parse.parse("var i=1;");
//当访问到Identifier的类型时进入该方法,pth.node表示节点数据
traverse.default(ast, {
//所有出现的type=="VariableDeclaration" 都进入以下方法
Identifier:(pth)=>{
console.log(pth.node.name)
}
}
)
State(状态)
状态是抽象语法树AST转换的敌人,状态管理会不断牵扯你的精力,而且几乎所有你对状态的假设,总是会有一些未考虑到的语法最终证明你的假设是错误的。
编写例子:将第二个参数替换为j
code="function add(i,i00){return (i+i00)/i00}"
实现代码
code="function add(i,i00){return (i+i00)/i00}"
ast=parse.parse(code)
console.log(ast);
traverse.default(ast,{
//首先将FunctionDeclaration的params[1].name修改为j,并且需要获取到参数名称替换掉方法内部的引用
FunctionDeclaration:(p1)=>{
//获取到第二个参数的名称
let oriName=p1.node.params[1].name;
p1.node.params[1].name="j"
p1.traverse({
//所有identify是否和传入的名字一致,一致就改写。
Identifier:(p2)=>{
if(p2.node.name==p2.state.oriName){
p2.node.name="j"
}
}
},{oriName})
}
})
//生成
var output = generate.default(ast,{},code);
console.log(output.code)
Scopes(作用域)
JavaScript 支持词法作用域,在树状嵌套结构中代码块创建出新的作用域。
// 全局作用域
function scopeOne() {
// 作用域1
function scopeTwo() {
//作用域2
}
}
在 JavaScript 中,每当你创建了一个引用,不管是通过变量(variable)、函数(function)、类型(class)、参数(params)、模块导入(import)还是标签(label)等,它都属于当前作用域。
var global = "我是全局作用域";
function scopeOne() {
var one = "我是在方法 `scopeOne()中创建的作用域`";
function scopeTwo() {
var two = "我是在方法 `scopeTwo()中创建的作用域";
}
}
更深的内部作用域代码可以使用外层作用域中的引用。
function scopeOne() {
var one = "我是在方法 `scopeOne()中创建的作用域``";
function scopeTwo() {
one = "在scopeTwo中更新 `scopeOne`的引用变量 ";
}
}
作用域的功能非常强大,比如找到变量path后可以通道引用找到所有他的引用和修改的位置。
作用域可以被表示为如下形式:
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...]
}
当你创建一个新的作用域时,需要给出它的路径和父作用域,之后在遍历过程中它会在该作用域内收集所有的引用(“绑定”)。
一旦引用收集完毕,你就可以在作用域(Scopes)上使用各种方法,稍后我们会了解这些方法。
Bindings(绑定)
所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding)。.
function scopeOnce() {
var ref = "这是一个binding";
ref; // 这是binding的一个引用
function scopeTwo() {
ref; // 这是binding的一个应用
var ref="这不是上面的binding的引用,是一个新的作用域"
}
}
单个绑定看起来像这样︰
Text for Translation
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path], //这个是给binding获取值的引用
constant: false,
constantViolations: [path] //这个是给binding设置值的引用
}
有了这些信息你就可以查找一个绑定的所有引用,并且知道这是什么类型的绑定(参数,定义等等),查找它所属的作用域,或者拷贝它的标识符。 你甚至可以知道它是不是常量,如果不是,那么是哪个路径修改了它。
在很多情况下,知道一个绑定是否是常量非常有用,最有用的一种情形就是代码压缩时。
function scopeOne() {
var ref1 = "This is a constant binding";
becauseNothingEverChangesTheValueOf(ref1);
function scopeTwo() {
var ref2 = "This is *not* a constant binding";
ref2 = "Because this changes the value";
}
}
以下来个小例子:将外层变量k1作用域使用的内容的替换成i
code="var k1=10;function set1(j){k1=j;k1=100;k1++;var s=k1;console.log(k1);console.log(k1);if(k1==100){k1++;}(function(){var k1=0;k1++;})();}set1(12);console.log(k1)"
ast=parse.parse(code)
console.log(ast);
i=0
traverse.default(ast,{
//首先将FunctionDeclaration的params[1].name修改为j,并且需要获取到参数名称替换掉方法内部的引用
VariableDeclaration:(p1)=>{
if(i==0){
p1.traverse({
Identifier:(p2)=>{
let nodeName=p2.node.name
p2.node.name="i";
//获取所有引用该变量的位置
for(let rp of p2.scope.bindings[nodeName].referencePaths){
rp.node.name="i";
}
//获取所有给变量赋值的位置
for(let rp of p2.scope.bindings[nodeName].constantViolations){
if(rp.type=="AssignmentExpression"){
rp.node.left.name="i"
}
}
}
})
i++;
}
}
})
//生成
output = generate.default(ast,{},code);
console.log(output.code)
最终输出的结果为:
var i = 10;
function set1(j) {
i = j;
i = 100;
i++;
var s = i;
console.log(i);
console.log(i);
if (i == 100) {
i++;
}
(function () {
var k1 = 0;
k1++;
})();
}
set1(12);
console.log(i);
API
Babel 实际上是一组模块的集合,可参考
https://www.babeljs.cn/docs/babel-types
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-stages-of-babel
以下来个小例子 :将以下混淆代码还原
var i=1,j=2;
var arr=["console","log","helloworld"];
arr[0][arr[1]](arr[2]);
最终应该输出
原始代码--------------:
babel/astrestore.js:147
var i=1,j=2;var arr=["console","log","helloworld"];arr[0][arr[1]](arr[2]);
babel/astrestore.js:148
生成之后代码--------------:
babel/astrestore.js:150
var i = 1,
j = 2;
console["log"]("helloworld");
代码参考:
/** 小例子练习4:读取source.js代码,还原(这里不要用scope去实现,就用遍历实现):
var i=1,j=2;
var arr=["log","helloworld"];
arr[0][arr[1]](arr[2]);
* **/
const codeBuff=fs.readFileSync(path.resolve('./')+"/babel/file/source.js")
code = codeBuff.toString('utf-8');
code = 'var i=1,j=2;var arr=["console","log","helloworld"];arr[0][arr[1]](arr[2]);'
ast=parse.parse(code)
var map=new Map();
traverse.default(ast,{
//遍历出所有的数组
VariableDeclarator:(p1)=>{
var varName=p1.node.id.name;
//如果是数组定义先存下来,然后删除节点
if(t.is("ArrayExpression",p1.node.init, {})){
map.set(varName,p1.node.init.elements)
p1.remove();
}
},
CallExpression:(p1)=>{
//获取函数调用参数是否有用到map中的数组
var args=p1.node.arguments
for(let [index, arg] of args.entries()){
//获取函数调用参数,如果参数是个表达式获取对象和属性,并通过之前数组定义拿到值
if(arg.type=="MemberExpression" && arg.object.type=="Identifier"){
var arrayVar=arg.object.name;
if(arg.property.type=="NumericLiteral"){
var arrayIndex=arg.property.value;
//将获取的参数MemberExpression删除替换为字符串
var paramStr=map.get(arrayVar)[arrayIndex].value
args.splice(index,1,t.default.stringLiteral(paramStr))
}
}
}
//获取调用函数名称,同上
var callee=p1.node.callee
if(callee.type=="MemberExpression" && callee.property.type=="MemberExpression"){
var paramStr=map.get(callee.property.object.name)[callee.property.property.value].value
callee.property=t.default.stringLiteral(paramStr)
}
if(callee.type=="MemberExpression" && callee.object.type=="MemberExpression"){
var paramStr=map.get(callee.object.object.name)[callee.object.property.value].value
callee.object=t.default.identifier(paramStr)
}
}
})
console.log("原始代码--------------:");
console.log(code);
output = generate.default(ast,{},code);
console.log("生成之后代码--------------:");
console.log(output.code);
3. ast混淆还原
JS混淆有很多种,这里举几个:UglifyJS,JScrambler,jsbeautifier.org,JSDetox,obfuscator.io 等
其中ob混淆可作为入门级的样例,ast的过程通过上面的内容基本已经清晰。
可通过ob官网混淆一段js代码,尝试分析原理后编写ast插件还原
https://obfuscator.io/
以下代码开源可阅读后作为参考:https://github.com/DingZaiHub/ob-decrypt
更多的混淆方式可以通过:https://www.sojson.com/jscodeconfusion.html
本文详细介绍了Babel的作用,它如何将ES6+代码转换为兼容旧版本浏览器的代码,涉及的主要步骤包括解析、转换和生成。文章还深入探讨了抽象语法树(AST)的概念,提供了AST分析工具和示例,展示了如何通过遍历和修改AST进行代码转换。此外,还讨论了作用域、绑定和状态管理在转换过程中的重要性,并给出了混淆代码还原的实践案例。
1076

被折叠的 条评论
为什么被折叠?



