RequireJS模块化编程详解

开发 前端
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

RequireJS模块化编程详解

1.模块的写法

模块化编程一般都有这么几个过渡过程,如下描述。

原始方法

  1. function m1(){ 
  2.   //... 
  3. function m2(){ 
  4.   //... 
  5.  

上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。

这种做法的缺点很明显:"污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

  1. var module1 = new Object({ 
  2.     _count : 0, 
  3.     m1 : function (){ 
  4.       //... 
  5.     }, 
  6.     m2 : function (){ 
  7.       //... 
  8.     } 
  9.   });  

上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。

  1. module1.m1(); 

但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

  1. module1._count = 5; 

立即执行函数写法

使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。

  1. var module1 = (function(){ 
  2.     var _count = 0; 
  3.     var m1 = function(){ 
  4.       //... 
  5.     }; 
  6.     var m2 = function(){ 
  7.       //... 
  8.     }; 
  9.     return { 
  10.       m1 : m1, 
  11.       m2 : m2 
  12.     }; 
  13.   })();  

使用上面的写法,外部代码无法读取内部的_count变量。

  1. console.info(module1._count); //undefined 

module1就是Javascript模块的基本写法。下面,再对这种写法进行加工。

放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

  1. var module1 = (function (mod){ 
  2.     mod.m3 = function () { 
  3.       //... 
  4.     }; 
  5.     return mod; 
  6.   })(module1);  

上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

宽放大模式(Loose augmentation)

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,***个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

  1. var module1 = ( function (mod){ 
  2.  
  3. //... 
  4.  
  5. return mod; 
  6.  
  7. })(window.module1 || {});  

与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。

输入全局变量

独立性是模块的重要特点,模块内部***不与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

  1. var module1 = (function ($, YAHOO) { 
  2.     //... 
  3.   })(jQuery, YAHOO);  

上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

2.AMD规范

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。

这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

  1. var math = require('math'); 

然后,就可以调用模块提供的方法:

  1. var math = require('math'); 
  2.  
  3. math.add(2,3); // 5  

因为这个系列主要针对浏览器编程,不涉及node.js,所以对CommonJS就不多做介绍了。我们在这里只要知道,require()用于加载模块就行了。

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且***两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?

  1. var math = require('math'); 
  2.  
  3. math.add(2, 3);  

第二行math.add(2, 3),在***行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

  1. require([module], callback); 

***个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:

  1. require(['math'], function (math) { 
  2.  
  3. math.add(2, 3); 
  4.  
  5. });  

math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。

3.require.js的加载

  1. <script src="js/require.js"></script> 

有人可能会想到,加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:

  1. <script src="js/require.js" defer async="true" ></script> 

async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。

加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:

  1. <script src="js/require.js" data-main="js/main"></script> 

data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会***个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

require.config()的配置

使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。

  1. require.config({ 
  2.     baseUrl: "js/lib"
  3.     paths: { 
  4.       "jquery""jquery.min"
  5.       "underscore""underscore.min"
  6.       "backbone""backbone.min" 
  7.     } 
  8.   });  

AMD模块的写法

模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:

  1. // math.js 
  2.   define(function (){ 
  3.     var add = function (x,y){ 
  4.       return x+y; 
  5.     }; 
  6.     return { 
  7.       addadd 
  8.     }; 
  9.   });  

加载方法如下:

  1. // main.js 
  2.   require(['math'], function (math){ 
  3.     alert(math.add(1,1)); 
  4.   });  

如果这个模块还依赖其他模块,那么define()函数的***个参数,必须是一个数组,指明该模块的依赖性。

  1. define(['myLib'], function(myLib){ 
  2.     function foo(){ 
  3.       myLib.doSomething(); 
  4.     } 
  5.     return { 
  6.       foo : foo 
  7.     }; 
  8.   });  

当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。

define()的完整定义:

  1. define('sample3' ,['sample','sample1'],function (sample,sample1) { 
  2.     var sample4 = require('sample4'); 
  3.     return function(){ 
  4.         alert(sample.name+':'+sample.sayhell()); 
  5.     } 
  6. });  

关于define函数的name和require函数的依赖名称之间的关系

1)define(name,[] , callback); 这个name可以省掉,默认是文件名称;当然也可以自定义,一旦我们定义了name,根据源代码我们可以发现define函数内部其实就是把这个name以及依赖模块、回调函数作为一个对象存储在全局的数组当中,也就是 defQueue.push([name,deps,callback]);那么这个name就是这个组件注册的的ID!

2)require([name , name2],callback); 系统首先会在全文检索path中是否对应的路径,如果没有自然把他作为路径拼接在baseUrl上去异步加载这个js文件,加载时从源代码中可以看到 ,var data = getScriptData(evt);返回的 data.id 其实就是name,然后执行contex.completeLoad(node.id),其内部就很清楚了,把define中注册的name和这里得到的name进行比较如果相等就执行,所以道理就是:require 和 define 的 name 必须保证一致!

标签加载完成之后,获取标签的唯一标识name

加载非规范的模块

举例来说,underscore和backbone这两个库,都没有采用AMD规范编写。如果要加载它们的话,必须先定义它们的特征。

  1. require.config({ 
  2.     shim: { 
  3.  
  4.       'underscore':{ 
  5.         exports: '_' 
  6.       }, 
  7.       'backbone': { 
  8.         deps: ['underscore''jquery'], 
  9.         exports: 'Backbone' 
  10.       } 
  11.     } 
  12.   });  

require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。

比如,jQuery的插件可以这样定义:

  1. shim: { 
  2.     'jquery.scroll': { 
  3.       deps: ['jquery'], 
  4.       exports: 'jQuery.fn.scroll' 
  5.     } 
  6.   }  

require.js插件

require.js还提供一系列插件,实现一些特定的功能。

domready插件,可以让回调函数在页面DOM结构加载完成后再运行。

  1. require(['domready!'], function (doc){ 
  2.     // called once the DOM is ready 
  3.   });  

text和image插件,则是允许require.js加载文本和图片文件。

  1. define([ 
  2.     'text!review.txt'
  3.     'image!cat.jpg' 
  4.     ], 
  5.  
  6.     function(review,cat){ 
  7.       console.log(review); 
  8.       document.body.appendChild(cat); 
  9.     } 
  10.   );  

类似的插件还有json和mdown,用于加载json文件和markdown文件。

参考地址:

Javascript模块化编程(一):模块的写法

Javascript模块化编程(二):AMD规范

Javascript模块化编程(三):require.js的用法

RequireJS define 详细介绍 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2013-08-20 18:39:34

JavaScript模requireJS

2023-12-25 22:24:36

C++模块Module

2018-03-21 21:31:28

Java9编程Java

2012-11-08 09:45:44

JavaScriptrequireJS

2019-08-28 16:18:39

JavaScriptJS前端

2010-08-18 09:03:07

模块化路由器

2010-08-16 13:49:39

模块化路由器

2010-01-21 09:27:30

模块化的优点NetBeans

2010-03-11 17:24:27

Python编程语言

2012-11-08 10:21:41

JSrequireJavaScript

2023-07-03 08:48:40

Web模块化设计

2020-09-17 10:30:21

前端模块化组件

2013-08-20 15:31:18

前端模块化

2020-09-18 09:02:32

前端模块化

2015-10-10 11:29:45

Java模块化系统初探

2022-03-11 13:01:27

前端模块

2015-10-10 10:01:28

前端模块化webpack

2013-11-06 13:36:23

开源谷歌手机

2018-06-21 09:36:09

模块化数据中心集中化

2022-09-05 09:01:13

前端模块化
点赞
收藏

51CTO技术栈公众号