文档

全方位介绍框架特性,包含接口、属性映射、通信、延迟加载等丰富内容,如果你已经阅读过《起步》中的相关内容,那么就从这里进阶吧!

概述

本文档从最基本的概念开始,全方位介绍了 xmlplus 框架的特性。它基本上按照循序渐近的思路编写,建议你从头开始阅读。在阅读本文档之前请确保已经了解过《起步》中的相关内容,以便于实践文档中的示例代码。

文档涉及到与 xmlplus 相关的几乎所有的基础知识。在已经熟练使用 xmlplus 的前提下,你也可以将本文档作为一个有用的参考。

文档的相关示例代码位于目录 example/docs/ 之下,现在以 参数映射 中的一个示例来说明如何找到示例源码。

// 06-01
Input: {
    xml: "<input id='input' type='text'/>",
    opt: { format: "string" },
    fun: function (sys, items, opts) {
        var parse = {"int": parseInt, "float": parseFloat, "string": String}[opts.format];
        function getValue() {
            return parse(sys.input.prop("value"));
        }
        function setValue(value) {
            sys.input.prop("value", parse(value));
        }
        return Object.defineProperty({}, "value", { get: getValue, set: setValue });
    }
}

注意到示例开头的一个注释 06-01,那么你可以根据此注释定位到目录 /example/docs/06-parameter-mapping/01/,注释中的 06 即章节序,01 就是示例所在目录的名称。

下面给出的是各个章节的简单介绍,这可以作为阅读的一个索引。

  • 组件与空间:包含组件、命名空间以及与它们相关的路径的概念,这是最基础的内容。

  • 命名:有关组件对象的命名规则以及已命名对象的使用。

  • 抽象:通过一个 IP 输入框的编写,叙述了如何设计自定义组件以及如何抽象地看待组件。

  • 动态接口:分门别类地介绍了组件对象的动态接口。

  • 静态接口:描述了静态接口的类型以及它们之间优先级别。

  • 参数映射:参数映射是数据对象之间的一种映射机制。

  • 继承:继承是组件复用的一种方式,通过这章的学习,你将掌握如何使用继承特性来创建组件。

  • 检索:详细地介绍各类检索接口,这些接口通过视图项的搜索来得到目标组件对象集。

  • 嵌套:组件的嵌套包含以 HTML 元素作为父级和以自定义组件作为父级两种情形。

  • 生命周期:视图项中的组件集,随宿主组件的实例化而实例化。除此以外,系统还允许动态地增删组件对象。

  • 事件与通信:事件通信基于 W3C 制定的 DOM 事件标准,但又有所不同。

  • 消息与通信:消息通信是除事件通信外的另一种组件对象之间的通信机制。它是事件通信的一个很好补充。

  • 共享:组件对象的共享与通常的单例概念有相似之处,但比单例有着更为丰富的内容。

  • 延迟实例化:该特性允许视图项中的组件对象选择实例化的时机,而不是立即实例化。

  • 主题:主题是样式项中的一种字符串替换机制,它与宏的概念类似。

  • 优化:对于较为复杂应用,通过一些简单的措施,可以明显地优化应用的性能,从而提高应用的体验性。

组件与空间

组件

组件是应用的基本构造块,它是一个普通对象。所谓普通对象指的是使用 {}new Object 创建的对象。例如,{} 是组件,而 RegExp 不是组件;{"key":"value"} 是组件,而 [] 不是组件。

组件内部可以包含七种可能的数据项,而忽略其它的数据项。这七种数据项分别为:

  • css:样式项,一个字符串,描述了组件的样式
  • xml:视图项,一个符合 XML 格式的字符串,描述了一个子组件对象集
  • ali:别名项,一个普通对象,为视图项中的部分组件对象提供集体名称
  • fun:函数项,一个函数,完成组件对象的初始化和公有接口的导出
  • opt:参数项,一个普通对象,给函数项提供默认的初始参数值
  • cfg:配置项,一个普通对象,给视图项中的子组件对象提供初始参数配置
  • map:映射项,一个普通对象,为组件本身提供额外的配置

组件可以不必包含这些项,也可以仅包含某些项。下面是一些合法的组件描述。

{ }
{ css: "" }
{ xml: "<div/>", fun: new Function }
{ opt: {}, map: {}, ali: {} }

而下面组件描述都是不符合要求的。

[]                                   // 数组不能成为组件描述
{ css: {} }                          // 样式项必需是字符串
{ xml: [], fun: new Function }       // 视图项必需是 XML 字符串
{ opt: {}, map: {}, ali: /[a-z]/i }  // 别名项不能是正则表达式

组件还可以是基本的 HTML 元素。HTML 元素是基组件,前面所说的组件叫做自定义组件。基组件是不可分解的,自定义组件可以由基组件或者其它自定义组件组合而成。比如下面的组件,它由基组件 div 和自定义组件 Calendar 组合而成的。

{ xml: "<div><Calendar/></div>" }

文本也作为组件而存在,且属于基组件。下面的组件由两个子组件组合而成,其中 h1 是一 HTML 元素,hello, world 是一文本。

{ xml: "<h1>hello, world</h1>" }

一段 CDATASection 描述也被看作组件,下面的组件包含了一个 CDATASection 子组件。CDATASection 描述也属于基组件。

{ xml: "<![CDATA[hello, world] ]>" }

一段注释也是组件,下面的组件包含了一段注释子组件。注释也属于基组件。

{ xml: "<!--这是一段注释-->" }

在视图项中,对组件集的描述应该是一个仅包含一个根节点并且格式良好的 XML 字符串,而不能是其他的不合法描述。下面几个对视图项的描述就是不符合要求的。

{ xml: "<div/><span/>" }      // 缺少根节点
{ xml: "div</span>" }         // 开头缺少<span>
{ xml: "<span><div></span>" } // div未闭合

应该注意区分组件与组件对象。组件可以看作一些面向对象编程语言里面的类或者模板。而组件对象也叫组件实例,它是组件实例化的结果,一个组件可以实例化出若干个组件对象。自定义组件实例化后称之为自定义组件对象。一个 HTML 元素、一段文本、一段 CDATASection 描述以及一段注释都称作组件。当它们实例化后,则分别称之为 HTML 元素对象、文本对象、CDATASection 对象以及注释对象。后续章节对相关内容的引用将遵守这些名称约定。

命名空间

命名空间是组件的容器,任何一个组件必然属于某一个特定的命名空间。命名空间可以是空的,它不包含任何的组件。下面的代码定义了一个命名空间 //xp,该命名空间为根命名空间,它不包含任何组件。

xmlplus("xp", function (xp, $_, t) {
   // 一个空的命名空间,不包含任何组件
});

一个命名空间的完整引用必需从根命名空间开始,并且以双斜杆开头。下面的代码定义了另一个根命名空间 //xp,它包含组件 Input 和组件 Calendar。这两个组件由函数 $_().imports 导入,其中不带参数的函数调用 $_(),用于表示函数 imports 导入的组件属于根命名空间 //xp

xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Input: {},
        Calendar: {}
    });
});

下面的代码定义了一个根命名空间 //mx,它包含组件 Input 和组件 Calendar。另外还定义了一个 //mx 的子命名空间 //mx/ui/layout,它包含了组件 Tab 和组件 ViewStack。这两个组件由函数 $_("ui/layout").imports 导入,其中带参数的函数调用 $_("ui/layout") 用于表示命名空间 //mx/ui/layout

xmlplus("mx", function (xp, $_, t) {
    $_().imports({
        Input: {},
        Calendar: {}
    });
    $_("ui/layout").imports({
        Tab: {},
        ViewStack: {}
    });
});

上面定义的命名空间叫做自定义命名空间。除了自定义命名空间外,系统中还存在着一个匿名的空间,该空间包含了所有的基组件。如上面所讲的 HTML 元素、文本等基组件都属于匿名空间。

在应用中,允许存在多个不同的根命名空间。如下面所示,该示例中定义了两个根命名空间,分别是 //alice//bob

xmlplus("alice", function (xp, $_, t) {
    // 组件定义区
});
xmlplus("bob", function (xp, $_, t) {
    // 组件定义区
});

路径

前面说过一个命名空间的完整引用必需从根命名空间开始,并且以双斜杆开头。这种方式的组件引用方式叫做绝对路径引用。除此以外,还有另一种组件的引用方式,叫做为相对路径引用。下面对这两者分别进行论述。

绝对路径

如前所述,绝对路径必需以双斜杆 // 开头,其后跟着的是根命名空间名称。请看下面的示例。

xmlplus("mx", function (xp, $_, t) {
    $_().imports({
        Calendar: {}
    });
});
xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Index: {
            xml: "<i:Calendar xmlns:i='//mx'/>"
        }
    });
});

该示例中包含两个根命名空间,分别是 //mx//xp。其中,根命名空间 //mx 包含组件 Calendar,根命名空间 //xp 包含组件 Index,并且在组件 Index 的视图项中通过绝对路径 //mx 引用了组件 Calendar。

相对路径

与绝对路径不同,相对路径不再从根命名空间开始,而是以组件当前所在路径作为基地址。与相对路径相关的通配符有三个。一个是斜杆 /,它代表根命名空间。另一个是单句点 .,它代表当前组件所在的路径。还有一个是双句点 ..,它代表当前组件所在路径的上一级路径。下面分别介绍。

下面示例中,组件 Calendar 位于命名空间 //xp/form 中,组件 Index 位于命名空间 //xp 中。现在将 //xp 代之以 /,从而在组件 Index 的视图项中可以通过路径 /form 来引用组件 Calendar。

xmlplus("xp", function (xp, $_, t) {
    $_("form").imports({
        Calendar: {}
    });
    $_().imports({
        Index: {
            xml: "<i:Calendar xmlns:i='/form'/>"
        }
    });
});

下面示例中,组件 Index 和组件 Calendar 属于同级组件,它们都属于根命名空间 //xp。现在将 //xp 代之以 .,从而在组件 Index 的视图项中可以通过路径 . 来引用同级组件 Calendar。

xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Index: {
            xml: "<i:Calendar xmlns:i='.'/>" // 或者<Calendar/>也可以
        },
        Calendar: {}
    });
});

正如上面注释内容所述,在不会造成冲突的情况下,当前路径标识 . 也可以忽略不写,直接写成 <Calendar/>,这样显得更为简洁。

现在对前一个示例做些修改,把组件 Index 移到命名空间 //xp/form 中。那么,相对组件 Index 而言,组件 Calendar 位于其上一层级。于是可以将 //xp 代之以 ..,从而在组件 Index 的视图项中可以通过路径 .. 来引用组件 Calendar。

xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Calendar: {}
    });
    $_("form").imports({
        Index: {
            xml: "<i:Calendar xmlns:i='..'/>"
        }
    });
});

最后需要说明的是,对于同根命名空间组件的引用,应尽可能使用相对路径的方式。这有两个好处:一来可以简化引用空间来源的书写;另外,当要更改根空间名称时,只需改动一个地方即可。

组件的实例化

定义好组件之后,就可以通过 xmlplus 提供的 startup 函数实例化一个指定的组件。下面的代码实例化了一个位于命名空间 //xp 的组件 Calendar。

var parent = document.getElementById("parent");
xmlplus.startup("//xp/Calendar", parent);

函数 startup 的第二个参数指定了组件实例化后被追加到的 DOM 元素对象. 该参数可以是某一 DOM 元素对象或者该 DOM 元素对象的 id 属性值。相比而言,后者更为简洁,比如上面两行可以简写如下。

xmlplus.startup("//xp/Calendar", "parent");

另外如果不提供第二个参数,那么将采用默认值作为组件实例化后被追加到的 DOM 元素对象。在浏览器端该默认值为 window.body,在服务端该值由系统内部创建。

下面是另一种组件的执行方式,它明确给出了组件的 XML 字符串描述,这与前一种方式等价。

var xml = "<i:Calendar xmlns:i='//xp/Calendar'/>";
xmlplus.startup(xml, "parent");

还可以先解析出 XML 节点再执行,这与前两种方式等价。

var xml = "<i:Calendar xmlns:i='//xp/Calendar'/>";
var xmlNode = xmlplus.parseXML(xml).lastChild;
xmlplus.startup(xmlNode, "parent");

当然,直接提供基组件也是可以的,但必需是 HTML 元素。下面的第一行会创建一个 span 元素对象,第二行则会抛出一个错误。

xmlplus.startup("<span/>", "parent");
xmlplus.startup("hello, world", "parent");

函数 startup 还有可选的第三个参数,该参数可以为目标组件提供初始输入值。如下面的示例,组件 Calendar 在初始化时将采用第三个参数提供的初始日期值。关于组件参数方面的详细内容,后续会有章节专门介绍,这里可先跳过。

xmlplus.startup("//xp/Calendar", "parent", {date: "2016/01/01"});

当代码运行于浏览器端时,一般不显示地调用函数 startup。请看下面的示例,该示例直接在 HTML 中以 XML 的形式书写要实例化的组件。

<!DOCTYPE html>
<html>
    <head>
        <script src="xmlplus.js"></script>
        <script src="index.js"></script>
    </head>
    <body>
        <i:Index xmlns:i="//xp"></i:Index>
    </body>
</html>

如果要禁用这种解析方式,并以函数 startup 启动当然也是可以的,只要给 body 添加属性 noparse 即可。例如,默认情况下,下面示例中的组件 Index 不会实例化,除非你使用函数 startup 显示地实例化它。

<!DOCTYPE html>
<html>
    <head>
        <script src="xmlplus.js"></script>
        <script src="index.js"></script>
    </head>
    <body noparse="true">
        <i:Index xmlns:i="//xp"></i:Index>
    </body>
</html>

一个完整的示例

有别于前面零碎的代码片断,现在给出一个完整的可运行的示例。该示例由三个文件组成,下面是第一个文件,它是一个纯 JavaScript 文件,它向系统导入了一个名为 Index 的组件。现将其命名为 index.js

// 01-01
xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Index: {
            css: "#text { color: red; }",
            xml: "<h1 id='text'>hello, world</h1>",
            fun: function (sys, items, opts) {
                sys.text.css("font-size", "28px");
            }
        }
    });
});

