引子
在早先在的组件化演化一文中,通过对组件封装的一步一步推演,最终得到了一个集API权限管理,节省命名空间,以及调用方便的方式,但仍然没有解决所有的问题,比如模块依赖管理,而这是模块化演进到可用级别的必要条件
如果不是把工作重点转移到公司前端的工作上,无论如何都不会想到前端为了处理命名空间冲突+组件加载及依赖管控+API接口权限管理
,这种绝大多数语言几乎天然拥有的能力,而为此付出了多少,在Java一个package,C++中一个import module 在前端的工作中确实在是大费周折,本质原因就是js原本是被设计成一门脚本语言,设计者也没有料到web会发展成今天这样复杂和多样,而只是以函数作为作用域标准,如果不加处理项目稍大,命名空间的管理和组件的依赖就是灾难级别的,所以modeul化是必然并还将继续发展,正如:
规范形成的过程是痛苦的,前端的先驱在刀耕火种、茹毛饮血的阶段开始,发展到现在初具规模
模块化?
道理都说了,那就从代码来直观的展示一下模块化的历程,以下每一个🔜
表示一种演进:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function foo1(){
// do something
}
function foo2(){
// do something
}
// 🔜 函数 => 类
var Module = {
var1: 1,
var2: 2,
foo1: function(){ /* do something */ },
foo2: function(){ /* do something */ }
}
Module.foo2();
Module.var2 = 100; // 无权限控制
// 🔜 类 => 借助立即执行函数
var myModule = (function(){
var var1 = 1;
var var2 = 2;
foo1: function(){ /* do something */ },
foo2: function(){ /* do something */ }
return {
fn1: foo1,
fn2: foo2
};
})();
myModule.foo1()
很容易看出利用立即执行函数的好处,在以上的基础上随着服务器端NodeJS的兴起,终于推出了第一个模块化的js规范 – CommonJS
服务器端的CommonJS
具体来说就是3个基本规范:
1 作用域:单独文件,单独作用域,除非是Global的
2 输出:指定module.exports
为唯一出口
3 依赖:通过require
加载模块
于是上述demo套用这个规范以后就是:1
2
3
4
5
6
7
8
9
10
11
12
13// --- Tony.js中 ---
var name = 'Tony Stark'
fucntion sayName() {
console.log('Iron man is' + name)
}
module.exports = {
run: sayName
}
// --- Other file ---
var tonyModule = require('./Tony.js') // 同步,并且相对路径时js需省略
tonyModule.sayName()
如果完全将CommonJS
移植到浏览器,那么require
同步加载势必会影响体验,但不同步呢依赖件的管理就不那么容易了,这个问题偏向的不同导致了2中浏览器端的模块化标准:AMD
和CMD
AMD 和 CMD
但是万变不离其宗,不管是谁都需要解决模块定义,输出和输入(加载)
这3个问题
AMD(Asynchronous Module Definition)
: 通过RequireJS
库,实现了浏览器端异步模块定义,具体来说:
1 模块定义和输出:使用全局函数define(id?, dependencies?, factory/exportValue)
定义模块,其中的最后一个参数factory/exportValue
为输出,可以是一个对象的值,更多的是初始化工厂方法
2 异步输入/加载: require([dependency, ...], function() {})
函数,会自动异步加载依赖项,直到加载完成才执行对应操作
1 | <script data-main="js/app.js" src="lib/require.js"></script> |
页面上只出现一个<script data-main="js/app.js" src="js/require.js"></script>
即可,这里的data-main
是require的入口路径有的类似于main()
函数一样,requirejs使用它来启动脚本的自动加载过程,这样就可以组织一个典型的页面结构:
然后使用时借助require([dependency, ...], function() {})
函数1
2
3
4
5
6
7
8
9
10requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
}
});
requirejs(['jquery', 'canvas', 'app/sub'], function ($, canvas, sub) {
//using jQuery, canvas and the app/sub here
});
这就是AMD
规则
CMD(Common Module Definition)
: 以SeaJS
为基础,国内发展出来的通用模块加载规则,其定义和输入输出如下define(id?, [dependency...], function(require, exports, module) { });
,一个简单的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Tony.js
define(function(require, exports, modules)) {
var $ = require('jQurey.2.1.js') // 假设tony需要使用jQurey
var name = 'Tony Stark'
fucntion sayName() {
console.log('Iron man is' + name)
}
module.exports = sayName // 输出
}
// ----- Other file ------
var sayName = require('lib/Tony.js')
var ironMan = new Tony()
ironMan.sayName()
可看出更commondJS更相似,推崇一个文件一个模块,推崇就近加载,也就是用到就去加载
关于这两个的区别网上可以搜出一堆文章,简单总结一下
- 最明显就是:在模块定义时,对依赖模块的执行时机处理不同(注意不是加载的时机或者方式不同)
AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
CMD推崇就近依赖,只有在用到某个模块的时候再去require
这种区别各有优劣,只是语法上的差距,而且就现在阶段来说,requireJS和SeaJS都支持对方的写法,所以写法的差距都不在具有区分度
很多人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解实际上是不准确的,其实加载模块都是异步的,只不过AMD依赖前置,js可以方便知道依赖模块是谁,立即加载,而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,但实际上解析模块用的时间短到可以忽略
同样都是异步加载模块,AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的
组合兼容式
CMD 和 AMD 最初从在写法上就能区分,但是现在逐渐都兼容对方的写法,虽然对于学习来说不免会带来一些困惑,但对于实际项目的便利性确是实实在在的,其实兼容起来很容易,对于AMD/CMD检测define
,对于CommonJS检查module.export
,实现起来大致如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29(function (name, definition, context) {
if (typeof module != 'undefined' && module.exports) {
// 在 CMD 规范下 (node)
module.exports = definition();
} else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd']) ) {
//在 AMD 规范下(RequireJS) 或者 CMD 规范下(SeaJS)
define(definition);
} else {
//在浏览器环境下
context[name] = definition();
}
})('sample', function () {
var sample = (function () {
;
var a = 1;
function inc(){
a++;
}
function get(){
return a;
}
return {
inc: inc,
get: get
}
})();
return sample;
}, this);
这样就实现了写法兼容,想怎么写都行了