[Из песочницы] Использование Marionette.Region для создания загрузочных представлений
В клиентских приложениях очень часто возникает необходимость как-то визуализировать процесс загрузки данных с сервера. В этой статье я опишу способ, позволяющий добиться такого поведения за счёт повторно используемой области Marionette.Region в MarionetteJS.Сразу скажу, что мой подход во многом основывается на подходе автора скринкастов на www.backbonerails.com. Это очень хорошая и полезная серия скринкастов не только (и не столько) с точки зрения того, что обсуждается здесь, но и в целом для изучения MarionetteJS.
ЦельИтак, нашей целью является разработка повторно используемого компонента для MarionetteJS, занимающегося визуализацией процесса загрузки. Как будет показано дальше, в случае Backbone/Marionette события начала и окончания загрузки можно считать частным случаем изменения состояния модели, поэтому назначение этого компонента можно сформулировать более абстрактно как визуализацию изменения состояния модели.Начинаем с BackboneJS Как всем хорошо известно, MarionetteJS является надстройкой над BackboneJS. И я бы хотел начать с того, как Backbone может помочь с решением этой задачи. Допустим, у нас есть модель (или коллекция) и представление, которое её отображает. На уровне BackboneJS поставленную задачу мы могли бы решить следующим образом: Рисунок 1 — События модели при загрузке данныхТо есть нехитрая идея состоит в том, что представление обозревает модель и использует события request и sync, генерируемые Backbone.Model, для перехода к «загрузочному» (например, рисует какую-нибудь анимацию загрузки) или к «синхронизированному» состоянию (убирает анимацию загрузки). Для краткости, я буду называть способность реагировать на события изменения состояния модели такие как request, sync, error и т.п. отзывчивостью. Под изменением состояния я имею в виду любые события, не связанные с изменением данных модели.
В общем, Backbone позволяет нам легко добиться нужного поведения, за счёт событий, генерируемых Backbone.Model (или Backbone.Collection), но есть проблемы с повторным использованием кода, который должен подписаться на события модели и обработать их. Собственно, с вопросом как реализовать отзывчивость проблем не возникает, главный вопрос где это можно сделать, чтобы использование этой реализации было наиболее ненавязчивым и удобным. Я не буду останавливаться на дальнейших рассуждениях, как можно было бы реализовать наш компонент на основе Backbone, потому что в любом случае лучше, чем на основе MarionetteJS, он не получится.
Используем представления MarionetteJS Когда я в первые столкнулся с задачей визуализации загрузки, я не думал, что по этому вопросу будет так мало готовых решений. Гугление находило несколько обсуждений, насчёт реализации атрибута Marionette.View.loadingView аналогично Marionette.CollectionView.emptyView. Позже появилось и какое-то готовое решение. Но честно говоря, я считаю, что визуализация загрузки модели на уровне представления, непосредственно отображающего эту модель, не самая лучшая идея. В общем случае, способ визуализации загрузки зависит не от представления модели, а от того, кто его отображает. Т.е. если мы отображаем разные модели в одном и том же месте документа, то и визуализация загрузки должна выглядеть единообразно для всех них. Короче говоря, этот вариант нам не подходит.Используем контроллеры MarionetteJS Теперь пришло время рассказать о подходе, на основе которого возникла моя идея. Это подход описан в этом выпуске уже упоминавшейся серии скринкастов. Если в двух словах, этот подход использует модель как источник событий и базовый абстрактный контроллер, определяющий способ отображения представлений. Вот в общих чертах этот подход: Непосредственно отзывчивость реализуется парой LoadingControler+LoadingView. Для работы с LoadingView используется метод базового класса контроллера show (view, region, options). Производные контроллеры должны использовать этот метод для отображения представлений. Этот метод в свою очередь создаёт экземпляр LoadingController и передаёт ему исходное представление и область для отображения. Далее LoadingController сразу же в своём конструкторе (а не по событию request) подставляет LoadingView в указанную область. LoadingController обозревает модель. Причём, что удобно, модель можно не указывать явно, по умолчанию будут обозреваться RealView.collection и RealView.model. Более конкретно, LoadingController обозревает не модель, а объекты xhr, ассоциированные с запросом данных этой модели. По завершении этих запросов вместо LoadingView в ту же область вставляется RealView. Рисунок 2 — Диаграмма последовательности при использовании LoadingView+LoadingControllerЗамечания LoadingController работает с LoadingView и RealView не напрямую, а через Marionette.Region, но для общего понимания работы это не важно.
Вот диаграмма классов, которые участвуют в этом взаимодействии: Рисунок 3 — Диаграмма классов при использовании LoadingView+LoadingControllerЭто первый стоящий подход из тех, что уже упоминались. Но мне он не подошёл по той причине, что он налагает некоторые требования на общую архитектуру приложения:
Все контроллеры должны наследоваться от одного базового класса, и использовать только его метод для отображения представлений. Для отображения загрузочного представления необходимо обязательно следовать последовательности: начать загрузку модели, создать новое представление, показать его. В то же время есть очень удачные решения, которые я позаимствовал: Возможность определения модели, которую требуется обозревать, без явного указания на неё. Ответственность за визуализацию загрузки выносится за рамки представления модели. Используем Marionette.Region Marionette.Region олицетворяет какую-то область экрана, в которой размещаются представления, и позволяет управлять временем жизни представления и отделять от представления способ его появления на экране. Если вы хотите показать в области представление, вам не нужно думать, что будет с тем представлением, которое уже помещено в область — оно будет удалено автоматически. Если вам нужно изменить способ появления представления на экране, вы можете просто унаследоваться от Marionette.Region и реализовать свою логику. Например, вы можете поменять способ появления, добавив анимацию, а можете изменить способ вставки представления в документ, оборачивая его какими-то своими элементами. Для примера, в этом выпуске как раз описывается реализация собственной области, оборачивающей произвольное представление в диалоговое окно.В моей реализации основная работа происходит в абстрактном классе ResponsiveRegion, являющимся наследником Marionette.Region. Конкретным классам остаётся только определить обработчики, меняющие меняется внешний вид области в зависимости от событий модели (и перечислить сами модели). Например, можно менять прозрачность, видимость элементов области, вставлять какой-нибудь оверлей с анимацией, — в общем, делать всё что угодно. Я не буду уделять много внимания оформительской части, а сосредоточусь на абстрактном классе. Вот как я реализовал отзывчивость на уровне области при помощи ResponsiveRegion:
Инициализация ResponsiveRegion не отличается Marionette.Region. Допустим, представление Layout (производное от LayoutView), определяющее область, указывает (декларативно), что должна использоваться наша реализация области ResponsiveRegion, а не стандартный Marionette.Region. List.Layout = Marionette.LayoutView.extend ({ … regions: { someRegion: { selector: '#region', regionClass: Marionette.ResponsiveVisiblityRegion } } … }); Чтобы показать представление в этой области, можно использовать region.show (view) (как с обычным Marionette.Region), тогда будут использоваться параметры по умолчанию, а можно через region.show (view, options) и указать расширенные настройки отзывчивости. Область помещает переданное представление в документ. Она может делать это не напрямую, а через обёртку.Пример
Рисунок 5 — Диаграмма классов при использовании ResponsiveRegionЗамечания Реально есть ещё зависимости Layout и Controller от ResponsiveRegion. Первая связь опущена в силу того, несмотря на то, что фактически представление Layout и зависит от ResponsiveRegion, реально оно работает с ним как c обычным Marionette.Region. Просто удобнее объявлять тип области в объявлении Layout. Вторая связь опущена, так как она необязательна и возникает только если контроллеру потребуется указать опции отзывчивости.
Чего мы добились, применив такой подход? Мы полностью уложились в структуру объектов MarionetteJS, не создав ни одной новой сущности, а только расширив Marionette.Region. ResponsiveRegion в большинстве случаев используется также как его предок, и не делает каких-то дополнительных предположений о компонентах, с которыми он используется. По сравнению с предыдущим подходом на основе абстрактного контроллера нет никакой завязки на то, как организовано ваше приложение: если вы используете Marionette, вы можете использовать ResponsiveRegion. Мы обозреваем модель всё время, пока её представление находится в ResponsiveRegion. ResponsiveRegion, как наследник Marionette.Region, естественным образом осведомлён о прекращении существования представления, что позволяет ему в нужный момент отписываться от событий модели и не оставлять за собой мусор. Событие начала и окончания загрузки модели мы рассматриваем как частный случай изменения её состояния, которое может происходить произвольное число раз за время жизни модели. Это позволяет свободно реагировать на другие события модели, такие как ошибка загрузки или валидация данных модели. Заключение Все из рассмотренных подходов могут успешно применяться в зависимости от ситуации. Но я считаю для визуализации изменения состояния модели вообще и загрузки её данных в частности, вариант с Marionette.Region подходит лучше всего. Во-первых, это как раз тот компонент, который в Marionette отвечает за отображение представлений на экране. А во-вторых, необходимая логика достаточно проста, чтобы её можно было реализовать внутри одного компонента. На этом всё. Исходный код того, о чём здесь говорилось, и небольшой пример доступны здесь.Ссылки backbonerails.com — cерии скринкастов по MarionetteJS Документация MarionetteJS Список событий BackboneJS