采用 Windows JavaScript 库 (WinJS) 构建自定义控件

如果您曾使用 JavaScript 开发过 Windows 应用商店应用,那么您很可能已经遇到过 Windows JavaScript 库 (WinJS)。该库为您提供了一系列 CSS 样式、JavaScript 控件和实用工具,以帮助您迅速构建符合 Windows 应用商店 UX 基准要求的应用。WinJS 所提供的实用工具包含一系列功能,您可使用这些功能来在您的应用中创建自定义的控件。

您可使用任何您所喜欢的模式或库来编写 JavaScript 控件,WinJS 中所提供的库功能仅是其中一个选项。使用 WinJS 构建控件的一个主要优势在于其可让您创建您自己的控件,且该控件能与库中的其他控件一致地运行。开发与使用您的控件的模式与 WinJS.UI 命名空间中的任何控件完全一样。

在本篇博文中,我将向您介绍如何构建您自己的控件,并在其中包含对可配置选项、事件和公共方法的支持。对于那些对 XAML 控件开发同样感兴趣的读者,敬请期待即将发布的有关该内容的博文。

在 HTML 页面中包含基于 JavaScript 的控件

首先,让我们回顾一下您在页面中包含 WinJS 控件的方式。进行该操作有两种不同的方法:命令性操作(以非介入的方式单独使用 JavaScript)或声明性操作(在 HTML 元素上使用额外的属性来在 HTML 页面中包含控件)。后者可让工具提供设计时体验,例如从工具箱中拖动控件。详情请查看添加 WinJS 控件和样式的 MSDN 快速入门一文。

在本文中,我将向您展示如何利用 WinJS 中的声明性处理模型来生成 JavaScript 控件。要在您的页面中采用声明性的方式包含一个控件,请完成下面的一系列步骤:

  1. 在您的 HTML 页面中包含 WinJS 引用,这是因为您的控件将从这些文件中使用 API。

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. 在上述脚本标记引用之后,包含带有您的控件的脚本文件引用。

     <script src="js/hello-world-control.js"></script>
    
  3. 在您的应用的 JavaScript 代码中调用 WinJS.UI.processAll();该函数将分析您的 HTML 并实例化其查找到的任何声明性控件。如果您在 Visual Studio 中使用应用模板,那么 WinJS.UI.processAll() 将在 default.js 中为您调用。

  4. 在您的页面中以声明性的方式包含控件。

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

简单的 JavaScript 控件

现在,我们将构建一个非常简单的控件:“Hello World”的控件。以下是定义该控件所使用的 JavaScript。使用以下代码在名为 hello-world-control.js 的项目中创建一个新文件:

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

随后,在您的页面的主体中使用以下标记来包含控件:

 <div data-win-control="HelloWorld"></div>

当您运行应用时,您将看到该控件已经加载,而且其将在页面主体部分显示文本“Hello, World!”。

本代码中唯一的 WinJS 特定的部分在于对 WinJS.Utilities.markSupportedForProcessing 的调用,其将代码标记为与声明性处理的使用相兼容。这是您告知 WinJS 您信任此代码来将内容注入您的页面的方式。有关该内容的详细信息,请参阅有关 WinJS.Utilities.markSupportedForProcessing 函数的 MSDN 文档。

为什么要使用 WinJS 实用工具或任意库来创建控件?

我刚刚向您展示了您可以如何在不真正使用 WinJS 的前提下创建声明性控件。现在,我们将看看以下代码段,该代码段仍未使用 WinJS 来进行其大部分的实现。这是一个较为复杂的控件,其中包含事件、可配置选项和公共方法:

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

许多开发人员采用这种方式(即使用匿名函数、构造函数、属性和自定义事件)来构建控件,如果您的团队也认为这是一个不错的方式,那么也请采用该方式!然而许多开发人员可能有些难于理解这一代码。许多 Web 开发人员并不熟悉其所采用的技术。而各种库却恰好能在这种情况下帮上您大忙,它们能够消除您对编写此代码的困惑。

