软件推荐 › wakaru
wakaru
Click or scan the QR code to visit
Click or scan the QR code to visit
Click or scan the QR code to visit
软件介绍
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改回true和false - 把
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 函数还原回去
智能重命名
压缩工具会把所有变量名都改成 a、b、c 这种单字母,完全看不出含义。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 和字符串之间来回转换,只在真正需要的时候才转
- 状态追踪:用
currentSource和currentRoot记录当前代码是什么形式 - 两种规则类型:
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 改回 true 和 false
// 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");
原理很简单:
- 用
j.find()找到 AST 里所有符合条件的节点 - 通过模式匹配精确定位
!0和!1 - 用
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.`
处理步骤:
- 找链:向上遍历 AST,把整个
concat调用链找出来 - 收集参数:把所有字符串和表达式都提取出来
- 转义:处理模板字符串里的特殊字符,比如
`和$ - 重构:构建新的模板字符串 AST 节点,替换掉原来的
复杂规则:可选链还原
这个就比较复杂了。目标是把这种东西:
// TypeScript/Babel 输出
(_foo = foo) === null || _foo === void 0 ? void 0 : _foo.bar
还原成这样:
foo?.bar
为什么说复杂呢?因为要处理好几个问题:
- 得识别出
=== null || === undefined这种判空模式 - 得追踪临时变量(比如
_foo)和原变量(foo)的关系 - 得处理嵌套的情况:
foo?.bar?.baz - 得处理可选调用:
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()
整个过程:
- 模式识别:用决策树表示嵌套的条件判断
- 状态追踪:用 flag 记录当前在检查 null 还是 undefined
- 变量追踪:找出临时变量(如
_foo)和原始变量(如foo)的对应关系 - 递归重构:深度优先遍历 AST,把匹配的节点替换成可选链节点
- 清理:删掉不再用的临时变量声明
智能规则:变量内联
这个规则会把对象属性访问转成解构赋值:
// 转换前
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);
算法分三步:
- 收集:扫描代码块,找出所有从同一个对象访问属性的变量声明
- 分析:检查这些变量能不能安全地重命名和合并
- 转换:生成解构赋值,重命名所有引用
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');
处理流程:
- 找模式:识别出访问同一个对象的多个属性
- 检查作用域:确保重命名不会造成变量冲突
- 追踪引用:用 jscodeshift 的作用域 API 找到所有引用的地方
- 批量重构:一次性完成声明合并和引用重命名
关键技术细节
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";
转换过程:
prettier - 先格式化一下,方便后面处理
unSequenceExpression - 拆分序列表达式
unVariableMerging - 拆分变量声明
unEsm - 把
require改成importimport r from '7462'; import o from '6854';unBoolean - 这里没有布尔值要还原,跳过
unUndefined - 还原
undefined(这里没有需要改的)unTemplateLiteral - 把
concat改成模板字符串`[${a}] Message from ${b}` className: `${n}`unIndirectCall - 把
(0, r.useState)改成r.useStatevar p = r.useState(""); var b = r.useState(c);smartInline - 生成解构赋值
const { children, className, visible } = e;smartRename - React Hooks 重命名
const [value, setValue] = r.useState(""); const [isVisible, setIsVisible] = r.useState(visible);unParameters - 还原默认参数
function _j(a = "Info", b) { alert(`[${a}] Message from ${b}`); }unJsx - 把
createElement改成 JSXreturn <div className={className} ref={z}>{children}</div>;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";