反壓縮 js ,我的萬花筒寫輪眼開了,CV 能力大幅提升
- 2022 年 3 月 7 日
- 筆記
- javascript, js, 反壓縮js, 反打包
前言
因為比較菜,所以經常需要讀一些別人的代碼學習學習。
有源碼的代碼當然好,但是很多網站不開源。這些網站的 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,需要用到我們的抽象語法樹。
所謂抽象語法樹,實際上就是一種樹形結構來表示編程語句。
具體可以百度,這裡不解釋太多,總之你可以理解為可以將一串代碼解析成一個樹形結構,這個樹形結構上面每個節點代表一種語法結構。
這裡列一個必備網站://astexplorer.net/,用來查看 js 被轉換為抽象語法樹後的樣子。
現在前端的基礎庫 babel 系列,就是通過抽象語法樹將 es6 轉換為 es5 的,當然也包括轉換 react 和 typescript。
因為抽象語法樹和代碼之間是可以相互轉換的。
所以我們的核心思路是將代碼轉換為抽象語法樹,然後在這個樹上做修改,修改完後再轉換為代碼。
應用 recast 去轉換代碼
js 代碼和抽象語法樹的轉換有很多 js 庫可以實現。
比如@babel/parser,recast,還有不少其他的庫,這裡我們使用 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);
總結
總的來說,做完這個小工具算是解放了我大把的時間。
但是它只是我遇到典型壓縮代碼後,針對性進行更改的結果。可能遇到一些其他壓縮後的語法,效果不大好,您也可以針對相應語法自行修改。
當然,如果您有更好的方法和建議,也希望能不吝賜教。