随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试。模块化是一种非常好的代码组织方式,本文试着对JavaScript模块化开发的一些基础知识和具体使用做一些阐释。
何为模块化开发?
“模块是为完成某一功能所需的一段程序或子程序。模块是任何robust(健壮、强壮)的应用架构不可缺少的一部分,是系统中职责单一且可替换的部分。”
简单理解:模块就是实现特定功能的一组方法,用用来实现代码的封装、增强代码可重用性,满足代码不断维护升级,分工合作的需要。如何开发新的模块,和复用已有模块来实现应用的功能,是我们需要考虑的事情,理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
关于模块,更多更详细部分请参考:深入理解JavaScript 模块模式
Javascript中的模块化开发
JavaScript 的当前版本,并没有为开发者们提供以一种简洁、有条理地的方式来引入模块的方法(ECMAScipt第六版表示会支持)。
作为代替,当前的开发者们只能*降级使用模块模式或是对象字面量模式的各种变体。通过很多这样的方法,各模块的脚本被串在一起注入到 DOM 中(作为 script
标签注入到 DOM 中)。
但好消息是在前端前辈们的不懈努力下,如今编写模块化的 JavaScript 目前已经变得极为简单,并摸索出一些普遍适用性的标准:AMD、CommonJS
Javascript模块化开发标准
CommonJS
wiki地址 http://wiki.commonjs.org/
JavaScript是一个强大、流行的语言,它有很多快速高效的解释器。官方JavaScript标准定义的API是为了构建基于浏览器的应用程序。然而,并没有定于一个用于更广泛的应用程序的标准库。
commonjs 是一个志愿性质的工作组,它致力于设计、规划并标准化 JavaScript API。从而填补原生JavaScript标准库过少的缺点。它的终极目标是提供一个类似Python,Ruby和Java标准库。它试图覆盖更宽泛的方面比如 IO、文件系统、promise 模式等等。这样的话,开发者可以使用CommonJS API编写应用程序,然后这些应用可以运行在不同的JavaScript解释器和不同的主机环境中。现在非常火爆的nodejs实际上就是commonjs的一个实现。 ——CommonJS是一种规范,NodeJS是这种规范的实现。
在CommonJS中,使用全局性方法require()加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
// 加载模块
var math = require('math');
// 调用模块提供的方法
math.add(2,3); //
注意,math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,在网站性能优化正在逐步成为产业的今天,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
AMD RequireJS介绍
AMD是“Asynchronous Module Definition”的缩写,意思就是“异步模块定义”。从名称上就可以看出,它是通过异步方式加载模块的,模块的加载不影响后续语句的执行,所有依赖加载中的模块的语句,都会放在一个回调函数中,等到该模块加载完成后,这个回调函数才运行。
它是适合script tag的,是专门为浏览器中JavaScript环境设计的规范,它有很多独特的优势,包括天生的异步及高度灵活等特性,这些特性能够解除常见的代码与模块标识间的那种紧密耦合。
RequireJS 是 AMD 规范最好的实现者之一,是一个非常小巧的 JavaScript 模块载入框架。 从架构层抽象出“模块化”开发方案,并已标准化了模块化开发,同时和其他的开发框架保持兼容。
主要用于浏览器端,但也适用于Rhino / Node 等环境,是当今最常用的JavaScript库之一,它兼容所有主流浏览器。
IE 6+ .......... compatible ✔
Firefox 2+ ..... compatible ✔
Safari 3.2+ .... compatible ✔
Chrome 3+ ...... compatible ✔
Opera 10+ ...... compatible ✔
使用RequireJS,我们能够更容易地实现更复杂,更强大的JS的富客户端程序。
使用它我们可以解决两个问题:
(1)实现js文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护。
它同时还起到了隐形命名空间的作用
注:无论当前 JavaScript 代码是内嵌还是在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成。JavaScript 执行过程耗时越久,浏览器等待响应用户输入的时间就越长,虽然可以使用async和defer关键字使得加载异步,但可能因此在加载过程中丢失加载的顺序。
require.js的使用
使用require.js的第一步,是先去官方网站下载最新版本。
下载后,假定把它放在js子目录下面,就可以加载了。
<!DOCTYPE html>
<html>
<head>
<title>My Sample Project</title>
<!-- data-main属性指定在require.js加载完成后,加载js/main.js文件. -->
<script data-main="js/main" src="js/require.js"></script>
</head>
<body>
<h1>My Sample Project</h1>
</body>
</html>
注意data-main属性,由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。
在mian中我们就可以很愉快地进行开发了,按照这种方式,整个页面我们只需要引入这一个js文件就可以了。
我们以使用jquery 为例:
// 配置jquery 模块的文件路径
require.config({
paths: {
jquery: 'jquery-1.8.3',
underscore: 'underscore.min' // 可以同时配置多个文件
}
}); // 使用模块
require(['jquery', 'underscore'], function($, _) {
console.log($().jquery, _.VERSION);
});
如果这些模块在其他目录,比如js/lib目录,可以改变基目录(baseUrl)。
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min",
"underscore": "underscore.min"
}
});
paths参数配置 模块名和路径, 路径可以是远程路径:http:http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js 后缀名可省略。
如果jquery就在main.js文件同目录下,则可以省略配置的步骤,直接使用即可。
// 使用模块
require(['jquery'], function($) {
console.log($().jquery);
});
创建AMD模块
AMD规范的API非常简单:
define(id?, dependencies?, factory);
define
函数接受三个参数:
- 第一个参数: 一个string字符串,它表示模块的标识(名称),其他模块可以用这个名称来引用本模块曝露出来的对象;可以省略该参数,缺省以文件名来命名该模块;
- 第二个参数: 一个数组,定义了本模块需要依赖的其他模块的列表,例如
jquery
或者其他用户自定义模块,没有依赖的话可以为 [] ; - 第三个参数: 一个回调函数,在依赖的模块加载成功后,会执行这个回调函数,它的参数是所有依赖模块的引用,该函数的返回值即为本模块曝露的对象;
例如: jQuery从1.7后开始支持AMD规范,即如果jQuery作为一个AMD模块运行时,它的模块名是“jquery”。注意“jquery” 是小写的。
jQuery中的支持AMD代码如下:
if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
define( "jquery", [], function () { return jQuery; } );
}
一个完整的模块定义包含模块名称,模块的依赖和回调函数,比如下面的代码:
define("adder", ["math"], function (math) {
return {
addTen : function (x) {
return math.add(x, 10);
}
};
});
如果这个模块并没有依赖,那么回调参数默认是 require,exports(一个空的输出对象,回调函数没有返回值的时候,默认返回 ), module( 模块自身 ),这时模块可以改写为:
define("adder", function (require, exports) {
exports.addTen = function (x) {
return x + 10;
};
});
如果省略第一个参数,则会定义一个匿名模块,见代码:
define( function (require, exports) {
exports.addTen = function (x) {
return x + 10;
};
});
在实际中,使用的更多的是匿名模块定义方式,因为这样更加的灵活,模块的标识和它的源代码不再相关,开发人员可以把这个模块放在任意的位置而不需要修改代码。一般只有在要使用工具打包模块到一个文件中时,才会声明第一个参数,所以应该尽量避免给模块命名。
在写模块的时候,也有可能没有依赖或者稍后才需要加载依赖
define(function (require, exports, module) { // …… var a = require('a'),
b = require('b'); exports.action = function () {
// ……
};
});
上述回调函数里的require的使用将被自动进行动态加载。