22 January 2014

原文:Consuming modules: Locating modules in AMD

模块标识符 一文中,我们注意到,斜杠用来分隔词条,表示模块的层次结构。不过说到底,AMD 环境必须能够定位到模块,标识符也必须以某种方式转换为 URI。URI 可能会被解析为数据库中的记录或 HTML5 localStorage 中的值,不会在大多数时候,URI 被解析为服务器上的一个文件路径或浏览器中的 URL。

默认位置和基础路径

对于非常简单的的应用程序,你可以把所有模块都放在同一个位置。这个位置称为“基础路径”。此时,模块标识符的解析仅仅是简单的字符串拼接:

模块 URL = 基础路径 + 模块标识符 + ".js"

默认情况下,大多数 AMD 加载器会把基础路径设置为当前 HTML 文档的地址。例如,如果 HTML 文档是“//know.cujojs.com/index.html”,标识符为“blog/controller”的模块应该位于“//know.cujojs.com/blog/controller.js”。当 AMD 环境解析模块 URL 时,扩展名“.js”被自动添加。

译注:Browser support for URLs beginning with double slash

把模块和 HTML 文档放在同一位置中可能不太方便。因此,基本上所有的 AMD 环境都允许以配置的方式设置基本路径。例如,如果配置基本路径为“client/”,模块标识符“blog/controller”将被解析为“//know.cujojs.com/client/blog/controller.js”。

变量 require 除了可以注入模块外,还含有一个方法 toUrl(id),用于把一个模块标识符转换为一段 URL。可能你从没在应用程序的代码中使用过该方法,但对于探索标识符和 URL 之间的转换,该方法是一个很好的工具。

// module app/billing/billTo/Customer
// base url is client/
// document is //know.cujojs.com/index.html
define(function (require) {

    // resolves to "//know.cujojs.com/client/app/billing/billTo/store"
    var url = require.toUrl('./store');

});

标识符不等于 URL

设置基本路径,然后在基本路径对应的文件夹中放置几个模块,这些都很容易做到,但是不要被这种简单的模式迷惑了,然后想当然地认为标识符不过就是些短网址!这种简单的模式只适用于小型应用程序,无法扩展到较大些的应用程序中。较大些的应用程序需要有策略地组织层次结构。在另一篇教程中,我们讲讨论“包 package”的组织策略。

标识符有时候不就是 URL 吗?

假使模块依赖了 CDN 上的库,将会怎么样呢?

大多数 AMD 加载器允许用 URL 来代替标识符。下面的代码是完全合法的:

define(function (require) {

    // attempt to get "moment" by url
    var moment = require('//cdnjs.cloudflare.com/ajax/libs/moment.js/2.0.0/moment.min.js');

});

但是,这段代码有几个问题:

  • 代码中的硬编码 URL 限制了可维护性。假使你要把它们更新到最新的版本,想想将会怎么样呢?
  • 扩展名“.js”可能会触发 AMD 环境采用传统的方式加载文件。例如,RequireJS 就会这么做。
  • 某些支持(可以感知) AMD 的库会硬编码一个标识符,这一点尤其不幸。例如,Moment.js 会在它的文件中把标识符硬编码为“moment”,即抢注了这个名字。在上面的例子中,AMD 环境期望获取一个名为“//cdnjs.cloudflare.com/ajax/libs/moment.js/2.0.0/moment.min.js”的模块,得到的却是一个名为“moment”的模块。AMD 环境可能会因为标识符不匹配而抛出一个错误。

那么,我们应该如何使用跨域的模块呢?例如 CDN 上的模块。

我们很快会给出答案!

配置 标识符-URI 映射

最终你还是不得不,告诉 AMD 环境如何把标识符映射为 URI。这个过程称为 路径映射包映射,需要通过配置完成。

通过集中配置这些模块,而不是在模块中配置,可以降低维护成本,提高代码的可移植性。

在大多数 AMD 环境中,配置代码看起来像是下面这样:

