为什么会出现模块化
早期的前端,只是简单的页面展示,代码量少,但是随着web技术的发展,ajax的出现,各种交互使得页面的功能越来越丰富, 这同时也让前端的代码越来越多,复杂度越来越高,带来了全局变量的污染,依赖关系混乱,维护性差,复用性差的问题。
前端模块化就是为了解决这些问题,提高代码的维护性和可服用性。
什么是模块化
模块化是将一个复杂的系统,分解为独立可复用的软件开发方式。每个模块负责特定的功能,组装起来成为一个整体。
前端模块化的发展
全局Function
将不同功能封装成不同的函数
function a(){...}
function b(){...}
缺点:方法直接挂在window下,污染全局变量,并且容易造成命名冲突。
命名空间模式
解决命名冲突的问题,使用对象封装函数
var module = {
a:function(){....},
b:function(){....},
}
缺点:外部可以修改模块内的数据
立即执行函数(IIFE)
解决模块暴露的问题,将数据私有
var MyModule = (function() {
var privateVar = 'private';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
//可以传入参数,达到引用其他模块的目的
现代模块化的基石。(commonJS的实现也是这个思路)
缺点:依赖过多传入,代码阅读困难;无特定语法支持,代码简陋。
存在的问题
- 模块拆分后,通过多个script标签引入,创建了多次请求。
- 依赖模糊,不能确定模块的先后顺序,加载顺序不同可能会带来错误。
- 没有明确的关系依赖,维护变得更困难
模块化标准
commonJS
nodeJS的出现,让js编写服务端成为可能。 为了解决服务端的模块化问题,出现了commonJS规范。
这个规范定义了模块的基本结构,模块的导入导出。
特点
- 每个文件都是一个独立的模块,独立作用域,不会污染全局空间。
- 同步加载模块,require导入之后,会立即执行这个模块, 将执行结果进行返回。(保证了模块的加载顺序)
- 当模块加载完成一次之后, 会有缓存,再次加载会直接返回缓存结果。
- 模块导出的值是值的拷贝, 导出之后,之前模块发生变化不会影响。
导出
可以使用module.export 或者 export 两种方式进行导出。
//导出一个变量
module.export.name = 'a';
module.exports = {
foo: function() {
console.log('foo');
}
};
export.foo = function(){...}
导入
nodejs在导入时,只有执行到require的时候,才会执行。 这样保证了模块的执行顺序。
当模块执行过后,会进行缓存,下次执行时,判断是否有缓存, 有缓存直接拿缓存。
动态加载:可以在代码的任何地方使用require。
var moduleA = require('./moduleA');
原理
实现commonJS,node做了什么事情呢?
两种导出方式有什么不同呢?
//导出
(function(exports, require, module, __filename, __dirname) {
module.exports = {};
var export = module.export;
return module.export
});
可以看出, module.export是真正的导出对象,export只是导出值的一个引用。不能直接对export直接赋值,否则就会断开和module.export的引用。 使用过程中尽量保持一种写法。
局限性
- 因为node时运行在服务端,所以可以直接读取本地文件,这样不需要考虑同步执行时的效率问题,但是在浏览器端是要从远端获取数据,同步执行会造成卡顿。
- 模块加载是动态加载, 无法静态分析。
要想实现浏览器模块化,需要解决两个问题: 第一解决浏览器远程加载数据的问题(异步)第二解决模块执行环境的问题(放在函数中执行)
AMD
异步模块定义规范。 适用于浏览器,RequireJS实现。
核心理念:异步加载,提前执行
//定义一个模块
define('module1', ['module2', 'module3'], function(module2, module3) {
// 模块逻辑
function foo() {
return module2.doSomething() + module3.doSomething();
}
// 导出模块功能
return {
foo: foo
};
});
// 加载模块 'module1'
require(['module1'], function(module1) {
module1.foo();
});
require会将第一个参数中的依赖,加载完成,才会执行第二个参数的回调函数。
优点
- 异步加载:适用于浏览器环境
- 依赖关系明确
使用的时候,需要依赖requireJS
CMD
解决浏览器模块化,seaJS实现。
核心理念:依赖就近,延迟执行
// 定义一个模块myModule
define(function(require, exports, module) {
// 就近依赖,不会理解执行。
var dep1 = require('./dep1');
var dep2 = require('./dep2');
function foo() {
//只有模块被使用时,才去加载依赖的模块
return dep1.doSomething() + dep2.doSomething();
}
// 导出模块功能
exports.foo = foo;
});
// 加载模块 'myModule'
seajs.use(['myModule'], function(myModule) {
myModule.foo();
});
优点
- 减少了无用依赖的加载,避免提前加载造成的资源浪费。
- 避免过早的加载未使用的模块, 启动更快
- 延迟执行可以避免不必要的代码执行
UMD
UMD模块化解决方案,旨在兼容多种JS模块加载机制(node使用commonJS,浏览器端使用AMD),他的目的是同一个模块在不同环境中可以使用。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// 如果环境支持 AMD(如 RequireJS),使用 AMD 定义模块
define(factory);
} else if (typeof module === 'object' && module.exports) {
// 如果环境支持 CommonJS(如 Node.js),使用 CommonJS 定义模块
module.exports = factory();
} else {
// 否则将模块挂载到全局变量(window或者global)
root.myModule = factory();
}
}(this, function () {
// 这里是模块的主体,返回模块暴露的内容
var myModule = {
hello: function () {
console.log('Hello, UMD!');
}
};
return myModule;
}));
ESM模块化标准
ES6引入了ESModule模块化标准。
使用方式:
// moduleA.js
export function foo() {
console.log('foo');
}
// main.js
// 所有路径必须以./或者../开头
import { foo } from './moduleA.js';
foo();
<!-- 在浏览器中加载ESModule模块 -->
<script type="module" src="./moduleA.js"></script>
//也支持动态导入
// 动态导入模块
import('./math.js').then(math => {
console.log(math.add(1, 2)); // 3
});
优点
- 一个文件一个模块,封装了自己模块的作用域,解决了全局变量污染的问题
- 依赖清晰
- 静态分析依赖,工具在编译时分析代码,进行一些优化(Tree Shaking)
- javeScript官方提出, 对浏览器和common都兼容。
对比commonJS
- commonJS模块输出的是值的拷贝,修改导出后的值不会影响引用的值,ESM是值的引用,修改会影响引用的值
- commonJS是运行时加载,在运行时才能知道模块的依赖关系。ESM是编译时就可以知道依赖关系。