模块化的 JavaScript 设计模式

随着网站越来越大,JavaScript 模块化编程,已经成为一个迫切的需求。但是,当前版本的 JavaScript(ECMA-262第5版)并不以简洁、有组织的方式为开发人员引入这种代码模块的方式。通过开发人员的不断努力,实现了模块的效果。下面主要介绍三种方式:AMD、CommonJS 以及 ES6 中的原生模块。

AMD

异步模块定义(AMD)提供了定义模块,以及异步加载模块的依赖的机制,本质上具有高灵活性,消除了代码和模块之间可能惯有的紧耦合,它非常适合于浏览器环境。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到依赖模块加载完之后,这个回调函数才会运行。

模块定义

define用于定义已命名或未命名模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define(
module_id,//可选,模块名,匿名模块更加 DRY
[dependencies],//可选,依赖模块
definition_function//实例化模块或对象的函数
);
define('myModule', ['jquery', 'layer'], function($, layer){
var myModule = {
doThat: function(){
$('#btn').onclick = function(){
layer.msg('点击按钮了!');
}
}
}
return myModule;
});

模块加载

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
require(
[module],//数组,要加载的模块
callback //加载成功后回调函数
);
require(['jquery', 'layer'], function($, layer){
$('#btn').onclick = function(){
layer.msg($('#ipt').value());
}
});
//动态加载依赖
define(function(){
var isReady = false,
foobar;
require(['foo', 'bar'], function(foo, bar){
isReady = true;
foobar = foo() + bar();
});
return{
isReady: isReady,
foobar: foobar
}
});

requireJS

require.js的加载

1
2
<script src="js/require.js" data-main="js/main"></script>
<!-- data-main属性的作用是指定网页程序的主模块,由于require.js默认后缀名是js,所以主模块可以把 js/main.js 写成 js/main -->

主模块写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require.config({
baseUrl: 'js/',
paths: {//模块路径,相对于 baseUrl
'jquery': 'jquery.min',
'layer': 'layer.min',
'moduleA': 'moduleA.min'
},
shim: {//加载非规范模块
'moduleA': {
deps: ['jquery'], //该模块的依赖模块
exports: 'moduleA' //输出的变量名(模块名)
}
}
});
require(['jquery', 'layer', 'moduleA'], function($, layer, moduleA){
//do something
});

JSONP服务依赖

JSONP是在javascript中服务调用的一种方式。它仅需简单地通过一个script标签发起HTTP GET请求,是实现跨域服务调用地一种公认手段。
为了在RequireJS中使用JSON服务,需要将callback参数指定为’define’。这意味着你可以将获取到地JSONP URL的值看成一个模块定义。

1
2
3
require(['http://example.com/api/data.json?callback=define'], function(data){
console.log(JSON.parse(data));
});

CommonJS

CommonJS 模块是一种适用于服务器端的模块化。基本上包含两个主要部分:自由变量 exports,它包含了一个模块希望其他模块能够使用的对象;以及require函数,模块可以使用该函数导入(import)其他模块的导出(exports)。

1
2
3
4
5
6
7
8
9
//引入依赖模块
var lib = require('package/lib');
function foo(){
lib.log('maxmeng');
}
//导出(暴露)foo给其他模块
exports.foo = foo;

ES6 Module

ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。现在浏览器没有完全支持需要使用 bable 转译。

1
2
3
//ES6模块
import {star, exists, readFile} from 'fs';
//上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。当然,这也导致了没法引用ES6模块本身,因为它不是对象。

浏览器使用ES6模块的语法如下

1
2
<script type="module" src="foo.js"></script>
上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个ES6模块。

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

export

1
2
3
4
5
//profile.js
export var firstName = 'Max';
export var lastName = 'Meng';
export var age = 24;
//使用export命令对外部输出了三个变量。

export的另一种写法:

1
2
3
4
5
6
//profile.js
var firstName = 'Max';
var lastName = 'Meng';
var age = 24;
export {firstName, lastName, age};//应该优先使用这种方法

export 不仅可以输出变量,还可以输出函数和类(class)。

1
2
3
4
5
6
7
8
9
10
11
12
13
export function multiply(x, y){
return x + y;
}
//或
function v1(){...}
export{
v1 as streamV1
}
//上面代码使用as关键字,重命名了函数v1的对外接口。

import

1
2
3
4
5
6
7
import {firstName, lastName, year} from './profile';
//为输入变量重命名
import { lastName as surname } from './profile';
//模块的整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
import * as user from './profile';