var config = {
    baseUrl: "client/apps",
    paths: {
        "blog": "blog", // this is redundant, unnecessary
        "dont": "../dont"
    }
};

在 curl.js 中,使用全局变量 curl 来执行配置。许多其他的 AMD 环境也采用了类似的 API:

// auto-sniff for an object literal:
curl(config);

// or, more explicitly:
curl.config(config);

选项 baseUrl 是一个 URL 路径,称为“基础路径”,AMD 环境在解析标识符时,会相对于该路径来解析。基础路径可以是绝对路径(以协议或 // 开头)、相对于 host 的路径(以 / 开头),或相对于当前页面的路径(如前面示例所示)。

选项 paths 是一个映射对象,其中包含了模块标识符到 URL 的映射。你不必为每个模块指定映射,而是可以简单地指定模块层级结构的顶层词条。在解析更深层次的模块时,会自动附加上相应的标识符层级。基于前面的配置,以模块“dont/even”为例,AMD 环境的解析过程如下:

  1. 把 baseUrl 附加到当前页面的地址,从而构建一个完整的基本路径:

     "//know.cujojs.com/" + "client/" => "//know.cujojs.com/client/"
    
  2. 在选项 paths 中查找“dont/even”

    • 因为没有找到映射关系,所以移除当前层级,转而查找“dont”。
    • 发现“dont”映射到了“../dont”。
  3. 把“../dont”附加到基本路径:

     "//know.cujojs.com/client/../dont/even"
    

选项 packages 也可以映射标识符到 URL,但是更加结构化。在 curl.js 中,该选项还提供了更高级的特性。在复杂一些的应用中,建议用 packages 代替 paths

避免出现多个 ../

对于 AMD 环境,基础路径指定了模块层级结构的“根”或“顶层”。试图遍历至根路径没有太大意义。假设有下面的场景:

// module blog/controller
// base url is //know.cujojs.com/client/
define(function (require) {

    var dont = require("../../dont/please");

});

为了解析相对标识符“../../dont/please”,AMD 环境在查找“dont”和“please”之前,必须向让遍历两层:

  1. 从当前层级开始:“blog/controller”处于“blog/”层。
  2. 向上遍历一层:“blog/”-->“”(到达顶层路径)
  3. 再向上遍历一层:???(无法实现,因为已经处于最顶层了!)

任何规范都没有定义 AMD 环境该如何处理这种情况。curl.js 会假设该标识符实际上是一个 URL,并保留 ../,这样就可以基于基础路径进行解析。言下之意是说,curl.js 会照常加载这样的模块,但不会认为“dont/please”和“../../dont/please”是同一个模块。这可能导致模块“dont/please”被加载两次,该模块的的工厂函数也将执行两次,进而引起各种各样的问题,例如,单例 singleton 不再是唯一的实例。

译注:Singleton,单例模式,确保只有一个实例,并提供一个全局访问点。

注:对于以 / 或协议(http:https: 等)开头的标识符,curl.js 也会把它们认为是 URL。

如果你需要用多个 ../ 才能引用其他模块,请务必读一读有关包 package 的教程。包 package 可以解决所有的问题。

配置 标识符-远程URL 映射

回到 moment.js 示例,它存储在 CDN 上,并且看起来确实像一个 URL。然后,正如我们前面所提到的,moment.js 却声明了一个标识符为“moment”的模块。下面的代码演示了如何协调远程 URL:

var config = {
    baseUrl: "client/apps",
    paths: {
        "moment": "//cdnjs.cloudflare.com/ajax/libs/moment.js/2.0.0/moment.min.js",
        "blog": "blog", // this is redundant, unnecessary
        "dont": "../dont"
    }
};
curl.config(config);

现在,如果某个模块需要 moment.js,可以通过标识符来引用它:

define(function (require) {

    // "moment" will resolve to //cdnjs.cloudflare.com/ajax/libs/moment.js/2.0.0/moment.min.js
    var moment = require('moment');

});



blog comments powered by Disqus