Создание настраиваемого элемента управления с помощью библиотеки Windows для JavaScript (WinJS)

Если вы разрабатывали приложения для Магазина Windows с использованием JavaScript, скорее всего вы познакомились с библиотекой Windows для JavaScript (WinJS). Данная библиотека предоставляет набор стилей CSS, элементов управления JavaScript и вспомогательных программ для быстрого создания приложений, соответствующих рекомендациям по взаимодействию с пользователем для Магазина Windows. WinJS также предоставляет набор функций, которые можно использовать для создания настраиваемых элементов управления в приложении.

Вы можете создавать элементы управления JavaScript с использованием любых шаблонов или библиотек. Функции в библиотеке WinJS — всего лишь один из вариантов. Основным преимуществом использования WinJS для создания элементов управления является то, что эта библиотека позволяет создавать собственные элементы управления, согласованно работающие с другими элементами управления в библиотеке. Принципы разработки собственного элемента управления и работы с ним — такие же, как и для любого другого элемента управления в пространстве имен WinJS.UI.

В этой записи я покажу, как создавать собственные элементы управления с поддержкой настраиваемых параметров, событий и общих методов. Если кого-то из вас интересует то же самое, но для XAML, вскоре выйдет запись блога, посвященная этому вопросу.

Добавление элемента управления на основе JavaScript на HTML-страницу

Сначала вспомним, как добавить элемент управления WinJS на страницу. Для этого существует два различных способа: императивный (используя только JavaScript естественным образом) или декларативный (включая элементы управления на HTML-страницу с использованием дополнительных атрибутов в HTML-элементах). Последний способ позволяет средствам предоставлять функции, доступные во время разработки, такие как перетаскивание элементов управления из набора инструментов. Подробную информацию можно найти в кратком руководстве по добавлению элементов управления и стилей WinJS в MSDN.

В этой статье я покажу, как создать элемент управления JavaScript с использованием преимуществ декларативной модели обработки в WinJS. Для декларативного добавления элемента управления на страницу, выполните следующие действия.

  1. Добавьте ссылки на WinJS на HTML-страницу, так как ваш элемент управления будет использовать 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. Вызовите функцию WinJS.UI.processAll() в коде JavaScript приложения. Эта функция обрабатывает HTML и создает экземпляры всех найденных декларативных элементов управления. Если вы используете шаблоны приложений в Visual Studio, WinJS.UI.processAll() вызывается в файле default.js автоматически.

  4. Добавьте элемент управления на страницу декларативно.

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

Простой элемент управления JavaScript

Теперь мы создадим очень простой элемент управления: что-то вроде "Здравствуй, мир!" среди элементов управления. Вот код 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, что вы разрешаете этому коду вставить контент на страницу. Дополнительную информацию об этом можно найти в документации MSDN для функции WinJS.Utilities.markSupportedForProcessing.

Зачем использовать вспомогательные программы 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 || {}); 

Многие разработчики создают элементы управления таким образом (с использованием анонимных функций, функций конструктора, свойств и настраиваемых событий), и если это подходит вам и вашей группе, то используйте этот способ. Но для многих разработчиков этот код может казаться немного запутанным. Многие веб-разработчики не знакомы с использованными здесь способами. В этом случае могут пригодиться библиотеки — они помогают избавиться от путаницы, связанной с написанием этого кода.

Помимо улучшения читаемости, WinJS и другие библиотеки решают многие другие небольшие проблемы, чтобы вам не нужно было о них волноваться (эффективное использование прототипов, свойств и настраиваемых событий). Они оптимизируют использование памяти и помогают устранить распространенные ошибки. WinJS — всего лишь один пример, выбор за вами. В качестве конкретного примера получения пользы от библиотеки я рекомендую еще раз изучить код из этого раздела после завершения чтения статьи и сравнить предыдущую реализацию с тем же элементом управления, реализованным в конце статьи с помощью WinJS.

Базовый шаблон для элементов управления JavaScript в WinJS

