【转】反压缩 js ,我的万花筒写轮眼开了,CV 能力大幅提升

前言

因为比较菜,所以经常需要读一些别人的代码学习学习。

有源码的代码当然好,但是很多网站不开源。这些网站的 js 又都是打包压缩过的,学习起来很难受。

所以我做了一个小工具,通过修改抽象语法树,来处理这些打包压缩过的 js,增强代码可读性,让我们学习起来更容易。

如果再借助重定向线上 js 到本地 js,或者使用 chrome 自带的 override 源码能力,甚至可以轻松调试别人的线上代码。

有了这个工具,我 CV 界大师兄的名号可谓实至名归。

需求

在此之前,其实面对这些压缩过的 js 我是不太想做这个工具的。

通常使用 prettier 美化一下,然后慢慢磨就好了。

但是这次我面对的是一个 canvas 相关的 js,压缩后的核心代码使用 prettier 格式化之后有 2 万多行,看到这份代码之后人都麻了。

这里随便写个压缩代码示例来举例:

function f() {
  var a = (c = 33, d = 12), b = 1, g = (e == 2 ? a === 1 && b == 1 || c == 1 && d == 1 && c == 4 : c = 2);
  for (var i; i < 10; i++)if (s < 1) s++
  return a = 2, d == 2, e = !1, e = !0
}

这份代码其实并不算特别复杂,因为没有十几个逻辑表达式和三元运算交杂在一起。

但是这份代码很典型,因为基本上比较影响阅读的点都有。

我们简单列一下:

  • 大量无意义的单字符变量,修改变量名也无法批量替换
  • 序列表达式很多,即大量语句以逗号分隔,调试时只算做一行代码
  • 多个逻辑表达式混杂不清,需要改为 if 表达式
  • if 或者 for 循环不加花括号
  • 大量三元运算,需要使用 if 和 else
  • !0 和!1 的表达反人类,需要使用 true 和 false
  • return 语句结合序列表达式,实际上只返回最后一个

这些就是主要的困难,特别是当它们各种互相嵌套,又和十几个逻辑运算和三元运算交杂在一起,光是拆解出来就得花个十几分钟。

如果人力拆解这些代码,也不是没有好处,至少你可以化身人肉低端编译器,反复巩固 js 基础,要是碰到一些奇葩公司让你手写代码你就是王者。

但是因为我需要留出时间打游戏的原因,所以还是写了这么个工具简化流程。

使用方法

  • 克隆仓库到本地后
  • yarn install 安装依赖包
  • 将需要转换地压缩代码,复制粘贴到test/from/index.js这个文件中
  • 终端运行脚本 yarn start
  • 最终会在test/to/这个文件夹下生成 index.js,也就是我们最后修改后的文件。

效果

使用工具转换后的 js 代码如下:

function func_f() {
  let var_a, var_b, var_g;
  c = 33;
  var_a = d = 12;
  var_b = 1;

  if (e == 2) {
    if (var_a === 1 && var_b == 1) {
      if (var_a === 1) {
        var_g = var_b == 1;
      } else {
        var_g = var_a === 1;
      }
    } else {
      if (c == 1 && d == 1) {
        var_g = c == 4;
      } else {
        if (c == 1) {
          var_g = d == 1;
        } else {
          var_g = c == 1;
        }
      }
    }
  } else {
    var_g = c = 2;
  }

  for (var var_i; var_i < 10; var_i++) {
    if (s < 1) {
      s++
    }
  }
  let result;
  var_a = 2;
  d == 2;
  e = false;
  result = e = true;
  return result;
}

可以看到相对于压缩后的代码,我们转换后的代码变长了很多。

这份代码相较于上一份,可读性大大增强了。

另外我已经使用 jQuery 压缩后的文件测试过了,转换没有任何问题。

然而,依然不保证转换后的代码一定正确,js 的 hack 玩法太多,只能说用这个转换肯定可控。

核心玩法:抽象语法树

想要解析修改这种压缩 js,需要用到我们的抽象语法树。

所谓抽象语法树,实际上就是一种树形结构来表示编程语句。

具体可以百度,这里不解释太多,总之你可以理解为可以将一串代码解析成一个树形结构,这个树形结构上面每个节点代表一种语法结构。

这里列一个必备网站:AST explorer,用来查看 js 被转换为抽象语法树后的样子。

现在前端的基础库 babel 系列,就是通过抽象语法树将 es6 转换为 es5 的,当然也包括转换 react 和 typescript

因为抽象语法树和代码之间是可以相互转换的。

所以我们的核心思路是将代码转换为抽象语法树,然后在这个树上做修改,修改完后再转换为代码。