除了改进可读性以外,WinJS 和其他库能够处理许多细微的问题,因而(您可以高效使用原型、属性和自定义事件),而不必思考这些问题。这优化了内存的使用并有助于您避免常见错误。WinJS 就是其中一个例子,但是最终还是将由您进行选择。如果您想了解一个有关库将如何有助于您的具体例子,我建议您在阅读完本文后再次查看本部分的代码,并在阅读完本文后采用 WinJS 实用工具将此前的实现与采用了相同控件的实现进行比较。

WinJS 中 JavaScript 控件的基本模式

以下是利用 WinJS 创建 JavaScript 控件的最低要求的最佳做法模式。

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

在此页面中您将采用声明性的方式包含控件:

 <div data-win-control="Contoso.UI.HelloWorld"></div>

您也许对其中一些部分并不熟悉,特别是如果您还是第一次接触 WinJS,因此让我们一同来看看其中的情况。

  1. 我们利用 JavaScript 中一个常见模式来在本示例中包装该代码,该模式也就是我们熟知的立即执行函数

     (function () {
    …
    })();
    

    进行该操作的目的在于确保我们的函数是自包含的,且不会留下任何意外的变量/全局分配。值得注意是,这是一个通用的最佳做法,同时也是我们对原始源所希望进行的一项变更。

  2. ECMAScript 5 严格模式将在我们函数启动之时通过采用 “使用严格” 的声明而得以启用。我们在整个 Windows 应用商店应用模板中都进行了这一操作,并将其作为最佳做法,以此来改善错误检查以及与 JavaScript 未来版本的兼容性。再次提醒您,这是一个通用的最佳做法,同时也是我们对原始源所希望进行的操作。

  3. 现在,让我们来看看 WinJS 特定的一些代码。系统将调用 WinJS.Class.define() 以为控件创建一个类,除了其他操作以外,该控件将为我们调用 markSupportedForProcessing(),并降低今后在控件中创建属性的难度。这确实能为标准 Object.defineProperties 函数提供不少帮助。

  4. 名为 Control_ctor 的构造函数得以定义。当 WinJS.UI.processAll() 从 default.js 中得以调用时,其将为采用 data-win-control 属性而引用的任何控件扫描页面中的标记,查找我们的控件,并调用此构造函数。

  5. 在此构造函数内,页面中元素的引用将得以与控件对象一同保存,而控件对象的引用将得以与元素一同保存。

    • 如果您还想了解 element || document.createElement("div") 部分的内容,那么让我来告诉您这过去曾用于支持命令性模型。这可让用户在此后将某一控件附加到页面中某一元素内。
    • 采用该方式将引用保留到页面中的元素,并通过设置 element.winControl 将元素中的引用保留到控件对象将是个不错的主意。当像事件一样添加功能时,这可让一些库函数恰到好处地运行。不用担心循环对象/DOM 元素引用导致的内存泄露,Internet Explorer 10 将帮您处理这一问题。
    • 构造函数将修改控件的文本内容,以将设置“Hello, World!”文本显示于屏幕中。
  6. 最后,WinJS.Namespace.define() 将用于发布控件类,并揭示应用中的任何代码均可公开访问控件。如果没有 WinJS.Namespace.define(),我们将需要采取另一解决方案来为我们正在使用的内联函数以外的代码而使用全局命名空间,进而显示我们的控件。

定义控件选项

为了让我们的示例变得更为有趣,让我们为控件添加对可配置选项的支持。在该情形中,我们将添加一个选项让用户闪烁内容。

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

这次,当在您的页面中包含控件时,可以采用 data-win-options 属性配置闪烁选项:

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

为了添加选项支持,我们对代码进行如下变更:

  1. 选项通过构造函数上(名为选项)的参数传递至控件。
  2. 默认设置采用类中的私有属性进行配置。
  3. WinJS.UI.setOptions() 得以调用,并在您的控件对象中传递。该调用将覆盖控件中可配置选项的默认值。
  4. (名为 blink 的)公共属性添加至新选项。
  5. 我们通过闪烁屏幕中的文本而添加了此功能(在实际操作中,您最好不要在此处对样式进行硬编码,而是切换一个 CSS 类)!