下面是第二个文件,它是一个 HTML 文件,它引用了框架代码文件以及如上的第一个文件。现将其命名为 index.html

<!-- 01-01 -->
<!DOCTYPE html>
<html>
    <head>
        <script src="xmlplus.js"></script>
        <script src="index.js"></script>
    </head>
    <body>
        <i:Index xmlns:i="//xp"></i:Index>
    </body>
</html>

确保三个文件位于同一个目录下,通过浏览器打开 index.html 文件,你将会看到一行红色的、字体大小为 28px、值为 hello, world 的文本。这个示例中涉及到部分本章未提及的内容,可不必深究,仅需有点印象即可,后面的章节会有详细的讲述。

命名

一个示例

在探讨组件对象的命名之前,先看一个示例,后面的讲述主要围绕这个示例来展开。

// 02-01
Index: {
   css: "#dog { color: red; }\
         #cat { color: blue; }\
         #animal { background: green; }",
   xml: "<div>\
             <h1 id='dog'>dog</h1>\
             <h1 id='cat'>cat</h1>\
         </div>",
   ali: { animal: "/div/h1" },
   fun: function (sys, items, opts) {
       console.log(sys.dog.text());
       console.log(sys.cat.text());
       sys.animal.call("css", "border", "1px solid black");
   }
}

此示例中存在一个名称为 Index 的组件,它包含了四个组成部分。其中视图项包含了要显示的对象,样式项包含了相关对象的样式,别名项是对集体对象的描述,函数项包含了相关的操作代码。

给个体对象命名

示例中,视图项包含三个 HTML 元素对象,其中两个都拥有 id 属性,其属性值分别为 dogcat,我们把 dog 看作是第一个 h1 元素对象的名字,把 cat 看作是第二个 h1 元素对象的名字,而 div 则是一个未命名的元素对象。

所以,要给某一组件对象命名,只要给其相应的元素对象设定 id 属性就可以了。下面给出一些命名方面的建议。

  • 名称中只包含字母、数字或下划线,且不能以数字开头
  • 所取名字做到见名思义
  • 确保各个元素的名称在视图项中不是冲突的

其中头两条规定不是强制性的,但遵守它们有助于书写更好的代码。如果违反第三条规定,则只有同名的最后一个对象才可以通过名称访问。

在程序运行时,视图项中的任何已命名节点都会实例化为对象。对于那些没有命名的节点也是如此。虽然,对于未命名节点的相应组件对象无法直接显示使用,但我们完全有办法获得并使用它,在后面章节中将会看到这点是如何做到的。

给集体对象命名

众多的个体对象聚合在一起形成集体对象。从整体的角度来看,集体对象包含有别于个体对象的独有性质,所以给集体对象命名有其必要性。下面来看如何给集体对象命名。

给集体对象命名时,首先需要懂得如何描述集体对象。集体对象的描述方式有很多种,可以是正则表达式或者 XPath 表达式,又或者是 css 选择器。对于用 XML 描述的对象集而言,使用专门配套的 XPath 表达式或者 css 选择器显然更好些。系统仅出于下面的理由,选择 XPath 表达式作为集体对象的描述。相对于 css 选择器,xpath 表达式的表达能力更为强大。比如对于文本对象,css 选择器就没法选择出来。

示例中,注意别名项部分包含的 animal 选项,它的值是一个表示 div 元素子级的 XPath 表达式 /div/h1,该表达式以文档根为上下文。于是 animal 代表了 dogcat 所组成的集体对象。集体对象类似于数组,它包含了表达式所描述的所有的个体对象,它拥有属于自己的接口属性。

在样式项中使用命名对象

在示例中,可以看到这样两行 css 字符串:

#dog { color: red; }
#cat { color: blue; }

此处,首行引用了组件对象 dog,并设置其字体颜色为红色;尾行引用了组件对象 cat,并设置其字体颜色为蓝色。

从这里可以看出,在样式项中引用个体对象的方式是:以 # 开头,再追加上个体对象名。这与通常做网页开发时,对拥有 id 属性的 HTML 元素的引用方式是一致的。

引用集体对象的方式与引用个体对象的方式是类似的,即以 # 开头,再追加上集体对象名。下面的样式将集体对象 animal 中的所有元素都加上了下划线。

#animal { text-decoration: underline; }

现在给视图项中的 div 元素对象也添加上名字属性,但删除组件的 ali 选项。如下代码所示, 那么在样式项中就存在另一种对集体对象的引用方式,它就是我们熟悉的包含选择符。它引用的集体对象所包含的元素与上面的组件对象 animal 是一样的。

// 02-02
Index: {
   css: "#animal h1 { background: green; }",
   xml: "<div id='animal'>\
             <h1 id='dog'>dog</h1>\
             <h1 id='cat'>cat</h1>\
         </div>"
}

有时候,会存在同名的集体对象和个体对象。这种情况下,个体对象具有优先权。也就是说,个体对象的样式会覆盖集体对象的样式。当然,如果出现了这种情况,说明你的组件设计出问题了,你需要重新审视你设计。

在函数项中使用命名对象

下面的两行来自示例的函数项。其中,sys.dogsys.cat 分别引用了已命名的组件对象 dogcat,并分别通过调用它们的接口函数 text 来获取对象所包含的文本。

console.log(sys.dog.text());
console.log(sys.cat.text());

函数项中引用个体对象的方式:以 sys. 开头,再追加上个体对象名。当然,如果读者没有遵守前面对元素的命名约定,即元素名中包含非 JavaScript 标识符,那么就只能以类似访问数组元素的方式进行对象的访问。现在假设 sys 中包含名为 cat& 的个体元素,那么引用该元素方式就只能是:sys["cat&"]

在函数项中引用集体对象的方式与引用个体对象的方式是一致的。集体对象有一个函数 call,它的第一个参数是一个字符串,代表一个函数名,其后是所代表函数的实参列表。该函数会遍历集体对象所包含的所有个体对象,并调用指定的函数(如果存在的话)。

现在来看看示例中对集体对象的使用。

sys.animal.call("css", "border", "1px solid black");

该语句会依次调用组件对象 dogcat的接口函数 css,该函数以 border1px solid black 作为函数实参列表。该语句的执行效果等同于下面两个语句。

sys.dog.css("border", "1px solid black");
sys.cat.css("border", "1px solid black");

与前一节类似,有时候,会存在同名的集体对象和个体对象 animal。这种情况下,个体对象将覆盖集体对象。也就是说,此时在函数项中是无法访问到集体对象的。

个体对象包含的两类接口

在示例中,读者一定还注意到了函数项部分包含的形参 itemsitemssys 包含了同样多的个体对象与集体对象,并且名称完全相同。比如 sys 中包含了一个对象 dog,items也包含一个对象 dog,前者我们称之为系统对象,后者我们称之为与系统对象相关联的值对象,简称值对象。这两者的关系如下。

sys.target.value() === items.target

也就是说系统对象函数 value 的返回值等于与系统对象相关联的值对象。

系统对象所包含的接口函数,叫做系统对象接口。虽然系统对象与值对象是一对一的,但它们包含的接口却不尽相同。前面的组件对象 sys.dogsys.cat 所调用的 text 函数即属于系统对象接口的一种。下面给出了系统对象包含的其它部分接口函数名:

on | off | trigger | append | before | remove | value……

这些接口函数由框架系统提供,它们是系统级别的,任何被实例化的对象都会有。系统对象接口提供了诸如组件对象之间通信或者组件对象的添加、移除之类的功能。它们的使用方式后续章节会陆续讲述。

与系统对象类似,值对象所包含的接口,叫做值对象接口。值对象所提供的接口取决于具体的组件,对于所有基组件对象,它们的接口均为空。至于非基组件接口方面的内容后续会有章节详细讲述,这里暂且不表。

到目前为止,我们遇到到不少可能容易混淆的名词。现在将它们列出来,并作简要说明。

  • 组件:应用的基本构造块,它是一个普通对象,它相当于面向对象编程语言里面的类或者模板
  • 组件对象:组件实例化的结果,一个组件可以实例化出多个组件对象
  • 系统对象:每一组件对象都对应一个系统对象,组件对象与系统对象可以同等看待
  • 值对象:它可以由系统对象接口函数 value 的执行结果得到,也可以从函数项的第二个实参中得到
  • 系统对象接口:系统对象所包含的接口函数
  • 值对象接口:值对象所包含的接口函数

样式项中的通配符

为了了解样式项中符号 # 所代表的内容,先来看看最终由样式项生成的 css 代码(与实际的内容有略有出入,但大体一致)。

.cadog { color: red; }
.cacat { color: blue; }
.caanimal { background: green; }

通过与样式项中的内容进行比对,可以发现符号 # 被替换成了字符串 .ca。再看看最终由视图项生成的 html 代码。

<div>
    <h1 class='cadog caanimal'>dog</h1>
    <h1 class='cacat caanimal'>cat</h1>
</div>

通过与视图项中的内容进行比对,可以发现原始的 id 属性不见了,代之以 class 属性。class 属性中的类名与样式项中的内容恰好是对应的。

除了符号 # 外,样式项中还允许存在一个通配符 $,该通配符与 # 的区别仅在于:后者比前者多一个句点。也就是说,如果上述的样式项中出现符号 $,那么 $ 最终会被替换为 ca

上述通配符有一个特性,对于同一个组件,不论实例化出多少对象,通配符所对应的字符串都是不变的。所以该字符串可以用于标识一个组件。

组件的封闭性

现在假设在同一个应用中,又定义了另一个组件,如下所示。也就是说,现在同一个应用中包含了两个不同的组件。

Zoon: {
   css: "#dog { color: red; }\
         #cat { color: blue; }",
   xml: "<div id='zoon'>\
             <h1 id='dog'>dog</h1>\
             <h1 id='cat'>cat</h1>\
         </div>"
}

可以看出,该组件内部同样包含了名为 dogcat 的组件对象。那么它们是否会和前面的定义和 dogcat 组件对象冲突呢?这种担心是完全不必要的。因为在 xmlplus 中,组件内部对象具有良好的封闭性,对于一个已实例化的组件对象而言,除了它开放的接口,你是无法访问其任何内部元素的。

抽象

前面两章,我们面对的都是一些简单的组件,并且对于如何创建自定义组件还没有足够的了解。这章就来探讨如何扩展基组件并创建实用的自定义组件,从而为复杂应用进行模块化设计奠定坚实的基础。

定义 IPv4 输入框组件

为了探讨抽象这个主题,本章会实现一个 IP 输入框,并达到类似下图所示的输入框的效果。

这种输入框包含四个小文本框,文本框之间以句点相间隔且仅允许输入最多三位的十进制数字。当文本框中输入数字达到三位时,焦点会自动跳转到下一文本框并选中该框文本。如果焦点位于最后一个文本框,则不进行跳转。如前面章节所述,一个组件需属于某一特定的命名空间。为简单起见,现在将其置于命名空间 //xp 中,该空间还包含另一个组件 Index,它是用于测试 IPv4Box 组件的。下面是代码的框架结构,具体实现细节由后面给出。

// 03-01
xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Index: {},
        IPv4Box: {}
    });
});

下面首先给出的是该组件的视图项,此视图项描述了该组件由哪些基组件组合而成。

<!-- 03-01 -->
<div id='box'>
    <input/>.<input/>.<input/>.<input/>
</div>

该视图项的顶层元素被命名为 box,box 的子级包含 4 个文本框以及 3 个分隔句点。下面给出它们的样式项:

/* 03-01 */
#box { border:1px solid #ABADB3; display: inline-block; }
#box input { width: 28px; line-height: 19px; border:0; text-align:center; outline:none; }

样式项设置了相应文本框的边框、颜色、尺寸和显示方式等样式。如果纯粹从外观上看,已经达到目的了,下面就来实现函数项部分,使得剩余功能得以生效。

// 03-01
function (sys, items, opts) {
    var inputs = sys.box.children();
    sys.box.on("keypress", "input", function(e) {
        var next, ch = String.fromCharCode(e.which);
        if (!/[0-9]/.test(ch))
            return e.preventDefault();
        if (this.prop("value").length == 2) {
            next = this.next();
            next && next.elem().select();
        }
    });
    function getValue() {
        return inputs.map(function (item) {
            return item.prop("value")
        }).join('.');
    }
    function setValue(input) {
        var input = input.split(".");
        for (var i = 0; i < inputs.length; i++)
            inputs[i].prop("value", input[i]);
    }
    return Object.defineProperty({}, "value", { get: getValue, set: setValue });
}

在函数项中,组件对象 sys.box 侦听了各文本框对象的 keypress 事件,回调函数过滤非数字输入,且当输入达到 3 个字符长时,光标自动跳转到下一输入框并选中该框内容。此外,函数项还返回了一个用于设置和读取 IP 值的接口。此处涉及到事件通信相关的内容,后续会有专门的章节讲述,这里先略过。

使用 IPv4 输入框组件

现在已经定义好 IP 输入框组件,下面来看看如何使用该组件。

// 03-01
Index: {
    css: "#addr { margin-bottom: 5px; }",
    xml: "<div id='index'>\
              地址:<IPv4Box id='addr'/><br/>\
              掩码:<IPv4Box id='musk'/>\
          </div>",
    fun: function (sys, items, opts) {
        items.addr.value = "192.168.0.1";
        items.musk.value = "255.255.255.0";
        console.log("addr", items.addr.value);
        console.log("musk", items.musk.value);
    }
}

由于组件 IPv4Box 与 Index 同属于一个命名空间,所以可以忽略命名空间的引用。在组件 Index 中,实例化了两个 IPv4Box 组件,分别命名为 addr 和 musk。函数项通过参数 items 引用了值对象 addr 和 musk,并调用接口 value 获取与设置相应的 IP 值。

注意,这里调用的组件 IPv4Box 返回的 value 接口是通过 items.addritems.musk 得到的。参数 sys 包含同名的系统对象 addr 和 musk 的引用,并且也有一个 value 函数,但与值对象接口中的 value 是两个不同的东西。

