跟着webpack产物来学习webpack是如何做模块处理

950 阅读6分钟

一、思考

webpack是何如实现 ESM(Es Module) 与 CMS(Commonjs) 相互导出引用呢?带着这些问题,咱们通过简单文件的打包来分析不同类型的模块是如何处理的

二、极简单的代码

webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  devtool: "none",
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
  ],
};

index.js

const home = require("./home");
console.log("Hello World");
console.log(home);

home.js

module.exports = "我是主页面";

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>跟着webpack产物来学习webpack是如何做模块处理</title>
  </head>
  <body>
    <h1>跟着webpack产物来学习webpack是如何做模块处理</h1>
  </body>
</html>

三、分析产物的公共方法和属性

1. 完整的产物

(function (modules) {
  // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    // Execute the module function
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }
    Object.defineProperty(exports, "__esModule", { value: true });
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            return module["default"];
          }
        : function getModuleExports() {
            return module;
          };
    __webpack_require__.d(getter, "a", getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  // __webpack_public_path__
  __webpack_require__.p = "";
  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/home.js":
    /*! no static exports found */
    function (module, exports) {
      module.exports = "我是主页面";
    },
  "./src/index.js":
    /*! no static exports found */
    function (module, exports, __webpack_require__) {
      const home = __webpack_require__(/*! ./home */ "./src/home.js");
      console.log("Hello World");
      console.log(home);
    },
});

是一个IIFE模块,实参是moduleId 和 对应的模块(函数)

2. 公共方法分析

a. 模块导入方法:__webpack_require__

// 模块导入方法:缓存模块信息,执行模块函数,将模块的导出return出去
function __webpack_require__(moduleId) {
  // 缓存已经加载过的模块,比如:key是 ./src/index.js,value 是模块信息
  if (installedModules[moduleId]) {
    // 将模块导出的内容 return 出去
    return installedModules[moduleId].exports;
  }
  // 创建一个新的模块,并将其放入缓存中
  var module = (installedModules[moduleId] = {
    i: moduleId, // 模块的id  比如:./src/index.js
    l: false, // 标记当前模块是否被加载 当时是未被加载
    exports: {}, // 存储模块导出的内容
  });
  // Execute the module function
  // 执行模块函数
  modules[moduleId].call(
    module.exports, // 模块内的this执行的是模块导出的内容
    module, // 当前的模块
    module.exports, // 存储模块导出的内容
    __webpack_require__ // // 导入方法
  );
  // 标记当前模块已经被加载
  module.l = true;
  // 返回模块导出的内容
  return module.exports;
}

b. 判断对象上是否有 xxx 属性

// 判断对象上是否有 xxx 属性
__webpack_require__.o = function (object, property) {
  return Object.prototype.hasOwnProperty.call(object, property);
};

c. 向exports对象上自定义属性和属性的getter方法

剧透:向ESM的exports添加导出内容,后面有具体的分析

// 向exports对象上自定义属性和属性的getter方法
__webpack_require__.d = function (exports, name, getter) {
  // 当前 导出对象中没有有 xxx 属性时候
  if (!__webpack_require__.o(exports, name)) {
    // 向 exports 对象添加 xxx 属性,并设置值为 getter 方法的返回值,可枚举的
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

d.标记 exports 是 esModule 模块方法

// 标记 exports 是 esModule 模块
__webpack_require__.r = function (exports) {
  // 自定义 exports.toString 为 [object Module],标记当前 exports 是 esModule 模块
  if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
    Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
  }
  // 在exports添加属性 __esModule 值为 true
  Object.defineProperty(exports, "__esModule", { value: true });
};

e.兼容 ESM 和 CMS 默认导出

__webpack_require__.n = function (module) {
  // 如果是 esModule 模块,创建 getter 获取 模块 default 值
  // 非 esModule 模块,创建 getter 获取 模块 exports 值
  var getter =
    module && module.__esModule
      ? function getDefault() {
          return module["default"];
        }
      : function getModuleExports() {
          return module;
        };
  // 在 getter 函数添加 a 为当前模块 默认 导出的值
  __webpack_require__.d(getter, "a", getter);
  // 获取 当前模块 默认 导出的值 的 getter
  return getter;
};

f.当所有的modules 挂在到 __webpack_require__ 方法上

__webpack_require__.m = modules;

g. 模块的缓存,挂在到 __webpack_require__ 方法上

__webpack_require__.c = installedModules;

h. 配置的public path 挂在到 __webpack_require__ 方法上

__webpack_require__.p = "";

以上属于挂在到 __webpack_require__ 方法上 主要是为了后面方便获取对应的值

四、分析ESM(Es Module) 与 CMS(Commonjs)的相互导出引用

1. 导出:CMS, 导入:CMS

a.源码

// index.js -- 导入
const home = require("./home");
console.log("Hello World");
console.log(home);
// home.js -- 导出
module.exports = "我是主页面";

b. 产物分析

实参

{
  "./src/home.js": function (module, exports) {
    module.exports = "我是主页面";
  },
  "./src/index.js": function (module, exports, __webpack_require__) {
    const home = __webpack_require__(/*! ./home */ "./src/home.js");
    console.log("Hello World");
    console.log(home);
  },
}

image.png

由上图可看出将 ./src/index.js 作为moduleId传给 __webpack_require__
这个函数作用:模块导入方法:缓存模块信息,执行模块函数,将模块的导出return出去 具体这个函数坐上,到【公共方法分析】查看,这里注重分析模块执行 image.png 上面执行加载 home 模块,并将home导出的内容答应出来,下面看看加载home的时候,怎样将内容导出去 image.png 由上图可以值,“index.js” 中home 值是 “我是主页面”

2. 导出:CMS, 导入:ESM

a.源码

// index.js -- 导入
import home from "./home";
console.log(home);
// home.js -- 导出
module.exports = "我是主页面";

b. 产物分析

实参

{
  "./src/home.js": function (module, exports) {
    module.exports = "我是主页面";
  },
  "./src/index.js": function (
    module,
    __webpack_exports__,
    __webpack_require__
  ) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    var _home__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
      /*! ./home */ "./src/home.js"
    );
    var _home__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
      _home__WEBPACK_IMPORTED_MODULE_0__
    );

    console.log(_home__WEBPACK_IMPORTED_MODULE_0___default.a);
  },
}

