乐趣区

关于前端:webpack模块化的原理

commonjs

在 webpack 中既能够书写 commonjs 模块也能够书写 es 模块,而且不必思考浏览器的兼容性问题,咱们来剖析一下原理。

首先搞清楚 commonjs 模块化的解决形式,简略配置一下 webpack,写两个模块编译一下看一下:

webpack.config.js

module.exports = {
    mode: "development",
    devtool: "none"
}

index.js

const a = require('./a')
console.log(a)

a.js

const a = 'a';
module.exports = a;

编译后果

查看编译后果,能够发现 webpack 对于每个模块的做法相似于 node,将每个模块放在一个函数环境中并向其中传入一些必要的参数。webpack 将这些模块组成一个对象(属性名是模块门路 (模块 id),属性值为模块内容)传入一个立刻执行函数,立刻执行函数中定义了一个函数 __webpack_require__ 相似 node 中的 require 函数,实现了导入模块的作用。

打包后果中删去了一些正文和临时用不要的代码,能够很显著的看进去实现 commonjs 模块化的要害就是这个 __webpack_require__ 函数,通过传入模块 id 来失去模块的导出。

require 函数

__webpack_require__ 函数的实现:

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;
}

如果相熟 node 就很容易了解这个函数了:

  1. 首先查看这个模块是否曾经被加载过,所以就须要一个全局变量 installedModules 用来记录所有被加载过模块的导出
  2. 没有加载过的模块就先结构一个 module 对象,要害是要有一个 exports 属性
  3. 执行模块代码并返回模块导出值

最终的一步就是须要加载启动模块,也就是 IIFE 的最初一句:

return __webpack_require__("./src/index.js");

ES Module

es 模块化的解决形式是须要借助 __webpack_require__ 实现的,首先看一些方才被删除的代码:

  1. __webpack_require__.r

    该函数用于标识 es 模块的导出

    // 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});
    };
  2. __webpack_require__.d

    用于解决 es 模块的具名导出

    // 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});
       }
    };
  3. __webpack_require__.o

    就是给 hasOwnPreperty 换了个名字

    __webpack_require__.o = 
       function (object, property) {return Object.prototype.hasOwnProperty.call(object, property); };

咱们改一下模块代码看看纯 es Module 导入导出的编译后果:

index.js

import a, {test} from './a'
import b from './b'
console.log(a);
test();
console.log(b)

a.js

const a = 'a';
function test() {}
export default a;
export {test}

b.js

const b = 'b';
export default b;

参考 前端进阶面试题具体解答

编译后果

{"./src/a.js": (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony export (binding) */
        __webpack_require__.d(__webpack_exports__, "test", function () {return test;});

        const a = 'a';

        function test() {}

        /* harmony default export */
        __webpack_exports__["default"] = (a);
    }),
    "./src/b.js": (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        const b = 'b';

        /* harmony default export */
        __webpack_exports__["default"] = (b);

    }),
    "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
        /* harmony import */
        var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/b.js");

        console.log(_a__WEBPACK_IMPORTED_MODULE_0__["default"])

        Object(_a__WEBPACK_IMPORTED_MODULE_0__["test"])();

        console.log(_b__WEBPACK_IMPORTED_MODULE_1__["default"])
    })
}

依据编译后果能够很明确的看进去,和 commonjs 编译进去的后果差不多,外围都是应用 __webpack_require__ 函数,区别在于 es 模块化,exports 对象首先就会被 __webpack_require__.r 标记为 es module,对于默认导出就是 exportsdefault 属性,对于具名导出应用 __webpack_require__.d 包装了一下,目标是让这些具名导出在模块之外只能读不能被批改(这是 es module 的特点)。

v5 的变动

然而为什么 default 没有被__webpack_require__.d 解决,这不合理啊。原本是应用的 webpack 4 打包的,而后换了 webpack 5 试了一下,webpack 5 打包的后果中 default 也被解决了,这可能是 webpack 4 的一个小 bug 吧。

webpack5 的编译后果有些许的不同,然而整个逻辑是没有变的:

两种模块化交互

webpack 是反对两种模块化代码共存的,尽管不倡议这样做。首先咱们先看一下他们相互导入的时候的导入后果是什么样的:

咱们来看看 webpack 是如何实现的,先批改一下模块:

index.js

const {a, test} = require('./a')

a.js

import b from './b'
import * as bbb from './b'
console.log(bbb)
console.log(b)
console.log(b.b)
const a = 'a';
function test() {}
export default a;
export {test};

b.js

module.exports = {b: () => { },
  moduleName: 'b'
}

编译后果

{
  "./src/a.js":
    (function (module, __webpack_exports__, __webpack_require__) {

      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */
      __webpack_require__.d(__webpack_exports__, "test", function () {return test;});
      /* harmony import */
      var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/b.js");
      /* harmony import */
      var _b__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_b__WEBPACK_IMPORTED_MODULE_0__);

      console.log(_b__WEBPACK_IMPORTED_MODULE_0__)

      console.log(_b__WEBPACK_IMPORTED_MODULE_0___default.a)

      console.log(_b__WEBPACK_IMPORTED_MODULE_0___default.a.b)

      const a = 'a';

      function test() {}

      /* harmony default export */
      __webpack_exports__["default"] = (a);
    }),
  "./src/b.js": (function (module, exports) {

    module.exports = {b: () => { },
      moduleName: 'b'
    }
  }),
  "./src/index.js": (function (module, exports, __webpack_require__) {const { a, test} = __webpack_require__("./src/a.js")
  })
}

能够发现当通过 es 模块的形式去 import 一个 commonjs 模块时,就会把导入的模块进行一层包装,通过 __webpack_require__.n,次要目标应该是为了兼容 import * as obj from '....' 这样的语法。

该函数的具体实现:

__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;
}

总结

webpack 中实现模块化的外围就是 __webpack_require__ 函数,无论是 commonjs 模块化还是 es 模块都是通过该函数来导入的。并且利用立刻执行函数的特点实现了作用域的关闭防止了全局变量的净化,十分的奇妙。

退出移动版