组件 Ipv4Box 所定义的抽象

前面我们定义了组件 Ipv4Box,并通过引用如下代码段实例化了该组件。

<Ipv4Box id='addr'/>
<Ipv4Box id='musk'/>

然后通过组件 addr 和 musk 的开放接口读取与设置 IP 值。所谓抽象,就是隐藏实现细节,只暴露使用者应该了解的接口。在 Index 中使用组件 IPv4Box 并不需要知道其内部是如何实现的,该组件只开放了接口 value,通过该接口可以设置或者获取 IP 值,这就是组件 Ipv4Box 所提供的抽象。从抽象的角度来看,基组件可以看作是原生抽象,而自定义组件则属于自定义抽象。

动态接口

在上一章,结合示例讲了如何构建自定义组件,以及如何从抽象的角度看待组件。抽象的意义在于封装用户不必知道的细节,只暴露用户需要知晓的接口。从这一章开始就来具体谈谈与组件相关的接口。

组件的接口可分为静态接口与动态接口两部分。所谓静态接口,可以理解为在组件初始化时允许给组件提供的参数。而由函数项所返回的公有属性或者函数,则是可被使用任意多次的,称为动态接口。

静态接口由标签属性、相关联的配置项以及参数项按一定的优先级综合指定,最后生成的参数值会作为函数项的第三个参数传入。动态接口按提供者划分,又可以分为全局接口、系统对象接口与值对象接口。本章只谈动态接口,静态接口留待下一章讲述。

全局接口

全局接口由全局对象 xmlplus 或 xp 提供。下面给出的是所有的全局接口列表。

  • startup:实例化一个组件
  • guid:获取系统内的唯一的全局标识符
  • error:抛出一个错误
  • ready:在页面文档加载后激活回调函数
  • type:判断一个对象的所属类型
  • isWindow:判断一个对象是不是窗体
  • isArray:判断一个对象是不是数组类型
  • isFunction:判断一个对象是不是函数类型
  • isNumeric:判断一个对象是否数值型
  • isPlainObject:判断一个对象是否简单对象
  • isEmptyObject:判断一个对象是否空对象
  • isSystemObject:判断一个对象是否框架的系统对象
  • extend:将两个或多个对象的内容合并到第一个对象中
  • expand:对系统接口进行扩展
  • each:遍历一个数组或者其它对象
  • parseXML:将给定的字符串解析为 XML 文档
  • hasNamespace:判定当前系统是否包含给定的命名空间
  • hasComponent:判定当前系统中是否包含给定的组件
  • clearLibrary:按照给定的模式清除当前系统中相关命名空间及组件
  • getElementById:由给定的标识符获取相关对象

对于这些接口的使用,下面仅给出了几个示例,具体内容请参看 全局接口

// 04-01
console.log(xp.isArray([]));          // true
console.log(xp.isPlainObject({}));    // true
console.log(xp.isSystemObject(null)); // false

系统对象接口

系统对象接口只有系统对象才拥有,系统对象接口可简称为系统接口。按功能来划分,系统接口可以分为五大类:

集合对象接口

集合对象接口也称为集体对象接口。如《命名》章节中所述,系统对象可以分为个体对象与集体对象。集体对象的部分接口源自数组,下面给出的是非数组包含的集体对象接口。

  • call:遍历集合对象,并调用给定的函数,其中函数的参数由函数名的后续实参提供
  • hash:将类数组形式的集合对象转化包含键值对的普通对象
  • values:将包含系统对象的集合转化成包含值对象的集合

对于上述接口的具体细节请参看 集合接口。下面的接口是集体对象拥有的数组对象接口。

every | forEach | indexOf | map | pop | push | shift | slice | some | splice | unshift

这些接口的用法与普通数组的接口用法一致。不过需要注意,上述的 slice 函数已经过改造,其返回的是集合对象。

与 DOM 元素相关的接口

由于每一组件对象都对应一个 DOM 元素对象,所以系统提供了相关的操作 DOM 元素对象的接口。通过这些系统接口可以间接地操作 DOM 元素对象,以下简称 DOM 元素对象为节点。

  • text:获取或者设置节点的文本
  • prop:获取或者设置节点的属性值
  • removeProp:移除节点的属性值
  • attr:获取或者设置节点的属性值
  • removeAttr:移除节点的属性值
  • addClass:添加类
  • removeClass:移除类
  • contains:判断当前对象的是否包含给定对象
  • css:获取或者设置样式值
  • show:显示节点
  • hide:隐藏节点
  • width:获取或者设置节点的宽度
  • height:获取或者设置节点的高度
  • offsetParent:获取最近定位的祖先元素
  • offset:获取或者设置节点的偏移
  • position:获取节点的位置
  • scrollTop:返回或设置当前对象的滚动条的垂直位置
  • scrollLeft:返回或设置当前对象的滚动条的水平位置

对于上述接口的详细用法请参看 DOM 接口

与组件生命周期相关的接口

这类接口主要完成组件对象的创建、移除与替换的操作。

  • append:给当前组件对象子级追加一个组件对象
  • before:在当前组件对象之前插入一个组件对象
  • replace:用新的组件对象替换掉当前组件对象
  • remove:移除掉当前组件对象

这些接口的用法在后续章节《生命周期》中有详细的介绍。

组件检索接口

这类接口用于在视图项的组件对象集中检索相关的组件对象。

  • sys:以文档节点为上下文查找对象,返回系统对象集
  • items:以文档节点为上下文查找对象,返回组件值对象集
  • find:以当前节点为上下文查找对象,返回系统对象集
  • get:获取当前节点某一子节点对象,返回系统对象
  • first:获取当前节点的第一个子节点对象,返回系统对象
  • last:获取当前节点的最后一个子节点对象,返回系统对象
  • next:获取当前节点的下一个节点对象,返回系统对象
  • prev:获取当前节点的前一个节点对象,返回系统对象
  • children:获取当前节点的所有子节点对象,返回系统对象集

上面的接口中,前两个比较特殊,它们属于通用检索接口,分别等于函数项的前两个参数。它们的用法在后续章节《检索》中会有详细的介绍。

与组件通信相关接口

这类接口用于在各组件对象之间进行通信。通信分为两类,一类是事件通信,另一类是消息通信。下面的系统接口中,前四者属于事件通信接口,后四者属于消息通信接口。

这些接口的用法在后续章节《事件与通信》《消息与通信》中有详细的介绍。

其它类别接口

这类接口用于获取或者设置与组件对象相关联的一些信息。

  • value:获取组件对象的值对象
  • localName:获取组件对象相应的组件名
  • namespace:获取组件对象所属的命名空间
  • guid:获取组件对象的唯一标识符
  • toString:获得组件对象的 id 或者唯一标识符
  • serialize:序列化视图项或者视图项所对应的 HTML DOM 文档树
  • data:获取或者设置组件所绑定的数据
  • removeData:移除组件所绑定的数据

值对象接口

不同于系统对象接口,值对象接口由函数项返回。值对象接口可以简称为值接口。通过函数项的第二个参数可以获取值对象接口。下面结合前面设计的组件 IPv4Box 来看看值对象接口的使用。

// 04-02
Index: {
    xml: "<Ipv4Box id='ipbox'/>",
    fun: function (sys, items, opts) {
        items.ipbox.value = "192,168,0,1";
        console.log(items.ipbox.value);
        console.log(sys.ipbox.value() == items.ipbox);
    }
}

对于接口 value 的使用,前一章已经讲过了,这里主要注意函数项的最后一行。运行示例,控制台打印出的值是 true,这说明系统函数 sys.ipbox.value 的返回值与 items.ipbox 是相等的。下面是 value 接口的一种可能的使用方式:

sys.ipbox.css("color","blue").value().value = "192,168,0,1";

上面代码中,系统函数 css 返回的是 sys.ipbox 的引用。系统函数 value 属于 sys.ipbox 的一个接口。value 函数不用提供任何的输入参数,它返回的是对等的 items.ipbox 的引用。在某些场合,这可以简化代码的书写。

不同类型对象之间的接口差异

《组件与空间》中,描述了系统内部的五种组件类型。按照拥有的接口数量划分,可以分为两类,其中 HTML 元素对象和自定义组件对象拥有所有的系统对象接口(排除 sysitems),而其余的组件,包括文本,CDATASection 描述以及注释,则只拥有如下的部分系统接口。

  • before:在当前对象之前插入一个对象
  • replace:替换掉当前对象
  • remove:移除掉当前对象
  • next:获取当前节点的下一个节点对象,返回系统对象集
  • prev:获取当前节点的前一个节点对象,返回系统对象集
  • text:获取或者设置节点的文本
  • guid:获取组件对象的唯一标识符
  • toString:获得组件对象的 id 或者唯一标识符

另外,根据系统的运行环境不同,HTML 元素对象和自定义组件对象所拥有系统对象接口也有区别。在浏览器端,它们拥有前面所述的所有系统对象接口。但在服务端,以下接口不可见。

  • width:获取或者设置节点的宽度
  • height:获取或者设置节点的高度
  • offsetParent:获取最近定位的祖先元素
  • offset:获取或者设置节点的偏移
  • position:获取节点的位置
  • scrollTop:返回或设置当前对象的滚动条的垂直位置
  • scrollLeft:返回或设置当前对象的滚动条的水平位置

静态接口

静态接口可以理解为组件在初始化时允许提供的参数。它与一些应用的配置文件类似,在组件实例化之前就准备就绪的。静态接口由组件的标签属性、配置项以及参数项按一定的优先级综合指定,最后生成的参数值会被当作函数项的第三个参数传入。

使用默认初始值

任何一个自定义组件都包含一个显式的或者隐式的参数项 opt,它提供了组件实例化时所使用的默认的初始参数值。

下面给出的一个按钮组件,它对 button 元素作了简单的封装。此组件的参数项包含一个名为 fontSize 的初始输入值。在组件实例化过程中,该参数会被复制到函数项的第三个参数 opts 中去。然后,函数项中用此值设置 button 元素的字体大小。这个值为 24fontSize 给组件在初始化时提供了一个默认值。

// 05-01
Button: {
    opt: { fontSize: 24 },
    xml: "<button id='foo'>hello</button>",
    fun: function (sys, items, opts) {
        sys.foo.css("font-size", opts.fontSize + "px");
    }
}

通过对象名指定初始值

参数项指定了元素的初始值,组件实例化时,有时需要覆盖初始值,这可以通过在被实例化的组件的配置项中重新设定来实现。下面是使用组件 Button 的一个示例:

// 05-02
Index: {
    cfg: { foo: { fontSize: 16 } },
    xml: "<Button id='foo'/>"
}

示例中,配置项指定组件对象 foo 的 fontSize 初始输入值为 16。该初始值会覆盖默认值 24,于是实例化后的按钮字体大小会是 16px

这里需要明确区分配置项 cfg 与参数项 opt 之间的异同。上面示例中,配置项指的是缩主组件 Index 的配置项,它会改变目标组件 Button 中已定义好的参数项 opt 的初始参数值。而目标组件 Button 中的配置项在组件 Index 中是不可见的。

通过集体名指定初始值

前面, 我们通过个体对象的名称在配置项中指定该对象的初始值,现在来看如何通过集体对象名来给多个对象指定相同初始值。

// 05-03
Index: {
    cfg: { button: { fontSize: 16 } },
    xml: "<div id='index'>\
              <Button id='foo'/>\
              <Button id='bar'/>\
          </div>",
    ali: { button: "//Button" }
}

在该示例中,别名项指定所有的 Button 组件对象的名称为 "button"。于是在配置项中,fontSize 的目标对象也就包含了所有的 Button 组件对象,从而组件对象 foo 和 bar 的 fontSize 初始值都会被设置成 16

属性值作为输入初始值

除了以上的初始参数的设定方式外,还有一种更为便捷的初始参数设定方式。那就是直接通过组件的标签属性值来更改组件对象参数的初始值。

// 05-04
Index: {
    xml: "<Button fontSize='16'/>"
}

上面的 fontSize 值在实例化时会以字符串的形式被主动映射到组件 Button 的参数项中,从而覆盖原始的默认值。这种指定初始值的方式是最方便,但也有其局限性。因为它只能提供简单形式的输入。如果初始输入是一个复杂的对象,它就无能为力了。

默认情况下,属性值会以字符串的形式被映射到组件的参数项中。如果期望得到的是数值型或者布尔型,就要在组件的映射项中指定格式化参数 format

// 05-05
Index: {
    xml: "<Format fontSize='16'/>"
},
Format: {
    opt: { setp: "28.5", fontSize: "24", width: "28px", disabled: "true" },
    map: { format: {"int": "step", "float": "fontSize width", "bool": "disabled"} },
    fun: function (sys, items, opts) {
        console.log(opts.step, typeof opts.step);
        console.log(opts.fontSize, typeof opts.fontSize);
        console.log(opts.width, typeof opts.width);
        console.log(opts.disabled, typeof opts.disabled);
    }
}

组件 Format 的映射项中的 format 参数指明如何格式化输入参数。在此组件中,step 被格式化为整型,fontSizewidth 被格式化为浮点型,而 disabled 则被格式化为布尔型。

注意,对于类型为 bool 的参数,如果相应的值是字符串 true,则为真值,否则为假值。

四种参数设定方式的优先级

上面描述了四种设定初始参数值的方式。现在来看看它们之间的优先级是怎样的。下面的示例给组件对象 foo 同时提供了 fontSize 的四种初始参数值,其中默认值为 24

// 05-06
Index: {
    cfg: { foo: { fontSize: 10 }, button: { fontSize: 11 } },
    xml: "<Button id='foo' fontSize='12'/>",
    ali: { button: "//button" }
}

为了便于说明它们的优先级别,现在给各种情形编号如下:

  • A:使用默认的 fontSize,其值为 24
  • B:使用 cfg 中 foo 的设置,其值为 10
  • C:使用 cfg 中 button 的设置,其值为 11
  • D:使用属性值,其值为 12

