注册 登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

百鬼夜行

身是菩提树,心如明镜台,时时勤拂拭,勿使惹尘埃。

 
 
 

日志

 
 

AMD终极揭秘(2)  

2014-04-03 10:18:51|  分类: 工具类 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

构建(builds)

AMD 被设计得非常容易被构建工具解析从而创建出将多个模块代码连接在一起并压缩的一个单独文件。模块化系统在这方面提供了巨大的优势因为构建工具能够基于模块中列出的依赖自动的生成这个构建文件,而不需要依赖任何手写或者更新的脚本来创建。由于请求数目的减少,构建极大的减少了加载时间,并且由于依赖已经清楚的列在代码中,因而使用AMD实现这一点简直轻而易举。

不使用构建


使用构建


(译者注:原文作者选用的实验截图可能是本地资源加载的情况,由于本地资源加载的随机性,在使用构建之后优势不明显。但实际在网络传输中,使用构建会大大减少加载时间。)

性能

就像前面提到的那样,使用脚本元素注入比其他的方法快是因为它更依赖于原生的浏览器脚本加载机制。我们基于dojo.js创建了一些模块的测试用例,脚本元素加载比使用XHR eval的方式快了大概60-90%。在Chrome中,如果有大量的小模块,每个模块加载的时间大概是5-6ms

,而XHR+eval方式平均每个模块加载时间则接近9-10ms。在Firefox中,同步XHR方式比异步方式更快,而在IE中异步XHR比同步的快,但脚本元素加载无疑是最快的一个。让我们感到意外的是IE9是最快的一个浏览器,不过这有可能是因为在Firefox和Chrome中debugger/inspector增加了一些额外的性能开销。


模块加载器

AMD API是开放的,现在已有有多个AMD模块加载器和构造器的实现。这里介绍几个重要的AMD加载器:

  • Dojo – 这是一个完全的包括插件和构造器的AMD加载器。这是我们通常用来实现Dojo工具包的加载器。
  • RequireJS – 这是AMD加载器的元老也是AMD加载器的典范。其作者James Burke是AMD的主要作者和倡导者。这也是一个完整的包含构造器的加载器。
  • curl.js – 这是一个快速的AMD加载器,具有超级棒的插件支持(以及它自带的插件库)和自带的构造器。
  • lsjs – 这是一个专门设计用于在本地存储缓存模块的AMD模块加载器。其作者同时还写了一个独立的优化器。
  • NeedJS – 一个轻量级的AMD模块加载器。
  • brequire – 另一个轻量级的AMD模块加载器。
  • inject – 它是由LinkedIn创建并使用的,是一个快速轻量级的加载器,不提供对插件的支持。
  • Almond – 这是RequireJS的轻量级版本。

获取AMD模块

现在可以找到越来越多的AMD格式的包和模块。Dojo 基础包网站集中的给出了一个可以找到的包的列表。CPM 安装器 可以用于安装任何通过Dojo基础包网站注册的包(同时自动安装他们的依赖)。


James Burke, RequireJS的作者创建了 Volo——一个支持直接从github上面安装包的安装器。当然你也可以直接从他们的项目网站(在github或者其他地方)上直接下载模块,然后自己组织你的目录结构。
有了AMD,我们就能够轻松的使用任何包而不仅仅是Dojo模块来构造应用程序。将普通的脚本转换成AMD也是非常简单的。你只需要在define中用一个空数组表面依赖,然后将脚本直接包含在define的回调函数体中。当然如果该脚本必须在其他某些脚本之后执行,你也可以添加依赖。例如:
[javascript] view plaincopy
  1. my-script.js:  
  2. // add this to top of the script  
  3. defined([], function(){  
  4. // existing script  
  5. ...  
  6. // add this to the end of script  
  7. });  
我们可以构造用各种方式导入模块的应用程序。
[javascript] view plaincopy
  1. require(["dgrid/Grid""dojo/query""my-script"], function(Grid, query){  
  2.     new Grid(config, query("#grid")[0]);  
  3. });  
