模块化编程,是强调将计算机程序的功能分离成独立的、可相互改变的“模”的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面所必需的所有东西。通过模块化编程,我们可以进行模块复用,从而减少重复代码,同时,降低了代码耦合,方便调试和维护。
ES6之前
如果不使用模块,会出现相同变量名的覆盖等问题。
例如,有如下代码:
<!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>Document</title>
</head>
<body>
</body>
<script src="./add1.js"></script>
<script src="./add2.js"></script>
<script src="./main.js"></script>
</html>
// add1.js
var a = 1;
function add1(num){ // 返回 输入 + 1
return a + num;
}
console.log(add1(1));
// add2.js
var a = 2;
function add2(num){ // 返回 输入 + 2
return a + num;
}
console.log(add2(2));
// main.js
console.log(add1(3)); // 希望返回 4
前两个js文件中定义了两个函数,add1
、 add2
,功能就是分别返回输入值加1、加2。
在html文件中,依次导入了这三个js文件。结果,main.js
打印4,结果打印了5。
这是因为,js是在全局环境中依次执行代码的,a的值被重新赋值。
立即执行函数
上面代码之所以出现这种情况,是因为,变量a
定义在全局作用域里。在ES6之前,除了全局作用域,还有函数作用域,借助函数作用域,我们可以实现初版的模块系统。
// add1.js
var add1 = (function(){ // 新加代码
var a = 1;
function add1(num){ // 返回 输入 + 1
return a + num;
}
console.log(add1(1));
return add1 // 新加代码
})() // 新加代码
// add2.js
var add2 = (function(){ // 新加代码
var a = 2;
function add2(num){ // 返回 输入 + 2
return a + num;
}
console.log(add2(2));
return add2 // 新加代码
})() // 新加代码
// main.js
console.log(add1(3)); // 希望返回 4
这里,不再将代码直接执行,而是将代码放到了一个立即执行函数中,并返回了定义的函数。
这样,就可以通过立即执行函数返回值来访问到定义函数。
原理:闭包。
CommonJS
CommonJS是提供给服务器端使用的规范。例如,使用Node.js开发一个服务器程序,就可以使用CommonJS 规范,因为Node.js实现了这一规范。
语法:require()指定依赖,使用exports 对象定义自己的公共API。
// ./moduelA.js
var moduleA = require('./moduleB') // 获取依赖
var add = moduleA.add
module.exports = { // 定义API
add:add,
}
值得注意的是,这段代码是同步运行的,因为CommonJS 是为服务器设计的,所以没有考虑网络延时。
原理:
每一个module都是一个Module实例。
module
之间,通过parent
children
,形成依赖图。
而在,require
中,有一个cache
对象用来存储加载的模块id->module字典
。执行require()
方法时,会从cache
取值。
AMD
CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD,Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。
由于AMD不是ES规范,所以是不能直接在浏览器中使用的,需要借助第三方包require.js
。
// src/math.js
// 定义模块
define('math', function () { // 第二个参数如果是依赖列表,回调函数可以通过入参获取模块
'use strict';
let add = function (x, y) {
return x + y;
};
return {
add: add,
}
});
// src/index.js
// 使用模块
require(['math'], function (math) {
alert(math.add(1, 1));
})
<!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>Document</title>
</head>
<body>
</body>
<script src="./js/require.js" async="true" data-main="./src/index.js"></script>
<-- 加载require工具包,并设置./src/index.js为入口文件 -->
</html>
运行过程:
- 当
./js/require.js
加载完后,会去请求./src/index.js
。 - 当
index.js
文件接收到后,开始执行,发现需要模块math
,于是创建一个异步函数,去请求math.js
文件。 - 当
math.js
文件接收到后,开始执行。define
方法检查参数,发现没有依赖,于是执行函数,并将返回值(模块api)与math
绑定。 - 执行回调函数。输入值为过程3返回值(模块api)。
原理:
- require: 获取依赖后执行函数。
- define: 用来定义模块,模块与模块间的依赖关系。
ES6模块
ECMAScript 6 模块是作为一整块JavaScript 代码而存在的。带有type="module"属性的
<script type="module">
</script>
<script type="module" src="path/to/myModule.js"></script>
原理:原生支持。
webpack与模块系统
不知道大家在开发项目时有没有发现,我们写代码时用的时ES6的模块。但是使用webpack打包后,似乎并没有使用type="module"属性
或许我们应该分析打包结果来进一步了解webpack。
我们创建一个简单的项目。
main.js
里分别使用ES6的模块和cms模块写法,调用了两个对应的文件,并执行了输出。配置文件指定main.js
为入口,dist/js/main.js
为出口。
执行:webpack --config ./webpack.config.js
进行打包。
在dist/js/main.js
中,我们可以看到打包结果。下面,开始逐段分析打包结果。
总体
(() => {
// 段落1
// 段落2
// 段落3
// 段落4
})();
代码总体结构是一个立即执行函数,这是为了避免污染全局变量。
段落1
var __webpack_modules__ = ({
"./src/add-cs.js":
((module) => {
eval(eval1);
}),
"./src/add-es.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval(eval2);
}),
"./src/main.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval(eval3);
})
});
var __webpack_module_cache__ = {};
这一段定义了两个变量:
__webpack_modules__
模块字典,键为模块路径,值为一个函数,函数后面进行分析。
__webpack_module_cache__
模块缓存字典。
段落2
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) { // 如果 缓存里 有 id为moduleId 的模块
return __webpack_module_cache__[moduleId].exports; // 从 缓存字典中返回 exports
}
var module = __webpack_module_cache__[moduleId] = { // 否则, 使用id定义缓存字典,exports为空
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
return module.exports;
}
这里定义了__webpack_require__
函数,函数接收模块id,返回值为模块对象的exports
属性。
- 对于存在
__webpack_module_cache__
中的模块,直接返回exports
属性。 - 对于不在
__webpack_module_cache__
中的模块,在__webpack_module_cache__
中定义一个空对象module
,并module
作为参数,执行__webpack_modules__
对应moduleId
的函数。最后,返回对象module.exports
。
段落3
(() => {
__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] });
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => { // r函数 标记函数:接收 exports 对象。 在 exports 对象上添加属性 __esMoudle 为 true
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
这里使用立即执行函数,在__webpack_require__
上定义了三个函数d
o
r
。
d
将对象definition
上的属性,定义在exports
上。
o
判断对象obj
上,是否存在属性prop
r
给对象exports
添加属性Symbol.toStringTag
和__esModule
。
段落4
var __webpack_exports__ = __webpack_require__("./src/main.js");
调用__webpack_require__
。
eval
通过上述代码分析,我们知道了webpack通过__webpack_require__
加载依赖,对于不在__webpack_module_cache__
中的模块回去执行__webpack_modules__
里的方法,而这些方法使用eval执行了一串代码串。下面,我将代码串改成了容易看懂的格式。
首先,我们需要明确,这三段代码有输入参数:
__unused_webpack_module__
module对象 值为{exports:{}}
__webpack_exports__
__unused_webpack_module__
的 exports
属性
__webpack_require__
函数__webpack_require__
// **eval1**
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(
__webpack_exports__,
{
"add": () => (add)
}
);
function add(a, b) {
return "es module:" + (a + b);
}
//# sourceURL=webpack://webpacktest/./src/add-es.js?
// **eval2**
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(
__webpack_exports__,
{
"add": () => (add)
}
);
function add(a, b) {
return "es module:" + (a + b);
}
//# sourceURL=webpack://webpacktest/./src/add-es.js?"
这两段代码做的几乎一样,但是,我们的原代码的模块导出不见了。
可以推测,webpack对代码进行了分析,将模块导出部分,重构成了d
函数的实现。
// **eval3**
__webpack_require__.r(__webpack_exports__);
var _add_es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add-es.js");
let add_cs = __webpack_require__("./src/add-cs.js");
console.log((0, _add_es__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2))
console.log(add_cs.add(3, 4))
//# sourceURL=webpack://webpacktest/./src/main.js?
第三段代码也就顺理成章了,模块导入的代码被替换为__webpack_require__
函数。
所以webpack的模块系统是这样工作的。
- 分析原代码,将原代码中的模块导入都替换为
__webpack_require__
函数,导出替换为d
函数。 - 替换完成后,生成了代码串,根据文件路径放到
__webpack_modules__
中。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!