必须基于AST分析标识符声明位置、引用关系及作用域语义,结合@babel/traverse检查binding.kind、Export/Import上下文、MemberExpression属性访问等,为每个Scope独立生成带前缀的混淆名,并显式控制source map与注释输出。

直接在字符串层面做替换会破坏作用域、误伤属性名和内置方法,必须基于 AST 才能安全识别哪些 Identifier 是可脱敏的局部变量或参数,哪些必须保留(如 API_URL、__DEV__、console)。
怎么判断一个 Identifier 是否该被脱敏
关键不是“它叫什么”,而是“它在哪声明、被谁引用、是否暴露给外部”。比如:
-
const token = getAuth();中的token是局部绑定,可脱敏 -
obj.token中的token是字面量属性名,不能动 -
export const API_KEY = 'xxx';中的API_KEY是导出名,业务要求必须保留 -
function logError(msg) { ... }的msg是参数,可脱敏;但若该函数被其他模块import { logError } from './utils'调用,则其形参名不影响外部,仍可安全替换
如何用 @babel/traverse 安全跳过不可脱敏节点
靠正则或字符串匹配跳过敏感标识符极易漏判。正确做法是在遍历中检查节点上下文语义:
- 遇到
Identifier节点时,先调用path.scope.getBinding(node.name)—— 如果返回undefined,说明是引用而非声明,不处理 - 若 binding.kind 是
'module'或'hoisted',且该名出现在ExportNamedDeclaration或顶层VariableDeclaration中,查白名单(如['API_URL', '__DEBUG_MODE']),命中则跳过 - 若节点位于
MemberExpression.property且computed === false(即obj.method这种写法),直接跳过,不进入重命名逻辑 - 对
ImportSpecifier.local.name和ExportSpecifier.local?.name,一律不脱敏——它们是模块契约的一部分
脱敏名生成必须带作用域隔离前缀
用全局计数器(如 _a、_b)会导致不同函数里同名变量映射到相同混淆名,引发作用域污染。例如:
function f() { let id = 1; return id; }
function g() { let id = 2; return id; }
若都替换成 _id,压缩后可能因变量提升或闭包捕获出错。实操建议:
- 为每个
Scope实例维护独立的Map<string string></string>,键为原始名,值为混淆名 - 混淆名格式用
_${scopeId.slice(0, 3)}${counter++},其中scopeId可取scope.path.node.loc?.start的哈希片段 - 必须调用
path.replaceWith(t.identifier(newName)),而不是直接改node.name—— 否则@babel/traverse不会更新引用路径映射,后续scope.references会失效
为什么生成代码时必须显式控制 source map 和注释
脱敏后如果堆栈无法定位到原始行号,或保留了 debugger、// TODO: remove before prod 这类注释,等于没脱敏。容易忽略的关键点:
-
@babel/generator默认保留所有注释,需显式传入{ comments: false } - 若输入文件有
sourceMappingURL,输出时必须用sourceFileName和sourceMapTarget重建映射,否则 Chrome DevTools 显示的是混淆后代码 - 不要依赖
minify: true选项自动压缩——它可能删掉你特意保留的调试用console.log,而业务脱敏工具通常要保留运行时日志结构,只换变量名 - 对
FunctionExpression和ArrowFunctionExpression的参数节点,脱敏后需确保params数组长度不变,否则arguments.length行为可能意外改变


















