JavaScript模块化编程
模块是指在一个大型复杂的系统中能够独立完成特定功能的子集,而模块化则是将一个复杂问题自顶向下逐层划分成若干模块的过程。
前端代码的复杂度在不断提升,经常需要使用到第三方控件,这时就需要引入控件的JS代码,JavaScript并不是一种模块化的编程语言,它不支持类,更别提模块了,所以传统的区分模块的方式是通过文件或者函数来实现的,但是这样做会出现因为引用了大量JS文件而导致网页打开慢的问题,JS文件太多同时也会出现依赖关系混乱的情况,而且各JS文件中的函数和变量会相互影响,污染了全局空间,很容易出现重复的名字,不易于维护,所以可以通过模块化编程的方式来解决这些问题。
JavaScript模块化规范
JS有一些非传统模式的开发规范,例如CommonJS、AMD、CMD,前一种用于服务端,后两种用于浏览器段。
CommonJS
2009年,美国程序员Ryan Dahl开发出了Node.js,将JavaScript语言用于服务端编程,标志着“JavaScript模块化编程”诞生。Node.js采用了CommonJS这种服务器端模块的规范。
一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,也就是说在模块内部定义的变量,无法被其他模块读取。每一个模块通过module.exports对象输出模块变量,通过require方法加载模块,最后返回文件内部的module.exports对象,如下代码所示:
// addition.js
exports.do = function(a, b){ return a + b };
上面的语句定义了一个加法模块,然后通过require语句获得exports对象
var add = require('./addition');
add.do(1,2)
CommonJS加载模块是同步的,所以只有脚本加载完成才能执行后面的操作,CommonJS主要用于服务端的编程,加载的模块文件一般存放在本地磁盘,加载比较快,但如果是浏览器,需要从服务端下载模块文件,如果网速慢,下载时间长,浏览器会处于“假死”状态,这就需要采用异步模式了,就有了AMD和CMD。
AMD
Asynchronous Module Definition,异步模块定义,它是RequireJS在推广过程中对模块定义的规范化产出。所有的模块将被异步加载,模块加载不影响后面语句的运行,所有依赖相应模块的语句均放置在回调函数中,在模块成功加载后才会执行。
AMD规范通过define函数来定义一个模块:
define( id?, dependencies?, factory );
其中id为模块的标识,dependencies为当前模块所依赖的模块,这两个参数是可选的,factory为一个需要进行实例化的函数或者对象,即为模块的内容。
require函数分为局部和全局,局部require出现场景如下:
define(['require'], function (require) {
//the require in here is a local require.
});
define(function (require, exports, module) {
//the require in here is a local require.
});
而全局的require和define一样,是一个全局作用域下的变量。require提供有三种类型的API,代码如下所示:
define(function (require) {
var a = require('a');
});
define(function (require) {
require(['a', 'b'], function (a, b) {
//modules a and b are now available for use.
});
});
define (function(require) {
var templatePath = require.toUrl('./templates/a.html');
});
CMD
Common Module Definition,通用模块定义,它是SeaJS在推广过程中对模块定义的规范化产出,和CommonJS一样,一个模块就是一个文件。
模块定义的define API同AMD规范类似,但是提供了两套require API用于同步和异步加载模块,require(id)与require.async(id, callback),同时,CMD不提供全局的require方法,require是作为define中工厂方法的第一个参数传入的,如下代码所示:
define(function(require, exports, module) {
// 模块代码
});
AMD与CMD的区别
AMD与CMD都是用于前端JavaScript模块化编程的规范,但是它们之间还是存在一定的差别:
- CMD推崇依赖就近,AMD推崇依赖前置,如下代码所示:
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
// AMD
define(['./a', './b'], function(a, b) { // 依赖一开始就写好
a.doSomething()
b.doSomething()
// ...
})
- AMD的API一个当多个用,CMD的API严格区分,推崇职责单一。比如AMD里,require分全局require和局部require,都叫require,而CMD里却没有。
- CMD的模块是延迟执行,也就是说在require的时候在执行模块代码,而AMD在确定依赖关系的时候就把模块代码执行了,据说RequireJS 2.0已经可以延迟执行了,不过还是没有找到方法,如果有找到的还望不吝赐教。
模块化编程示例
基于AMD的代表作有RequireJS,基于CMD的代表作有SeaJS,虽然这两款模块化管理的工具库目前对这两种规范都做出了一定的兼容,但是使用上还是存在差异,下面通过两个示例来看看这两个库是如何使用的。
下面的示例在页面上显示一个加法模块和一个减法模块的计算结果,这两个模块被主模块main依赖。
RequireJS
在HTML中添加如下标签引入RequireJS文件:
<script src="lib/require.js" data-main="js/main" ></script>
标签中通过data-main定义了入口模块,然后对require加载器进行配置:
require.config({
baseUrl: 'js',
paths:{
Module1: 'module1',
Module2: 'module2'
}
});
其中baseUrl为模块查找的根路径,paths对模块名进行映射,映射的内容为放置在baseUrl配置的根目录下的js文件,path不含.js后缀。
然后定义主模块,主模块中依赖加法模块和减法模块,通过require的方式引入:
define(function (require) {
console.log("main");
var module1 = require("module1");
var module2 = require("module2");
document.getElementById("module1").innerHTML = "module1的计算结果为:" + module1.add(1,2);
document.getElementById("module2").innerHTML = "module2的计算结果为:" + module2.minus(4,2);
return {};
});
这里require的两个模块可以作为define的参数传入,两种规范在写法上存在不同,大家可以自己试试,不过AMD兼容性更好一点。然后用同样的方法定义加法和减法模块:
//加法模块
define(function () {
console.log("module1");
return {
add : function( x, y ) {
return x + y;
}
};
});
//减法模块
define(function () {
console.log("module2");
return {
minus : function( x, y ) {
return x - y;
}
};
});
最后的运行结果:
SeaJS
在模块定义的时候使用了AMD和CMD兼容的方式,所以使用SeaJS的时候模块部分代码不需要做修改,但是在HTML文件中的引入和配置部分有不同:
<script src="lib/sea.js"></script>
<script>
// seajs 的简单配置
seajs.config({
base: "./js",
alias: {
"Module1": "module1",
"Module2": "module2"
}
});
// 加载入口模块
seajs.use("main");
</script>
与RequireJS不同的是,在引入JS文件的时候不需要配置主模块的入口,而是通过seajs.use去加载,而且配置项也有所不同,更多的选项可以参考SeaJS的API文档。
我在每个模块中都加入了log打印,RequireJS和SeaJS的结果不一样:
RequireJS: SeaJS:
结果印证了上面列的不同点,RequireJS采用的AMD规范会先执行依赖的模块,而SeaJS不会。