JavaScript诞生之初的应用比较简单,例如用户输入校验。代码量少,一般直接写在html中。
<script>
var name="kobe"
console.log(name)
script>
随着用户体验要求变高,前端承载的功能变多了,代码量也随着膨胀。
例如,axios的出现带来了前后端分离,前端通过后端接口获取数据,动态渲染页面;SPA让页面切换更丝滑,但需要实现前端路由,状态管理等功能。
为了提高代码的可复用性,人们开始将JavaScript从html解耦,封装成独立的模块。
// index.html
<script src="moduleA.js">script>
// moduleA.js
var name="kobe";
引入的外部JavaScript文件中声明的变量是全局作用域的,当引用的第三方模块多了,势必会造成全局变量冲突。
// index.html
<script src="moduleA.js">script>
<script src="moduleB.js">script>
// moduleA.js
var name="kobe";
// moduleB.js
var name="iverson";
解决全局变量冲突的方法之一是给每个模块分配命名空间,进行隔离。
// index.html
<script src="moduleA.js">script>
<script src="moduleB.js">script>
// moduleA.js
var moduleA = {};
moduleA.name="kobe";
// moduleB.js
var moduleB = {};
moduleB.name="iverson";
但是,JavaScript对象属性默认是公有的,这意味着模块内的变量既能被外部访问let scopeVar = moduleA.name,也可能被外部修改moduleA.name = ‘xxx’。那么,如何保护数据不被外部修改?
为了保护数据不被外部修改,人们将模块封装在函数作用域内。
// index.html
<script src="moduleA.js">script>
<script src="moduleB.js">script>
// moduleA.js
let moduleA = (function (){
let name = 'kobe';
return {
getName:function(){
return name;
}
}
})();
// moduleB.js
let moduleB = (function (){
let name = 'iverson';
return {
getName:function(){
return name;
}
}
})();
使用立即调用的函数表达式(IIFE) 创建闭包,将模块封装在函数作用域内,并对外提供可访问模块的公共API。这样,在moduleB中可以通过moduleA.getName()访问数据,但是必须保证moduleA在moduleB之前完成加载。当模块数量多了,如何管理好模块依赖又是一个问题。
综上所述,为了提高JavaScript代码的可复用性,开发者尝试利用JavaScript语言特性来模拟实现模块化。分别用命名空间和闭包来解决全局变量冲突和实现数据保护。然而,管理模块依赖是比较复杂的问题,因此诞生了CommonJS,AMD,UMD等模块化方案。
模块化思想就是把代码拆分成独立的模块,逻辑独立,各自实现,然后再把它们连接起来实现完整功能。对应的实现模式就叫模块模式,它是所有模块化系统的基础。
每个模块都有一个可用于引用它的标识符。
import axios form "axios"
import msgBox from "/src/ui.js"
模块化系统的核心是管理依赖,即保证模块正常运行时所需要的外部依赖能够完成加载和初始化。
// moduleA.js
import moduleB form "moduleB.js"
console.log(moduleB.getName())
加载模块的一般步骤是: 加载模块及其依赖的代码,在所有依赖加载和初始化之后,才会执行入口模块。
浏览器加载JavaScript文件默认是同步的。
例如moduleA.js依赖了moduleB.js,moduleB.js依赖了moduleC.js,则必需按以下顺序加载依赖。
<script src="moduleC.js">script>
<script src="moduleB.js">script>
<script src="moduleA.js">script>
同步加载的缺点很明显:
异步加载模块,不会阻塞页面,只需要在加载完成后执行回调。
例如上面同步加载的例子改用异步加载,则不会阻塞页面渲染,只需要在moduleC.js和moduleB.js完成加载和初始化之后,回调执行moduleA.js。
可使用的defer或async属性实现异步加载。
模块模式是模块化思想的一种实现模式,也是所有模块化系统的基础。具体包括模块标识符,模块依赖,模块加载等。
CommonJS是一种面向服务器端的模块化规范,它规定一个文件就是一个模块,使用require()导入模块,使用module.exports导出模块。
var moduleB = require('./moduleB');
module.exports = {
stuff: moduleB.doStuff();
};
require()的返回值就是module.exports导出的对象。
首次require一个模块时,会加载并执行该模块文件,并返回模块的module.exports值,该值会被缓存。下次require该模块时,取的是缓存数据,不需要再加载执行模块了。
require()返回的是模块导出对象的拷贝,也就是说,模块内部的变化不会影响已导出的对象。
CommonJS使用同步加载,这并不适用浏览器端,因为网络延迟会阻塞页面渲染。
npm模块是遵循CommonJS的,不能直接在浏览器运行,需要做格式转换。
实现思路:
使用IIFE创建闭包来封装模块,并传入module,module.exports,require。
(function(module,exports,require){
require("a.js");
// 模块内容
module.exports.b=2;
})(module,module.exports,require)
将所有模块都封装在各自的IIFE中,从入口模块开始,递归处理依赖,最终打包成一个能在浏览器中运行的JavaScript文件。
(function(modules){
require("index.js");
})({
"a.js":function(module,exports,require){
模块a的内容
},
"index.js":function(module,exports,require){
模块index的内容
}
})
定义一个require函数来加载模块,实现缓存加载和拷贝导出。
(function(modules){
let installedModules={};
function require(moduleName){
if(installedModules[moduleName]){
return installedModules[moduleName].exports;
}
let module = {
moduleName,
exports:{}
}
modules[moduleName].call(module,module,module.exports,require);
modules[moduleName]=module;
return module.exports;
}
return require("index.js");
})({
"a.js":function(module,exports,require){
模块a的内容
},
"index.js":function(module,exports,require){
模块index的内容
}
})
CommonJS规定每个文件是一个模块,使用require()导入模块,使用module.exports导出模块。其中,还使用了缓存加载和拷贝导出。
CommonJS使用同步加载,这不适用浏览器,因为网络延迟会阻塞页面渲染。
npm模块是遵循CommonJS的,不能直接在浏览器运行,需要做格式转换。
AMD是一种面向浏览器端的模块化规范,使用异步加载,并在依赖加载后再回调执行入口模块。
定义模块:
define('moduleA', ['moduleB', 'moduleC'], function factory(moduleB, moduleC){
let nameB = moduleB.getName();
let nameC = moduleC.getName();
let name = nameB + nameC;
return {
getName: function(){
return name;
}
}
})
define()的第一个参数是模块标识符。第二个参数是依赖。第三个参数是模块工厂函数,用来封装模块。
导入模块:
require( ['moduleB', 'moduleC'], function main(moduleB, moduleC){
let nameB = moduleB.getName();
let nameC = moduleC.getName();
let name = nameB + nameC;
console.log(name);
})
ES6 Module是JavaScript语言标准的模块化方案,浏览器和node环境都是原生支持的,不需要格式转换。
在ES6 Module中使用import导入模块,使用export导出模块。
import moduleA from 'moduleA'
export default {
name:'moduleB'
}
浏览器加载ES6 Module
<script type="module">
// 模块代码
</script>
<script type="module" src="module path"></script>
在