前言
在目前的日常开发中,ES Module模块化的应用已经无处不在了。怀着对司空见惯的事情的好奇心理,我对前端模块化展开了一番探索学习。下面就将我的学习成果分享一下,希望可以帮助到跟我一样有困惑的小伙伴。
模块化有什么优势吗?为什么要使用模块化呢?
- 独立作用域
- 可复用
- 解耦
模块化发展史
- 初代模块化
在模块化刚开始的时候,智慧的前辈们想出了将不同模块封装到不同的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的值也会发生变化,这样就会对程序造成不可预知的影响。
- 命名空间
前辈们为了解决作用域冲突的问题,又提出了命名空间的解决办法。通过命名空间将变量放置到不同作用域中来解决作用域冲突问题。
// 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变量的值仍然会被修改掉,这不是我们期望的结果。
- 立即执行函数
这个时候函数作用域(独立于全局作用域)浮现出来了,通过立即执行函数进行包裹,形成新的函数作用域,然后利用闭包原理,将我们想要其它模块获取的值或者方法对外抛出,来解决我们之前说到的问题。模拟代码如下:
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);
- commonJS、AMD、ES Module 随着模块化的发展,完善的模块化解决方案诞生了。之前浏览器端使用的是AMD规范,node端使用的是commonJS,现在基本就是使用ES Module。
webpack中模块化实现
- 核心变量
webpack实现模块中最重要的两个函数作用域下的变量就是 webpack_modules 和 webpack_module_cache,__webapck_modules__用来存储所有的模块,__webpack_module_cache__是用来存储所有加载过的模块,实现模块加载的缓存。
- 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;
}
- 模块引入逻辑 其实,引入逻辑说穿了就很好理解,就是通过将模块挂载到同一个命名空间下,然后再在引入的时候,通过去这个命名空间下去查找就可以了。
__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"])
})();
})();
代码中注释纯属我的个人理解,如果有问题或者有错误的地方,欢迎小伙伴们指正。