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


    前言

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

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

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

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

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

    下面是这个工具的代码仓库:boompack

    需求

    在此之前,其实面对这些压缩过的 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,需要用到我们的抽象语法树。

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

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

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

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

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

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

    应用 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 代码后,通过 recastparse 函数转换为 ast语法树 ,再通过我自定义的函数 modifyAst 来修改语法树后,最后使用 recastprint 函数将 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 语句加上花括号。

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

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

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

    this.traverse(path);
    

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

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

    return false
    

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

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

    return literal(true);
    

    总结

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

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

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

  • 相关阅读:
    浏览器打开一个网页的全流程
    C专家编程 第1章 C:穿越时空的迷雾 1.9 阅读ANSI C标准,寻找乐趣和裨益
    【JavaEE】HashMap
    Pulsar 之Messaging 消息
    GoFrame框架
    C++----智能指针
    Java类加载器详解
    1036 Boys vs Girls
    记一次服务间调用失败的bug
    【CEOI 2020】 Roads(道路)
  • 原文地址:https://www.cnblogs.com/vvjiang/p/15976301.html