执行顺序和上面的一样,咱们直接分析 ./src/index.js 模块的执行

image.png 首先执行 __webpack_require__.r(__webpack_exports__);,这个函数就是标记 当前模块exports 对象 为 esModules image.png 接着加载home模块

var _home__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
  /*! ./home */ "./src/home.js"
);

此时 home__WEBPACK_IMPORTED_MODULE_0_ 值 为 “我是主页面” image.png 接着 做一波 兼容 ESM 和 CMS 默认导出 __webpack_require__.n 的操作

var _home__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
  _home__WEBPACK_IMPORTED_MODULE_0__
);

image.png image.png 可以看出,_home__WEBPACK_IMPORTED_MODULE_0___default就是一个模块导出的的getter,获取home的值。
最后 console.log(_home__WEBPACK_IMPORTED_MODULE_0___default.a); 打印的就是 ‘我是主页面’

3. 导出:ESM, 导入:CMS

a.源码

// index.js -- 导入
const home = require("./home");
console.log("default", home.default);
console.log("name", home.name);
// home.js -- 导出
export const name = "Hello Home";
export default "我是主页面";

b. 产物分析

实参

{
  "./src/home.js":
    /*! exports provided: name, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "name",
        function () {
          return name;
        }
      );
      const name = "Hello Home";
      /* harmony default export */ __webpack_exports__["default"] =
        "我是主页面";
    },
  "./src/index.js":
    /*! no static exports found */
    function (module, exports, __webpack_require__) {
      const home = __webpack_require__(/*! ./home */ "./src/home.js");
      console.log("default", home.default);
      console.log("name", home.name);
    },
}

执行顺序和上面的一样,咱们直接分析 ./src/index.js 模块的执行 image.png 这块,上面其实已经分析过了,加载 home模块,并将home导出的内容打印出来,核心关注 home是做模块导出的 image.png 从这里就能看到 ./src/index.js 打印的default 和 name 就是 home模块 的 "Hello World" 和 "我是主页面"

4. 导出:ESM, 导入:ESM

a.源码

// index.js -- 导入
const home = require("./home");
console.log("default", home.default);
console.log("name", home.name);
// home.js -- 导出
export const name = "Hello Home";
export default "我是主页面";

b. 产物分析

实参

{
  "./src/home.js":
    /*! exports provided: name, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "name",
        function () {
          return name;
        }
      );
      const name = "Hello Home";
      /* harmony default export */ __webpack_exports__["default"] =
        "我是主页面";
    },
  "./src/index.js":
    /*! no exports provided */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var _home__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(/*! ./home */ "./src/home.js");
      console.log("home", _home__WEBPACK_IMPORTED_MODULE_0__["default"]);
      console.log("name", _home__WEBPACK_IMPORTED_MODULE_0__["name"]);
    },
});

执行顺序和上面的一样,咱们直接分析 ./src/index.js 模块的执行 image.png 那么 我们去看看 home 模块做了哪些 image.png 从这里就能看到 ./src/index.js 打印的default 和 name 就是 home模块 的 "Hello World" 和 "我是主页面"

五、总结

  1. ESM导出做了三步:
    a. 标记当前导出对象为 ES Module
    b. 如果导出 有default,直接将值挂在到 导出对象的 default属性上
    c. 如果导出具名的内容,首先将当前属性通过 defineProperty 定义在 导出对象上,getter就是获取当前作用于下的该属性,并在当前模块定声明该属性并赋值

  2. CMS导出:直接将内容挂在到 导出的对象上

  3. ESM导入做了四步:
    a. 标记当前导出对象为 ES Module
    b. 导入被导入的模块
    c. 对导入的内容 default 做 ESM 和 CMS的 getter 兼容
    d. 在getter的a属性获取被导入的值

  4. CMS导入:直接将导入的内容打印出来即可