YongSir

专业程序员伪装者

js组件化演进探讨-Commom.js/AMD/CMD规范

引子

在早先在的组件化演化一文中,通过对组件封装的一步一步推演,最终得到了一个集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
31
function 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中浏览器端的模块化标准:AMDCMD

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使用它来启动脚本的自动加载过程,这样就可以组织一个典型的页面结构:
Site页面组织结构

然后使用时借助require([dependency, ...], function() {})函数

1
2
3
4
5
6
7
8
9
10
requirejs.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 () {
"use strict";
var a = 1;
function inc(){
a++;
}
function get(){
return a;
}
return {
inc: inc,
get: get
}
})();
return sample;
}, this);

这样就实现了写法兼容,想怎么写都行了