为了测定优先级,你可以每次测定出最高优先级的输入后,将其记录下来,然后移除它。之后再测定出次优先级别的配置,以此类推。那么你最终得到的优先级次序是这样的:

D > B > C > A

也就是说组件的标签属性值具有最高有优先级,配置项中的值次之,然后是通过别名项设定的值,而默认值的优先级是最低的。

参数映射

组件在初始化时,初始数据由函数项的第三个参数提供,并且此参数仅允许在函数项中使用。参数映射机制使得该参数的部分内容可以被拷贝到子组件对象中去,从而为子组件对象的初始化提供初始输入。参数映射分为两类,一类是到内部组件对象属性的映射,另一类是到组件配置项的映射。下面分别从这两个方面来讲述。

到内部组件对象属性的映射

这节要使用模块化技术对 HTML 文本框进行简单的封装扩展,增加数据的格式化输入输出能力。为了明确组件的功能,下面首先给出该组件的应用示例。

// 06-01
Index: {
    xml: "<div id='index'>\
             <Input id='foo'/>\
             <Input id='bar' format='int'/>\
          </div>",
    fun: function (sys, items, opts) {
        items.foo.value = "hello, world";
        items.bar.value = 27.1828;
        console.log("foo", items.foo.value);
        console.log("bar", items.bar.value);
    }
}

此示例实例化了两个组件 Input。组件 Input 允许接收一个 format 参数作为其静态接口输入,并提供一个属性 value 作为其动态输入输出接口。format 参数有三种可能的值:string (默认)、int 以及 float。这三种值分别对应三种数据类型:字符串型、整型和浮点型。属性 value 根据 format 的值来进行格式化输入输出。下面是示例的输出结果:

hello, world
227

组件对象 foo 的参数 format 的默认值是 string,所以输入值 hello, world 以字符串原样输出。组件对象 bar 的 format 值是 int,所以输入值 27.1828 会被格式化为整型数 27 输出。

在了解完组件 Input 的功能与行为后,现在给出的 Input 实现也就不难理解了。

// 06-01
Input: {
    xml: "<input id='input' type='text'/>",
    opt: { format: "string" },
    fun: function (sys, items, opts) {
        var parse = {"int": parseInt, "float": parseFloat, "string": String}[opts.format];
        function getValue() {
            return parse(sys.input.prop("value"));
        }
        function setValue(value) {
            sys.input.prop("value", parse(value));
        }
        return Object.defineProperty({}, "value", { get: getValue, set: setValue });
    }
}

该实现包含一个格式化函数表,函数表根据 format 的值选择相应的格式化函数来格式化输入输出。当然,我们的目的不仅于此,还希望能保留部分原有的行为接口。下面试着通过组件 Input 来使用 input 标签的原始功能。

// 06-02
Index: {
    xml: "<Input disabled='true'/>"
}

上述给 Input 标签设置了 disabled 属性。 然而,文本框并没有被禁用。回顾下前面的内容,这里的 disabled 属性最终会被拷贝到组件 Input 的函数项的第三个参数中去。而函数项中并没有设定 input 标签 disabled 属性的代码,所以文本框没有被禁用也就合情合理了。好了,现在来改造组件 Input,使它支持属性 disabled 的功能。

// 06-03
Input: {
    xml: "<input id='input' type='text'/>",
    opt: { format: 'string' },
    fun: function (sys, items, opts) {
        var parse = {"int": parseInt, "float": parseFloat, "string": String}[opts.format];
        if (opts.disabled)
            sys.input.attr("disabled", opts.disabled);
        function getValue() {
            return parse(sys.input.prop("value"));
        }
        function setValue(value) {
            sys.input.prop("value", parse(value));
        }
        return Object.defineProperty({}, "value", { get: getValue, set: setValue });
    }
}

在新组件的函数项中,添加了设定属性 disabled 的代码。虽然,现在的组件 Input 已经支持 disabled 属性了,但是 input 的标签还有许多其它的属性,比如 value、placeholder、readonly 等等。自然可以在函数项中逐一添加进去,但这就显得有些烦索了。要简化这些操作,需要系统能自动完成从函数项的第三个参数 opts 到 input 属性的映射。这可以通过在组件 Input 的映射项中做些配置来实现。现在使用属性映射,重新构建组件 Input 如下:

// 06-04
Input: {
    xml: "<input id='input' type='text'/>",
    opt: { format: 'string' },
    map: { attrs: { input: "disabled value placeholder readonly" } },
    fun: function (sys, items, opts) {
        var parse = {"int": parseInt, "float": parseFloat, "string": String}[opts.format];
        function getValue() {
            return parse(sys.input.prop("value"));
        }
        function setValue(value) {
            sys.input.prop("value", parse(value));
        }
        return Object.defineProperty({}, "value", { get: getValue, set: setValue });
    }
}

此组件的映射项包含一个 attrs 配置。该配置用于建立从函数项的第三个参数 opts 到相应内部对象属性的映射,这里指定的对象是唯一的 input 元素对象。需要映射的属性列由一字符串给出,该字符串由空格分隔且每一分隔项对应一个需要映射的属性名。下面是新组件的一个应用示例:

// 06-04
Index: {
    xml: "<Input placeholder='please input' value='hello world'/>"
}

到组件配置项的映射

将组件对象的初始参数映射到配置项与映射到内部组件对象的属性部分,所做的配置是类似的。

现在有这么一个需求,它要求将上面实现的两个 Input 组件组合成一个新组件。新组件仅需要一个 format 输入值,该值最终会被映射给内部的两个 Input 组件对象。同时新组件还提供一个只读属性接口 value,该接口可将两个 Input 的值组合成数组输出。我们首先想到的是使用上述的属性映射来实现。

// 06-05
Form: {
    xml: "<div id='form'>\
             price: <Input id='foo' value='2.2'/><br/>\
             count: <Input id='bar' value='3.3'/>\
          </div>",
    map: { attrs: { foo: "format", bar: "format" } },
    fun: function (sys, items, opts) {
        function getValue() {
            return [items.foo.value, items.bar.value];
        }
        return Object.defineProperty({}, "value", { get: getValue });
    }
}

这样的实现是完全没有问题的。然而,还有一种实现方式是使用配置项映射。它不是将参数项中的参数映射给相关对象属性,而是映射到当前组件的配置项中去。请看下面的实现。

// 06-06
Form: {
    xml: "<div id='form'>\
             price: <Input id='foo' value='2.2'/><br/>\
             count: <Input id='bar' value='3.3'/>\
          </div>",
    cfg: { foo: {format: "string"}, bar: {format: "string"} },
    map: { cfgs: { foo: "format", bar: "format" } },
    fun: function (sys, items, opts) {
        function getValue() {
            return [items.foo.value, items.bar.value];
        }
        return Object.defineProperty({}, "value", { get: getValue });
    }
}

为了清楚地了解映射行为是如何发生的,上面的组件明确给出配置项,尽管该配置项是多余的。配置项中的内容与标签的属性类似,它给出了相关对象的部分初始输入值。请注意映射项中的 cfgs 参数,它的值与前面的 attrs 是一样的。该参数指出在本组件实例化时,函数项的第三个参数 opts 中的 format 值会被拷贝到哪里。对于此示例,该位置是与组件对象 foo 和 bar 相关联的配置项。现在来看使用该组件的一个示例。

// 06-06
Index: {
    xml: "<div id='index'>\
            <Form id='foo' format='int'/>\
            <Form id='bar' format='float'/>\
            <button id='btn'>check</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.btn.on("click", function(e) {
            console.log("foo", items.foo.value);
            console.log("bar", items.bar.value);
        });
    }
}

该示例中,组件对象 foo 设置 format 的值为 int,组件对象 bar 设置 format 的值为 float。它们会被映射到组件 Form 的配置项的相应项中,最终作用于组件内部的两个子组件 Input。读者可以在文本框中输入不同的内容,以验证输出情况是否符合预期。

下面给出组件 Form 的另一种写法。它首先在组件的别名项命名一个集体对象 inputs,然后在配置项和映射项中使用此集体名来做相关的配置。这种做法达到的效果与前面是一致的。当要对众多对象作相同的配置时,采用这种方式会更方便些。

// 06-07
Form: {
    xml: "<div id='form'>\
             price: <Input id='foo' value='2.2'/><br/>\
             count: <Input id='bar' value='3.3'/>\
          </div>",
    ali: { inputs: "//Input" },
    map: { cfgs: { inputs: "format" } },
    fun: function (sys, items, opts) {
        function getValue() {
            return [items.foo.value, items.bar.value];
        }
        return Object.defineProperty({}, "value", { get: getValue });
    }
}

现在思考一个问题:在组件 Form 中,无论使用到属性的映射还是到配置项的映射,都能达到所要求的目的。那么对于 Input 组件,情况还是一样吗?答案是否定的。这是由于 Input 组件包含了基本的 HTML 元素标签 input,input 元素在实例化时,并不使用配置项的指定值,它只使用节点的属性值。所以,对于组件 Input 而言,只能选择到属性的映射,这是属性映射机制存在的一个重要理由。

同名映射与异名映射

前面所讲的映射都是同名映射,也就是原来属性名或者对象名是什么,映射后目标的对象名也就是什么。现在来看如何进行异名映射,请看下面的示例。

// 06-08
Input_1: {
    xml: "<input id='foo'/>",
    opt: { val: "hello, world" },
    map: { attrs: { foo: "val->value" } }
}

在这个例子中,参数项中包含一个名为 val 的字符串。同时,映射项中包含一个异名映射描述,该描述包含一个单向箭头。该描述指出,参数项中的名为 val 的字符串被映射给组件对象 foo 的 value 属性。下面的示例描述了到指定对象的异名配置项映射。

// 06-08
Input_2: {
    xml: "<Input id='foo'/>",
    opt: { val: "hello, world" },
    map: { cfgs: { foo: "val->value" } }
}

继承

继承是组件复用的一种方式。自定义组件只能继承自其它的自定义组件,而无法继承来自匿名空间的组件。一个组件 A 要继承另一个组件 B,需要在该组件的映射项中用 extend 项指明。其中组件 A 叫继承组件,组件 B 叫被继承组件。例如,下面的组件 Button 继承了当前命名空间的组件 Widget。extend 中的 from 项指明了被继承的原组件。

// 07-01
Widget: {
    css: "#btn { color: blue; }",
    xml: "<button id='btn'>label</button>"
},
Button: {
    css: "#btn { border: 1px solid red; }",
    map: { extend: {"from": "Widget"} }
}

上述的 from 值是一个路径的字符串描述,它可以是绝对路径,也可以是相对路径。

样式项的继承

对于原组件样式项的继承,默认情况下将采用字符串拼接的方式。如上例中,最终得到组件的样式项将会是如下的样子。

#btn { color: blue; }
#btn { border: 1px solid red; }

如果希望替换掉原组件的样式项,可以在 extend 项中明确指出,下面的组件 Button 的描述指出:该组件不使用被继承组件的样式项,而只使用组件自有的样式项。

// 07-02
Button: {
    css: "#btn { border: 1px solid red; }",
    map: { extend: {"from": "Widget", "css": "r"} }
}

上述的 rreplace 的缩写,表示用继承组件的样式项替换来自被继承组件的样式项。

视图项的继承

对于视图项的继承,如果继承组件中不包含视图项,那么就采用被继承组件的视图项。如下面的组件 Button,由于其内部不指定自有的视图项,所以它直接引用来自组件 Widget 的视图项。

// 07-03
Button: {
    map: { extend: {"from": "Widget"} }
}

如果组件中指定了视图项,那么就采用组件自身的视图项。如下面的组件 Button,它使用的是自已的视图项,而忽略继承组件的视图项。

// 07-04
Button: {
    xml: "<span id='btn'>label</span>",
    map: { extend: {"from": "Widget"} }
}

函数项的继承

对于函数项的继承,分四种情况讨论。

首先,如果继承组件中存在函数项的替换配置,那么就采用继承组件的函数项,而不论被继承组件是否包含函数项。如下面的示例所示,组件 Button 最终会采用自身的函数项。

// 07-05
Widget: {
    xml: "<button id='btn'>label</button>",
    fun: function (sys, items, opts) {
        console.log("hello, widget");
    }
},
Button: {
    map: { extend: {"from": "Widget", "fun": "r"} },
    fun: function (sys, items, opts) {
        console.log("hello, button");
    }
}

其次,若继承组件中不存在函数项的替换配置,且被继承组件不包含函数项,那么也采用继承组件自身的函数项。下面的示例给出了这种情况。

// 07-06
Widget: {
    xml: "<button id='btn'>label</button>"
},
Button: {
    map: { extend: {"from": "Widget"} },
    fun: function (sys, items, opts) {
        console.log("hello, button");
    }
}

第三种情况是,继承组件中不存在函数项的替换配置,且继承组件也不包含函数项,这时会延用的被继承组件的函数项。下面的示例给出了这种情况。

// 07-07
Widget: {
    xml: "<button id='btn'>label</button>",
    fun: function (sys, items, opts) {
        console.log("hello, widget");
    }
},
Button: {
    map: { extend: {"from": "Widget"} }
}

最后一种情况是,继承组件中不存在函数项的替换配置,且继承组件和被继承组件均包含函数项。在这种情况下,函数项将由下面的匿名函数替代。

function () {
    var foo = source.fun.apply(this, [].slice.call(arguments));
    var bar = target.fun.apply(this, [].slice.call(arguments));
    return bar ? $.extend(foo, bar) : foo;
}

其中的 source 代表继承组件自身,target 代表被继承组件。可以看出,最终函数的导出接口会由两个函数的返回值综合给出。下面的示例展示了这一点。

// 07-08
Index: {
    xml: "<Button id='index'/>",
    fun: function (sys, items, opts) {
        console.log(items.index);  // { "a": 2, "b": 1, "c": 3 }
    }
},
Widget: {
    fun: function (sys, items, opts) {
        return { "a": 0, "b": 1 };
    }
},
Button: {
    xml: "<button id='btn'>label</button>",
    map: { extend: {"from": "Widget"} },
    fun: function (sys, items, opts) {
        return { "a": 2, "c": 3 };
    }
}

