随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试。模块化是一种非常好的代码组织方式,本文试着对JavaScript模块化开发的一些基础知识和具体使用做一些阐释。
“模块是为完成某一功能所需的一段程序或子程序。模块是任何robust(健壮、强壮)的应用架构不可缺少的一部分,是系统中职责单一且可替换的部分。”
简单理解:模块就是实现特定功能的一组方法,用用来实现代码的封装、增强代码可重用性,满足代码不断维护升级,分工合作的需要。如何开发新的模块,和复用已有模块来实现应用的功能,是我们需要考虑的事情,理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
关于模块,更多更详细部分请参考:深入理解JavaScript 模块模式
JavaScript 的当前版本,并没有为开发者们提供以一种简洁、有条理地的方式来引入模块的方法(ECMAScipt第六版表示会支持)。
作为代替,当前的开发者们只能被迫降级使用模块模式或是对象字面量模式的各种变体。通过很多这样的方法,各模块的脚本被串在一起注入到 DOM 中(作为 script
标签注入到 DOM 中)。
但好消息是在前端前辈们的不懈努力下,如今编写模块化的 JavaScript 目前已经变得极为简单,并摸索出一些普遍适用性的标准:AMD、CommonJS
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); // 5
注意,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的第一步,是先去官方网站下载最新版本。
下载后,假定把它放在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规范的API非常简单:
define(id?, dependencies?, factory);
define
函数接受三个参数:
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的使用将被自动进行动态加载。