本示例中最重要的环节在于对 WinJS.UI.setOptions() 的调用。实用工具函数 setOptions 将在选项对象中的每个字段间循环,并将其值分配到目标对象中相同名称的字段内,而这也是 setOptions 的首个参数。

在我们的示例中,我们通过 data-win-options 参数为我们的 win-control 配置了选项对象,为“blink”字段传递了“true”值。对我们构造函数中 setOptions() 的调用随后将查看名为“blink”的字段,并将其值复制到我们控件对象中相同名称的字段内。我们已经定义了名为“blink”的属性,而且其提供了一个 setter 函数;我们资源库函数是 setOptions() 所调用的函数,而且这将设置我们控件中的 _blink 成员。

为事件添加支持

现在我们已经实现了用处如此之大的闪烁选项,接下来让我们添加事件支持,因而我们可在闪烁的时候做出响应:

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

像以前一样,在页面中包含控件。请注意我们已经为元素添加了一个 ID,因此我们可在稍后检索该元素:

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

通过准备好这些变更,现在我们可附加一个事件侦听器以侦听“闪烁”事件(请注意:本示例中,我已为 document.getElementById 命名别名为 $):

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

当运行该代码时,您每 500 毫秒将在 Visual Studio 内的 JS 控制台窗口中看到一条写出的消息。

为支持该行为,控件共进行了 3 处变更:

  1. WinJS.Class.mix 得以调用(Contoso.UI.HelloWorld、WinJS.Utilities.createEventProperties("blink"));这创建了一个“onblink”属性,用户能以编程的方式进行设置,或者用户可在 HTML 页面中以声明性的方式闪烁。
  2. WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) 的调用为控件添加了 addEventListener、removeEventListener 和 dispatchEvent 函数。
  3. 闪烁事件得以触发, 其触发的方式是通过调用 that.dispatchEvent("blink", {element:that.element}) 得以实现;而且一个包含元素字段的自定义事件对象也得以创建。
  4. 系统附加了一个事件处理程序,以侦听闪烁事件;在响应过程中,该处理程序将访问自定义事件对象的元素字段。

在这里我要指出,仅当您已在控件的构造函数中设置了 this.element 时,才能对 dispatchEvent() 的进行调用;事件混合的内部项需要其访问 DOM 中的元素。这是我在此前提到的情形之一,在此情形中,控件对象需要一个元素成员。这可让事件以 DOM 第 3 级事件的模式升至页面中父级元素。

揭示公共方法

作为对我们控件的最后一项变更,让我们添加一项公共 doBlink() 函数,该函数可随时调用,从而强制执行闪烁。

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

这仅仅是一个约定变化,我们可将 our _doBlink 函数的名称变为 doBlink。

为了通过 JavaScript 调用 doBlink() 函数,您需要为您的控件准备一个对对象的引用。如果您采用命令性的方式创建控件,您可能已经拥有一个引用;如果您采用声明性的处理方式,您可使用 HTML 元素中的 winControl 属性为您的控件访问控件对象。例如,假设标记与此前相同,您可通过以下函数访问控件对象:

$("hello-world-with-events").winControl.doBlink();

兼收并蓄

我们已经向您介绍了您希望在一个控件中实现的最常见的方面:

  1. 在页面中包含控件。
  2. 传递配置选项。
  3. 分派并响应事件。
  4. 通过公共方法揭示功能。

我希望这一教程对您有所帮助,并让您了解如何构建一个简单的基于 JavaScript 的自定义控件!如果您在构建自己的控件时遇到了任何疑问,请联系 Windows 开发中心,并在论坛中提问。此外,如果您是一名 XAML 开发人员,敬请期待即将发布的有关 XAML 控件开发的博文。

Jordan Matthiesen 
Microsoft Visual Studio 项目经理