Далее представлен минимальный рекомендуемый шаблон для создания элемента управления JavaScript с помощью WinJS.

 (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. Режим Strict ECMAScript 5 включается при использовании оператора "use strict" в начале функции. Это является рекомендацией для шаблонов приложений Магазина 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() используется для публикации класса элемента управления и предоставления доступа к нему любому коду в приложении. Без этого нам пришлось бы искать другое решение для предоставления доступа к нашему элементу управления с использованием глобального пространства имен для кода за пределами встроенной функции, в которой мы работаем.

Определение параметров элемента управления

Чтобы сделать пример более интересным, добавим поддержку настраиваемых параметров в наш элемент управления. В этом случае мы добавим для пользователя возможность включения мигания содержимого.

 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. Параметры передаются элементу управления через параметр (options) в функции конструктора.
  2. Параметры, используемые по умолчанию, настраиваются с помощью закрытых свойств класса.
  3. Вызывается функция WinJS.UI.setOptions() , в нее передается ваш объект элемента управления. Этот вызов переопределяет значения по умолчанию для настраиваемых параметров элемента управления.
  4. Добавляется открытое свойство (blink) для нового параметра.
  5. Мы добавили возможность мигания текста на экране (на практике лучше не кодировать стили таким образом, а использовать свойства класса CSS).

В этом примере часть кода, выполняющая сложные операции, — это вызов функции WinJS.UI.setOptions(). Служебная функция setOptions проходит по каждому полю в объекте options и назначает его значение полю с таким же именем в целевом объекте — первом параметре setOptions.

В нашем примере мы настраиваем объект options с помощью аргумента data-win-options для элемента управления win-control, передавая значение true для поля "blink". Теперь функция setOptions() в конструкторе увидит поле с именем "blink" и скопирует его значение в поле с таким же именем в объекте control. Мы определили свойство с именем blink, предоставляющее функцию настройки. Наша функция настройки — та, которую вызывает функция setOptions(), после чего устанавливается член _blink нашего элемента управления.

Добавление поддержки событий

После реализации параметра 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);

Добавьте элемент управления на страницу, как и ранее. Обратите внимание, что мы добавили идентификатор в элемент, чтобы мы могли получить этот элемент позднее:

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

После этих изменений мы можем подключить прослушиватель события "blink". (Примечание. В этом примере я использовал оператор $ в качестве псевдонима для document.getElementById.)

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

При выполнении этого кода вы увидите сообщение, которое каждые 500 мс записывается в окно консоли JS в Visual Studio.

Чтобы добавить поддержку этого поведения, мы внесли следующие изменения в элемент управления:

  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}), после этого в поле element создается объект настраиваемого события.
  4. Подключается обработчик события blink. В ответ на событие он обращается к полю element объекта настраиваемого события.

Здесь я подчеркну, что вызовы dispatchEvent() работают, только если вы установили this.element в конструкторе элемента управления. Для события mix-in требуется доступ к данному элементу в DOM. Это один из тех случаев, о которых я упомянул ранее и в которых требуется член element для объекта элемента управления. Это позволяет передавать события родительским элементам на странице в шаблоне события 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);

Это всего лишь изменение соглашения — мы можем изменить имя нашей функции _doBlink на doBlink.

Чтобы вызвать doBlink() с помощью JavaScript, вам потребуется ссылка на объект вашего элемента управления. Если вы создаете элемент управления императивно, у вас уже есть ссылка. Если вы используете декларативную обработку, вы можете получить доступ к объекту элемента управления с помощью свойства winControl в HTML-элементе вашего элемента управления. Например, с использованием той же разметки, что и ранее, вы можете получить доступ к элементу управления следующим образом:

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

Объединение кода

Только что мы проработали основные аспекты элемента управления, которые вам потребуется реализовать:

  1. добавление элемента управления на страницу;
  2. передача параметров конфигурации;
  3. передача событий и реагирование на них;
  4. предоставление функциональной возможности через общие методы.

Надеюсь, что это руководство помогло вам понять, как создать простой элемент управления на основе JavaScript. Если у вас есть вопросы по работе с собственными элементами управления, посетите Центр разработки для Windows и задайте вопросы на форумах. Кроме того, для разработчиков на XAML вскоре будет опубликована запись с подробным описанием того же процесса разработки элемента управления на XAML.

Джордан Маттьесен (Jordan Matthiesen) 
Руководитель программы, Microsoft Visual Studio