该示例中,items.index 的结果由组件 Button 的函数项返回值与组件 Widget 的函数项返回值综合给出。其中 ac 的值来自组件 Button,b 的值继承自组件 Widget。

其它项的继承

除了上面所描述的情形外,其它项包括别名项、参数项、配置项以及映射项。这些项具有相同的继承方式。如果继承组件中存在替换指定项的配置,那么就使用继承组件的指定项。比如下面的组件 Button,它会使用自身的参数项。

// 07-09
Widget: {
    xml: "<button id='btn'>label</button>",
    opt: { border: "2px", background: "red" }
},
Button: {
    opt: { border: "1px", color: "blue" },
    map: { extend: {"from": "Widget", "opt": "r"} },
    fun: function (sys, items, opts) {
        console.log(opts); // { border: "1px", color: "blue" }
    }
}

如果继承组件中不存在指定项的替换配置,那么就继承组件的项有保留地覆盖被继承组件的相关项来得到最终项。就拿上面的组件 Button 来说,如果除去参数项的替换配置,那么最终采用的会是如下所示的参数项。

{ border: "1px", background: "red", color: "blue" }

此参数项是这样得到的,首先,由于组件 Button 中不存在 background 样式,所以组件 Widget 中的 background 样式得以保留。其次,组件 Widget 和组件 Button 都包含 border 样式。此时,组件 Button 的 border 样式就会覆盖组件 Widget 的 border 样式。下面的语句给出了这种情形的直观的但非正式的描述:

Button.opt = extend({}, Widget.opt, Button.opt);

检索

视图项中包含的是 XML 字符串。当组件实例化后,由视图项得到的对象集的逻辑结构与视图项的 XML 结构一致。从而组件对象集的检索可以通过 XML 结点集的检索来实现。系统检索函数通过检索相应的 XML 节点集,再由获取的节点集通过关联组件对象以得到所需要的对象集。常用的 XML 节点集的检索描述包括 XPath 表达式和 CSS 选择器,本系统采用的是前者。

《动态接口》中已经说过,组件的检索接口属于系统对象接口。为方便起见,这里重新列出这些接口如下。

  • sys:以文档节点为上下文查找对象,返回系统对象集
  • items:以文档节点为上下文查找对象,返回值对象集
  • find:以当前组件对象为上下文查找对象,返回系统对象集
  • get:根据给定的索引返回当前组件对象子级的某一系统对象
  • first:获取当前组件对象子级的第一个系统对象
  • last:获取当前组件对象子级的最后一个系统对象
  • next:获取当前组件对象的下一个系统对象
  • prev:获取当前组件对象的前一个系统对象
  • children:获取当前组件对象的所有儿子对象

按照检索能力来划分,上述接口可以分为通用检索接口和专用检索接口两类,前者仅包含上述的前两个接口,其余则为专用检索接口,下面分别讲述。

通用检索接口

通用检索接口实际上指的是函数项的前两个形参,它们均以函数的形式存在。下面是它们的接口形式。

sys(selector[,context])
items(selector[,context])

对于这两个接口,XPath 选择符是需要提供的第一个输入参数。另外还有一个可选的上下文参数。默认情况下,它以文档对象为上下文,也就是进行全局检索。下面是一个全局检索的一个示例。

// 08-01
Index: {
   xml: "<div id='index'>\
             <button>foo</button>\
             <button>bar</button>\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys("//*").length); // 3
       sys("//button").call("css", "color", "blue");
   }
}

该示例中,语句 sys("//*") 会检索出所有的组件对象,而语句 sys("//button") 则只会检索出所有的按钮对象。当然,由系统函数 sys 返回的对象都是系统对象。下面是使用 items 函数作检索的例子。

// 08-02
Index: {
   xml: "<div id='index'>\
             <Button>foo</Button>\
             <Button>bar</Button>\
         </div>",
   fun: function (sys, items, opts) {
       console.log(items("//*").length); // 3
       items("//Button").call("color", "blue");
   }
},
Button: {
    xml: "<button id='button'/>",
    fun: function (sys, items, opts) {
        function color(value) {
            sys.button.css("color", value);
        }
        return { color: color };
    }
}

语句 items("//*") 会检索出所有的组件对象,而语句 items("//Button") 则只会检索出所有的按钮对象。注意,由 items 函数检索出的对象都是值对象。

对于前面的示例中通用检索函数的使用,都采用默认的文档对象为上下文。现在来显示的带有上下文参数的通用检索函数的使用。

// 08-03
Index: {
   xml: "<div id='index'>\
             <div id='sub'>\
                 <button id='foo'>foo</button>\
             </div>\
             <button id='bar'>bar</button>\
         </div>",
   fun: function (sys, items, opts) {
       console.log(items("button", sys.index).length); // 1
       sys("button", sys.sub).call("css", "color", "blue");
   }
}

该示例中,由于检索函数均带有上下文,所以检索范围就被限制于上下文之内。语句 items("button", sys.index) 仅检索出按钮对象 bar,而语句 sys("button", sys.sub) 则仅检索出按钮对象 foo。需要注意的是,检索语句检索的范围并不包含上下文本身。

最后需要说明的是,通用检索接口检索出的结果只包含两类对象:HTML 元素对象和自定义组件对象。而像文本对象、注释对象等其它的基组件对象则不在检索的结果中。

专用检索接口

与通用检索接口不同的是,专用检索接口属于系统对象的接口,而通用检索接口则由函数项的形参引入。下面针对各个专用检索接口分别以示例说明它们的用法。

find

系统函数 find 以当前对象为上下文检索所需的对象集。下面是函数 find 的用法示例。

// 08-04
Index: {
   xml: "<div id='index'>\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>\
         </div>",
   fun: function (sys, items, opts) {
       var res = sys.index.find("button");
       res.call("css", "color", "blue");
   }
}

上面示例中,系统函数 find 的用法等价于语句 sys("button",sys.index)。与通用检索接口类似,函数 find 对于检索的结果只包含 HTML 元素对象和自定义组件对象,而忽略其他类型的对象。

get

系统函数 get 根据给定的索引返回当前组件对象子级的某一系统对象,若无则不返回任何对象。下面是该函数的用法示例。

// 08-05
Index: {
   xml: "<div id='index'>first\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>last\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys.index.get(0).text());  // foo
       console.log(sys.index.get(1).text());  // bar
   }
}

与系统函数 find 类似,该函数检索的结果只包含 HTML 元素对象和自定义组件对象,而忽略其他类型的对象。

first 和 last

系统函数 first 用于获取当前对象子级的第一个对象,系统函数 last 用于获取当前对象子级的最后一个对象。下面是函数 firstlast 的用法示例。

// 08-06
Index: {
   xml: "<div id='index'>first\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>last\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys.index.first().text());  // foo
       console.log(sys.index.last().text());   // bar
       console.log(sys.index.first(3).text()); // first
       console.log(sys.index.last(3).text());  // last
   }
}

从示例可以看出,函数 first 和函数 last 包含一个可选的参数 nodeType,其可能值如下。

var ELEMENT_NODE                = 1;
var TEXT_NODE                   = 3;
var CDATA_SECTION_NODE          = 4;
var COMMENT_NODE                = 8;

默认情况下 nodeType 的取值为 1,也就是函数返回的是 HTML 元素对象和自定义组件对象。如果想得到其他类型的对象,则需要明确给定 nodeType 值。

next 和 prev

系统函数 next 用于获取当前组件对象的后一个对象,系统函数 prev 用于获取当前组件对象的前一个对象。下面是它们的用法示例。

// 08-07
Index: {
   xml: "<div id='index'>first\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>last\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys.foo.next().text());               // bar
       console.log(sys.bar.prev().text());               // foo
       console.log(sys.index.prev(), sys.index.next());  // undefined undefined
       console.log(sys.foo.prev(3).text());              // first
       console.log(sys.bar.next(3).text());              // last
   }
}

从示例中可以看出,对于最顶层的节点,如果调用函数 next 或者函数 prev 获取到的会是空值。另外,这两个函数也包含一个可选的参数 nodeType,其用法与函数 firstlast 的类似。

children

系统函数 children 用于获取当前对象的所有儿子对象。在不提供参数的情况下,该函数返回的是 HTML 元素对象和自定义组件对象。下面是其用法示例。

// 08-08
Index: {
   xml: "<div id='index'>\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys.index.children().length); // 2
   }
}

上面的系统函数 children 获取到了所有的组件对象 button,其用法等价于语句 sys("*", sys.index)。函数 children 有一个可选的参数 nodeType,其用法与函数 firstlast 的用法类似。不同的是,此函数的 nodeType 参数可以取 0 值。当 nodeType 参数取 0 值时,此函数返回所有的儿子对象。请看下面的示例。

// 08-09
Index: {
   xml: "<div id='index'>\
             <button id='foo'>foo</button>\
             <button id='bar'>bar</button>\
         </div>",
   fun: function (sys, items, opts) {
       console.log(sys.index.children(0).length); // 5
   }
}

该示例打印出的结果是 5,这是由于当给 children 函数指定实参为 0 后,检索的结果把空白文本也包含在内了。

嵌套

概述

在组件的视图项中,组件具有树形的层级结构,也就是可以层层嵌套。在下面的示例中,button 元素作为 div 元素的子级而嵌套在内,文本作为 button 元素的子级而嵌套在内。

<div>
    <button>foo</button>
    <button>bar</button>
</div>

此处,div 元素称为嵌套父级,而 div 元素的子级,即所有的 button 元素,则叫做 div 元素的嵌套子级。在此示例中,嵌套父级是一个 HTML 元素。然而,嵌套父级还可以是自定义组件。请看下面的示例。

<Phone>
    <button>foo</button>
    <button>bar</button>
</Phone>

此示例中,嵌套父级是自定义组件 Phone,而两个 button 元素作为 Phone 的嵌套子级而存在。相对于以 HTML 元素作为嵌套父级,我们对于以自定义组件作为嵌套父级的情形还一无所知。故本章的主要目的是弄清楚包含这类嵌套组件的工作机制。

组件实例和 DOM 元素对象

为了弄清楚嵌套组件的呈现机制,需要先来了解下组件实例和 DOM 元素对象之间的关系。每个组件实例都唯一对应一个 DOM 元素对象。如下面的 Index 组件的视图项包含三个组件(忽略文本组件),每一个组件在实例化后都分别对应唯一的 DOM 元素对象。该 DOM 元素对象可由系统函数 elem 获取。

// 09-01
Index: {
    xml: "<div id='index'>\
              <Widget id='widget'/>\
              <input type='text'/>\
          </div>",
    fun: function (sys, items, opts) {
        console.log(items.widget == sys.widget.elem()); // true
    }
},
Widget: {
    xml: "<div id='widget'>hello,world</div>",
    fun: function (sys, items, opts) {
        return sys.widget.elem();
    }
}

虽然,每个组件实例都唯一对应一个 DOM 元素对象,但一个 DOM 元素对象却可以对应多个组件实例。也就是说,组件实例与 DOM 元素对象是多对一的关系。如上面的示例所示,可以看出,组件 Index 中的组件 Widget 和组件 Widget 中的 div 元素在实例化后都对应一个共同的 DOM 元素对象。

现在假定上面 Widget 组件是空的,也就是下面这样子。那么 Widget 组件实例是否对应一个 DOM 元素对象?如果答案是肯定的,那么该元素又是什么呢?

Widget: {}

实际上,当如上的空组件 Widget 实例化后,确实对应一个组件名为 void 的 DOM 元素对象。该元素对象由系统生成。在后续章节《事件与通信》中,可以看出该对象的存在意义。

自定义组件作为嵌套父级

对于以 HTML 元素作为嵌套父级的情形比较简单,系统会直接生成相应的 DOM 元素。所以这里着重介绍当以自定义组件作为嵌套父级时,组件对象是如何呈现的。请看下面的示例。

// 09-02
Index: {
    xml: "<Wrapper>\
             <button>foo</button>\
             <button>bar</button>\
         </Wrapper>"
},
Wrapper: {
    xml: "<div>\
              <h1 id='alice'>alice</h1>\
              <button>bob</button>\
          </div>"
}

该示例中,组件 Index 的视图项由三个组件组成,其中两个 button 元素嵌套在自定义的组件 Wrapper 中。组件 Wrapper 含有一个 div 元素。现在来看这种情形下生成的文档树(与实际的内容有所偏差,但大体一致)。

<div>
    <h1>alice</h1>\
    <button>bob</button>\
    <button>foo</button>\
    <button>bar</button>\
</div>

可以看出,组件 Wrapper 子级的内容被直接追加到了组件 Wrapper 的 div 元素中。故将子级内容直接追加到嵌套父级所对应的 DOM 元素是此类组件对象的基本呈现方式。

现在提出一个要求,能否将组件 Index 中的两个 button 元素添加到组件 Wrapper 的 h1 元素的子级。这当然可以实现,只需在组件 Wrapper 的映射项中指定 appendTo,其值为 alice 即可。下面是改进后的组件 Wrapper。

// 09-03
Wrapper: {
    xml: "<div>\
              <h1 id='alice'>alice</h1>\
              <button>bob</button>\
          </div>",
    map: { appendTo: "alice" }
}

映射项中的 appendTo 指出,当以当前组件作为嵌套父级时,嵌套子级的元素相应的 DOM 元素应该被追加到的位置。

嵌套子级对象的获取

下面的组件 Wrapper 由上面的更改而来,它添加了函数项,展示了如何获取并使用嵌套子级的元素。

// 09-04
Wrapper: {
    xml: "<div>\
              <h1 id='alice'>alice</h1>\
              <button>bob</button>\
          </div>",
    map: { appendTo: "alice" },
    fun: function (sys, items, opts) {
        this.children().forEach(function(item) {
            console.log(item.text());
        });
    }
}

自定义组件的函数项的 this 来源于组件实例化时系统的注入,它是当前组件实例化后的引用。所以可以通过系统函数 children 来获取并使用嵌套子级的元素。

应用

