В первой части этой серии статей мы обсуждали приложение Backbone.Marionette. В этот раз мы рассмотрим модульную систему, встроенную в Backbone.Marionette. Попасть к списку модулей вы можете через пункт Application
(приложение). Модули сами по себе – это довольно информативная тема, которая заслуживает подробного рассмотрения в отдельной статье.
Что такое модули?
Прежде чем мы углубимся в основные детали использования модульной системы Marionette, необходимо четко определиться со значением термина модуль. Модуль – это независимый фрагмент кода, который выполняет определенную функцию. Конкретно взятый модуль можно использовать совместно с другими модулями для создания целостной системы. Чем более независимым является фрагмент кода, тем легче его можно будет заменить или провести внутреннюю модификацию, не затрагивая другие части системы.
В этой статье мы в основном будем рассматривать идентификацию модулей. Если же вы хотите больше узнать о том, как правильно создавать модульный код, то можно воспользоваться множеством источников из интернета. Я хотел бы порекомендовать к ознакомлению главу «Удобство обслуживания зависит от модульности» из книги Подробно об одностраничных приложениях, которая к слову считается одной из лучших в своей области.
В настоящее время язык программирования JavaScript не имеет никаких встроенных функциональных инструментов для инициализации модулей (следующая версия должна обеспечить эту возможность). Однако существует множество библиотек, которые позволяют организовать инициализацию и загрузку модулей. К сожалению, модульная система Marionette не поддерживает загрузку модулей из других файлов, но с другой стороны предоставляет функциональные возможности, которых нет в других системах, например, возможность запускать и отключать модули. Позже мы рассмотрим этот вопрос более детально. Сейчас мы рассмотрим пример процесса инициализации модуля.
Инициализация модуля.
Давайте начнем с основных пунктов инициализации модуля. Как уже упоминалось, перейти к списку модулей можно через раздел Application
. Все действия мы будем подкреплять примерами. Затем мы сможем использовать команду module
для инициализации модуля.
var App = new Backbone.Marionette.Application();
var myModule = App.module(‘myModule’);
Это довольно просто, правда? Так и есть, но это всего лишь пример создания простейшего модуля. А что конкретно мы создали? По сути, мы сказали приложению, что хотим создать шаблон модуля, без добавленного нами функционала, и что он будет называться myModule
(в соответствии с аргументом, который мы передали в module
). Но что такое шаблон модуля? Это экземпляр объекта Marionette.Module
.
Module
содержит встроенные функциональные возможности, такие как обработка событий (через EventAggregator
, которые мы подробно обсудим в следующей статье), начиная с инициализаторов (как у Application
) и заканчивая финализаторами (мы рассмотрим их в разделе «Запуск и отключение модулей»).
Стандартная инициализация модуля.
Теперь давайте рассмотрим пример инициализации модуля для заданных нами функциональных требований.
App.module("myModule", function(myModule, App, Backbone, Marionette, $, _){
// Private Data And Functions
var privateData = "this is private data";
var privateFunction = function(){
console.log(privateData);}
// Public Data And Functions
myModule.someData = "public data";
myModule.someFunction = function(){
privateFunction();
console.log(myModule.someData);}
});
Как вы видите, здесь представлено много команд. Давайте посмотрим на первую строку и разберем последовательность действий. Как и раньше, мы вызываем App.module
и присваиваем имя модулю. Но теперь мы также передаем значение функции. Функция содержит несколько аргументов. Готов поспорить, вы можете определить какие именно, просто взглянув на имена, которые я им задал, но я все равно объясню:
- myModule является тем самым модулем, который вы пытаетесь создать. Помните, что он уже создан для вас, и это новый экземпляр объекта
Module
. Возможно, что вы захотите расширить его при помощи каких-то новых свойств или методов; в противном случае можно просто придерживаться краткого синтаксиса, который не требует передачи в функцию. App
— этоApplication
объект, который вызывается функциейmodule
.Backbone
— это, естественно, ссылка на Backbone библиотеку.Marionette
— это ссылка на Backbone.Marionette библиотеку. На самом деле к ней можно попасть черезBackbone
, но вы также можете создать псевдоним и сделать имя короче.$-
это ваша DOM библиотека, которая будет либо JQuery либо Zepto (или, возможно, что-то еще в будущем)._
— это ссылка на Underscore или Lodash, в зависимости от того, что вы используете.
После представления этой информации, вы можете передавать и использовать собственные аргументы. Мы не будем этим заниматься.
Нужно отметить, что большинство из этих аргументов не нужны; в конце концов, у вас уже есть доступ ко всем ссылкам? Тем не менее, данная возможность полезна в нескольких ситуациях:
- Когда появляется необходимость сократить имена аргументов, для экономии байтов памяти.
- Если вы используете RequireJS или другой модуль загрузки, вам нужно только, создать зависимость объекта от функции
Application
. Все остальное будет доступно благодаря аргументам, вызываемым функциейModule
.
Как бы там ни было, но давайте вернемся к объяснению остальных действий, выполняемых кодом выше. Внутри функции, вы можете использовать символы закрытия и создавать частные переменные и функции, что мы, собственно говоря, и сделали. Вы также можете представлять данные и функции открыто, добавляя их в качестве свойств myModule
. Это и есть процедура создания и расширения нашего модуля. Нет необходимости что-либо возвращать, потому что модуль будет доступен непосредственно через команду App
, обо всем этом я расскажу в теме “доступ к модулю” в разделе ниже.
Примечание: убедитесь, что вы всего лишь добавляете свойство к module
переменной и не устанавливаете ее значение равным чему-нибудь (например, myModule = {…}
). В случае, если вы все же присвоили какое-то значение module
переменной, то поменяется адресация, и позже в модуле появятся соответствующие изменения.
Ранее я отмечал, что можно использовать в качестве пользовательских аргументов. Фактически, вы можете использовать столько пользовательских аргументов, сколько захотите. Взгляните на код ниже, чтобы увидеть, как это сделать.
App.module("myModule", function(myModule, App, Backbone, Marionette, $, _, customArg1, customArg2){
// Create Your Module
}, customArg1, customArg2);
Как вы уже могли заметить, если вы передаете дополнительные аргументы в module
, то они будут переданы и в функцию, которая описывает данный модуль. Главное преимущество, которое я вижу от этого действия – это экономия нескольких байт памяти; кроме этого, никакой пользы я больше не вижу.
Еще один момент, который следовало бы отметить это то, что ключевое слово this
доступно в функции и на самом деле относится к модулю. Это означает, что не нужно обязательно использовать первый аргумент, но вы должны иметь ввиду, что потеряете преимущество экономии памяти. Давайте переделаем представленный выше код, используя ключевое слово this.
Вы увидите, что данный пример кода во многом похож на пример с myModule
.
App.module("myModule", function(){ // Private Data And Functions
var privateData = "this is private data";
var privateFunction = function(){
console.log(privateData);}
// Public Data And Functions
this.someData = "public data";
this.someFunction = function(){
privateFunction();
console.log(this.someData);}
});
Если вы заметили, я не использую какие-либо из аргументов, на этот раз я решил не перечислять их. Вы также должны понимать, что можно пропустить первый аргумент и использовать только ключевое слово this
.
Разбиение процесса инициализации.
Последнее, что мне хотелось бы упомянуть о процессе инициализации – это возможность ее разбиения. Я не знаю точно, для чего это может вам понадобиться, но, возможно, кто-то захочет в дальнейшем расширить модуль, так что разбиение инициализации может помочь им избавится от зависимости перед исходным кодом. Вот пример разбиения процесса инициализации:
// File 1
App.module("myModule", function(){
this.someData = "public data";});
// File 2
App.module("myModule", function(){ // Private Data And Functions
var privateData = "this is private data";
var privateFunction = function(){
console.log(privateData);}
this.someFunction = function(){
privateFunction();
console.log(this.someData);}
});
Этот код дает тот же результат, что и предыдущая инициализация, с тем лишь отличием, что она распределена. Это работает, потому что в File 2
происходит вызов модуля, который мы инициализировали в File 1
(при условии, что File 1
был запущен до File 2
). Если вы пытаетесь получить доступ к частной переменной или функции, она должна быть определена в модуле инициализации, там, где она используется, потому что доступ возможен только в рамках структурного блока.
Вызов модуля.
Что хорошего в создании модулей, если мы не можем получить к ним доступ? Для полноценного использования модуля необходимо правильно организовать функцию его вызова. В самом первом примере фрагментов кода, вы видели, что когда я вызывал функцию module
, я присвоил ее возвращение значения переменной. Это потому, что мы используем один и тот же метод и инициализации и вызова модулей.
var myModule = App.module("myModule");
Обычно, если вы просто пытаетесь вызвать модуль, то обращаетесь к первому аргументу, а функция module
прекращает свое выполнение. Но если вы переходите к функции обращаясь ко второму аргументу, то модуль будет расширен с новой функциональностью и все равно будет вызываться с вновь созданными или измененными модулями. Это означает, что вы можете инициализировать свой модуль и получить к нему доступ с помощью одного метода вызова.
Однако, это не единственный способ вызова модулей. Когда модуль будет создан, он получит привязку непосредственно к Application
объекту, для которого был создан. Это означает, что вы можете также использовать нормальное обозначение точки доступа к модулю; но на этот раз, обозначение должно быть определено заранее, иначе вы получите неопределенность
.
// Works but I don't recommend it
var myModule = App.myModule;
Хотя представленный синтаксис короче, но он не передает нужный смысл для других разработчиков. Я бы рекомендовал использовать функцию module
для получения доступа к нужным модулям. Прежде всего потому, что очевидным становится вызов модуля, а не какого-то из свойств App
. Одновременно это удобно и рискованно, ведь вам придется создавать новый модуль, если он еще не существует. Риск заключается в возможной ошибке написания имени модуля. У вас не будет никакого способа проверить правильность работы модуля и его существование, пока вы не попытаетесь организовать его вызов.
Подмодули.
Модули также могут содержать подмодули. К сожалению, функция Module
не имеет своего собственного module
метода, поэтому вы не можете добавлять к ней подмодули напрямую, но это нас не остановит. Вместо того чтобы создавать подмодуль, вы вызываете функцию module
на App
, как вы это привыкли делать; но для имени модуля, вам необходимо поставить точку (.
) после имени родительского модуля, а затем разместить название подмодуля.
App.module('myModule.newModule', function(){ ... });
Благодаря использованию точечного разделителя в имени модуля, Marionette может определить, что команда перед точкой должна создавать подмодуль. Всегда присутствует потенциальная опасность в том, что если родительский модуль не был создан до момента вызова, он будет создан вместе с его подмодулем. Проблемы могут также возникнуть из-за неправильного написания, о котором я упоминал ранее. Вы могли бы завершить создание модуля, который вам не нужен, но обратите внимание, что подмодуль должен быть приложен к нему, а не к модулю, который вы будете использовать.
Доступ к подмодулям.
Подмодули могут быть вызваны те же самым способом, которым они были инициализированы, или вы можете организовать их вызов как свойств модуля.
// These all work. The first example is recommended
var newModule = App.module('myModule.newModule');
var newModule = App.module('myModule').newModule;
var newModule = App.myModule.newModule;
// These don't work. Modules don't have a 'module' function
var newModule = App.myModule.module('newModule');
var newModule = App.module('myModule').module('newModule');
Любой из этих методов доступа к подмодулю будет одинаково хорошо работать только в том случае, если и модуль и подмодуль уже созданы.
Запуск и остановка модулей.
Если вы читали предыдущие публикации статей из серии о функции Application
(приложение), то знаете, что организацию функции Application
нужно начинать с команды start
. Подобная организационная структура запуска применяется и к модулями, а остановлены они могут быть с помощью команды stop
.
Как вы помните (если вы читали предыдущую статью), вы можете добавить инициализаторы с помощью команды addInitializer
к функции Application
, и они будут вызываться одновременно при запуске функции (или будут немедленно вызваны, если функция Application
уже работает). Несколько другие события происходят, когда вы начинаете работу с функции Application
. Вот все события по порядку:
- срабатывание
initialize:before
события, - начинается вызов всех инициализированных модулей,
- запускаются все инициализаторы в том порядке, в котором они были добавлены,
- срабатывание
initialize:after
события, - срабатывание
start
события.
Функция Module
ведет себя очень похожим образом. Число событий, и некоторые из имен событий различны, но в целом это тот же самый процесс. При запуске модуля, это:
- срабатывание
before:start
события, - начинается вызов всех его инициализированных подмодулей,
- запускаются все его инициализаторы в том порядке, в котором они были добавлены,
- срабатывание
start
события.
Метод stop
также имеет схожие действия. Вместо добавления инициализаторов, вы должны добавлять финализаторы. Это можно сделать с помощью команды addFinalizer
, и, передавать значение функции, которая сработает при вызове команды stop
. В отличие от инициализаторов, никакие данные или опции не передаются функциям. При вызове команды stop
происходят следующие события:
- срабатывание
before:stop
события, - остановка работы подмодулей,
- запуск финализаторов в том порядке, в котором они были добавлены,
- срабатывание
stop
события.
Инициализаторы и финализаторы могут иметь и несколько другое применение. На самом деле, они являются весьма полезными при использовании в модуле определения. Таким образом, вы можете инициализировать модуль без фактического создания любых объектов, которые будут использоваться, а затем написать собственные инициализаторы, чтобы начать создавать объекты и их установки, такие, как в примере ниже.
App.module("myModule", function(myModule){
myModule.startWithParent = false;
var UsefulClass = function() {...}; // Constructor definition
UsefulClass.prototype ... // Finish defining UsefulClass...
myModule.addInitializer(function() {
myModule.useful = new UsefulClass();
// More setup
});
myModule.addFinalizer(function() {
myModule.useful = null;// More tear down
});
});
Автоматический и ручной запуск.
Когда модуль определен, по умолчанию происходит его автоматический вызов в то же время, что и его родительской функции (или корневой объект Application
или родительский модуль). Если модуль инициализирован в родительской функции, которая уже начались, его вызов происходит немедленно.
Вы можете убрать автоматический запуск модуля, изменяя его инициализацию одним из двух способов. Внутри блока инициализации, можно разместить команду startWithParent
со значением false
, или вы можете передать значение объекта (вместо функции) в module
, который уже имеет команду startWithParent
со значением, установленным на false
и свойство define
, чтобы заменить нормальную функцию.
// Set 'startWithParent' inside function
App.module("myModule", function(){
// Assign 'startWithParent' to false
this.startWithParent = false;});
// -- or --
// Pass in object
App.module("myModule", {
startWithParent: false,
define: function(){
// Define module here }
});
App.start();
// myModule wasn't started, so we need to do it manually
App.module('myModule').start("Data that will be passed along");
Сейчас у модуля не будет автозапуска. Вы должны вызвать команду start
вручную, чтобы запустить его, как сделал я в приведенном выше примере. Данные, которые передаются командой start
, могут быть абсолютно любого типа, и они будут приняты вместе с подмодулями, когда те начнут свое действие, инициализаторами, и событиями before:start
и start
.
Как я уже говорил, данные не передаются при вызове команды stop
. Кроме того, команда stop
должна вызываться вручную, и это действие всегда будет вызывать команду stop
и для подмодулей; нет способа обойти это действие. В этом есть смысл, потому что подмодуль не должен быть запущен, когда его родительская функция не работает, хотя есть случаи, когда подмодуль должен быть выключен, в то время как его родительская функция продолжает работать.
Другие события и встроенные функциональные возможности.
Я уже упоминал, что Module
содержит некоторые встроенные функциональные возможности, такие как EventAggregator
. Мы можем использовать команду on
для модуля, чтобы наблюдать за событиями, связанными с запуском и остановкой. Это еще не все. Если нет никаких других встроенных событий, то модуль может определить и инициировать свои собственные события. Взгляните:
App.module('myModule', function(myModule) {
myModule.doSomething = function() {
// Do some stuff
myModule.trigger('something happened', randomData); }
});
Теперь, когда мы вызвали doSomething
в модуле, произойдет вызов события something happened
(что-то случилось), на которое вы можете подписаться:
App.module('myModule').on('something happened', function(data) {
// Whatever arguments were passed to `trigger` after the name of the event will show up as arguments to this function
// Do something with `data`
});
Это очень похоже на то, что мы делаем с событиями в коллекциях, моделями и представлениями в нормализированном Backbone коде.
Как в действительности можно использовать модуль.
Способ инициализации модулей в Marionette очень похож на определение модулей любой другой библиотеки, на самом деле они используются не для тех целей, для которых были разработаны. Встроенные команды start
и stop
являются показателем этого. Модули, которые содержит Marionette, призваны для организации значительно больших подсистем приложений. Например, давайте посмотрим на Gmail.
Gmail является единственным приложением, которое на самом деле состоит из нескольких небольших приложений: почтовое приложение, чат-приложение, приложение для телефонных звонков и менеджер контактов. Каждое из них является независимым — оно может существовать само по себе — но все они расположены в рамках одного приложения и могут взаимодействовать друг с другом. Когда мы впервые запускаем Gmail, менеджер контактов не появляется, также нет и окна чата. Если бы мы попытались представить такую структуру с помощью приложения Marionette, каждое из этих подчиненных приложений будет представлено в виде модуля. Когда пользователь нажимает кнопку чтобы открыть менеджер контактов, нужно сначала остановить приложение электронной почты (потому что оно скрывается для нормальной скорости работы, хотя мы можем оставить его работающим и просто убедиться, что оно не отображается в объектной модели документов DOM), а затем запускать менеджер.
Рассмотрим другой пример приложения, построенного в основном из виджетов. Каждый виджет будет представлять собой отдельный модуль, который вы можете запускать и останавливать для того, чтобы показать или скрыть его. Данная возможность характерна для настраиваемой панели, такой как iGoogle или приборной панели в задней части WordPress.
Естественно, что мы не ограничены в использовании модулей Marionette для подобных случаев, хотя это и трудно использовать в традиционном смысле. Все потому, что модули Marionette полностью содержат значения объектов, в то время как традиционные модули “классы” предназначены для создания экземпляров.
Вывод.
Было представлено очень много информации. Если вы зашли настолько далеко, то мне стоит похвалить вас (хотя следует понимать, что прочитать эту статью вам было значительно легче, чем мне написать все это). Во всяком случае, я надеюсь, вы узнали много нового о возможностях Marionette, ручной инициализации, вызове, запуске и остановке модулей и подмодулей. Надеюсь, вы поняли, что это очень удобный инструмент и не стоит полностью игнорировать его существование. Напоследок мне бы хотелось сказать одну важную вещь о Backbone и Marionette: большинство их характеристик в значительной степени независимы, так что вы можете выбрать, что использовать.
Источник изображения на первой странице: ruiwen.
(al) (ea)