webpack 模块提取

保姆级教程:用 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 最常见)

js 复制代码
!function(modules) {
  // 解释器:根据 ID 取模块、按需 require
}({
  123: function(module, exports, __webpack_require__) {
    var foo = __webpack_require__(456); // 依赖模块 456
  }
});

格式二:chunk push 式(异步分包)

js 复制代码
(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)。

js 复制代码
// build_decode.js 顶部配置区
const SOURCE_FILE = './source.js';   // webpack 解释器模板(IIFE 参数为空)
const SOURCE_DIR  = './source';      // 把要分析的 .js 都丢进这个文件夹
const ENTRY_IDS   = [73271, 91318, 4455]; // 你要提取的入口模块 ID
const OUTPUT_FILE = './decode.js';   // 输出文件

目录结构建议:

bash 复制代码
project/
├── build_decode.js   # 本工具
├── source.js         # 解释器模板(参数留空)
├── source/           # 放待分析的 bundle
│   ├── app.1.js
│   └── app.2.js
└── decode.js         # 运行后生成的产物

装好依赖就能跑:

bash 复制代码
npm install acorn
node build_decode.js

预期输出(关键行已标注含义):

text 复制代码
=== 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 运行时辅助方法),收集直接依赖:

js 复制代码
function extractModules(source, filePath) {
  const ast = acorn.parse(source, PARSE_OPTS);
  const modules = new Map();

  // 第一遍:收集 数字key + 函数value
  walk(ast, (node) => {
    if (node.type !== 'Property') return;
    const key = node.key;
    let moduleId = null;
    if (key?.type === 'Literal') {
      if (typeof key.value === 'number') moduleId = key.value;
      else if (typeof key.value === 'string' && /^\d+$/.test(key.value))
        moduleId = parseInt(key.value, 10);
    }
    if (moduleId === null) return;
    const value = node.value;
    if (value.type !== 'FunctionExpression' &&
        value.type !== 'ArrowFunctionExpression') return;

    // 第三个参数即 require 函数(webpack 标准签名)
    let requireParam = 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] of modules) {
    if (!mod.requireParam) continue;
    const depSet = new Set();
    walk(mod.node, (node) => {
      if (node.type !== 'CallExpression') return;
      const callee = node.callee;
      if (callee.type !== 'Identifier' || callee.name !== mod.requireParam) return;
      const arg = node.arguments[0];
      if (arg?.type === 'Literal' && typeof arg.value === 'number')
        depSet.add(arg.value);
    });
    mod.deps = [...depSet].sort((a, b) => a - b);
  }
  return modules;
}

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

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

js 复制代码
function collectDeps(modules, entryIds) {
  const visited = new Set();
  const missing = [];
  const queue = [...entryIds];

  while (queue.length > 0) {
    const id = queue.shift();
    if (visited.has(id)) continue;
    visited.add(id);
    const mod = modules.get(id);
    if (mod) {
      for (const dep of mod.deps)
        if (!visited.has(dep)) queue.push(dep); // 依赖入队,继续向外扩散
    } else {
      missing.push(id); // 当前 bundle 池里没有这个模块
    }
  }
  return { visited, missing };
}

3. locateIIFEArg:在模板里定位注入点

从文件末尾往前找 }({ 这个 IIFE 调用特征,用括号配平算出参数对象的起止偏移,再把模块塞进去:

js 复制代码
function locateIIFEArg(source) {
  const callStart = source.lastIndexOf('}({');
  if (callStart === -1) return null;
  const contentStart = callStart + 3; // 跳过 "}({"
  let depth = 1, i = contentStart;     // 已进第一层 {
  while (i < source.length && depth > 0) {
    if (source[i] === '{') depth++;
    else if (source[i] === '}') depth--;
    i++;
  }
  if (depth !== 0) return null;
  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 版本可能有微调,遇到问题对照上文"排错表"即可。

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

相关推荐
IMPYLH1 小时前
HTML 的 <tfoot> 元素
前端·javascript·html
像风一样自由20201 小时前
42.VueReactNextjs如何为AI应用设计前端交互
前端·人工智能·大模型·交互·rag·智能体
鬼手点金1 小时前
Claude Code示范案例-修复 Bug 工作流
java·服务器·前端·javascript·bug·openclaw
柳杉1 小时前
用 GPT-6 Astra 和 Tripo3D 做智慧农业 3D 大屏:从调研到可巡检园区全流程实录
前端·ai编程·数据可视化
特创数字科技1 小时前
【uni-app x】告别系统相机限制!三端通用自定义相机组件:任意布局、静默拍照、录像、水印叠加、连拍回图一次搞定
前端·数码相机·uni-app
IT_陈寒2 小时前
用Vue时这个响应式陷阱坑了我一整天
前端·人工智能·后端
风骏时光牛马2 小时前
企业客户信息管理数据库
前端
paopaokaka_luck2 小时前
个人综合管理小程序(echarts数据看板、健康数据记录与趋势查看、目标拆解和进度管理、日程待办与番茄钟、习惯打卡、收支统计、个人生活记录、跨模块智能提醒)
前端·javascript·spring·小程序·数据分析·echarts·生活