下面是一个窗体组件的简单示例,窗体一般包含标题栏和内容框两个部分。下面的窗体组件封装了标题栏和内容框部分内容,这样当用户使用该组件时,只需提供内容就可以了。

// 09-05
Index: {
    xml: "<Window id='index'>\
            <p>this is a test.</p>\
          </Window>"
},
Window: {
    css: "#window { width: 600px; height: 480px; border: 1px solid blue; }\
          #header { background: #AAA; height: 36px; }\
          #content { width: 90%; height: calc(100% - 60px); margin: 10px auto 0; border: 1px solid blue; }",
    xml: "<div id='window'>\
            <div id='header'/>\
            <div id='content'/>\
          </div>",
    map: { appendTo: "content" }
}

另一个使用嵌套特性的例子是路由组件。路由组件涉及组件的延迟实例化特性,所以这里并不打算介绍。如果需要了解详情,可以访问《延迟实例化》 的相关内容。

生命周期

组件对象的生命周期包含两方面的内容,一个是组件对象的创建,另一个是组件对象的移除。视图项中包含的组件集,随着父级组件的实例化而实例化,这属于组件对象的创建。除此以外,组件还可以在运行时动态地实例化,这也属于组件的创建。

任何已经实例化的组件对象都可以被移除或者替换。由于组件的替换可以分为新组件对象的创建与旧组件对象的移除,所以本质上组件对象的生命周期仅包含组件对象的创建与移除这两方面的内容。下面给出的是相关的系统对象接口,后面会逐一讲述。

  • append:给当前组件对象子级追加一个组件对象
  • before:在当前组件对象之前插入一个组件对象
  • replace:用新的组件对象替换掉当前组件对象
  • remove:移除掉当前组件对象

组件对象的追加

系统函数 append 用于在指定的组件对象子级追加一个组件对象, 它与《组件与空间》中的启动函数 startup 在许多方面是类似的。下面是动态追加组件的一个示例,它通过点击按钮来动态追加一个组件对象 Widget。

// 10-01
Index: {
    xml: "<div id='index'>\
              <button id='foo'>append</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.on("click", function (e) {
            sys.index.append("Widget");
        });
    }
},
Widget: {
    xml: "<button>hello, world</button>"
}

下面是组件对象另一种追加方式,它明确给出 xml 字符串。这与前一种方式是等价的。

sys.index.append("<Widget xmlns='.'/>");

还可以先解析出 xml 节点,然后追加解析后的节点,这与前两种方式等价。

var xml = "<Widget xmlns='.'/>";
var xmlNode = xmlplus.parseXML(xml).lastChild;
sys.index.append(xmlNode);

以上给系统函数 append 提供的都是自定义组件的描述。当然,提供基组件的描述也是可以的。下面的第一行会创建一个 span 元素对象,第二行会创建一个值为 hello,world 的文本对象。

sys.index.append("<span/>");
sys.index.append("hello,world");

上面的第一条语句创建的是一个 span 元素对象而不是一个文本对象。这是由于函数 append 在遇到输入为字符串的参数时,会首先调用函数 parseXML,将其当成 xml 字符串来解析。如果解析失败了,才把它当成文本来解析。所以,如果想要得到值为 <span/> 的文本对象,只能给函数 append 提供一个 xml 文本节点。

var textNode = document.createTextNode("<span/>");
sys.index.append(textNode);

系统函数 append 第二个参数是可选的,它为追加的组件对象提供初始输入值。如下面的示例所示,由于系统函数 append 的第二个参数提供了按钮的文本值,所以最终追加的按钮对象的文本不再是 hello,world,而是 I'm Button!

// 10-02
Index: {
    xml: "<div id='index'>\
              <button id='foo'>append</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.on("click", function (event) {
            sys.index.append("Widget", { label: "I'm Button!" });
        });
    }
},
Widget: {
    xml: "<button id='widget'>hello, world</button>",
    fun: function (sys, items, opts) {
        sys.widget.text(opts.label);
    }
}

组件对象的插入

通过系统函数 before ,可以在一个组件对象之前插入一个新的组件对象,请看下面的示例。

// 10-03
Index: {
    xml: "<div id='index'>\
              <button id='foo'>before</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.on("click", function (e) {
            sys.foo.before("Widget");
        });
    }
},
Widget: {
    xml: "<button>hello, world</button>"
}

该示例通过点击按钮来在按钮对象之前动态插入一个 Widget 组件。不过,不要试图在组件对象 index 之前插入组件对象,否则系统将抛出错误。因为组件对象 index 属于 xml 的顶层元素,系统函数 before 是无法在一个顶级元素对象之前插入组件对象的。

系统函数 before 最多只能包含两个参数,它的意义与系统函数 append 的前两个参数一致,只是插入的位置有别。

组件对象的替换

系统函数 replace 用于替换一个已经实例化的组件。该函数包含两个参数,第一个是欲替换的目标组件,第二个是目标组件的初始化输入值。它们与系统函数 append 的两个参数意义一致。

// 10-04
Index: {
    xml: "<div id='index'>\
             <button id='foo'>replace</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.once("click", function (e) {
            sys.foo.replace("<h1>Hello, world</h1>");
        });
    }
}

该实例侦听按钮的 click 事件。当点击按钮时,按钮本身会被替换成相应的 h1 元素对象。

组件对象的移除

对于已经实例化的组件,可以调用系统函数 remove 来将其移除。对于此函数的调用,你不用提供任何参数。

// 10-05
Index: {
    xml: "<div id='index'>\
             <button id='foo'>destory</button>\
             <h1 id='bar'>Hello, world</h1>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.once("click", sys.bar.remove);
    }
}

该示例的视图项包含了一个 button 元素对象 foo 和一个 h1 元素对象 bar。组件对象 foo 注册了 click 事件的侦听器。当点击按钮时,组件对象 bar 的系统函数 remove 得到执行。此系统函数 remove 用于移除组件对象 bar 本身。注意,这里按钮的点击事件仅被侦听一次,如果多次侦听将会抛出错误。因为当一个组件对象被移除后,该对象上的所有接口就失效了。

组件对象被移除后,原来绑定在该对象上的所有的事件侦听器和消息侦听器一并被清除。虽然一个组件对象被移除了,但系统还是尽可能地保留了组件对象的部分部件,这样下回创建新的同类型组件对象时就可以复用这些缓存的部分,使得创建新对象的开销降到最低。

另外,组件对象被移除之前会派发一个名为 willRemoved 的事件。派发此事件的主要目的是在组件对象移除之前清除函数项中可能出现的计时器,下面给出的示例演示了这一点:

// 10-06
Index: {
    xml: "<div id='index'>\
             <button id='foo'>destory</button>\
             <Widget id='bar'/>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.once("click", sys.bar.remove);
    }
},
Widget: {
    xml: "<h1>Hello, world</h1>",
    fun: function (sys, items, opts) {
        var timer = setInterval(function() {
            console.log("Hello, world");
        }, 1000);
        this.on("willRemoved", function() {
            clearInterval(timer);
        });
    }
}

组件 Widget 的函数项包含一个 timer 计时器,该计时器每一秒会执行一次信息打印。如果 Widget 组件对象未在被移除之前清除该计时器,那么该计时器将会继续工作。在许多时候,这种情况是我们不愿意看到的。

使用已动态实例化的组件

通过系统函数 append 或者 replace 的返回值,可以获取已动态实例化的组件对象的引用,从而可以访问其相关接口。下面是一个简单的例子,它通过引用系统函数 append 的返回值设置了 h1 元素对象的下划线。

// 10-07
Index: {
    xml: "<div id='index'>\
             <button id='foo'>append</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.on( "click", function (e) {
            var xml = "<h1>Hello, world</h1>";
            var result = sys.index.append(xml);
            result.css("text-decoration", "underline");
        });
    }
}

当然,如果相关的对象是命名过的,也可以通过名称直接访问。下面的示例中,我们通过目标对象的名称直接访问该对象,达到的效果与上面的一致。

// 10-08
Index: {
    xml: "<div id='index'>\
             <button id='foo'>append</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.on("click", function (e) {
            var xml = "<h1 id='text'>Hello, world</h1>";
            sys.index.append(xml);
            sys.text.css("text-decoration", "underline");
        });
    }
}

事件与通信

组件之间的通信,包含事件通信与消息通信两方面的内容。本章讲述的事件通信基于 W3C 制定的 DOM 事件标准,但略有不同,读者应注意区分。下面列出的是与事件通信相关的系统函数,后面的内容主要与这几个函数有关。

  • on:侦听事件
  • off:取消事件侦听
  • once:仅一次侦听事件
  • trigger:派发事件

事件的侦听

侦听事件,需要调用系统函数 on 来实现。下面的示例中,组件对象 index 侦听了 click 事件。当点击按钮时,控制台上会打印出相应的字符串。

// 11-01
Index: {
    xml: "<button id='index'>click</button>",
    fun: function (sys, items, opts) {
        sys.index.on("click", function (e) {
            console.log("hello, world");
        });
    }
}

组件对象可以侦听某一对象的事件,也可以侦听若干个对象的事件。其中,后一种事件侦听方式叫做事件委托。要使用事件委托,需要在系统函数 on 的第二个参数中指定一个 XPath 表达式。如下面示例所示,顶层组件对象 index 侦听了所有的 button 元素对象派发的 click 事件。

// 11-02
Index: {
    xml: "<div id='index'>\
             <button>button-A</button>\
             <button>button-B</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("click", "button", function (e) {
            console.log(this.localName(), this.text());
        });
    }
}

在该示例的侦听器中,this 指向的是被点击的按钮对象。另外,上面示例中的 XPath 选择器 * 选取的结果并不包含组件对象 index 本身,该组件对象只是检索操作的一个上下文。

当一个系统对象侦听一个事件时,在侦听器中可以获取派发事件的对象引用。如下面的示例所示,e.target、和 sys.button 同为派发事件的对象引用。

// 11-03
Index: {
    xml: "<div id='index'>\
             <button id='button'>click</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("click", function (e) {
            console.log(e.target == sys.button); // true
        });
    }
}

当然,当一个系统对象侦听一个事件时,在侦听器中也可以获取该系统对象的引用。如下面的示例所示,e.currentTargetthissys.index 同属于侦听事件的对象的引用。

// 11-04
Index: {
    xml: "<div id='index'>\
             <button>click</button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("click", function (e) {
            console.log(e.currentTarget == sys.index, sys.index == this); // true
        });
    }
}

事件侦听器的移除

系统函数 off 用于注销一个事件的侦听。此函数允许提供最多两个实参,分别是事件类型和侦听函数。如果不提供任何实参,则该函数执行后系统将注销相关联的所有侦听器。

在下面的示例中,由于在回调函数中注销了被侦听的事件,所以这个回调函数只能被执行一次。

// 11-05
Index: {
    xml: "<button id='index'>click</button>",
    fun: function (sys, items, opts) {
        sys.index.on("click", function (e) {
            sys.index.off("click");
            console.log("hello, world");
        });
    }
}

另外,可以使用系统函数 once 达到与上例同样的目的,这时无需在回调函数中显示地移除事件的侦听。该函数确保注册的侦听器仅被执行一次。下面的示例展示了这一点。

// 11-06
Index: {
    xml: "<button id='index'>click</button>",
    fun: function (sys, items, opts) {
        sys.index.once("click", function (e) {
            console.log("hello, world");
        });
    }
}

一个事件侦听器只能由侦听该事件的对象移除,利用其它对象来移除是无效的。下面的组件对象 Index 自身侦听了 click 事件,但回调函数中 sys.index 对象试图移除该事件侦听器,这是无效的。

// 11-07
Index: {
    xml: "<button id='index'>click</button>",
    fun: function (sys, items, opts) {
        this.on("click", function (e) {
            sys.index.off("click");
            console.log("hello, world");
        });
    }
}

事件的派发

除了默认产生的事件外,组件还可以派发自定义的事件,系统函数 trigger 就专门干这事的。下面的函数项中,组件对象 span 在结尾处派发了一个自定义事件,该事件最终被顶层组件对象 index 侦听到。

// 11-08
Index: {
    xml: "<div id='index'>\
             <span id='span'>trigger</span>\
          </div>",
    fun: function(sys, items, opts) {
        sys.index.on("event", function (e) {
            console.log("hello, world");
        });
        sys.span.trigger("event");
    }
}

系统函数 trigger 在派发事件时可以携带数据,事件侦听方可以在回调函数中获取到数据。下面的组件对象 span 派发的事件携带了两个数据,该数据分别由侦听方的回调函数的第二和第三个参数获得。

// 11-09
Index: {
    xml: "<div id='index'>\
             <span id='span'>trigger</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("event", function (e, a, b) {
            console.log(a, b); // 1 hello
        });
        sys.span.trigger("event", [1,"hello"]);
    }
}

事件的冒泡行为

默认情况下,许多事件是允许冒泡的。如下面的示例所示,按钮的父级 div 层可以侦听来自此按钮的点击事件。

// 11-10
Index: {
    xml: "<div id='index'>\
             <Button>click me</Button>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("click", function (e) {
            console.log("I'm in Index");
        });
    }
},
Button: {
    xml: "<button id='button'/>",
    fun: function (sys, items, opts) {
        sys.button.on("click", function(e) {
            console.log("I'm in Button");
        });
    }
}

可以通过调用事件的 stopPropagation 函数来阻止事件的冒泡行为。下面的组件 Button 是在上面的组件 Button 的基础上修改而来的,它阻止了事件的冒泡行为。

// 11-11
Button: {
    xml: "<button id='button'/>",
    fun: function (sys, items, opts) {
        sys.button.on("click", function (e) {
            e.stopPropagation();
            console.log("I'm in Button");
        });
    }
}

当派发自定义事件时,默认情况下是允许事件冒泡的。但当指定 trigger 函数的第三个参数为 false 值时,派发的事件就不是冒泡的。在下面的示例中,由于组件对象 span 在派发事件时指定不冒泡,所以组件对象 index 是无法侦听到来自组件对象 span 的 event 事件的。

