Базовая архитектура веб-приложения на Backbone.js

    Разработчики часто просят рассказать о моём опыте использования Backbone.
    Многие слышали об этом MVC-фреймворке, смотрели примеры и документацию, но не решаются начать с ним работать. Поэтому вчера я сделал мини-доклад на встрече MoscowJS, призванный рассказать о том, как мы построили базовую архитектуру и какие получили плюсы. И сегодня в этой статье я публикую слайды, схемы и краткое описание.




    REST API


    У проекта Островок.ru есть пользователи, которые ищут номера в отелях и пользователи, которые вводят информацию об этих номерах. Экстранет — это интерфейс для отелей и мы решили создать его на базе REST API. Собственно, и бэкенд-разработчики были очень рады унификации CRUD-процесса.


    Плюсы и перспективы
    • HTML-шаблоны одним запросом
    • компактность JSON
    • Local Storage
    • mobile-приложение
    • desktop-приложение


    Базовая архитектура




    Что нужно, чтобы добавить новую страницу
    1. описать модель и, при необходимости, коллекцию, с которой будет работать страница
    2. написать вью, который наследуется от класса PageView
    3. добавить кусочек HTML-шаблона


    Открытие страницы




    Профит методов initialize и render
    Пока данные грузятся с сервера, мы можем отрисовать часть страницы. Это, например, колонка с фильтрами, в которой есть куча сложных компонентов. А средний ajax-запрос составляет 150 мс, которых будет достаточно для этой рутины.

    Компонент изменений и сохранения



    Для нас очень важны данные и пользователи должны быть уверены, что они всё сделали верно. Поэтому мы написали общий для всех коллекций и моделей компонент, который позволяет:
    • следит за изменениями модели и предлагает сохранить
    • позволяет показать и откатиться до изначальных данных
    • останавливает закрытие страницы, если что-то не сохранено


    Зачем это, если у каждой модели Backbone есть previousAttributes?
    Они содержат только один шаг и несколько изменений сделают previousAttributes отличными от изначального состояния.

    А если пользователь вернул изначальные значения в поля?
    Этот компонент использует функцию isEqual, которая полностью сравнивает изначальное состяние и текущее. Поэтому, если они не отличаются, то панель будет автоматически скрыта.

    Но у коллекций нету метода save! Как вы их сохраняете?
    Мы написали метод, который находит модели в коллекции, которые были созданы, изменены или удалены. И затем мы делаем три запроса, в которых участвуют только эти модели. Таким образом, мы можем легко работать с большими объёмами данных.

    Код в студию!
    Мы дошлифуем этот компонент, приведём его в красивый вид и поделимся со всеми от лица фронтендеров Островка.

    Итого


    В итоге, пользователи получили быстрый и удобный инструмент для работы с данными, а разработчики — хорошую основу для дальнейшей разработки. Сейчас над проектом работают четыре человека: два фронтендера и два бэкендера, один из которых регулярно и с лёгкостью пишет новые страницы. И я уверен, что новый разработчик сможет легко присоединиться и продолжить разработку проекта.

    Лично для себя, я увидел, что Backbone — это отличный первый шаг для знакомства с MVC на клиенте. Он содержит минимум компонент и методов, что позволяет не зарываться в документации, а пробовать его в работе.

    Пользуясь случаем, хочу поблагодарить Илью Батия bait, Егора Виноградова и Антона Егорова satyrius за помощь, советы и пожелания в создании проекта на Backbone. Спасибо, мужики!

    Комментарии 33

      +6
      Спасибо за то, что делителсь своим опытом. Был вчера на митапе, очень понравилась ваша презентация.
        +3
        Спасибо! Я мало выступаю и очень рад это слышать.
        И я постараюсь подготовить ещё интересного материала к следующей встрече.
        +1
        TastyPie сырой ужасно
        1. Все OneToMany и ManyToMany поля приходится заполнять в dehydrate иначе начинаются проблемы когда tastypie пытается сохранять related объекты
        2. Нет нормальной возможности валидации прав на сейве. Пришлось писать кучу оберток для прокидывания request в full_hydrate например.
        А в целом — нормально, если нужно каркас накидать.
          0
          PS Еще в связке tastypie + backbone нужно пофиксить метод save ибо после создания объекта сервер не отдает никаких данных, посему нужно получить данные с сервера самостоятельно.
          joshbohde.com/blog/backbonejs-and-django — тут код есть
            +1
            Да tastypie действительно сырой. Мы от него откажемся и сделаем свой, в нём не устраивает:
            1) производительность
            2) реализация фильтров и ограничений авторизации.

            По поводу прокидывания request в dehydrate. такая функциональность совершенно не понадобилась, всё смогли сделать через apply_authorization_limits.

            Валидация очень удобно построена с помощью Django Forms, но её пришлось впилить в код Resource.

            Ещё немного камней в огород tastypie. Если вчитаться в код put_list'а можно увидеть что в случае ошибки при update из базы будут удалены все записи которые update'ились и никакой информации не останется — короче это источник потери данных.

            0
            Но у коллекций нету метода save! Как вы их сохраняете?
            Мы написали метод, который находит модели в коллекции, которые были созданы, изменены или удалены. И затем мы делаем три запроса, в которых участвуют только эти модели. Таким образом, мы можем легко работать с большими объёмами данных.

            Правильно, потому что логичней будет работать атомарно с отдельным инстансом модели за раз. Отредактировали — отправили изменения.
              0
              На самом деле, мы отправляем массив моделей.
              Если пользователь изменил 10 моделей из 100, то все 10 уйдут PUT-запросом.
              Если пользователь удалил 2 модели из 100, то 2 уйдут DELETE-запросом.
                0
                Ок, понял.
                Интересно будет посмотреть вашу реализацию.
                  0
                  А не лучше ли отправлять все 12 одним POST запросом, в котором в некоторых моделях стоит _delete: true?
                  Такой подход есть в рельсах и он очень удобен.
                    0
                    Это противоречит философии RESTful архитектуры.
                      +2
                      HTTP медленный и это приходится учитывать.

                      Тем более, что можно расценивать это не как склеенное обновление нескольких объектов, а как апдейт мастер-объекта, у которого просто есть коллекция.
                    0
                    А каким образом вы удаляете два экземпляра одним DELETE запросом? Это же не правильно с точки зрения RESTful? Или вы не очень строго придерживаетесь RESTfull принципов?
                      0
                      Удаление довольно редкая операция.
                      Но даже в этом случае, можно передать id через запятую.

                      И мы не строгие в плане REST.
                      Принцип «меньше запросов» — намного существеннее.
                        +1
                        Посмотрите как это сделано в CouchDB.
                    0
                    Как решили момент конфликта конвенций стиля кода? Имею ввиду то что в питоне подчеркивание, в JS — camelCase. Вариант предложенный у них на сайте с автоматическим переводом подчеркиванием в кемелКейс отметаем сразу, как не прозрачный.
                      +1
                      Мы оставили нижнее подчёркивание.
                      Во-первых, свойств с несколькими словами немного.
                      Во-вторых, в Backbone.js всё происходит через set/get и такая конструкция не вызывает смущения:
                      Model.get('some_prop');
                        0
                        Ну допустим если предавать в шаблон underscore.js model.getJSON() то уже немного не айс.
                        Но я понял вашу точку зрения.
                      0
                      Мне очень интересны подобные технологии.
                      Тоже подобную методику использую, только у меня «основой» страницы является не приложение (app), а (независимые) объекты на странице, которые имеют личную область отображения, свои шаблоны, свои методы, свою логику, и могут быть вложенными в другие объекты.
                      К примеру страница «список постов», в вашем случае (как я понял) приложение будет управлять и отрисовывать целиком список постов, у меня каждый пост сам по себе. Или вот ещё хороший пример -«блок пользователя» который обычно отображается на каждой странице, логический должен быть отдельным объектом, который без необходимости не «перерисовывается» при хождении по ajax приложению.
                        0
                        В принципе, у нас так и устроено.

                        К примеру, App имеет несколько блоков: contentBlock, userBlock.
                        Когда модель user изменится, то перересуется userBlock.
                        Когда нужно отрисовать страницу, то меняется содержимое contentBlock.

                        И это суть ajax-based приложения, без этого никак, да :-)
                          0
                          Как у вас устроена идентификация однотипных объектов которым принадлежит сигнал? например нужно пометить один из постов на странице: при этом в команде указывается ид объекта (<a onclick=«post.do_mark(123)»)? или что-то типа (<a onlick=«post.do_mark()») при этом отрабатывает целевой объект.?
                            +3
                            Похоже что вы не совсем врубаетесь в то чем является Backbone.js и как он устроен. Советую прочитать вот этот пост habrahabr.ru/blogs/javascript/118782/ и все станет более понятно.
                              0
                              Backbone.View.events
                                0
                                View является абстракцией DOM-элемента.

                                То есть создавая вью, я создаю некий div.
                                В его свойствах я указываю:
                                events: 'click': 'mark'

                                И в методе mark я получу view как контекст.
                                Всё просто и удобно :-)
                            0
                            Вопрос — как вы наследуете страницы от PageView? Я имею ввиду код — штатный extend делает не то что надо.
                              0
                              Именно extend и используем.
                              А что с ним не так?
                                +1
                                Хм, попробовал ещё раз — всё так.
                              +1
                              3. добавить кусочек HTML-шаблона

                              интересует, как вы загружаете HTML-шаблоны на страницу — все сразу при загрузке приложения или каждый кусочек отдельно по первому запросу этой страницы?
                                0
                                Все сразу, в коде основной страницы.
                                Они обёрнуты в тег script с типом text/template, что препятствует обработке этих тегов.

                                Также, каждый из этих тегов имеет id, который позволяет легко обращаться к коду шаблона.
                                0
                                А как с браузерами дружит, неприятных сюрпризов не всплывало?

                                Недавно случайно получилось нечто очень похожее на backbone.js.
                                Менеджеры коллекций, менеджеры экземпляров, напрямую завязанные на REST взаимодействие с сервером, стек изменененных элементов и хранилища коллбеков из Элементов наверх, привязанные к менеджеру коллекций (например экземпляру нужно дернуть менеджер коллекции обнулить параметр у всех членов коллекции и сохранить)

                                Настороженно отношусь к штукам вроде django-tastypie и вообще созданию сквозного model штыря. Да, нам помогают выдать и получить json/xml, но это и так просто, а вот поддерживать дружбу API-обертки, с усложняющейся моделью может оказаться сложновато.
                                  0
                                  Различий в браузерах пока не встречал.
                                  Наверное, потому что используется jQuery и underscore.js, которые как раз и обеспечивают совместимость.

                                  По поводу tastypie я, как фронтендер, много сказать не могу.
                                  Как мне кажется он не совсем сквозной, это просто удобная прослойка для выдачи/сохранения некоторых полей моделей. Правда, он не даёт всего необходимого функционала и мы хотим своё решение.
                                    0
                                    Клево, значит беру в работу.

                                    Tastypie похож на облегченную версию Piston, поработав с последним я понял, что в принципе решается несуществующая проблема и при этом создается новая в виде еще одного слоя абстракции. В итоге у меня просто есть приложение /a/ в котором объявлен специальный класс вьюшки, заточенный на прием и выдачу json ((де)сериализация, заголовки и т.д.) с объявленной операцией и сущностью (crud). Что и как связано с моделью расписывается непосредственно во вьюхе
                                      0
                                      В большинстве проектов все эти прослойки не нужны, да.
                                  0
                                  Мы дошлифуем этот компонент, приведём его в красивый вид и поделимся со всеми от лица фронтендеров Островка.


                                  Еще планируете поделиться?

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое