软件推荐 › wakaru

wakaru

Javascript 反编译器

适用系统:CLI

软件介绍

Unminify 是什么

Unminify 是 Wakaru 的核心功能,把那些压缩过的、几乎看不懂的 JavaScript 代码变回人能读的样子。

你肯定见过用 Webpack、Vite 这些工具打包出来的代码吧?那些代码一般会经过两道工序:

先是转译。Babel、TypeScript、SWC 这些工具会把你写的 ES6+ 代码降到 ES5,好让老浏览器也能跑。然后是压缩。Terser 这类工具会把代码里的空格删掉、变量名改短、表达式简化,反正就是能省则省

举个例子,你原本写的代码可能是这样的:

async function fetchUserData(userId = 1) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response?.json();
  return data ?? {};
}

打包压缩后就成了这样:

function t(e){return r(this,void 0,void 0,function(){var t,n;return o(this,function(r){switch(r.label){case 0:return[4,fetch("/api/users/".concat(void 0===e?1:e))];case 1:return t=r.sent(),[4,null===(n=t)||void 0===n?void 0:n.json()];case 2:return[2,(n=r.sent())!==null&&void 0!==n?n:{}]}})})}

完全看不懂了对吧?Unminify 就是要把这种天书般的代码还原回去,让你能看懂生产环境跑的到底是什么东西。


核心目标

Unminify 要做三件事:

让代码重新可读

首先得把压缩工具搞出来的那些奇怪写法改回正常人能看懂的:

  • !0!1 改回 truefalse
  • void 0 改回 undefined
  • a(), b(), c() 这种挤在一起的表达式拆成独立的语句
  • var a=1,b=2,c=3 拆成三个独立声明
  • "dark" === theme 反转成 theme === "dark"(符合人的阅读习惯)
  • obj['prop'] 简化成 obj.prop

把降级的语法升回去

Babel 这些工具为了兼容老浏览器,会把 ES6+ 的新语法降级成 ES5。Unminify 要做的就是识别出这些模式,再升回去:

  • 模板字符串:"Hello ".concat(name) 还原成 `Hello ${name}`
  • 可选链:obj === null || obj === void 0 ? void 0 : obj.prop 还原成 obj?.prop
  • 空值合并:foo !== null && foo !== void 0 ? foo : bar 还原成 foo ?? bar
  • 默认参数:把那一长串 arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'default' 还原成简单的 param = 'default'
  • ES6 类:从原型链模式还原成 class 语法
  • async/await:从 __awaiter__generator 这些 helper 函数还原回去

智能重命名

压缩工具会把所有变量名都改成 abc 这种单字母,完全看不出含义。Unminify 会根据一些规则猜测这些变量的用途,给它们起个更有意义的名字:

  • React Hooks:看到 const [a, b] = useState(0) 会改成 const [value, setValue] = useState(0)
  • useRef:看到 const t = useRef(null) 会加个 Ref 后缀变成 const tRef = useRef(null)
  • 解构赋值:如果发现连续几行都在访问同一个对象的属性,就合并成解构赋值
  • 内联优化:把那些没啥用的临时变量直接内联掉

核心架构

整体流程

Unminify 用的是规则引擎的架构,简单来说就是:

源代码 → 解析成 AST → 一条条规则过一遍 → 生成最终代码

核心就三个东西:

  • Runner(执行器):负责协调整个流程
  • Rule(规则):每条规则负责一种具体的转换
  • AST(抽象语法树):代码的中间表示形式,方便操作

Runner 执行器

Runner 的工作就是管理代码在不同格式之间的转换,然后按顺序执行所有规则。

核心实现看这里:

// packages/shared/src/runner.ts
export async function executeTransformationRules(
    source: string,      // 源代码
    filePath: string,    // 文件路径
    rules: TransformationRule[],  // 转换规则列表
    params: Record<string, any>,  // 参数
) {
    // 为了性能优化,尽可能保持 AST 格式,避免重复解析和序列化
    let currentSource: string | null = null;
    let currentRoot: Collection | null = null;  // jscodeshift AST

    for (const rule of rules) {
        switch (rule.type) {
            case 'jscodeshift': {
                // 如果需要,解析为 AST
                currentRoot ??= j(currentSource ?? source);

                // 执行 AST 转换
                rule.execute({ root: currentRoot, filename, params });

                // 标记源代码失效
                currentSource = null;
                break;
            }
            case 'string': {
                // 如果需要,从 AST 生成源代码
                currentSource ??= currentRoot?.toSource() ?? source;

                // 执行字符串转换
                currentSource = await rule.execute({
                    source: currentSource,
                    filename,
                    params
                });

                // 标记 AST 失效
                currentRoot = null;
                break;
            }
        }
    }

    // 返回最终代码
    return {
        code: currentSource ?? currentRoot?.toSource() ?? source,
        timing
    };
}

这里有几个设计亮点:

  • 延迟解析:不会无脑地在 AST 和字符串之间来回转换,只在真正需要的时候才转
  • 状态追踪:用 currentSourcecurrentRoot 记录当前代码是什么形式
  • 两种规则类型
    • jscodeshift:基于 AST 的转换,大部分规则都是这种
    • string:基于字符串的转换,比如 Prettier 格式化

Rule 规则系统

每个规则都要实现同样的接口:

interface TransformationRule {
    type: 'jscodeshift' | 'string';
    id: string;        // 唯一标识符
    name: string;      // 规则名称
    tags: string[];    // 标签(用于分类)
    execute(context): void | string;  // 执行转换
}

创建一个 JSCodeshift 规则的示例:

export const transformAST: ASTTransformation = (context) => {
    const { root, j } = context;

    // 使用 jscodeshift API 查找和修改 AST 节点
    root
        .find(j.UnaryExpression, {
            operator: '!',
            argument: { type: 'NumericLiteral' }
        })
        .forEach((path) => {
            // 转换逻辑
        });
};

export default createJSCodeshiftTransformationRule({
    name: 'un-boolean',
    transform: transformAST
});

规则执行顺序

规则的顺序很重要,因为后面的规则可能要用到前面规则的结果。所以 Wakaru 把规则分成了 4 个阶段:

// packages/unminify/src/transformations/index.ts
export const transformationRules = [
    // ===== 第一阶段:基础整理 =====
    prettier,              // 格式化,便于后续处理
    unCurlyBraces,        // 添加花括号
    unSequenceExpression, // 拆分序列表达式
    unVariableMerging,    // 拆分变量声明
    unAssignmentMerging,  // 拆分赋值表达式

    // ===== 第二阶段:准备工作 =====
    unRuntimeHelper,      // 消除 runtime helper
    unEsm,               // CJS → ESM
    unEnum,              // 还原 TypeScript 枚举

    // ===== 第三阶段:一对一转换 =====
    lebab,               // 使用 lebab 进行 ES5 → ES6 转换
    unBoolean,           // !0 → true
    unUndefined,         // void 0 → undefined
    unInfinity,          // 1/0 → Infinity
    unTypeof,            // typeof x > "u" → typeof x === "undefined"
    unTemplateLiteral,   // concat → 模板字符串
    unBracketNotation,   // obj['prop'] → obj.prop
    unReturn,            // 简化 return 语句
    unWhileLoop,         // for(;;) → while(true)
    unIndirectCall,      // (0, fn)() → fn()
    unFlipComparisons,   // 反转比较

    // ===== 第四阶段:高级语法升级 =====
    unIife,              // 改进 IIFE 可读性
    unImportRename,      // 还原 import 重命名
    smartInline,         // 智能内联变量
    smartRename,         // 智能重命名
    unOptionalChaining,  // 还原可选链
    unNullishCoalescing, // 还原空值合并
    unConditionals,      // 条件表达式 → if/switch
    unParameters,        // 还原函数参数
    unArgumentSpread,    // fn.apply → fn(...args)
    unJsx,              // React.createElement → JSX
    unES6Class,         // 原型 → class
    unAsyncAwait,       // 还原 async/await

    // ===== 最后:格式化 =====
    prettier,            // 最终格式化
];

典型转换规则

下面挑几个有代表性的规则,看看 Unminify 具体是怎么工作的。

简单规则:布尔值还原

就是把 !0!1 改回 truefalse

// packages/unminify/src/transformations/un-boolean.ts
export const transformAST: ASTTransformation = (context) => {
    const { root, j } = context;

    // 查找所有一元表达式:operator 为 '!',参数为数字字面量
    root
        .find(j.UnaryExpression, {
            operator: '!',
            argument: { type: 'NumericLiteral' }
        })
        .forEach((path) => {
            const { value } = path.node.argument;
            const is01 = value === 0 || value === 1;

            // 只处理 !0 和 !1
            if (!is01) return;

            // !0 → true, !1 → false
            path.replace(j.booleanLiteral(!value));
        });
};

转换前后对比:

// 转换前
if (!0) console.log("always true");
if (!1) console.log("never runs");

// 转换后
if (true) console.log("always true");
if (false) console.log("never runs");

原理很简单:

  1. j.find() 找到 AST 里所有符合条件的节点
  2. 通过模式匹配精确定位 !0!1
  3. path.replace() 替换成布尔字面量

中等难度:模板字符串还原

concat 调用链还原成模板字符串

// packages/unminify/src/transformations/un-template-literal.ts
export const transformAST: ASTTransformation = (context) => {
    const { root, j } = context;

    root
        .find(j.CallExpression, {
            callee: {
                type: 'MemberExpression',
                object: { type: 'StringLiteral' },
                property: { name: 'concat' }
            }
        })
        .forEach((path) => {
            const object = path.node.callee.object;

            // 向上遍历,找到 concat 链的起点
            let parent = path;
            const args = [object];

            while (parent) {
                args.push(...parent.node.arguments);

                // 检查父节点是否也是 concat 调用
                if (isParentConcat(parent)) {
                    parent = parent.parent.parent;
                    continue;
                }
                break;
            }

            // 将所有参数合并为模板字符串
            const templateLiteral = args.reduce((acc, arg) => {
                if (j.StringLiteral.check(arg)) {
                    // 字符串字面量:转义特殊字符
                    const escaped = arg.value
                        .replace(/(?<!\\)`/g, '\\`')
                        .replace(/(?<!\\)\$/g, '\\$');
                    return acc + escaped;
                }
                // 表达式:包裹在 ${}
                return `${acc}\${${j(arg).toSource()}}`;
            }, '');

            // 替换为模板字符串
            j(parent).replaceWith(
                j.templateLiteral([
                    j.templateElement({ raw: templateLiteral }, true)
                ], [])
            );
        });
};

转换示例:

// 转换前
"Hello ".concat(name, "! You are ").concat(age, " years old.");

// 中间状态(concat 链)
//   "Hello ".concat(name, "! You are ")
//                         .concat(age, " years old.")

// 转换后
`Hello ${name}! You are ${age} years old.`

处理步骤:

  1. 找链:向上遍历 AST,把整个 concat 调用链找出来
  2. 收集参数:把所有字符串和表达式都提取出来
  3. 转义:处理模板字符串里的特殊字符,比如 `$
  4. 重构:构建新的模板字符串 AST 节点,替换掉原来的

复杂规则:可选链还原

这个就比较复杂了。目标是把这种东西:

// TypeScript/Babel 输出
(_foo = foo) === null || _foo === void 0 ? void 0 : _foo.bar

还原成这样:

foo?.bar

为什么说复杂呢?因为要处理好几个问题:

  1. 得识别出 === null || === undefined 这种判空模式
  2. 得追踪临时变量(比如 _foo)和原变量(foo)的关系
  3. 得处理嵌套的情况:foo?.bar?.baz
  4. 得处理可选调用:foo?.bar?.()

核心算法是决策树

可选链的模式本质上就是嵌套的条件判断,可以画成一棵树:

      foo === null?
       /         \
     null      foo === undefined?
                 /              \
               null           foo.bar

算法分两步:

第一步:构建决策树

function makeDecisionTree(
    j: JSCodeshift,
    expression: ConditionalExpression | LogicalExpression,
    negate: boolean
): DecisionTree {
    // 三元表达式:test ? consequent : alternate
    if (j.ConditionalExpression.check(expression)) {
        return {
            condition: expression.test,
            trueBranch: makeDecisionTree(j, expression.consequent, negate),
            falseBranch: makeDecisionTree(j, expression.alternate, negate)
        };
    }

    // 逻辑表达式:left || right 或 left && right
    if (j.LogicalExpression.check(expression)) {
        if (expression.operator === '&&') {
            return {
                condition: expression.left,
                falseBranch: makeDecisionTree(j, expression.right, negate)
            };
        }
        // ... || 处理
    }

    // 叶子节点
    return { condition: expression };
}

第二步:从决策树重构可选链

function constructOptionalChaining(
    j: JSCodeshift,
    tree: DecisionTree,
    flag: 0 | 1  // 0: 寻找 null 检查, 1: 寻找 undefined 检查
): ExpressionKind | null {
    const { condition, trueBranch, falseBranch } = tree;

    // 检查 trueBranch 是否返回 null/undefined(短路分支)
    if (!isFalsyBranch(j, trueBranch)) return null;

    if (flag === 0) {
        // 当前节点是 null 检查
        if (isNullBinary(j, condition)) {
            // 递归处理 falseBranch,寻找 undefined 检查
            const result = constructOptionalChaining(j, falseBranch, 1);
            if (!result) return null;

            // 应用可选链
            return applyOptionalChaining(j, result, tempVariable, targetExpression);
        }
    }
    else if (flag === 1) {
        // 当前节点是 undefined 检查
        if (isUndefinedBinary(j, condition)) {
            return constructOptionalChaining(j, falseBranch, 0);
        }
    }

    return null;
}

第三步:应用可选链操作符

function applyOptionalChaining(
    j: JSCodeshift,
    node: ExpressionKind,
    tempVariable: Identifier,
    targetExpression?: ExpressionKind
): ExpressionKind {
    // 递归遍历 AST
    if (j.MemberExpression.check(node)) {
        // 找到匹配的对象引用
        if (areNodesEqual(j, node.object, tempVariable)) {
            // foo.bar → foo?.bar
            return j.optionalMemberExpression(
                targetExpression || node.object,
                node.property,
                node.computed
            );
        }

        // 递归处理 object
        node.object = applyOptionalChaining(j, node.object, tempVariable, targetExpression);
    }

    if (j.CallExpression.check(node)) {
        // 处理可选调用:foo.bar() → foo?.bar()
        // ...
    }

    return node;
}

几个例子:

// 例子 1:简单可选链
// 转换前
(_foo = foo) === null || _foo === void 0 ? void 0 : _foo.bar
// 转换后
foo?.bar

// 例子 2:嵌套可选链
// 转换前
(_foo = foo) === null || _foo === void 0
    ? void 0
    : (_foo_bar = _foo.bar) === null || _foo_bar === void 0
        ? void 0
        : _foo_bar.baz
// 转换后
foo?.bar?.baz

// 例子 3:可选调用
// 转换前
(_foo = foo) === null || _foo === void 0 ? void 0 : _foo.call()
// 转换后
foo?.call()

整个过程:

  1. 模式识别:用决策树表示嵌套的条件判断
  2. 状态追踪:用 flag 记录当前在检查 null 还是 undefined
  3. 变量追踪:找出临时变量(如 _foo)和原始变量(如 foo)的对应关系
  4. 递归重构:深度优先遍历 AST,把匹配的节点替换成可选链节点
  5. 清理:删掉不再用的临时变量声明

智能规则:变量内联

这个规则会把对象属性访问转成解构赋值:

// 转换前
const t = e.x;
const n = e.y;
const r = e.color;
console.log(t, n, r);

// 转换后
const { x, y, color } = e;
console.log(x, y, color);

算法分三步:

  1. 收集:扫描代码块,找出所有从同一个对象访问属性的变量声明
  2. 分析:检查这些变量能不能安全地重命名和合并
  3. 转换:生成解构赋值,重命名所有引用
function handleDestructuring(j: JSCodeshift, body: Statement[], scope: Scope) {
    // 存储:对象名 → 访问该对象的变量声明列表
    const objectAccessMap = new MultiMap<string, VariableDeclaration>();

    // 第一遍:收集所有符合条件的变量声明
    body.forEach((node) => {
        if (isObjectPropertyAccess(node)) {
            const decl = node as VariableDeclaration;
            const init = decl.declarations[0].init as MemberExpression;
            const objectName = init.object.name;

            objectAccessMap.set(objectName, decl);
        }
    });

    // 第二遍:对每个对象,尝试生成解构
    objectAccessMap.forEach((declarations, objectName) => {
        if (declarations.length < 2) return;  // 至少 2 个才值得解构

        // 提取所有属性名和变量名
        const properties = declarations.map(decl => {
            const declarator = decl.declarations[0];
            const varName = declarator.id.name;
            const propName = declarator.init.property.name;
            return { varName, propName };
        });

        // 检查是否可以安全重命名
        const canRename = properties.every(({ varName, propName }) => {
            return canRenameInScope(scope, varName, propName);
        });

        if (!canRename) return;

        // 生成解构赋值
        const destructuring = j.variableDeclaration(
            'const',
            [j.variableDeclarator(
                j.objectPattern(
                    properties.map(({ propName }) =>
                        j.property('init', j.identifier(propName), j.identifier(propName))
                    )
                ),
                j.identifier(objectName)
            )]
        );

        // 重命名所有引用
        properties.forEach(({ varName, propName }) => {
            renameIdentifier(j, scope, varName, propName);
        });

        // 替换第一个声明,移除其他声明
        j(declarations[0]).replaceWith(destructuring);
        declarations.slice(1).forEach(decl => j(decl).remove());
    });
}

几个例子:

// 例子 1:对象解构
const t = e.x;
const n = e.y;
const r = e.color;
console.log(t, n, r);
// ↓
const { x, y, color } = e;
console.log(x, y, color);

// 例子 2:数组解构
const t = e[0];
const n = e[1];
const r = e[2];
console.log(t, n, r);
// ↓
const [t, n, r] = e;
console.log(t, n, r);

// 例子 3:临时变量内联
const a = document;
const b = a.createElement('div');
// ↓
const b = document.createElement('div');

处理流程:

  1. 找模式:识别出访问同一个对象的多个属性
  2. 检查作用域:确保重命名不会造成变量冲突
  3. 追踪引用:用 jscodeshift 的作用域 API 找到所有引用的地方
  4. 批量重构:一次性完成声明合并和引用重命名

关键技术细节

AST 操作:jscodeshift

Wakaru 用 jscodeshift 来操作 AST。这玩意儿的 API 设计得挺像 jQuery 的:

// 查找所有 if 语句
root.find(j.IfStatement)

// 模式匹配
root.find(j.BinaryExpression, {
    operator: '===',
    left: { type: 'Identifier' },
    right: { type: 'StringLiteral' }
})

// 遍历和修改
.forEach((path) => {
    // path.node 是 AST 节点
    // path.parent 是父节点
    // path.scope 是作用域信息

    path.replace(newNode);  // 替换节点
    path.insertBefore(node);  // 在前面插入
    path.insertAfter(node);   // 在后面插入
})

性能优化:延迟解析

Runner 有个很重要的优化,就是避免重复地解析和序列化 AST:

let currentSource: string | null = null;
let currentRoot: Collection | null = null;

for (const rule of rules) {
    if (rule.type === 'jscodeshift') {
        // 只在第一次需要时解析
        currentRoot ??= j(currentSource ?? source);
        rule.execute({ root: currentRoot });
        currentSource = null;  // 源码失效
    }
    else if (rule.type === 'string') {
        // 只在第一次需要时序列化
        currentSource ??= currentRoot?.toSource() ?? source;
        currentSource = await rule.execute({ source: currentSource });
        currentRoot = null;  // AST 失效
    }
}

这么做的好处:

  • 连续几个 jscodeshift 规则可以共用同一个 AST
  • 连续几个 string 规则可以共用同一个字符串
  • 只在类型切换的时候才转换一次

作用域分析

很多转换(比如变量重命名、内联)都需要准确的作用域信息:

import { renameIdentifier, findReferences } from '@wakaru/ast-utils/reference';

// 重命名变量
renameIdentifier(j, scope, 'oldName', 'newName');

// 查找所有引用
const refs = findReferences(j, scope, 'varName');

// 移除未使用的声明
removeDeclarationIfUnused(j, path, 'varName');

这些工具函数把复杂的作用域分析都封装好了,保证重命名不会把代码搞坏。

模式匹配

Wakaru 大量使用 AST 模式匹配来识别特定的代码模式:

import { isNull, isUndefined, isNullBinary } from '@wakaru/ast-utils/matchers';

// 检查节点是否是 null
if (isNull(j, node)) { ... }

// 检查是否是 === null 或 !== null
if (isNullBinary(j, node)) { ... }

// 检查两个节点是否相等(结构相同)
if (areNodesEqual(j, node1, node2)) { ... }

有了这些匹配器,规则代码写起来就简洁多了。


实际案例:完整转换流程

来看个实际例子,感受一下整个流程:

转换前的代码

"use strict";
var r = require(7462);
var o = require(6854);

function _j() {
    var a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Info";
    var b = arguments.length > 1 ? arguments[1] : undefined;
    alert("[" + a + "] Message from " + b);
}

var d = function (e) {
    var t = e.children, n = e.className, c = e.visible;
    var p = (0, r.useState)(""), h = (0, o.Z)(p, 2);
    var g = h[0], y = h[1];
    var b = (0, r.useState)(c), v = (0, o.Z)(b, 2);
    var w = v[0], x = v[1];

    return r.createElement("div", { className: "".concat(n), ref: z }, t);
}
d.displayName = "CssTransition";

转换过程

  1. prettier - 先格式化一下,方便后面处理

  2. unSequenceExpression - 拆分序列表达式

  3. unVariableMerging - 拆分变量声明

  4. unEsm - 把 require 改成 import

    import r from '7462';
    import o from '6854';
  5. unBoolean - 这里没有布尔值要还原,跳过

  6. unUndefined - 还原 undefined(这里没有需要改的)

  7. unTemplateLiteral - 把 concat 改成模板字符串

    `[${a}] Message from ${b}`
    className: `${n}`
  8. unIndirectCall - 把 (0, r.useState) 改成 r.useState

    var p = r.useState("");
    var b = r.useState(c);
  9. smartInline - 生成解构赋值

    const { children, className, visible } = e;
  10. smartRename - React Hooks 重命名

    const [value, setValue] = r.useState("");
    const [isVisible, setIsVisible] = r.useState(visible);
  11. unParameters - 还原默认参数

    function _j(a = "Info", b) {
        alert(`[${a}] Message from ${b}`);
    }
  12. unJsx - 把 createElement 改成 JSX

    return <div className={className} ref={z}>{children}</div>;
  13. prettier - 最后再格式化一次

转换后的代码

import r from "7462";
import o from "6854";

function _j(a = "Info", b) {
  alert(`[${a}] Message from ${b}`);
}

const CssTransition = function ({ children, className, visible }) {
  const [value, setValue] = r.useState("");
  const [isVisible, setIsVisible] = r.useState(visible);

  return (
    <div className={className} ref={z}>
      {children}
    </div>
  );
};
CssTransition.displayName = "CssTransition";
分享至:

评论

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.153.0. UTC+08:00, 2026-02-15 03:34
浙ICP备14020137号-1 $Carte des visiteurs$