// 11-12
Index: {
    xml: "<div id='index'>\
             <span id='span'>trigger</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("event", function (e, a, b) {
            console.log(a, b);
        });
        sys.span.trigger("event", [1,"hello"], false);
    }
}

阻止事件的默认行为

在浏览器端,默认情况下,当用户点击一个 url 链接时,浏览器会跳转到相应的页面。若要阻止这类的默认行为,可以通过调用事件的 preventDefault 函数来实现。

// 11-13
Index: {
    xml: "<a id='link' href='/'>click</a>",
    fun: function (sys, items, opts) {
        sys.link.on("click", function (e) {
            e.preventDefault();
        });
    }
}

事件通信的介质

在 W3C 制定的 DOM 事件标准中,事件在 DOM 元素之间传递。虽然表面上系统中的事件传递发生在组件对象之间,但在其本质上,事件的传递发生于 DOM 元素之间。在《嵌套》章节中讲过,每一组件实例都与一个 DOM 元素相对应,所以 DOM 元素成为事件通信的介质就再自然不过了。

在浏览器端,事件之间的传递依托于浏览器提供的 DOM 元素之间的通信能力。在服务端,事件之间的传递则由经过扩展的 xmldom 软件包提供。非空组件对象之间的事件传递不难理解。这里主要通过下面的示例来看看空组件对象之间是如何传递事件的。

// 11-14
Index: {
    xml: "<div id='index'>\
              <Widget id='widget'/>\
          </div>",
    fun: function (sys, items, opts) {
        sys.index.on("event", function (e) {
            console.log(e.target.elem());
        });
        sys.widget.trigger("event");
    }
},
Widget: {}

此示例包含一个空组件 Widget,在组件 Index 的函数项中,组件对象 widget 派发了一个 event 事件,该事件由其父级组件对象 index 捕获。由于组件对象默认 widget 对应一个 DOM 元素 void,并且组件对象 index 对应一个 DOM 元素 div ,所以由组件对象 widget 派发的事件就从 void 元素传递给 div 元素。这样就使得空组件也能传递事件,这可以说是系统为空组件生成 DOM 元素 void 的一个重要原因。

消息与通信

消息通信是除事件通信外的另一种组件对象之间的通信机制。下面列出的是与消息通信相关的系统函数,它与事件通信有许多相似之处。

消息的派发与捕获

派发一个消息,使用系统函数 notify。捕获一个消息,由系统函数 watch 来执行。对于下面的示例,在函数项执行完毕之前,组件对象 foo 派发了消息 msg,该消息由组件对象 bar 捕获。

// 12-01
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.bar.watch("msg", function (e) {
            console.log(this.text());
        });
        sys.foo.notify("msg");
    }
}

侦听一个消息时,在侦听器中可以获取派发消息的对象引用。如下面的示例,e.targetthissys.foo 同属于一个对象的引用。

// 12-02
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.bar.watch("msg", function (e) {
            console.log(e.target == this, this == sys.foo); // true true
        });
        sys.foo.notify("msg");
    }
}

侦听一个消息时,在侦听器中还可以获取侦听消息的对象引用,如下面的示例,e.currentTargetsys.bar 同属于一个对象的引用。

// 12-03
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.bar.watch("msg", function (e) {
            console.log(sys.bar == e.currentTarget); // true
        });
        sys.foo.notify("msg");
    }
}

系统函数 notify 在派发消息时可以携带数据,同时在消息的侦听器中可以获取到数据。下面示例中的组件对象 foo 派发的消息携带了两个数据:数值 37 和字符串 hello,world ,这两个数据依次由侦听器的第二和第三个形参获得。

// 12-04
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function(sys, items, opts) {
        sys.bar.watch("msg", function (e, a, b) {
            console.log(a, b); // 37 hello,world
        });
        sys.foo.notify("msg", [37, "hello,world"]);
    }
}

在示例中,派发消息时,数据是以数组格类型出现的。如果数据非数组类型并且仅传递一个数据对象,那么该数据可以无需要封装成数组对象而直接发送,正如下面的语句所示。

sys.foo.notify("msg", "hello,world");

指定消息侦听器的优先级别

默认情况下,对于侦听同一个消息的不同组件对象,先注册的侦听器比后注册的会优先得到回馈。如果想改变这种默认的次序,可以为系统函数 watch 指定第三个参数。请看下面的示例。

// 12-05
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
             <span id='alice'>alice</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.watch("msg", function (e) {
            console.log("foo");
        });
        sys.bar.watch("msg", function (e) {
            console.log("bar");
        }, 1);
        sys.alice.notify("msg");
    }
}

在该示例的函数项中,对于系统函数 sys.bar.watch,如果不指定第三个参数,那么控制台会先打印 foo,再打印 bar。而现在的结果却是相反的,这表明该优先级参数改变了侦听器的调用次序。此参数是一个数值,不指定则等同于 -Infinity。此参数值越大的,优先级越高,相应的侦听器也就越先被调用。

消息的注销

系统函数 unwatch 用于注销一个消息的侦听。此函数允许提供最多两个实参,分别是消息类型和侦听函数。如果不提供任何的实参,则该函数执行后将注销与相应对象相关联的所有侦听器。

在下面这个例子中,由于在 sys.foo.watch 的回调函数中注销了被侦听的消息,所以这个回调函数只能被调用一次。

// 12-06
Index: {
    xml: "<span id='index'>foo</span>",
    fun: function (sys, items, opts) {
        sys.index.watch("msg", function (e) {
            sys.index.unwatch("msg");
            console.log(this.text());
        });
        sys.index.notify("msg").notify("msg");
    }
}

另外,可以使用系统函数 glance 达到与上例同样的目的,这时无需在回调函数中显示地移除侦听器。该函数确保注册的侦听器仅被调用一次,下面的示例展示了这一点。

// 12-07
Index: {
    xml: "<span id='foo'>foo</span>",
    fun: function (sys, items, opts) {
        sys.foo.glance("msg", function (e) {
            console.log(this.text());
        });
        sys.foo.notify("msg").notify("msg");
    }
}

一个消息的注销只能由侦听该消息的对象来执行,利用其它对象来注销是无效的。在下面示例的事件的侦听方的回调函数中,组件对象 sys.bar 试图注销由组件对象 sys.foo 注册的消息,这是无效的。

// 12-08
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.watch("msg", function (e) {
            sys.bar.unwatch("msg");
            console.log(this.text());
        });
        sys.foo.notify("msg").notify("msg");
    }
}

消息的作用域

默认情况下,消息具有全局作用域,某对象派发一个消息,任何对象都可以对其进行捕获。当在组件的映射项中指定 msgscope 参数来限制消息的作用域时,由本组件及其子级派发的消息只能由本组件及其子级捕获。在下面的示例中,组件 Foo 的消息作用域被限制为本组件及其子级,所以在所有侦听消息 msg 的对象中,只有组件对象 foo 才能捕获该消息,而组件对象 bar 则无法捕获该消息。

// 12-09
Index: {
    xml: "<div id='index'>\
             <Foo id='foo'/>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.watch("msg", function (e) {
            console.log("I can receive message.");
        });
        sys.bar.watch("msg", function(e) {
            console.log("I can't receive message.");
        });
        sys.foo.notify("msg");
    }
},
Foo: {
    xml: "<span id='foo'>foo</span>",
    map: { msgscope: true },
    fun: function (sys, items, opts) {
        sys.foo.watch("msg", function (e) {
            console.log("I can receive message too.");
        });
    }
}

另外,当一个消息作用域建立后,其它区域的对象无论派发什么消息,本作用域都无法接收。本作用域只能接收来自本作用域的消息。下面示例中,由于 Foo 组件建立了消息作用域,尽管 Index 中的组件对象 bar 派发了消息 msg ,组件对象 foo 也无法捕获该消息。

// 12-10
Index: {
    xml: "<div id='index'>\
             <Foo id='foo'/>\
             <span id='bar'>bar</span>\
          </div>",
    fun: function (sys, items, opts) {
        sys.foo.watch("msg", function (e) {
            console.log("I can't receive message.");
        });
        sys.bar.notify("msg");
    }
},
Foo: {
    xml: "<span id='foo'>foo</span>",
    map: { msgscope: true }
}

消息通信与事件通信的异同

事件通信基于 W3C 制定的 DOM 事件标准,组件对象之间的事件通信实际上是浏览器上 DOM 元素之间的事件通信。而消息通信则是纯粹组件对象之间的通信,与 DOM 元素无关。

消息有作用域的概念,组件之间消息的传递发生的作用域之内。而事件则是以由下而上的冒泡方式来传递的,当然也可以控制事件是否冒泡。

事件是无法在除了由下而上的组件之间传递信息的,而消息则不然,这是事件机制的一个局限性,当然也算是优点。

如果在浏览器端,在所有的事件集中,包含默认的浏览器事件,比如按钮的点击事件,鼠标的移动事件等等。而对于所有的消息,都是由组件自定义的,并不存在默认的消息集。

共享

在有些应用中,要求只能实例化出一个或者若干个特定的组件对象,这会涉及到组件实例的共享问题。在 xmlplus 中,组件的共享与通常的单例概念有相似之处,但比单例有着更为丰富的内容,应注意对比区分。

共享一个组件实例

下面的组件 Index 包含两个 Audio 组件,当组件 Index 实例化时,会同时实例化出两个 Audio 组件对象,尽管它们实现了相同的功能。

// 13-01
Index: {
    xml: "<div id='index'>\
             <Audio id='foo'/>\
             <Audio id='bar'/>\
          </div>",
    fun: function (sys, items, opts) {
         console.log(sys.foo == sys.bar);     // false
         console.log(items.foo == items.bar); // false
    }
},
Audio: {
    xml: "<audio autoplay='autoplay'/>",
    fun: function (sys, items, opts) {
        return { desc: "audio desc" };
    }
}

若要求组件 Audio 只被实例化一次,则需要在映射项中指定 share 选项,以指明组件 Audio 在组件 Index 及其子级中是作为共享组件存在的。该选项的值是一个以零个或多个空格分隔的字符串,每一分隔值是一组件路径,用于指出哪些组件需要被共享。下面的示例中,组件对象 foo 是组件 Audio 实例化的产物,叫做共享组件 Audio 的原实例,而组件对象 bar 则是该实例的一个映像。

// 13-02
Index: {
    xml: "<div id='index'>\
             <Audio id='foo'/>\
             <Audio id='bar'/>\
          </div>",
    map: { share: "Audio" },
    fun: function (sys, items, opts) {
         console.log(sys.foo == sys.bar);     // false
         console.log(items.foo == items.bar); // true
    }
}

从上面的打印出的内容可以看出系统对象 sys.foosys.bar 是不等的,但值对象 items.fooitems.bar 则同属于一个对象的引用。实际上,对于实例的映像(如上面的组件对象 bar),它是傀儡组件的一个实例,其组件名是 void,它被归类于 HTML 元素组件。傀儡组件对象包含独立的系统接口,但从原实例引用了值对象。也就是说,组件共享只共享值对象,而不共享系统对象,这点一定要注意。

共享组件的作用域

组件的共享仅在一定范围内有效。在下面的示例中,Index 组件以一个组件 Audio 和另一个组件 Music 作为其子级。其中,组件 Music 包含一个 Audio 声明,所以这里总共涉及到两个 Audio 组件声明。但在 Index 中,已经声明组件 Audio 是共享的,所以实际上当组件 Index 实例化时,只实例化了一个 Audio 组件。

// 13-03
Index: {
    xml: "<div id='index'>\
             <Audio id='foo'/>\
             <Music id='bar'/>\
          </div>",
    map: { share: "Audio" }
},
Music: {
    xml: "<Audio id='music'/>",
    fun: function (sys, items, opts) {
        return items.music;
    }
}

如果在组件 Music 中也加上组件 Audio 的共享声明,结果会是如何呢?这时组件 Music 中的组件对象 Audio 将不会成为组件 Index 中组件对象 foo 的映像,系统将会自行实例化组件 Audio。也就是说,共享组件声明的作用域是可以被子级覆盖的。

// 13-04
Music: {
    xml: "<Audio id='music'/>"
    map: { share: "Audio" }
}

共享组件的动态添加

在动态添加一个组件时,添加的组件有可能是共享组件。共享组件对象分为原型和映像,所以添加的情形也分为两种。现在以下面的示例来作说明,其中的 Audio 组件与上面的一致,此处略去。

// 13-05
Index: {
    xml: "<div id='index'/>",
    map: { share: "Audio" },
    fun: function (sys, items, opts) {
         var foo = sys.index.append("Audio");
         var bar = sys.index.append("Audio");
         console.log(foo == bar);                 // false
         console.log(foo.value() == bar.value()); // true
    }
}

此示例的组件 Index 声明了组件 Audio 为共享组件,并且在函数项中动态实例化了两个 Audio 组件。从输出结果可以看出,通过动态实例化出来的组件对象,其中第一次得到的对象是原型,第二次得到的对象是映像。

共享组件的移除

共享组件的移除涉及组件原实例的移除和映像的移除。在下面示例中,函数项调用了组件对象 foo 的移除函数 remove,这时连同组件对象 bar 也会从上下文消失。这是因为,对于共享组件而言,当原实例被移除时,其映像也就没有存在的必要了。

// 13-06
Index: {
    xml: "<div id='index'>\
             <Audio id='foo'/>\
             <Audio id='bar'/>\
          </div>",
    map: { share: "Audio" },
    fun: function (sys, items, opts) {
         sys.foo.remove();
         console.log(sys.foo, sys.bar); // undefined undefined
    }
}

若只移除共享组件的映像,那么原实例和其他映像还是存在的。在下面示例中,函数项调用了组件对象 bar 的移除函数 remove,它只会移除组件对象 bar,而不会影响到原实例 foo 和另一个映像 alice。

// 13-07
Index: {
    xml: "<div id='index'>\
             <Audio id='foo'/>\
             <Audio id='bar'/>\
             <Audio id='alice'/>\
          </div>",
    map: { share: "Audio" },
    fun: function (sys, items, opts) {
         sys.bar.remove();
         console.log(sys.foo, sys.bar, sys.alice); // Object undefined Object
    }
}

