关于 webpack 打包结构 详解

一、整体结构概览

Webpack 打包代码示例:

javascript 复制代码
(function (t, n) {
  ...
  i = function () {
    return function (t) {
      var e = {}; // 模块缓存

      function n(r) {...} // 模块加载器 __webpack_require__

      // 工具函数定义 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
      n.m = t;            // 模块表(每个模块对应一个函数)
      n.c = e;            // 缓存对象
      n.d = function(...) // 定义导出
      n.r = function(...) // 标记为 ESModule
      n.t = function(...) // 兼容 CommonJS/ESModule 模块转换工具
      n.n = function(...) // 获取默认导出(兼容 require().default)
      n.o = function(...) // 判断对象是否有某属性(hasOwnProperty)
      n.p = "";           // 公共路径(publicPath)
      // 工具函数定义 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

      return n(n.s = 4);  //  启动主模块(模块 ID = 4)
    }
  }([...]);
})

二、详细解释每一部分

1)var e = {}:模块缓存对象

javascript 复制代码
e = {
  "./src/math.js": {
    i: "./src/math.js",
    l: true,
    exports: { add: [Function] }
  }
}
  • webpack 保证一个模块只加载一次(类似 Node.js 的 require.cache

2)function n(r):模块加载器(等价于 __webpack_require__

javascript 复制代码
function n(r) {
  if (e[r]) return e[r].exports;

  // 创建空模块
  var o = e[r] = {
    i: r,
    l: false,
    exports: {}
  };

  // 执行模块体函数(写到 exports 上)
  t[r].call(o.exports, o, o.exports, n);

  o.l = true;
  return o.exports;
}

作用:

  • 首次加载模块:执行模块函数、保存 exports

  • 重复加载:直接返回缓存

流程是:

  • 先看模块缓存(e)

  • 没有缓存就初始化模块对象

  • 执行模块函数**t[r].call(...)**(等于把源码执行)

  • 设置 loaded

  • 返回 exports

3)n.m = t

javascript 复制代码
n.m = t;
  • 保存模块表(t 是一个数组或对象,里面存放的是每个模块的函数体)

4)n.c = e

javascript 复制代码
n.c = e;
  • 保存模块缓存,调试或工具使用(你可以在 DevTools 中访问**__webpack_require__.c**)

5)n.d:定义模块导出属性(defineProperty)

javascript 复制代码
n.d = function (t, e, r) {
  if (!n.o(t, e)) {
    Object.defineProperty(t, e, {
      enumerable: true,
      get: r
    });
  }
}

作用:

  • 用于 export { foo } 的实现,把 foo 注册为访问器属性

6)n.r:标记模块为 ESModule

javascript 复制代码
n.r = function (t) {
  if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
    Object.defineProperty(t, Symbol.toStringTag, {
      value: "Module"
    });
  }
  Object.defineProperty(t, "__esModule", {
    value: true
  });
}

作用:

  • 给导出对象加 __esModule: true

  • 让其它模块在 import 时能识别它是 ESModule

7)n.t:模块兼容处理(很关键!)

javascript 复制代码
n.t = function (t, mode) {
  if (mode & 1) t = n(t); // 递归加载模块
  if (mode & 8) return t;
  if (mode & 4 && typeof t === 'object' && t && t.__esModule) return t;

  var r = Object.create(null);
  n.r(r);
  Object.defineProperty(r, "default", { enumerable: true, value: t });

  if (mode & 2 && typeof t !== "string") {
    for (var key in t) {
      n.d(r, key, function (k) { return t[k] }.bind(null, key));
    }
  }

  return r;
}

用途:

mode 含义
1 加载模块(递归)
2 混合命名导出
4 是 ESModule
8 直接返回原始值

这个函数是 处理各种导入模式 的万能桥梁。

8)n.n:获取默认导出

javascript 复制代码
n.n = function (t) {
  var getter = t && t.__esModule ? function () { return t.default } : function () { return t };
  n.d(getter, "a", getter);
  return getter;
}

作用:

  • CommonJS 写法中**require()** 得到的是对象,统一处理**.default**提取

9)n.o:判断属性是否存在

javascript 复制代码
n.o = function (t, e) {
  return Object.prototype.hasOwnProperty.call(t, e);
}

10)n.p = "":publicPath

  • 用于配置静态资源路径前缀(比如图片路径前加 CDN 地址)

  • n.p = "/static/" 这样的配置很常见


三、入口执行模块

javascript 复制代码
return n(n.s = 4)
  • 表示:执行模块 ID 为 4 的模块

  • 这是**entry: './src/index.js'**编译后分配的模块 ID(数字 4)


四、模块表结构(t = [...]

看到的后面这部分:

javascript 复制代码
[function (t, e, n) {...}, function (t, e, n) {...}, ...]

就是每个模块对应的函数,比如:

  • index.js 编译成了模块 ID 4

  • **math.js**编译成了模块 ID 0

  • 每个函数接收的参数是标准的:module, exports, require


五、逆向分析

如果在分析 webpack 打包文件,只要识别出以下代码块:

javascript 复制代码
var e = {}, function n(r) {...}, n.m = ..., n.r = ..., n.d = ...

说明它是 Webpack 构建的典型打包结构,可以:

  • 找出 **n.m**就能列出所有模块

  • 手动调用**n("模块 ID")** 得到模块对象

  • n.m[x].toString() 打印出源码

  • Hook 某个模块函数(例如加密逻辑)


六、总结(完整映射表)

代码 作用
n() 模块加载器(require)
n.m 所有模块函数集合(模块表)
n.c 模块缓存
n.d 定义模块导出(导出 getter)
n.r 标记模块为 ESModule
n.t 兼容 CommonJS 与 ESModule
n.n 提取默认导出
n.o hasOwnProperty 判断
n.p publicPath 路径前缀
n(n.s = X) 启动入口模块(通常就是 index.js)