☰
webpack 模块提取
2026/9/27 5:57:21 网站建设 项目流程

保姆级教程:用 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.webpackJsonp=window.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.js
IIFE 参数为空 {}] --> B[acorn 解析所有 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'DIAMOND_START'
  1. 读模板:读取一个 webpack 解释器模板source.js,它的 IIFE 参数目前是空的({})。
  2. 建模块表:用 acorn 把所有 bundle 解析成 AST,找出所有数字key + 函数value的属性,存进Map。
  3. 算直接依赖:对每个模块,定位第三个参数(require 函数名),再扫描requireParam(数字)调用,收集它直接依赖的 ID。
  4. BFS 闭包:从你指定的入口模块 ID 出发做广度优先遍历,把整条依赖链上用到的模块都标记出来,并记录缺失项。
  5. 注入输出:把需要的模块按 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. extractModules:AST 提取模块 + 直接依赖

第一遍遍历找出所有「数字 ID + 函数」的属性;第二遍在每个模块函数体内,只匹配requireParam(数字)这种调用(排除.n / .e / .t等 webpack 运行时辅助方法),收集直接依赖:

functionextractModules(source,filePath){constast=acorn.parse(source,PARSE_OPTS);constmodules=newMap();// 第一遍:收集 数字key + 函数valuewalk(ast,(node)=>{if(node.type!=='Property')return;constkey=node.key;letmoduleId=null;if(key?.type==='Literal'){if(typeofkey.value==='number')moduleId=key.value;elseif(typeofkey.value==='string'&&/^\d+$/.test(key.value))moduleId=parseInt(key.value,10);}if(moduleId===null)return;constvalue=node.value;if(value.type!=='FunctionExpression'&&value.type!=='ArrowFunctionExpression')return;// 第三个参数即 require 函数(webpack 标准签名)letrequireParam=null;if(value.params.length>=3&&value.params[2].type==='Identifier')requireParam=value.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;constdepSet=newSet();walk(mod.node,(node)=>{if(node.type!=='CallExpression')return;constcallee=node.callee;if(callee.type!=='Identifier'||callee.name!==mod.requireParam)return;constarg=node.arguments[0];if(arg?.type==='Literal'&&typeofarg.value==='number')depSet.add(arg.value);});mod.deps=[...depSet].sort((a,b)=>a-b);}returnmodules;}

2. collectDeps:BFS 收集传递依赖闭包

入口入队,逐层展开。访问过的标记visited,bundle 里找不到的记进missing,方便你补文件:

functioncollectDeps(modules,entryIds){constvisited=newSet();constmissing=[];constqueue=[...entryIds];while(queue.length>0){constid=queue.shift();if(visited.has(id))continue;visited.add(id);constmod=modules.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){constcallStart=source.lastIndexOf('}({');if(callStart===-1)returnnull;constcontentStart=callStart+3;// 跳过 "}({"letdepth=1,i=contentStart;// 已进第一层 {while(i<source.length&&depth>0){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 版本可能有微调,遇到问题对照上文"排错表"即可。

如果对你有帮助,点赞收藏关注不迷路~ 有问题欢迎在评论区交流,一起把这套提取流程打磨得更稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询