应用

如果你在写 Node.js 应用,那么你可能会涉及到数据库。下面仅以文本型数据库 Sqlite 的使用为例,来看看组件共享特性的应用。

封装

要方便使用 Sqlite,首先需要对相关的驱动代码进行抽象,下面是一个实用的组件封装。

Sqlite: {
    fun: function (sys, items, opts) {
        var sqlite = require("sqlite3").verbose(),
        return new sqlite.Database("data.db");
    }
}

对于较为复杂的应用,该组件会多次得到使用。如果不使用共享技术,那么该组件就会经过多次实例化,这显然是一种资源浪费。

共享

对于这种整个应用只需要一个实例的组件,将共享声明放在入口组件处是最好了。下面给出是一个简单的示范。

Index: {
    xml: "<HTTP id='index'>\
            <Signin id='signin'/>\
            <Logout id='logout'/>\
          </HTTP>
    map: { share: "/db/Sqlite" }
}

在此示例中,假定组件 Signin 和 Logout 中都使用了组件 Sqlite,那么组件 Sqlite 仅在组件 Signin 中实例化一次。在组件 Logout 中将会使用共享的实例。

延迟实例化

延迟实例化是 xmlplus 中最有用的特性之一,尤其在单页应用中,使用它能明显地提升应用的用户体验性。

基本概念

当一个组件对象不需要立即使用时,就可以选择延迟实例化。映射项中的 defer 选项用于指明需要延迟实例化的组件对象。该选项的值是一个以零个或多个空格分隔的字符串,每一分隔值是一组件对象名。下面的示例中,组件对象 foo 和组件对象 bar 都是需要延迟实例化的组件。

// 14-01
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <span id='bar'>bar</span>\
          </div>",
    map: { defer: "foo bar" }
}

对于被指定为延迟实例化的组件对象,如果需要实例化它,可以调用该对象的系统函数 show ,该函数和非延迟实例化组件对象的函数 show 是不同的,前者用于实例化组件,而后者用于显示组件对象的内容。在下面示例中,当用户点击按钮 bar 时,组件对象 foo 才会被实例化。

// 14-02
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <button id='bar'>bar</button>\
          </div>",
    map: { defer: "foo" },
    fun: function (sys, items, opts) {
        sys.bar.once("click", sys.foo.show);
    }
}

请注意上面函数项中,对于 click 事件的侦听使用的是函数 once,而不是 on。因为若使用后者,首次点击按钮时,会成功完成组件对象的实例化,但若再次点击,系统就会抛出错误。这是由于仅当组件对象 foo 未实例化时,用于延迟实例化的系统函数 sys.foo.show 才是可用的。

原理

对于被声明为需要延迟实例化的组件对象,当系统解析到该组件时,会实例化出一个傀儡组件对象以替代原组件对象。傀儡组件的组件名是 void,它被归类于 HTML 元素组件。该对象包含的系统函数 show 用于实例化原组件。傀儡组件对象与原组件对象有相同的 id 值,所以在函数项中可以对其直接访问。下面给出的是组件对象 foo 未实例化之前对应的 HTML 代码片断,可以清楚地看到傀儡组件标签 void。注意,这里略去了部分的属性值。

<div>
     <void></void>
     <button>bar</button>
</div>

而下面给出的是组件对象 foo 实例化后的 HTML 代码片断。可以看出此时 void 元素标签不见了,替代它是 span 元素标签。这里同样略去了部分的属性值。

<div>
     <span>foo</span>
     <button>bar</button>
</div>

傀儡组件与其它一些较复杂的组件相比,除了系统函数 show 的功能差异外,该有的系统函数它都有,且功能也一致。只是它是一个空的组件,缺少样式项、函数项等部件。所以它的执行效率高,速度快。用来替代较复杂的,需要延迟实例化的组件对象再适合不过了。

可以延迟实例化的组件对象分为两类,一类是匿名空间中的 HTML 元素组件对象,另一类是自定义组件对象。其余的包括共享组件对象、文本对象、注释对象以及 CDATASection 对象都是不能延迟实例化的。不过请注意,由于 void 元素被归类为 HTML 元素,所以 void 元素对象也是可以延迟实例化的,尽管没多大意义。

应用

应用延迟实例化特性的一个典型例子是路由组件 ViewStack。该组件允许包含多个不同视图页面作为子级,其中只有一个页面是可见的。该组件在初始化时,最多只实例化一个可见页面,而其余的页面则在需要时才实例化。该组件可以在单页面应用中作为路由组件使用。下面先来看看 ViewStack 组件的一个应用示例。

// 14-03
Index: {
    xml: "<ViewStack id='index'>\
             <button id='foo'>to bar</button>\
             <button id='bar'>to foo</button>\
          </ViewStack>",
    map: { defer: "bar" },
    fun: function (sys, items, opts) {
        sys.index.on("click", "button", function (e) {
            this.trigger("switch", this.text().substr(3));
        });
    }
}

该示例由一个 ViewStack 组件和两个 button 组件组成,button 组件是 ViewStack 组件的子级。该示例允许用户通过点击按钮,在两个页面之间跳转。其中第二个页面被设定为需要延迟实例化,只有当切换到该页面时才进行实例化。下面给出 ViewStack 组件的实现。

// 14-03
ViewStack: { 
    xml: "<div id='viewstack'/>",
    fun: function (sys, items, opts) {
        var args, children = this.children(),
            table = children.call("hide").hash(),
            ptr = table[opts.index] || children[0];
        if (ptr) ptr = ptr.trigger("show").show();
        this.on("switch", function (e, to) {
            table = this.children().hash();
            if ( !table[to] || table[to] == ptr ) return;
            e.stopPropagation();
            args = [].slice.call(arguments).slice(2);
            if(ptr) ptr.trigger("hide", [to+''].concat(args)).hide();
            ptr = table[to].trigger("show", [ptr+''].concat(args)).show();
        });
        return Object.defineProperty({}, "selected", { get: function() {return ptr;}});
    }
}

该实现巧妙地利用系统函数 show,如果子级组件对象被设定为延迟实例化,那么只有当切换到此页面时该组件对象才实例化。对于已经实例化的组件对象,系统函数 show 只是起到显示该组件的作用。

主题

定义

主题是样式项中的一种字符串替换机制,它与宏的概念类似。下面结合示例,来看看如何定义一个主题。

// 15-01
xmlplus("xp", function (xp, $_, t) {
    t("gray").imports({
        "color": "gray"
    });
});

示例中,定义了一个名为 gray 的主题。该主题是一个普通的 JSON 对象,包含一个键值对,其中键名为 color,值为 gray,这里的键名也叫宏名。

默认情况下,系统中存在一个名为 default 的主题,这是一个空的普通的 JSON 对象。当然,你可以覆盖该默认主题。

// 15-02
xmlplus("xp", function (xp, $_, t) {
    t("default").imports({
        "color": "black"
    });
    t("gray").imports({
        "color": "gray"
    });
});

如上的示例中,默认主题 default 被新定义的主题覆盖,同时还定义了一个名为 gray 的新主题。

主题内容的引用

主题内容由样式项引用。样式项通过 % + 宏名 引用当前主题中的内容。请看下面的示例。

// 15-03
xmlplus("xp", function (xp, $_, t) {
    $_().imports({
        Index: {
            css: "#index { color: %color; }",
            xml: "<h1 id='index'>hello, world</h1>"
        }
    });
    t("default").imports({
        "color": "blue"
    });
});

该示例中,由于未定义其他类型的主题,从而当前使用的是名为 default 的默认主题。样式项中的字符组 %color 最终会被替换成值为 blue 的字符串。

主题的切换

如果应用定义了多个主题,那么就有在多个主题中切换的需求。请看下面的示例。

// 15-04
var app;
xmlplus("xp", function (xp, $_, t) {
    t("default").imports({
        "color": "blue"
    });
    t("green").imports({
        "color": "green"
    });
    $_().imports({
        Index: {
            css: "#index { color: %color; }",
            xml: "<div id='index'>\
                    <h1>hello, world</h1>\
                    <button id='change'>change</button>\
                  </div>",
            fun: function (sys, items, opts) {
                sys.change.on("click", function (e) {
                    app.theme("green");
                });
            }
        }
    });
}).ready(function() {
    app = xp.startup("//xp/Index");
    console.log(app.theme());
});

有一个名为 theme 的接口函数,该函数用于返回当前主题或切换当前主题为其它主题。由于主题与具体的应用密切相关,所以你仅能在全局函数 startup 的返回值中访问该函数。示例中,不但重新定义了默认主题,还定义了一个名为 green 的新主题。当用户点击按钮时,当前主题会由默认主题切换至为新定义的主题。

使用扩展的主题接口

由于主题仅存在于应用,所以上面的主题切换函数仅由应用返回,这样会在使用上带来诸多的不便。下面给出一个扩展,让该函数附加到系统对象接口上。

// 15-05
xmlplus.expand({
    theme: function (value) {
        return this.env.smr.theme(value);
    }
});

下面使用扩展的主题接口重新修改上一节的示例如下。该示例直接上面新定义的扩展函数来切换主题。

// 15-05
xmlplus("xp", function (xp, $_, t) {
    t("default").imports({
        "color": "blue"
    });
    t("green").imports({
        "color": "green"
    });
    $_().imports({
        Index: {
            css: "#index { color: %color; }",
            xml: "<div id='index'>\
                    <h1>hello, world</h1>\
                    <button id='change'>change</button>\
                  </div>",
            fun: function (sys, items, opts) {
                console.log(this.theme());
                sys.change.on("click", function (e) {
                    this.theme("green");
                });
            }
        }
    });
});

在系统中定义扩展接口,可以更为便捷地使用主题切换函数,但它会轻微地加大系统的资源开销。如果不能带来明显的好处,建议不要随意扩展系统接口函数。

优化

一个好的应用,既应该对维护者友好,还应该对使用者友好。这一章主要谈谈如何通过一些措施优化应用的性能,这属于对使用者友好的范畴。

减少组件对象的命名

对于未命名的组件对象,默认不会对其生成系统对象和值对象。对于某些对象,如果仅在样式项中使用,那么可以通过一些小技巧以避免对相关的组件对象命名。请看下面的示例。

// 16-01
Index: {
    css: "#index button { color: blue; }",
    xml: "<div id='index'>\
            <button>hell</button>\
            <button>world</button>\
          </div>"
}

在这个示例中,包含两个 button 元素对象,且仅在样式项中设定它们的颜色样式。这种情况下,就没有必要给这两个 button 元素对象命名了。

使用文档碎片

在浏览器端,系统默认开启文档碎片功能,也就是全局函数 startup 执行时,会使用如下的代码先创一个文档碎片对象。

document.createDocumentFragment()

之后所有的新建 HTML 元素都会被添加到该对象上。等这些工作都完成后,文档碎片对象才被追加到目标 HTML 元素上。

使用文档碎片特性可以明显地提升应用的性能,因为只需一次屏幕的刷新,就可以完成页面的显示。然而,必要时可以针对某些组件来禁用该功能,请看下面的示例。

// 16-02
Index: {
    xml: "<h1 id='index'>hello,world</h1>",
    fun: function (sys, items, opts) {
        console.log(sys.index.width());
    }
}

该示例中,函数 sys.index.width 的返回值为 0。这是由于该语句执行时,组件对象相应的 HTML DOM 元素还未被加入根为 document 的文档树。如果希望函数 width 返回实际的值,可以在映射项中加入值为 truenofragment 的配置以禁用文档碎片功能。请看下面的示例。

// 16-03
Index: {
    xml: "<h1 id='index'>hello,world</h1>",
    map: { nofragment: true },
    fun: function (sys, items, opts) {
        console.log(sys.index.width());
    }
}

应用延迟实例化特性

如果你的应用足够复杂,不妨考虑将部分组件对象延迟实例化。这在大型应用中,它能明显地提升应用的用户体验。

// 16-04
Index: {
    xml: "<div id='index'>\
             <span id='foo'>foo</span>\
             <button id='bar'>bar</button>\
          </div>",
    map: { defer: "foo" },
    fun: function (sys, items, opts) {
        sys.bar.once("click", sys.foo.show);
    }
}

上面的示例来自 延迟实例化,该示例中,组件对象 foo 被设计成延迟实例化的。当然这个示例较为简单,你可以尝试把组件对象 foo 替换成复杂的,需要初始化较长时长的组件对象以观察延迟实例化特性的功效。

更近一步,你可以利用 require.js 等工具动态获取组件包并导入系统,然后实例化相关的组件对象。此方案适用于体积较大的应用,它能按需分块加载组件集,使得应用的初始化快速进行。

复用已创建的组件对象

下面通过一个简单的示例来说明如何通过复用已创建的组件对象来提升应用的性能。下面给出的是两个组件,其中组件 Item 是 HTML 元素 li 的简单封装。列表组件 List 接收一个数组作为数组源并创建列表子项。

// 16-05
List: {
    xml: "<ul id='list'/>",
    fun: function (sys, items, opts) {
        function setValue(array) {
            var list = sys.list.children();
            for ( var i = 0; i < array.length; i++ )
                (list[i] || sys.list.append("Item")).show().text(array[i]);
            for ( var k = i; k < list.length; k++ )
                list[k].hide();
        }
        return Object.defineProperty({}, "value", { set: setValue });
    }
},
Item: {
    xml: "<li id='item'/>"
}

注意,函数 setValue 中的两个 for 语句。其中第一个 for 语句会尝试复用已创建的组件对象,只有当未存在已创建对象时才新建一个。第二个 for 语句则隐藏剩余未利用的组件对象,而不是将其移除。下面是一个应用示例。

// 16-05
Index: {
    xml: "<List id='list'/>",
    fun: function (sys, items, opts) {
        items.list.value = ["hello","world"];
        items.list.value = ["1","2","3","4"];
    }
}

上述应用示例的函数项中,第二个语句将利用第一个语句创建的两个 Item 组件对象,所以该语句只创建两个新的组件对象,而不是四个。