必须提醒的一点是当将脚本转换成模块时,如果脚本含有顶层的函数或者变量,它们原本是全局函数或者变量,但是在define()回调函数之内它们变成了回调函数的局部函数和变量,因此不会产生全局的函数或者变量。你或者显示的改变你的代码来创建一个全局函数或者变量(删掉那些变量前面的var或者function前缀(当你知道该脚本要与其他依赖于这些全局变量的脚本共同工作时你很可能需要这么做),或者改变转换后的模块使其返回函数或者变量作为模块输出并且让其依赖模块来使用这些输出(这种方法使你能够追求无全局变量的AMD典范)。

直接加载非AMD脚本

大多数模块加载器同时支持直接加载非AMD脚本。我们可以将一个普通脚本包含在我们的依赖中,并用“.js”后缀或者提供一个URL绝对路径或者用“\”开头的URL来表示它们是非AMD的。加载的脚本不能够提供直接的AMD输出,但能够通过标准的创建全局变量或者函数的形式来提供它自身的功能。例如我们可以加载Dojo和jQuery:
[javascript] view plaincopy
  1. require(["dojo""jquery.js"], function(dojo){ // jquery will be loaded as plain script  
  2.     dojo.query(...); // the dojo exports will be available in the "dojo" local variable  
  3.     $(...);   // the other script will need to create globals  
  4. });  

保持小的代码体积

AMD能够轻松的与多种脚本库协同组合工作。然而,虽然能够很容易实现这一点,但在某些方面你必须小心。将Dojo和jQuery这样的脚本库组合也许能够正常工作,但因为Dojo和jQuery在功能上绝大部分是重合的,它会导致增加很多不必要的下载量。事实上,Dojo新模块策略的一个关键之处就在于避免下载任何多余的模块。除了向AMD转换,Dojo基础功能也被拆分成多个能够单独使用的模块,使得Dojo能够尽可能的为某个应用程序保持最小的体积。事实上,最新的Dojo 应用程序和组件开发(像dgrid)经常使得整个应用程序比原先版本的Dojo基类还要小。

AMD的反对意见

当然也有一些对AMD的反对声音存在。一个反对意见是使用原来的CommonJS格式(AMD正是由此产生)比AMD更简单明了,更不容易出错。CommonJS格式的确没有繁复的使用“礼节”。然而,这种格式也是有一些问题的。我们可以让源文件原封不动的传给浏览器。这需要模块加载器将代码封装在一个注入了必须的CommonJS变量的头部中,用这种方式来调用XHR和eval来加载模块。这种方法的缺点我们之前已经讨论过了,包括降低性能,难于在老版本的浏览器上调试,以及跨域访问的限制。另一种方法是使用一个实时的构造过程,或者在服务器端按需封装的机制对CommonJS模块只提供必要的封装,这实际上跟AMD的思想是一致的。这些方法在很多情况下不一定会带来太多麻烦,也是合理的开发方式。但是为了满足最广泛的用户需求,这里用户可以在工作在一个非常简单的服务器上,或者面对跨浏览器的情况,或者使用老版本的浏览器,AMD减少了这些问题发生的几率,这也是Dojo的使命之一。
AMD的依赖列举机制也因为容易出错而经常被诟病,因为它需要维护依赖列表和回调函数的参数列表时刻一致。如果这两个列表不一致,模块的引用就会大错特错。实际应用中,我们在这个问题上并没有碰见太多的困难。另外还有一种使用AMD的替代方法可以解决这一问题。AMD支持调用仅有单个回调参数的define(),回调参数是一个require工厂方法,它包含了多个require()调用而不是一个依赖列表。这种方法不仅帮助解决如何保持依赖列表的同步问题,而且还使得添加CommonJs封装变得轻而易举。因为这个工厂函数的函数体是符合CommonJS模块形式的。下面就是一个使用这种方法来定义模块的例子:
[javascript] view plaincopy
  1. define(function(require){  
  2.     var query = require("dojo/query");  
  3.     var on = require("dojo/on");  
  4.     ...  
  5. });  

当提供一个参数时,请求,输出和模块就会自动的提供给该工厂函数。AMD模块加载器将会扫描该工厂函数的require调用,并自动的在运行该工厂方法之前加载他们。因为require调用直接内联在变量赋值语句中,你可以很容易的删除某个依赖声明而不需要任何保持依赖同步的操作。
关于require() API的一个说明:当使用一个字符串来调用require(),它是同步执行的,但是如果把它放在队列里面,它是一部执行的。因此例子中的依赖模块仍然是在执行回调函数之前异步加载的,当依赖都加载到内存中,代码中单字符串的require调用就被以同步的方式执行。

AMD的局限

AMD为我们提供了一个模块加载并协同工作的重要层面。然而,AMD仅仅是模块定义。它并不能为模块创建的API开出任何通用的“特别处方”。比如,你不能指望模块加载器给你提供查询引擎,并期望它从一堆可替换的查询模块中给你返回一个通用的API。当然定义这样的API更利于模块交互,但这不在AMD的范畴内。大多数模块加载器不支持将模块标识映射到不同的路径,因此如果你有可替换的模块,你最好自己定义一个模块标识到不同目标路径的映射来解决这一点。

渐进式加载

目前我们看到的AMD最大的问题不在于API,而是在实际应用中有种趋势是预先声明所有的依赖(这一点上文中我们一直这么提,真抱歉我们现在才解释这个问题!)。然而,很多模块能够正常的工作,而将某些依赖的加载延迟到它们实际需要的时候再加载。采用延迟加载策略是非常对提供一个渐进式加载页面是非常有价值的。有了这样一个渐进式加载页面,页面上的组件能够在其组件代码下载完后就显示,整个页面不需要等到所有JavaScript代码都下载完就能渲染和使用。我们可以通过使用异步请求require([])API,将我们的模块编写成支持延迟加载特定模块的形式。在下面的例子中,我们只为该函数加载必须的代码来创建子容器节点,延迟加载容器内小部件的代码,从而实现即时的视觉交互:
[javascript] view plaincopy
  1. // declare modules that we need up front  
  2. define(["dojo/dom-create""require"],  
  3.         function(domCreate, require){  
  4.     return function(node){  
  5.        // create container elements for our widget right away,  
  6.        // these could be styled for the right width and height,  
  7.        // and even contain a spinner to indicate the widgets are loading  
  8.        var slider = domCreate("div", {className:"slider"}, node);  
  9.        var progress = domCreate("div", {className:"progress"}, node);  
  10.        // now load the widgets, we load them independently  
  11.        // so each one can be rendered as it downloads  
  12.        require(["dijit/form/HorizontalSlider"], function(Slider){  
  13.           new Slider({}, slider);  
  14.        });  
  15.        require(["dijit/Progress"], function(Progress){  
  16.           new Progress({}, progress);  
  17.        });  
  18.     }  
这种方式提供了绝妙的用户体验,因为人们可以在组件一下载完成是就与之交互,而不需要等到整个页面全部加载完。如果用户看到页面逐步渲染,很可能会感觉程序运行得比原来快,更富有响应。

require,exports

在上面的例子中,我们使用了一个特殊的依赖——“请求(require)”,它是一个模块内的require()函数的引用,允许我们使用相对于该模块的引用。(如果你使用全局的“require”,相对模块标识则不是相对于当前模块的)
另外一个特殊的依赖是“输出(exports)”。有了输出依赖,输出对象体现在参数中,而不是返回输出对象。模块可以向输出对象添加属性。这一点在模块有循环引用的情况时特别有用。因为模块工厂函数可以开始运行,并添加输出对象,而另外一个函数可以在前者运行结束前就使用前者的输出对象。关于循环引用使用输出对象的一个简单例子如下:
[javascript] view plaincopy
  1. main.js:  
  2. define(["component""exports"],  
  3.         function(component, exports){  
  4.     // we define our exported values on the exports  
  5.     // which may be used before this factory is called  
  6.     exports.config = {  
  7.        title: "test"  
  8.     };  
  9.     exports.start = function(){  
  10.         new component.Widget();  
  11.     };  
  12. });  
  13. component.js:  
  14. define(["main""exports""dojo/_base/declare"],  
  15.         function(main, exports, declare){  
  16.     // again, we define our exported values on the exports  
  17.     // which may be used before this factory is called  
  18.     exports.Widget = declare({  
  19.         showTitle: function(){  
  20.             alert(main.config.title);  
  21.         }  
  22.     });  
  23. });  

如果我们只是依赖于函数的返回值,这个例子就不可能正常的工作,因为在循环中工厂函数需要先执行完毕,不可能访问另一个函数的返回值。
就像再前面的例子提到的那样,如果我们省略依赖性列表,那么依赖就被认做是缺省的”require“和”exports“,require()调用会被扫描,因此上例可以写成:
[javascript] view plaincopy
  1. define(function(require, exports){  
  2.     var query = require("dojo/query");  
  3.     exports.myFunction = function(){  
  4.        ....  
  5.     };  
  6. });  


展望

EcmaScript 委员会致力于在JavaScript中增加原生模块的支持。它们提供的添加方法是基于JavaScript语言中定义和引用模块的新语法。这种新语法包括了一个module关键字用于在脚本中定义模块,一个expoert关键字用于定义输出,一个import关键字用以定义需要引入的模块属性。这些操作符与AMD中的概念一一对应,使得转换变得相对容易。如果我们要将本文的第一个例子用Harmony 模块系统定义的方法来写,下面是一种写法:
[javascript] view plaincopy
  1. import {query} from "dojo/query.js";  
  2. import {on} from "dojo/on.js";  
  3. export function flashHeaderOnClick(button){  
  4.     on(button, "click"function(){  
  5.        query(".header").style("color""red");  
  6.     });  
  7. }  
现在提出的新模块系统包括支持定制的模块加载器,它能够与新的模块系统交互,还能够用于保留某些AMD现存的某些特性,例如用插件访问非JavaScript资源。

结论

AMD为基于浏览器的网络应用提供了强大的模块化系统,它利于原生的浏览器加载方式实现异步加载,支持对各种形式的资源灵活访问的插件,同时用一个简单明了的格式来实现它的功能。由于有了类似Dojo,RequireJS等众多出色的AMD项目,AMD世界是让人兴奋的,并且为发展快速,可操作的JavaScript模块带来栩栩生机。.

  评论这张
 
阅读(109)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018