应用 recast 去转换代码

js 代码和抽象语法树的转换有很多 js 库可以实现。

比如@babel/parserrecast,还有不少其他的库,这里我们使用 recast

我对这个研究也不深入,没怎么了解他们的优缺点,不过当时看到 recast满足需求就直接用了。

可以在 npmjs 上找到 recast,里面有简单的介绍文档:地址,也有仓库地址。

但是 recast 的文档不太够,有的关键点还得自己看下具体的示例和源码才能弄明白,不过也不难。

这里就不展开了,先上一段我自己写的简单代码:

import { parse, print } from "recast";
import { readFile, writeFile } from "fs";
import path from "path";
import modifyAst from "./utils/modifyAst.js";

const fromPath = path.join("./test/from/index.js");
const toPath = path.join("./test/to/index.js");

readFile(fromPath, { encoding: "utf8" }, (err, sourceCode) => {
    // 通过recast的parse函数转换为ast语法树
    const ast = parse(sourceCode);
    modifyAst(ast);
    writeFile(toPath, print(ast).code, () => {
        console.info("搞完");
    });
});

这段代码的用处是从 from 文件夹下的文件获取 js 代码后,通过 recast 的 parse 函数转换为 ast语法树 ,再通过我自定义的函数 modifyAst 来修改语法树后,最后使用 recast 的 print 函数将 ast语法树 转换为 js 代码。

这段内容比较简单,主要就是借助 recast 将代码转成抽象语法树,再转回代码。

具体修改抽象语法树在 modifyAst 里面:

import addBlock from "./addBlock.js";
import modifyReturn from "./modifyReturn.js";
import modifyUnaryExpression from "./modifyUnaryExpression.js";
// 修改声明中的表达式
import replaceVarName from "./modifyVariableDeclaration/replaceVarName.js";
import modifyDeclarationInit from "./modifyVariableDeclaration/modifyDeclarationInit.js";

// 修改表达式
import modifyExpressionStatement from "./modifyExpressionStatement/index.js";

/**
* 修改抽象语法树
*/
const modifyAst = (ast) => {
  modifyUnaryExpression(ast);
  replaceVarName(ast);
  addBlock(ast);
  modifyReturn(ast);
  modifyDeclarationInit(ast);
  modifyExpressionStatement(ast);
};

export default modifyAst;

在 modifyAst 中,我将不同的语句修改按照功能进行了划分到,写在了不同的文件中。

本篇博客也不宜展开过多,我只挑一部分代码展示:

import { types, visit } from "recast";

const { blockStatement } = types.builders;

/**
* 找到所有的if和for语句,给他们增加花括号
* @param {抽象语法树} ast
*/
const addBlock = (ast) => {
  visit(ast, {
    // 找到所有的if语句给他们增加花括号
    visitIfStatement: function (path) {
      if (
        path.node.consequent != null &&
        path.node.consequent.type != "BlockStatement"
      ) {
        path.node.consequent = blockStatement([path.node.consequent]);
      }
      if (
        path.node.alternate != null &&
        path.node.alternate.type != "BlockStatement"
      ) {
        path.node.alternate = blockStatement([path.node.alternate]);
      }
      this.traverse(path);
    },
  });
};

export default addBlock;

上面这部分代码的作用是遍历抽象树中所有的 if 语句,给那些没加花括号的 if 语句加上花括号。

实际上就是使用 recast 的 visit 方法遍历抽象语法树。visitIfStatement 这个回调函数,就是在遍历到 if 语句后执行的函数。

在函数中有两个 if 语句,那就是判断以及修改的代码,这个不多讲。

需要注意的是,recast 遍历抽象语法树时,如果识别到 if 语句后,不会继续遍历这个 if 语句里包裹的 if 语句,所以这里使用

this.traverse(path);

这行代码是用来继续遍历当前节点的子节点的,继续往下找 if 语句。

如果你自己判断出不需要向下遍历,不能简单地删掉这段代码,需要用这行代码替换:

return false

返回 false 表示不再向下遍历。

另外如果此时想直接使用新语句替换当前语句,可以直接返回一个新语句,例如:

return literal(true);

总结

总的来说,做完这个小工具算是解放了我大把的时间。

但是它只是我遇到典型压缩代码后,针对性进行更改的结果。可能遇到一些其他压缩后的语法,效果不大好,您也可以针对相应语法自行修改。

当然,如果您有更好的方法和建议,也希望能不吝赐教。


作者:韩子卢
出处:https://www.cnblogs.com/vvjiang/
本博客文章均为作者原创,转载请注明作者和原文链接。

猜你喜欢

转载自blog.csdn.net/u014683833/article/details/123429059