http://bbs.html5cn.org/thread-83855-1-1.html
随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等......,开发者不得不使用软件工程的方法,管理网页的业务逻辑。Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)Javascript社区做了很多努力,在现有的运行环境中,实现"模块"的效果。本文总结了当前"Javascript模块化编程"的最佳实践,说明如何投入实用。虽然这不是初级教程,但是只要稍稍了解Javascript的基本语法,就能看懂。
一、原始写法
模块就是实现特定功能的一组方法。只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。
- function m1(){
- //...
- }
- function m2(){
- //...
- }
二、对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
- var module1 = new Object({
- _count : 0,
- m1 : function (){
- //...
- },
- m2 : function (){
- //...
- }
- });
- module1.m1();
- module1._count = 5;
使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
- var module1 = (function(){
- var _count = 0;
- var m1 = function(){
- //...
- };
- var m2 = function(){
- //...
- };
- return {
- m1 : m1,
- m2 : m2
- };
- })();
- console.info(module1._count);//undefined
四、放大模式
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。
- var module1 = (function(mod){
- mod.m3 = function (){
- //...
- };
- return mod;
- })(module1);
五、宽放大模式(Loose augmentation)
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。
- var module1 = (function (mod){
- //...
- return mod;
- })(window.module1 || {});
六、输入全局变量
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
- var module1 = (function ($, YAHOO){
- //...
- })(jQuery, YAHOO);
七、模块的规范
先想一想,为什么模块很重要?因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了。目前,通行的Javascript模块规范共有两种:CommonJS和AMD。我主要介绍AMD,但是要先从CommonJS讲起。
八、CommonJS
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
- var math = require('math');
- var math = require('math');
- math.add(2,3);//5
九、浏览器环境
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?
- var math = require('math');
- math.add(2, 3);
十、AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
- require([module], callback);
- require(['math'], function(math){
- math.add(2, 3);
- });
十一、为什么要用require.js?
最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。下面的网页代码,相信很多人都见过。
- <script src="1.js"></script>
- <script src="2.js"></script>
- <script src="3.js"></script>
- <script src="4.js"></script>
- <script src="5.js"></script>
- <script src="6.js"></script>
(1)实现js文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护;
十二、require.js的加载
使用require.js的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就可以加载了。
- <script src="js/require.js"></script>
- <script src="js/require.js" defer async="true" ></script>
- <script src="js/require.js" data-main="js/main"></script>
十三、主模块的写法
上一节的main.js,我把它称为"主模块",意思是整个网页的入口代码。它有点像C语言的main()函数,所有代码都从这儿开始运行。下面就来看,怎么写main.js。如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。
- //main.js
- alert("加载成功!");
- //main.js
- require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
- //some code here
- });
- require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
- //some code here
- });
十四、模块的加载
上一节最后的示例中,主模块的依赖模块是['jquery', 'underscore', 'backbone']。默认情况下,require.js假定这三个模块与main.js在同一个目录,文件名分别为jquery.js,underscore.js和backbone.js,然后自动加载。使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。
- require.config({
- paths:{
- "jquery": "jquery.min",
- "underscore": "underscore.min",
- "backbone": "backbone.min"
- }
- });
- require.config({
- paths:{
- "jquery": "lib/jquery.min",
- "underscore": "lib/underscore.min",
- "backbone": "lib/backbone.min"
- }
- });
- require.config({
- baseUrl: "js/lib",
- paths:{
- "jquery": "jquery.min",
- "underscore": "underscore.min",
- "backbone": "backbone.min"
- }
- });
- require.config({
- paths:{
- "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
- }
- });
十五、AMD模块的写法
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:
- //math.js
- define(function(){
- var add = function (x,y){
- return x+y;
- };
- return {
- add: add
- };
- });
- //main.js
- require(['math'], function (math){
- alert(math.add(1,1));
- });
- define(['myLib'], function(myLib){
- function foo(){
- myLib.doSomething();
- }
- return {
- foo:foo
- };
- });
十六、加载非规范的模块
理论上,require.js加载的模块,必须是按照AMD规范、用define()函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。那么,require.js是否能够加载非规范的模块呢?回答是可以的。这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。举例来说,underscore和backbone这两个库,都没有采用AMD规范编写。如果要加载它们的话,必须先定义它们的特征。
- require.config({
- shim:{
- 'underscore':{
- exports:'_'
- },
- 'backbone': {
- deps:['underscore', 'jquery'],
- exports:'Backbone'
- }
- }
- });
(1)exports值(输出的变量名),表明这个模块外部调用时的名称;
(2)deps数组,表明该模块的依赖性。
比如,jQuery的插件可以这样定义:
- shim:{
- 'jquery.scroll':{
- deps:['jquery'],
- exports:'jQuery.fn.scroll'
- }
- }
require.js还提供一系列插件,实现一些特定的功能。domready插件,可以让回调函数在页面DOM结构加载完成后再运行。
- require(['domready!'], function(doc){
- //called once the DOM is ready
- });
- define([
- 'text!review.txt',
- 'image!cat.jpg'
- ],
- function(review,cat){
- console.log(review);
- document.body.appendChild(cat);
- }
- );