随着互联网的发展,前端的技术已经发生了巨大的改变,早期的前端技术标准根本没有预料到前端行业会有今天这个规模,所以在设计上存在很多缺陷,导致我们现在去实现前端模块化时会遇到诸多问题。虽然说,如今绝大部分问题都已经被一些标准或者工具解决了,但在这个演进过程中依然有很多东西值得我们思考和学习。
1. 文件划分
约定一个文件就是一个模块
// module-a.js
fucntion foo() {
// xxxx
}
// module-b.js
var zoo = 1;
// index.html
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 直接使用全局成员
foo() // 可能存在命名冲突
console.log(zoo)
zoo = 2 // 数据可能会被修改
</script>
</body>
缺点:
- 模块直接在全局工作,大量模块成员污染全局作用域。
- 没有私有空间,所有模块内部的成员都可以在模块外部被访问或者修改。
- 容易造成命名冲突。
- 无法管理模块之前的依赖关系。
2. 命名空间
约定一个模块暴露一个全局对象,所有模块成员都挂载到这个全局对象上。
// module-a.js
window.a = () => {
// xxx
}
// module-b.js
window.b = {
data: 1
}
// index.html
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 直接使用全局成员
a();
console.log(window.b.data);
window.b.data = 2 // 数据可能会被修改
</script>
</body>
缺点:
- 模块直接在全局工作,模块成员污染全局对象。
- 没有私有空间,模块成员对象之前可以相互修改。
- 无法管理模块之间的依赖关系。
3. IIFE
约定使用立即执行函数表达式包裹模块。将模块放在立即执行函数的私有作用域中。对于需要向外暴露的成员,挂载全局对象上。
/ module-a.js
;(function() {
function method1 () {
console.log(1)
}
window.moduleA = {
method1: method1
}
})();
/ module-b.js
;(function() {
function method1 () {
console.log(1)
}
window.moduleB = {
method1: method1
}
})();
缺点:
- 无法管理模块之间的依赖关系。
4. IIFE 依赖参数
在 IIFE 的基础上,利用 IIFE 参数作为依赖声明使用。
// module-a.js
;(function ($) { // 通过参数明显表明这个模块的依赖
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
window.moduleA = {
method1: method1
}
})(jQuery)
到这里虽然已经解决了最开始文件划分模块的所有问题,但是还有一个通病没有得到解决,就是模块的加载,通过 script 这种方式直接在页面中加载模块,这意味着模块的加载并不受控制。
5. AMD
异步模块定义规范,AMD 模式比较出门的库 require.js,异步加载模块,模块加载后立即执行,依赖必须提前申明好,依赖前置。
在 AMD 规范中约定每个模块通过 define() 函数定义,这个函数默认可以接收两个参数,第一个参数是一个数组,用于声明此模块的依赖项;第二个参数是一个函数,参数与前面的依赖项一一对应,每一项分别对应依赖项模块的导出成员,这个函数的作用就是为当前模块提供一个私有空间。如果在当前模块中需要向外部导出成员,可以通过 return 的方式实现。除此之外,Require.js 还提供了一个 require() 函数用于自动加载模块,用法与 define() 函数类似,区别在于 require() 只能用来载入模块,而 define() 还可以定义模块。当 Require.js 需要加载一个模块时,内部就会自动创建 script 标签去请求并执行相应模块的代码。
// a.js
define(['./moduleA.js', './moduleB.js'], function(moduleA, moduleB) {
// 导出成员
return {
doing: () => {}
// xxx
};
});
require(['./a.js'], function(a) {
a.doing();
});
缺点:
- 使用复杂
- 模块划分过于细致时,一个页面可能会产生对 js 的重复加载,导致效率降低。
6. CMD
淘宝 SeaJs,异步加载模块,所有依赖先提前加载,但是不会执行依赖模块的代码,懒执行。等到模块在被调用到时才会去执行代码。可以依赖就近。SeaJs 算是重复造轮子了,随着时代的发展,SeaJs 也被 Require.js兼容了。
define(function(require, exports, module) {
var $ = require('jquery');
module.exports = function () {};
});
7. CommonJS
Node.js 中遵循的模块规范,该规范中一个文件就是一个模块,每一个模块都有自己的作用域,通过 module.exports 导出成员,再通过 require 函数载入模块。CommonJS 约定的是以同步的方式加载模块。
缺点:
不适合在浏览器环境中运行,原因是同步的模块加载机制,导致运行效率低下。
8. UMD
因为 AMD,CommonJS 规范是两种不一致的规范,虽然他们应用的场景也不太一致,但是人们仍然是期望有一种统一的规范来支持这两种规范。于是,UMD(Universal Module Definition,称之为通用模块规范)规范诞生了。客观来说,这个UMD规范看起来的确没有 AMD 和 CommonJS 规范简约。但是它支持 AMD 和 CommonJS 规范,同时还支持古老的全局模块模式。个人觉得 UMD 规范更像一个语法糖。应用 UMD 规范的 js 文件其实就是一个立即执行函数。函数有两个参数,第一个参数是当前运行时环境,第二个参数是模块的定义体。在执行 UMD 规范时,会优先判断是当前环境是否支持 AMD 环境,然后再检验是否支持 CommonJS 环境,否则认为当前环境为浏览器环境(window)。当然具体的判断顺序其实是可以调换的。
9. ES Modules
深入了解 ES Modules