保姆级教程用 AST BFS 精准提取 webpack 模块依赖闭包摘要当你手里有一个被 webpack 打包、还做了代码混淆 / 拆 chunk 的前端产物只想单独运行或逆向分析其中某个入口模块时面对动辄几千上万个模块该怎么办本文给你一个基于AST 解析 BFS 依赖闭包的小工具输入入口模块 ID自动算出它的全部传递依赖并注入可运行的 webpack 解释器模板。省去反复手动抠代码也避免把整个几 MB 的 bundle 全搬下来。环境 / 版本Node.js 18解析依赖 acorn成文于 2026-09。webpack 4 / 5 的「模块对象数组」格式通用但不同项目的require形参命名、chunk push 写法存在差异适用边界见文末。配套资源完整的build_decode.js提取脚本 可直接运行的source.js解释器模板已打包上传按需自取 https://pan.baidu.com/s/58TwdNOLrh9io14LC6RVhtg提取后把 bundle 放进source/、填好入口 ID 即可跑通。一、为什么需要这个工具从手动抠模块说起先看一个真实场景你拿到一个线上前端产物想本地跑一下其中某个功能对应的模块比如逆向分析一个加密逻辑、或者复用某个业务组件。但 webpack 早把源码揉成了一个巨大的 bundle所有模块被编号塞进一个对象里。常见的两种土办法都很痛苦做法操作痛点反复跑代码抠运行 bundle打印出模块再去源文件里按 ID 找下来缺一个依赖再跑一次依赖链一长就陷入跑 → 缺 → 找 → 再跑的死循环效率极低全量提取把整个 bundle 里的所有模块一次性导出来几 MB 的无关代码全搬进项目体积爆炸真正用到的可能只有几十个本工具给定入口模块 ID自动算出传递依赖闭包只导出用到的模块一次跑通体积可控还顺手标出缺失模块核心差异别人是我要什么就慢慢找什么或者全都要我们做的是从入口出发只把依赖链上真正用到的模块捞出来。二、先看清 webpack bundle 的模块长什么样动手前得先认得 webpack 产出的模块长啥样。它有两类典型格式我们的工具两种都兼容格式一IIFE 参数式主 bundle 最常见!function(modules){// 解释器根据 ID 取模块、按需 require}({123:function(module,exports,__webpack_require__){varfoo__webpack_require__(456);// 依赖模块 456}});格式二chunk push 式异步分包(window.webpackJsonpwindow.webpackJsonp||[]).push([[0],{789:function(module,exports,__webpack_require__){// ...}}]);无论哪种格式一个模块的本质都是数字ID: function(...)。注意函数签名webpack 标准模块签名是function(module, exports, __webpack_require__)第三个参数就是require函数。模块内部通过requireParam(数字ID)的方式引用其他模块——这些被传进 require 的数字就是这个模块的直接依赖。只要我们能拿到每个模块调用了哪些 ID再做一次图遍历就能得到任意一个入口的完整依赖闭包。三、工具核心思路AST 提取 BFS 依赖闭包整体流水线分五步关键是先建模块表再算依赖图最后注入模板渲染错误:Mermaid 渲染失败: Parse error on line 2: ...ce.jsIIFE 参数为空 {}] -- B[acorn 解析所有 -----------------------^ Expecting SQE, DOUBLECIRCLEEND, PE, -), STADIUMEND, SUBROUTINEEND, PIPE, CYLINDEREND, DIAMOND_STOP, TAGEND, TRAPEND, INVTRAPEND, UNICODE_TEXT, TEXT, TAGSTART, got DIAMOND_START读模板读取一个 webpack 解释器模板source.js它的 IIFE 参数目前是空的({})。建模块表用 acorn 把所有 bundle 解析成 AST找出所有数字key 函数value的属性存进Map。算直接依赖对每个模块定位第三个参数require 函数名再扫描requireParam(数字)调用收集它直接依赖的 ID。BFS 闭包从你指定的入口模块 ID 出发做广度优先遍历把整条依赖链上用到的模块都标记出来并记录缺失项。注入输出把需要的模块按 ID 排序后塞进source.js的 IIFE 参数里写出可直接运行的decode.js。四、动手跑通一次提取只需要改 3 个配置工具配置集中在文件顶部实际要改的只有三处SOURCE_FILE解释器模板、SOURCE_DIR放 bundle 的目录、ENTRY_IDS入口模块 ID。// build_decode.js 顶部配置区constSOURCE_FILE./source.js;// webpack 解释器模板IIFE 参数为空constSOURCE_DIR./source;// 把要分析的 .js 都丢进这个文件夹constENTRY_IDS[73271,91318,4455];// 你要提取的入口模块 IDconstOUTPUT_FILE./decode.js;// 输出文件目录结构建议project/ ├── build_decode.js# 本工具├── source.js# 解释器模板参数留空├── source/# 放待分析的 bundle│ ├── app.1.js │ └── app.2.js └── decode.js# 运行后生成的产物装好依赖就能跑npminstallacornnodebuild_decode.js预期输出关键行已标注含义 decode.js 构建工具 [1] 读取模板: ./source.js IIFE 参数位置: 字符 12345 ~ 67890 [2] 解析 2 个 bundle 文件 app.1.js (120.5 KB) → 342 个模块 app.2.js (88.3 KB) → 210 个模块 ↳ 去重: 跳过 12 个重复模块 合计: 540 个唯一模块 [3] 收集传递依赖 (入口: 73271, 91318, 4455) 需要: 47 个模块 # ← 几千个模块里真正用到的只有 47 个 [4] 注入模块到 source.js [5] 输出: /abs/path/decode.js 包含模块: 47 个 (其中 0 个缺失) 下一步 所有模块已补全, 可以直接运行 decode.js node /abs/path/decode.js看到需要: 47 个模块就说明成功了——原本几百 KB 的模块池最终只导出了真正服务于这三个入口的 47 个。五、关键代码拆解三个核心函数下面把工具里最关键的三个函数拆开讲方便你按需改。1. extractModulesAST 提取模块 直接依赖第一遍遍历找出所有「数字 ID 函数」的属性第二遍在每个模块函数体内只匹配requireParam(数字)这种调用排除.n / .e / .t等 webpack 运行时辅助方法收集直接依赖functionextractModules(source,filePath){constastacorn.parse(source,PARSE_OPTS);constmodulesnewMap();// 第一遍收集 数字key 函数valuewalk(ast,(node){if(node.type!Property)return;constkeynode.key;letmoduleIdnull;if(key?.typeLiteral){if(typeofkey.valuenumber)moduleIdkey.value;elseif(typeofkey.valuestring/^\d$/.test(key.value))moduleIdparseInt(key.value,10);}if(moduleIdnull)return;constvaluenode.value;if(value.type!FunctionExpressionvalue.type!ArrowFunctionExpression)return;// 第三个参数即 require 函数webpack 标准签名letrequireParamnull;if(value.params.length3value.params[2].typeIdentifier)requireParamvalue.params[2].name;if(!modules.has(moduleId))modules.set(moduleId,{id:moduleId,node:value,deps:[],file:filePath,requireParam});});// 第二遍分析每个模块内部的 require(ID) 调用for(const[,mod]ofmodules){if(!mod.requireParam)continue;constdepSetnewSet();walk(mod.node,(node){if(node.type!CallExpression)return;constcalleenode.callee;if(callee.type!Identifier||callee.name!mod.requireParam)return;constargnode.arguments[0];if(arg?.typeLiteraltypeofarg.valuenumber)depSet.add(arg.value);});mod.deps[...depSet].sort((a,b)a-b);}returnmodules;}2. collectDepsBFS 收集传递依赖闭包入口入队逐层展开。访问过的标记visitedbundle 里找不到的记进missing方便你补文件functioncollectDeps(modules,entryIds){constvisitednewSet();constmissing[];constqueue[...entryIds];while(queue.length0){constidqueue.shift();if(visited.has(id))continue;visited.add(id);constmodmodules.get(id);if(mod){for(constdepofmod.deps)if(!visited.has(dep))queue.push(dep);// 依赖入队继续向外扩散}else{missing.push(id);// 当前 bundle 池里没有这个模块}}return{visited,missing};}3. locateIIFEArg在模板里定位注入点从文件末尾往前找}({这个 IIFE 调用特征用括号配平算出参数对象的起止偏移再把模块塞进去functionlocateIIFEArg(source){constcallStartsource.lastIndexOf(}({);if(callStart-1)returnnull;constcontentStartcallStart3;// 跳过 }({letdepth1,icontentStart;// 已进第一层 {while(isource.lengthdepth0){if(source[i]{)depth;elseif(source[i]})depth--;i;}if(depth!0)returnnull;return{contentStart,contentEnd:i-1,callEnd:i};}注入时缺的模块不会报错而是写成// ID: *** 缺失 — 请提供包含此模块的 bundle 文件 ***注释你补上对应 bundle 重跑即可。六、常见问题与排错指南现象原因解决缺少 acorn, 请运行: npm install acorn没装解析依赖npm install acorn后重跑source 文件夹为空bundle 没放进去把.js丢进SOURCE_DIR输出包含模块: 47 个 (其中 3 个缺失)某些依赖在现有 bundle 里没有把含这些 ID 的 chunk 文件补进source/重跑无法定位 IIFE 参数位置模板不是标准的}({形式检查source.js是否被二次压缩/改写手动确认 IIFE 调用特征提取出的模块依赖不全混淆后 require 不是第三个参数或被改名在extractModules里放宽requireParam判定如按调用特征而非固定形参位风险提示该工具用于本地逆向分析、调试、复用自有代码等合法场景。对来源不明的第三方产物做分析前请确认不违反对方的服务条款与当地法律法规不要用于绕过付费 / 授权校验等不当用途。总结思路闭环先建「ID → 模块 → 直接依赖」的表再 BFS 求传递闭包最后注入解释器模板——从给入口到拿到可运行子集一步到位。体积可控从几千个模块里精准捞出几十个比全量提取省下大量无关代码。缺失可见找不到的依赖会被注释标出补文件即可不会让你在运行时才踩坑。立即可用把source.js模板、bundle 文件、入口 ID 配好node build_decode.js就能产出decode.js。适用边界与时效适用于 webpack 4 / 5 的「模块对象数组」产物对ESM 未被降级为函数模块、或模块 ID 非数字、或依赖通过动态import() magic comment 走运行时加载的场景需针对性调整提取规则。依赖 acorn 做 AST 解析对极度压缩 / 语法变形的 bundle 可能需要先美化或调PARSE_OPTS如ecmaVersion。原理层模块签名、依赖图遍历长期有效具体特征字符串__webpack_require__、chunk push 写法随 webpack 版本可能有微调遇到问题对照上文排错表即可。如果对你有帮助点赞收藏关注不迷路 有问题欢迎在评论区交流一起把这套提取流程打磨得更稳。
阅读完成 · 觉得有帮助?