模块化的演变与实现

402 阅读4分钟

前言

在目前的日常开发中,ES Module模块化的应用已经无处不在了。怀着对司空见惯的事情的好奇心理,我对前端模块化展开了一番探索学习。下面就将我的学习成果分享一下,希望可以帮助到跟我一样有困惑的小伙伴。

模块化有什么优势吗?为什么要使用模块化呢?

  • 独立作用域
  • 可复用
  • 解耦

模块化发展史

  1. 初代模块化

在模块化刚开始的时候,智慧的前辈们想出了将不同模块封装到不同的js文件中,然后通过script前置引入的方式实现。

    <header>
        <script src="./moduleA.js"></script>
        <script src="./moduleB.js"></script>
        <script src="./moduleC.js"></script>
    </header>

这种方式初步解决了模块化的功能,但是也产生了新的问题。moudleA、moudleB、moudleC三个模块公用一个顶级作用域——window/global,这就很容易产生作用域污染的问题。举个简单的例子:

假设moduleA中定义了一个名称为name的变量,不巧的是moduleB中也定义了一个同样名称为name的变量,这时候就会很尬尴。如果在moudleA中修改一下name的值,那么moduleB中的name的值也会跟着发生变化;如果在moduleB中修改了一下name的值,那么moduleA中的name的值也会发生变化,这样就会对程序造成不可预知的影响。

  1. 命名空间

前辈们为了解决作用域冲突的问题,又提出了命名空间的解决办法。通过命名空间将变量放置到不同作用域中来解决作用域冲突问题。

// moduleA.js
var namespaceA = {
    name: 'moduleA',
}

// moduleB.js
var namespaceB = {
    name: 'moduleB',
}

这个时候,如果moduleB中的name变量发生了改变,namespaceB.name = 'newModuleB',不会影响moduleA中变量name的值,namespaceA.name仍然是moduleA,但是比较尴尬的是,如果你在moduleB中通过namespaceA.name = 'newModuleB' 来修改的话,moduleA中的name变量的值仍然会被修改掉,这不是我们期望的结果。

  1. 立即执行函数

这个时候函数作用域(独立于全局作用域)浮现出来了,通过立即执行函数进行包裹,形成新的函数作用域,然后利用闭包原理,将我们想要其它模块获取的值或者方法对外抛出,来解决我们之前说到的问题。模拟代码如下:

var moduleA = (function () {
    var namespaceA = {
        name: 'moduleA',
    }
    
    return {
        name: namespaceA.name,
        hello: function() {
            console.log('Hello, my name is ' + this.name);
        }
    }
})();

当然,像JQuery这些代码库中采用了另外一种类似的实现方式:

(function (window) {
    var moduleA = {
        name: 'moduleA',
        hello: function () {
            console.log('Hello, my name is ' + this.name);
        }
    }
    
    window.moduleA = moduleA;
})(window);

  1. commonJS、AMD、ES Module 随着模块化的发展,完善的模块化解决方案诞生了。之前浏览器端使用的是AMD规范,node端使用的是commonJS,现在基本就是使用ES Module。

webpack中模块化实现

  1. 核心变量

webpack实现模块中最重要的两个函数作用域下的变量就是 webpack_moduleswebpack_module_cache,__webapck_modules__用来存储所有的模块,__webpack_module_cache__是用来存储所有加载过的模块,实现模块加载的缓存。

  1. require逻辑

引入一个模块的时候,先通过moduleId(引入模块的路径)去__webapck_modules_cache__中查找,如果有缓存,那么直接就返回缓存好的模块 webpack_modules_cache[moduleId],如果没有的话那么就从__webapck_modules__中加载模块,并且添加到缓存中,方便下次使用模块时减少不必要的加载。

    function __webpack_require__(moduleId) {
      var cachedModule = __webpack_module_cache__[moduleId];

      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }

      var module = __webpack_module_cache__[moduleId] = {
        exports: {}
      };

      return module.exports;
    }
  1. 模块引入逻辑 其实,引入逻辑说穿了就很好理解,就是通过将模块挂载到同一个命名空间下,然后再在引入的时候,通过去这个命名空间下去查找就可以了。
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

上面的就是webpack实现模块化的核心部分,下面附上完整代码:

(() => {
  "use strict";

  // 所有的模块
  var __webpack_modules__ = ({
    // moduleId:模块加载方法
    // 将 export 的默认模块或其它模块添加到 __webpack_exports__ 上
    "./src/test.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        "default": () => __WEBPACK_DEFAULT_EXPORT__
      });

      // 当前模块中的逻辑
      function __WEBPACK_DEFAULT_EXPORT__() { }
    })
  });

  // 缓存的模块
  var __webpack_module_cache__ = {};

  // 模块加载方法
  // moduleId 文件引入路径
  function __webpack_require__(moduleId) {
    // 判断模块是否已经缓存
    // 如果已缓存,就直接返回缓存的模块
    var cachedModule = __webpack_module_cache__[moduleId];

    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }

    // 引入模块,并添加到缓存
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };

    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    return module.exports;
  }

  (() => {
    // 如果 key 在 definition 上存在,而 exports 上不存在的话
    // 将 definition 的 key 挂载到 exports 上
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  (() => {
    // 判断键 prop 是否存在于 obj 自身,而不是存在于 obj 的原型链上
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();

  (() => {
    // 添加模块化标记 __esModule
    __webpack_require__.r = (exports) => {
      // 将 __esModule 挂载到 __webpack_exports__ 上
      // Symbol兼容处理
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();


  var __webpack_exports__ = {};

  (() => {
    __webpack_require__.r(__webpack_exports__);

    // 加载当前所需要的所有模块
    // ./src/test.js 引入的模块
    var _src_test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");

    // 当前模块的代码逻辑
    console.log('aaaaaaaaa', _src_test__WEBPACK_IMPORTED_MODULE_0__["default"])
  })();
})();

代码中注释纯属我的个人理解,如果有问题或者有错误的地方,欢迎小伙伴们指正。