zoukankan      html  css  js  c++  java
  • Dojo学习笔记(二):模块定义

    Dojo现在支持在ADM机制下写模块,这使得代码更容易编写和调试。在这片文章中我们将详细地介绍这种机制,并尝试使用这种机制开发应用程序。

    概述

    Dojo1.7以及更新的版本Dojo引入了Asynchronous Module Definition (AMD)新的模块机制,代替了dojo.provide, dojo.require, dojo.requireIf, dojo.requireAfterIf, dojo.platformRequire, 和 dojo.requireLocalization。这种机制令新Dojo比旧Dojo模块有了很大的增强,这些增强方面包括:完善的异步操作;真正的包可移植性;更好的依赖管理机制;改善调试体验。它也成为了一种业界驱动标准,这就表明使用AMD规范编写的模块可以被其他任何AMD解释加载器或类库所使用。在这篇文章中我们将向你介绍如何使用这种新机制并突出那些比老的模块机制更优越的新特性。

    AMD模块标识符简介

    在讨论新的模块加载器之前,我们有必要简单地了解一下模块是怎样被标识的。

    如果你对Dojo1.6或更早版本的模块机制有所了解,现在你首先要注意的是:新的AMD模块标识符看上去像是文件路径而非对象对象引用.举个例子,dojox.encoding.crypto.Blowfish 被现在的 dojox/encoding/crypto/Blowfish 所代替.值得庆幸的是,这些新的标识符在工作方式上更像路径,比如你可以使用"./"和"../"来引用同一个包里其他模块。你甚至不使用AMD语法,而使用完整的(URLs)路径来作为模块的标识符。

    稍后我们将更详细地讨论这些新的特征。现在让我们从demo应用程序的加载器配置讲起。

    配置加载器

    在整个文章里,我们假设应用的文件结构如下图所示:

    在使用AMD模块之前我们要做的第一件事就是配置加载器,使加载器在异步模式下运行。只要将async属性值改为ture就行:

    <script data-dojo-config="async: true" src="js/lib/dojo/dojo.js"></script>

     我们也可以在dojoConfig对象中设置async属性,但使用任意一种方法我们都得保证这个值在loader包含到网页之前设置。如果不这样,加载器就以兼容模式的同步模式运行。

    在异步模式下,加载器只会定义两个全局函数:require用来加载模块;define用来定义模块。新的加载器和旧的加载器最鲜明的对比就是旧加载器会一股脑加载Dojo Base里的所有模块,而新的加载器就不会这样做。

    接下来我们要做的就是给loader配置信息,这些信息里包含我们的模块地址:

    var dojoConfig = {
    	baseUrl: "/js/",
    	tlmSiblingOfDojo: false,
    	packages: [{
    		name: "dojo",
    		location: "lib/dojo"
    	}, {
    		name: "dijit",
    		location: "lib/dijit"
    	}, {
    		name: "dojox",
    		location: "lib/dojox"
    	}, {
    		name: "my",
    		location: "my",
    		main: "app"
    	}]
    };
    

     在这个配置信息里,baseUrl设置包含所有我们JavaScript代码文件夹的地址,tlmSiblingofDojo属性被设置为false代表那些没有指定包、顶层模块的路径默认为baseUrl的地址。如果tlmSiblingofDojo被设置为true,那么认为他们和dojo包处于同一目录。这时,即使没有明确地定义until包,我们仍然调用until文件夹里的代码。我们的demo应用配置信息的最后一块就是同上述定义的packages一样。

    从最基本原理上讲,包就是模块的集合。dojo,dijit以及dojox都是包的例子。与一个文件夹下模块集合不同的是,包还包含着额外的特征,这些特征的目的就是显著地增强模块的可移植性并更加易用。一个便携包是自包含的,也可以通过工具安装,例如 cpm

    包通常包含三个主要的配置选项。name,够明了,就是包的名子。location就是包的位置,可以是baseUrl的相对地址,也可以是绝对地址。main是一个可选参数,这样好像更它的字面意思"main"有些出入,这个main的作用就是当一个包要引用自己的包时,它就会查找正确模块来加载。举个例子,如果你尝试引用"dojo",那么这时候真正被加载的是"/js/dojo/main.js"。我们覆写了"my"包的属性,这时如果引用"my",".js/my/app.js"就会被加载。如果我们引用"util",但这个包并没有定义,这时加载器就会尝试加载"/js/util.js"。

    现在我们已经正确地配置了加载器,接下来就从分析require方法入手来进一步学习如何使用它

    引用模块

    以下的例子能够很好的解释AMD加载模块的风格

    require(
    ["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin"], 
    function(declare, _WidgetBase, _TemplatedMixin) {
    	// "declare" holds the dojo declare function
    	// "_WidgetBase" holds the dijit _WidgetBase constructor
    	// "_TemplatedMixin" holds the dijit _TemplatedMixin constructor
    	// Do whatever you want with these modules here.
    });
    

     正如你所看到的,require方法接受了一个模块标识符("dependencies")的数组,这个数组将作为它的第一参数,回调函数作为第二参数。数组里的依赖引用是有顺序的。一旦所有的依赖项准备好,它们会被按顺序传入回调函数作为函数的参数。回调函数是可选的,如果你只是想加载他们而并不想做些什么,你就可以忽略回调函数。如果忽略了模块标识符数组这一参数,那就会一起另一中运行方式,所以要确保至少有一个依赖项在,哪怕它是空的。

    require方法也可以用来在加载器运行中修改配置,它是通过将配置信息对象作为第一个参数传进去实现的:

    require({
        baseUrl: "/js/",
        packages: [
            { name: "dojo", location: "//ajax.googleapis.com/ajax/libs/dojo/1.8/" },
            { name: "my", location: "my" }
        ]
    }, [ "my/app" ]);
    

     这样我们改变了配置信息,将dojo包指向了谷歌的CDN,不同于老版本模块机制,AMD机制支持隐式跨域加载,所以没有特殊的跨域版本是必要这样做的。

    我们提供了一个配置对象,你也可以将依赖数组作为第一个参数传入进去,回调函数作为第三参数。

    注意,不是所有的配置选项都可以再运行时修改。需要特别说明的是,async,tlmSiblingofDojo还有以前用的has这些属性一旦加载器加载了就不能被修改了。此外,所有的配置数据都是浅复制,这意味着你不能使用这个机制,举个例子,添加多余的值到配置对象里,对象将被重写。

    定义模块

    定义模块是通过define函数完成的。除了回调函数返回值被新模块保存和利用外, define调用和require调用时完全等同的。

    // in "my/_TemplatedWidget.js"
    define([ "dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function(declare, _WidgetBase, _TemplatedMixin){
        return declare([ _WidgetBase, _TemplatedMixin ], {});
    });
    

     注意,这里我们省略了第一个可选参数:模块签名。也可以这样:return declare("my._TemplatedWidget", [ _WidgetBase, _TemplatedMixin ], {}); 你可能会看到这样的代码,其实这样做事为了能够兼容旧版AMD语法

    在以上例子中,我们利用dojo.declare创建并返回了一份构造函数,当定义模块时一个重要的事就是回调函数一旦被调用,加载器就会缓存它返回的值。从实践层面上来看,这就意味着模块可以依据相同的模块来轻松地共享对象。

    作为参考,看下面用旧版本模块格式编写的示例代码:

    dojo.provide("my._TemplatedWidget");
    dojo.require("dijit._WidgetBase");
    dojo.require("dijit._TemplatedMixin");
    dojo.declare("my._TemplatedWidget", [ dijit._WidgetBase, dijit._TemplatedMixin ], {});
    

     当定义模块时,值也可以作为对象:

    // in "my/nls/common.js"
    define({
        greeting: "Hello!",
        howAreYou: "How are you?"
    });
    

     记住,如果你在定义模块是没有回调函数,你将不可能引用任何的依赖项,所以这种定义方法是罕见的。

     

    新的AMD加载器的一个最总要的特征是它能够创建完整的可移植包。例如,如果你的应用需要使用来至两个不同版本的Dojo,新的加载器能够轻松实现。通过向包配置信息中加入packageMap对象,就能将包重新映射到另一个包。加载两个不同版本Dojo时,包信息应该这样:

    var map16 = { dojo: "dojo16", dijit: "dijit16", dojox: "dojox16" },
        dojoConfig = {
            packages: [
                { name: "dojo16", location: "lib/dojo16", packageMap: map16 },
                { name: "dijit16", location: "lib/dijit16", packageMap: map16 },
                { name: "dojox16", location: "lib/dojox16", packageMap: map16 },
                { name: "my16", location: "my16", packageMap: map16 },
                { name: "dojo", location: "lib/dojo" },
                { name: "dijit", location: "lib/dijit" },
                { name: "dojox", location: "lib/dojox" },
                { name: "my", location: "my" }
            ]
        };
    

     在这种配置下,任何情况下使用map16包的dojo,dijit或者dojox,它们将被重新映射到dojo16,dojit16,dojox16,而其他的代码继续使用正常的

    也可以使用paths配置属性来重新映射完整的路径。paths从模块标识符字符串的第一个字符开始匹配,直到匹配到最长的路径,例如:

    var dojoConfig = {
        paths: {
            "my/debugger/engine": "my/debugger/realEngine",
            "my/debugger": "other/debugger"
        }
    };
    

     根据给出的paths配置,下面的路径将在重新解析时被替换:

    • my/debugger => other/debugger
    • my/debugger/foo => other/debugger/foo
    • my/debugger/engine/ie => my/debugger/realEngine/ie
    • not/my/debugger => not/my/debugger

    最后,新的加载器还提供了alisaes配置属性,这个属性跟paths不同,它只匹配complete模块标识符。别名还会递归匹配aliases直到不能匹配为止。举例:

    var dojoConfig = {
        aliases: [
            [ "text", "dojo/text" ],
            [ "dojo/text", "my/text" ],
            [ "i18n", "dojo/i18n" ],
            [ /.*\/env$/, "my/env" ]
        ]
    };
     
    

     根据给出的alisaes配置,下面的路径将在重新解析时被替换:

    • text => dojo/text
    • dojo/text => my/text
    • i18n => dojo/i18n
    • foo => foo
    • [anything]/env => my/env

    使用别名时,目标别名必须是绝对的模块标识符,源别名必须是绝对的模块标识符或正则表达式。

    编写可移植包

    为了使加载器能够执行可移植包,任何包内的模块引用都得使用relatie标识符。看下面的例子:

    // in "my/foo/blah.js"
    define([ "my/otherModule", "my/foo/bar" ], function(otherModule, bar){
        // …
    });
    

     使用相对模块标识符代替绝对模块标识符:

    // in "my/foo/blah.js"
    define([ "../otherModule", "./bar" ], function(otherModule, bar){
        // …
    });
    

     有条件地引用模块

    有时候你希望在特定情况下有选择地引用模块。举个例子,你也许会希望延迟加载一个可选模块,直达某事件发生再加载。下面是个很简单明了的例子:

    // in "my/debug.js"
    define([ "dojo/dom", "dojo/dom-construct", "dojo/on" ], function(dom, domConstruct, on){
        on(dom.byId("debugButton"), "click", function(evt){
            require([ "my/debug/console" ], function(console){
                domConstruct.place(console, document.body);
            });
        });
    });
    

     很不幸的是,如果想使模块完全可移植,"my/debug/console"需要转换为相对路径。只修改路径也是不行的,因为当require被调用的时候,源模块的内容就会丢失。为了解决这个问题,Dojo加载器提供了context-sensitive require这个工具。为了使用它们中的一个,在你的define调用时加入模块标识符"require"作为依赖;

    // in "my/debug.js"
    define([ "dojo/dom", "dojo/dom-construct", "dojo/on", "require" ], function(dom, domConstruct, on, require){
        on(dom.byId("debugButton"), "click", function(evt){
            require([ "./debug/console" ], function(console){
                domConstruct.place(console, document.body);
            });
        });
    });
    

     现在我们可以安全地引用"my/debug"的相对路径模块了。

    使用插件

    为了规则化模块,AMD加载器也提供了一种新的模块类型叫插件,插件是用来扩展加载器功能的。插件的加载方式和常规模块没什么区别,只是在模块标识符的结尾使用了特殊符号"!"来表明它的请求时插件请求。在"!"后的数据直接传入到插件中执行。我们看一些例子就会更明白。Dojo默认带有一些插件,四个最重要的插件是:dojo/text, dojo/i18n, dojo/has and dojo/domReady,让我们看一下它们是怎么使用的。

    dojo/text

    dojo/text是来更换dojo.cache的,你可以在任何需要从文件(比如HTML模板)中加载字符串时使用它。看下面的例子,为了记载窗口控件模板,你需要如下定义:

    // in "my/Dialog.js"
    define([ "dojo/_base/declare", "dijit/Dialog", "dojo/text!./templates/Dialog.html" ], function(declare, Dialog, template){
        return declare(Dialog, {
            templateString: template // 模板包含文件"my/templates/Dialog.html"的内容
        });
    });
    

     在老版本加载器中,需要如下写:

    dojo.provide("my.Dialog");
    dojo.require("dijit.Dialog");
    dojo.declare("my.Dialog", dijit.Dialog, {
        templateString: dojo.cache("my", "templates/Dialog.html")
    });
    

    dojo/i18n

    dojo/i18n用来代替dojo.requireLocalization 和 dojo.i18n.getLocalization,用法如下:

    // in "my/Dialog.js"
    define([ "dojo/_base/declare", "dijit/Dialog", "dojo/i18n!./nls/common"], function(declare, Dialog, i18n){
        return declare(Dialog, {
            title: i18n.dialogTitle
        });
    });
    

     在老版本加载器中,需要如下写:

    dojo.provide("my.Dialog");
    dojo.require("dijit.Dialog");
    dojo.requireLocalization("my", "common");
    dojo.declare("my.Dialog", dijit.Dialog, {
        title: dojo.i18n.getLocalization("my", "common").dialogTitle
    });
    

     dojo/has

    Dojo的新加载器包含了一个检查和指向has.js的API;dojo/has插件有选择地为引用模块调整功能。它的用法如下:

    // in "my/events.js"
    define([ "dojo/dom", "dojo/has!dom-addeventlistener?./events/w3c:./events/ie" ], function(dom, events){
        // events is "my/events/w3c" if the "dom-addeventlistener" test was true, "my/events/ie" otherwise
        events.addEvent(dom.byId("foo"), "click", function(evt){
            console.log("Foo clicked!");
        });
    });
    

     在老版本加载器中,需要如下写:

    dojo.requireIf(window.addEventListener, "my.events.w3c");
    dojo.requireIf(!window.addEventListener, "my.events.ie");
    my.events.addEvent(dom.byId("foo"), "click", function(evt){
        console.log("Foo clicked!");
    });
    

     dojo/domReady

    dojo/domReady替换了dojo.ready,当只有当DOM初始化后才会执行模块,用法如下:

    // in "my/app.js"
    define(["dojo/dom", "dojo/domReady!"], function(dom){
        // This function does not execute until the DOM is ready
        dom.byId("someElement");
    });
    

     在老版本加载器中,需要如下写:

    dojo.ready(function(){
        dojo.byId("someElement");
    });
    

     即使没有数据传给插件,感叹号也是必要的。少了感叹号,你就只能将dojo/domReady模块作为一个依赖项,而不会激活它的插件的特性。

    dojo/load

    dojo/load和dojo/has很相似,但dojo/load使用了两次模块定义并返回模块加载集合

    1.// in my/loadRenderer.js  
    2.define([ "dojo/load!./sniffRenderer" ], function (renderer) {  
    3.   // do something with renderer  
    4.});  
    5.   
    6.// in my/sniffRenderer.js  
    7.define([], function () {  
    8.  // returns an array of module identifiers to load;  
    9.  // the first module listed is the one returned by dojo/load  
    10.  return [ sniffModuleIdOfRenderer() ];  
    11.});  
    

     但有多个模块被定义时,只取第一个模块。如果没有模块被定义,将标识undefined

    在老版本加载器中,需要如下写:

    1.var module = sniffModuleIdOfRenderer();  
    2.dojo.require(module);  
    3.var renderer = dojo.getObject(module);  
    4.// do something with renderer  
    

     处理循环依赖

    你在写代码的时候,你可能会遇到两个模块需要相互引用的情况,这就造成了模块的循环引用。为了解决循环引用的问题,加载器会在第一次递归调用的时候解决问题,看下面的例子:

    // in "my/a.js"
    define([ "b" ], function(b){
        var a = {};
        a.stuff = function(){
            return b.useStuff ? "stuff" : "things";
        };
     
        return a;
    });
     
    // in "my/b.js"
    define([ "a" ], function(a){
        return {
            useStuff: true
        };
    });
     
    // in "my/circularDependency.js"
    require([ "a" ], function(a){
        a.stuff(); // "things", not "stuff"
    });
    

     在以上情况下,加载器会尝试加载模块A,然后加载模块B,当再一次加载模块A的时候,加载器就意识到模块A已经是循环引用中德一部分了。为了摆脱循环引用,这时模块A就会自动地转换为空对象,空对象会代替A的值传给模块B,这时A的回调函数被调用并返回无效的值。在以上的例子中,这就代表A是个空对象并不带有stuff函数,所以代码运行的结果就不会像我们期待的那样。

    为了解决这个问题,加载器提供了一个叫"exports"的模块标识符。当它被用的时候,这个模块就会返回空值以解决循环引用问题。当它的回调函数被调用的时候,它将会将属性附加给"exports",这时候stuff函数就可以被成功调用并使用:

    // in "my/a.js"
    define([ "b", "exports" ], function(b, exports){
        exports.stuff = function(){
            return b.useStuff ? "stuff" : "things";
        };
     
        return exports;
    });
     
    // in "my/b.js"
    define([ "a" ], function(a){
        return {
            useStuff: true
        };
    });
     
    // in "my/circularDependency.js"
    require([ "a" ], function(a){
        a.stuff(); // "stuff"
    });
    

     请记住,虽然我们解决了这两个模块的问题,这也是相当不稳定的情形。我们不推荐应用中存在循环引用,尽可能的情况下,我们需要通过代码重构来避免循环引用。

    加载非AMD代码

    正如模块标识符那一块所提到的,AMD加载器也可以通过JavaScrpit文件的路径的标识符来加载非AMD代码。加载器会通过以下三种方式标识这些特殊的标识符:

    1. 以"/开头的标识符"
    2. 以协议(如"http:","https:")开头的标识符
    3. 以".js"结尾的标识符

    任意代码被加载为模块,它的模块值都是undefined,你需要直接访问代码脚本的全局定义,Dojo加载器的最后一个特点就是能够匹配和混合老版本Dojo模块,这一功能全靠AMD形式模块。这就使得它可以有条不紊地将老版本codebase转换为AMD codebase,而非一下子全部改写。这种机制可以在同步和异步模式下执行。当在同步模式下的时候,老版本的模块值就是文件里第一个dojo.provide声明后所运行的值。如下:

    // in "my/legacyModule.js"
    dojo.provide("my.legacyModule");
    my.legacyModule = {
        isLegacy: true
    };
    

     当通过AMD加载器加载代码的时候,加载器会调用require(["my/legacyModule"]),这时模块的值就是my.legacyModule的对象。

    服务端的JavaScript

    新的AMD加载器的一个特征就是能够通过 node.js 或者 Rhino 来加载服务端的JavaScript

    使用命令行来加载Dojo的方法如下:

    # node.js:
    node path/to/dojo.js load=my/serverConfig load=my/app
     
    # rhino:
    java -jar rhino.jar path/to/dojo.js load=my/serverConfig load=my/app
    

     每一个load=参数都会添加一个模块到依赖列表数组,当加载器准备好后就会自动执行依赖数组。在浏览器里,代码需要这样:

    <script data-dojo-config="async: true" src="path/to/dojo.js"></script>
    <script>require(["my/serverConfig", "my/app"]);</script>
    

     总结

    新的加载器给Dojo带来了很多令人激动的特征和功能。尽管这篇文章稍长,但也只简单地介绍了新的加载器提供的新特性。想要进一步学习AMD加载器带来的所有新特性,查看 Dojo loader reference guide


    本系列文章翻译至Dojo官网,旨在学习Dojo并提高英语阅读能力,有翻译错误的地方请多指正!谢谢

  • 相关阅读:
    VBS基础篇
    AcWing249 蒲公英(分块)
    CF1338B Edge Weight Assignment(思维+dfs)
    CF785E Anton and Permutation(分块)
    UCF Local Programming Contest 2015(Practice)D题
    AcWing851 spfa求最短路
    CF479E Riding in a Lift (dp)
    AcWing267 莫基亚(CDQ分治)
    P4093 [HEOI2016/TJOI2016]序列 (CDQ分治)
    2019ICPC南昌区域赛C题 And and Pair(数位dp)
  • 原文地址:https://www.cnblogs.com/ruowind/p/2891507.html
Copyright © 2011-2022 走看看