Изоморфные JavaScript-приложения с Catberry.js



    UPD:
    Время шло… Фреймворк развивался и много чего из этой статьи уже устарело.
    Но не смотря ни на что, свежий материал можно найти вот на этих слайдах, а к ним еще есть видео.

    Catberry.js — это фреймворк для разработки изоморфных JavaScript-приложений на node.js с использованием модульной архитектуры и быстрых механизмов рендеринга. Этот фреймворк позволяет написать модуль приложения один раз и использовать его как на сервере для рендеринга страниц для поисковых роботов, так и в браузере для одностраничного приложения, запрашивая только данные для шаблонов.

    Впервые термин «изоморфные приложения» лично я увидел в блоге компании Airbnb, перевод этой статьи можно прочитать на Хабре, хотя этот термин начал звучать немного раньше, например в блоге nodejitsu. Поэтому немного сложно сказать, кто это придумал, но факт в том, что в наше время существует целый класс веб-приложений, которые принято называть изоморфными. На Хабре этот термин в основном упоминался в статьях о фреймворках от gritzko и zag2art.

    Что такое изоморфные приложения?


    Увидев это странное название, многие разработчики пугаются и стараются избегать знакомства с ним. И очень зря, я считаю, что именно изоморфные приложения — это будущее (а, может быть, уже и настоящее) веб-приложений.

    Как бы страшно оно ни звучало, на самом деле всё достаточно просто — это приложения, которые могут переиспользовать серверный код в браузере и вести себя одинаково как на сервере, так и в браузере. Другими словами, вы пишете один раз код вашего приложения и получаете серверный бэк-енд, который рендерит страницы для поисковых роботов и отзывчивое одностраничное приложение в браузере. Оно, в свою очередь, может и вовсе не грузить больше ни байта HTML с сервера, а запрашивать только данные для рендеринга в браузере. Практически мечта, не правда ли?

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

    Лично я все это хочу столько, сколько занимаюсь веб-разработкой, и, очевидно, не я один, так как в мире радуги, единорогов и изоморфизма уже есть множество фреймворков для разработки изоморфных приложений:
    • Rendr (Airbnb);
    • Kraken (PayPal);
    • Mojito (Yahoo);
    • Meteor;
    • Derby;
    • наверное, ещё с пару десятков менее известных решений.

    Ещё есть такой подход как MEAN (MongoDB+Express+Angular+node.js), который делает Angular-приложения изоморфными.

    Почему бы просто не взять один из них?


    Когда я хотел начать очередной веб-проект, я стал изучать все существующие на тот момент решения и увидел ряд недостатков:
    • некоторые решения использовали фронт-енд фреймворки для рендеринга на бэк-енде. Это означало виртуализацию и построение DOM прямо на сервере, а затем его рендеринг в HTML. Этот подход показался крайне неэффективным как по памяти, так и по сложности, которая определённо приведёт к низкой производительности. Например, Rendr использует Backbone, Mojito — YUI, MEAN — Angular.
    • ещё один недостаток — зависимость от определённой БД. Ничего не имею против MongoDB и даже сам её использовал несколько раз, но иногда нам нужна надежность и транзакционность, а иногда отсутствие схемы и скорость. Считаю, что разработчика не надо ограничивать в выборе. Речь идёт о Rendr, Meteor, Derby, которые привязаны намертво к MongoDB, а Derby ещё требует Redis.
    • Real-time data binding — это, безусловно, очень полезная фича для приложений, таких как SCADA (АСУ ТП) или приложений для совместной работы над чем угодно. Однако если нужно реализовать обычный сайт, как это делают разработчики на RoR или на PHP-фреймворке — это лишние сложности.

    Хочется иметь возможность разрабатывать изоморфные и просто сайты, чтобы они работали быстро и просто. И так как мои поиски завершились неудачей, я понял, что можно разработать фреймворк, который заполнит эту нишу хотя бы для меня.

    Сatberry.js


    Называется этот фреймворк Catberry.js, и сейчас он уже в версии 3.0. Не то чтобы он стар, но для версионирования используется Semantic Versioning 2.0.0, а фреймворк пару раз претерпел изменения без обратной совместимости. Catberry достаточно лёгкий фреймворк со своей идеологией, которая гласит «Хранение и обработка данных не должна быть частью приложения Catberry, это должен делать отдельный RESTful сервис».

    Знакомство с фреймворком стоит начать с подхода SMP (Service-Module-Placeholder), который заменяет привычное многим MVC (Model-View-Controller), но, обещаю, будет выглядеть знакомо. Опять же, стоит оговориться, я не против MVC, он очень даже хорош, но для определенного класса приложений, где как раз необходимо обновление данных в реальном времени. Тот MVC, который используется сейчас в разработке веб-приложений, часто оказывается не тем, что изначально было придумано в качестве MVC, а как некая интерпретация. Так вот свою интерпретацию я решил назвать иначе, чтобы не путать коллег.

    Service-Module-Placeholder


    Как видно из названия, приложение строится на трёх компонентах.

    Service

    Service — внешний компонент, представляющий из себя RESTful сервис, к которому постоянно обращается наше Catberry-приложение. Этот сервис может быть реализован на абсолютно любой платформе: Erlang, Go, PHP, .NET, что угодно. Вы ограничены только протоколом HTTP 1.1.

    Module

    Модуль в понятии Catberry-приложения это набор логики, которая подготавливает данные для шаблонизатора и обрабатывает события от пользователя. Другими словами, если нужно отрендерить часть страницы, Catberry находит ответственный за эту часть страницы модуль и просит его подготовить контекст данных для шаблона, учитывая текущее состояние всего приложения (например, параметры в текущем URL).

    Placeholder

    С первого взгляда можно сказать, что это просто шаблон. На самом деле он может иметь HTML-элементы, которые ссылаются на другие плейсхолдеры, причём такие ссылки могут появляться динамически во время рендеринга, и это вставит другой плейсхолдер внутрь текущего. Например, в содержимое элемента плейсхолдер «paw» модуля «cat». Почему используется id, спросите вы? А для того, чтобы плейсхолдеры можно было очень быстро найти в DOM, когда рендеринг работает в браузере. Это действительно очень экономит время.

    Принадлежность Placeholder к Module

    Как было упомянуто, плейсхолдер связан с модулем, а точнее модуль владеет неким набором плейсхолдеров, и никому больше эти плейсхолдеры принадлежать не могут. Отсюда может возникнуть вопрос: «А как же разбить приложение на плейсхолдеры и модули?». Есть два достаточно простых правила:
    1. Если какой-то участок веб-страницы может быть отображен за один запрос к RESTful сервису, то, скорее всего, это плейсхолдер.
    2. Если несколько плейсхолдеров зависят от одних параметров в URL и при изменении этих параметров необходимо их одновременно обновить, то эти плейсхолдеры стоит сгруппировать в один модуль.

    Отличия от MVC

    Наверное, кто-то из читателей обязательно подумает: «А в чём же отличие от MVC?». Если постараться, можно сказать, что Service==Model, Controller==Module и View=Placeholder. Но это как раз то, о чём я говорил ранее, термин MVC в наше время очень искажён, и когда люди интерпретируют его по-своему, они называют это MVC. Я же счёл нужным указать другое название, потому что:
    • Service не часть приложения, это внешнее приложение, с которым общается Module через HTTP-запросы, это нельзя назвать Model;
    • как следствие, у нас нет хранения и обработки данных в Catberry-приложении, значит нет Model вовсе;
    • по классическому описанию MVC с активной моделью, она должна оповещать о своих изменениях все заинтересованные View, как это делается, например, в Meteor. В Catberry ничего подобного нет.
    • есть ещё MVC с пассивной моделью, но в таком случае Controller должен отслеживать изменения модели и обновлять View, ничего подобного в Catberry тоже нет;
    • вместо этого обновления происходят только от действия пользователя, когда он меняет URL или отправляет данные формы. Разумеется, никто не мешает вам дополнительно обновлять Placeholder вызовом запроса на обновление в коде Module, например, по событиям или используя long-polling. Но контент плейсхолдера будет зависеть от состояния приложения, описанного в URL. В этом смысл подхода — возможность на сервере и в браузере по URL полностью восстановить состояние приложения, чтобы отрендерить идентичные страницы.

    Как это работает


    Catberry-приложение работает именно так, как я ранее описывал изоморфные приложения:
    • сначала пользователь делает запрос на сервер по URL;
    • сервер рендерит страницу по заданному URL;
    • вместе со страницей в браузер загружается браузерная версия приложения;
    • и дальше всё работает без перезагрузки страницы как одностраничное приложение.

    Для большего понимания картинка

    Потоковый рендеринг на сервере


    Как можно понять из описания и схемы, когда происходит рендеринг на сервере, для отрисовки каждого плейсхолдера обычно надо сделать запрос на RESTful-сервис. Это может занять значительное для пользователя время. Чтобы пользователь не разглядывал пустую страницу с крутящимся лоадером, пока мы делаем запросы для всех плейсхолдеров, был разработан потоковый (stream-based) движок отрисовки. Его задача — доставлять в браузер страницу по мере готовности. Другими словами, как только запрос к сервису для отрисовки заголовка страницы выполнен, пользователь тут же увидит этот заголовок у себя в браузере, не дожидаясь готовности всей страницы.

    Я сам очень сильно не люблю, когда при открытии страницы несколько секунд вижу белый экран и даже не знаю, дошёл ли мой запрос до бэк-енда, или я сейчас увижу «504 Gateway Timeout». Обычно я закрываю такие сайты через 3—4 секунды белого экрана.

    С потоковым рендерингом я сразу же увижу отклик и то, что сайт для меня работает, старается и пыхтит, собирая данные для отрисовки. Ещё один приятный момент, что стриминг не буферизирует данные в большом объеме, что будет хорошо экономить память нашего сервера с приложением. Ну и самый приятный момент, то, что браузер, получив HEAD-элемент, который приходит с сервера почти сразу, начинает парсить JavaScript и CSS, а также загружать все указанные в странице ресурсы, всё это будет работать параллельно, как на диаграмме ниже.



    Параллельный рендеринг в браузере


    Потоковый рендеринг это, конечно, хорошо, но только когда мы ограничены последовательной загрузкой страницы по потоку TCP-соединения. Когда у нас уже есть готовая страница в браузере, и пользователь кликает по ссылке, нам нужно перестроить часть страницы под новое состояние приложения, здесь мы уже ничем не ограничены. Можем выполнять запросы к RESTful сервису параллельно, а по результатам тут же обновлять плейсхолдеры. А если внутри есть ссылки на другие, то снова параллельно запрашивать для них данные. Таким образом получается невероятно быстрый рендеринг плейсхолдеров в браузере. К тому же, если один из запросов будет получать ответ очень долго, то это не повлияет на рендеринг остальных плейсхолдеров.

    Инструменты для изоморфизма


    Когда мы разрабатываем веб-приложение, нам часто нужно воспользоваться действиями, которые зависят от реализации в определенной среде, то есть работают по-разному в браузере и на сервере. Для это в Catberry есть изоморфные реализации таких действий. Они внешне работают идентично, имеют одинаковый программный интерфейс, но внутри реализованы с использованием средств текущей среды. Вот перечень таких реализаций:
    • получение Location;
    • получение Referrer;
    • получение User Agent;
    • очистка URL fragment (hash);
    • получение или установка Cookie;
    • Redirect;
    • HTTP/HTTPS запросы;
    • кеш данных, которые использовались для последнего рендеринга каждого плейсхолдера.

    Именно это API даёт изоморфность приложения на Catberry.js.

    Как устроено приложение на Catberry


    Service Locator и DI

    Архитектура фреймворка построена на реализации паттернов Service Locator и Dependency Injection.

    Например,
    var cat = catberry.create(config); // создаётся экземпляр приложения
    cat.locator.register('uhr', UHR); // можно регистрировать конструкторы по имени
    cat.locator.registerInstance('uhr', new UHR()); // или сразу экземпляры
    cat.locator.resolve('uhr'); // получить экземпляр
    cat.locator.resolveAll('uhr'); // получить все экземпляры под таким именем
    cat.locator.resolveInstance(SomeConstructorDependsOnUHR); // создать экземпляр с внедрением зависимостей
    
    //Зависимости внедряются достаточно просто:
    function ModuleConstructor ($uhr, someConfigSection) {
      // можно использовать зависимость $uhr
      // и даже секцию конфига someConfigSection
    }

    Такие внедрения зависимостей не ломаются при минификации, так как она делается самим фреймворком с использованием UglifyJS2.

    Как устроен модуль

    Каждый модуль — это директория с файлом index.js, который должен экспортировать конструктор модуля (модуль — это конструктор с объявленным прототипом). Также у модуля может быть директория placeholders, в которой располагаются шаблоны-плейсхолдеры модуля.

    Методы

    Каждый модуль может реализовывать три группы методов: render, handle и submit. Тут используется конвенция именования – если ваш плейсхолдер называется some-awesome-placeholder, то вы должны реализовать метод renderSomeAwesomePlaceholder, если хотите подготовить данные для него. Можете и не реализовывать, ничего от этого не сломается, а шаблон отрендерится с пустым контекстом, что тоже вполне допустимо. Такая конвенция применяется и к handle/submit методам, которые обрабатывают события со страницы.

    Пример реализации всех трёх методов:
    ModuleConstructor.prototype.renderSome = function () {
      // получение данных
      return {some: data}; // или Promise
    };
    ModuleConstructor.prototype.handleSome = function (event) {
      // как-то обрабатываем событие
      // event.args
      // event.element
      // event.isEnding
      // event.isHashChanging
      // можно вернуть Promise
    };
    ModuleConstructor.prototype.submitSome = function (event) {
      // отправляем данные формы
      // event.values
      // event.element
    };
    

    Иногда необходимо выполнить привязку к элементам DOM после того, как плейсхолдер будет отрендерен, для этого предусмотрены after methods, например для метода renderSome выше:
    ModuleConstructor.prototype.afterRenderSome = function (dataContext) {
      // можно делать что угодно с отрендеренным плейсхолдером
    };
    

    Можно добавить такие методы также для handle и submit методов.
    Пример реализации модуля можно посмотреть на Гитхабе.

    Promises

    Как уже упоминалось в примерах, везде, где используются асинхронные вызовы, в Catberry используются Promises (недавно была отличная статья). Причём если таковые уже есть в браузере, будет использоваться нативная реализация, иначе — библиотека-полифил от одного из авторов спецификации. Тип Promise, при этом доступен глобально, ничего подключать не нужно, как будто вы работаете с нативными промисами.

    Где используется


    Сейчас на основе фреймворка уже запущен сайт проекта Конфеттин, где можно ощутить производительность и отзывчивость приложения на основе Catberry. К тому же вовсю идет разработка следующей версии Flamp, которая в обозримом будущем уже увидит свет. Чего я лично жду с нетерпением.

    С чего начать


    Если это довольно беглое описание фреймворка вас заинтересовало, то можно начать с этих двух строк в терминале:
    npm -g install catberry-cli
    catberry init example
    

    Таким образом, вы получите код рабочего примера, который работает с GitHub API и инструкции как его запустить. В этом примере продемонстрированы типовые реализации вещей, которые часто приходится делать в веб-приложении. Этой же CLI-утилитой можно делать ещё много чего интересного. Например, создать новый проект или добавить в проект модуль.

    Если устанавливать на свой компьютер ничего не хочется или нет такой возможности, есть этот же готовый проект на Runnable, но там можно превысить лимит запросов к GitHub API.

    Подробную документацию и примеры можно найти на официальном сайте.
    Ну и, конечно, страница репозитория на GitHub и Twitter-аккаунт catberryjs, в котором всегда самые свежие новости о фреймворке.
    2ГИС 224,12
    Карта города и справочник предприятий
    Поделиться публикацией
    Похожие публикации
    Комментарии 35
      +3
      Ещё есть такой подход как MEAN (MongoDB+Express+Angular+node.js), который делает Angular-приложения изоморфными.

      Отнюдь :) Это просто связка. С таким же успехом это могло быть MongoDB+Rails+Angular или MongoDB+PHP+Angular.

      А подход, в целом, интересный, нужно глянуть, что предлагает Catberry
        +4
        Хотелось бы для полноты картины посмотреть на то, как выглядят плейсхолдеры (я так понимаю, это шаблоны разметки, правильно?)

        (ну, я-то уже сходил и посмотрел в репозитории, но другим читателям это может быть интересно, да и пояснения по механизму работы и синтаксису бы хотелось бы поглядеть)
        • НЛО прилетело и опубликовало эту надпись здесь
          0
          Спасибо, достаточно внятный пост.

          На предложенном вами «Конфетине» по ощущениям — отзывчивость на высоте. Со стороны пользователя к скорости претензий нет. Но в отладчике происходит нечто непонятное.

          Для примера страница konfettin.ru/services (все что я делаю, это выбираю разные разделы в выпадушке «услуги во всех категориях»). И смотрю в раздел networks отладчика.

          1)
          Запрос к api на список товарных предложений типа ?limit=12&category=handmade — реакция разная. Либо это два запроса OPTIONS + GET (первый закономерно пуст, второй — с данными), либо два GET + GET (первый с пустым массивом, второй — с данными). Иногда — только один GET с данными в ответе.
          ** Кажется разумным, что в данном кейсе должен быть только один GET-запрос на список товарных предложений в категории.

          2)
          Постоянно повторяющиеся запросы на cities и categories (очевидно — для получения списков городов и категорий и пере-рендеринга выпадушек).
          ** Кажется разумным, что этого делать не нужно совсем.

          Вопрос таков: такое поведение — это злой баг или наоборот — киллер-фича? может быть, это ограничения фреймворка или его неправильное использование? ваш вариант? :-)
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              1. Нет (считаем, что на OPTIONS api-сервер реагирует правильно), на самом деле более интересно — почему два GET, когда в первый раз api отдает пустой массив. А фреймворк, можно подумать, демонстрирует ум и упрямство, выполняя запрос повторно.

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

              На самом деле для меня вопрос в том, насколько сложные фронтовые бизнес-логические кейсы может позволить реализовать фреймворк без чрезмерной костыльности, и чтобы результат в итоге «казался разумным» и не содержал «лишних» запросов и других странностей. Но я бы сам не хотел на этот вопрос отвечать, поэтому я его вам и не задаю :-)
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  1. Прошу прощения. Разобрался. Все хорошо. Когда товарных предложений в списке мало, происходит еще один запрос с целью получить «остаток» списка, который пуст. Это все же небольшой баг, но уже совсем на другую тему, мне неинтересную.

                  потому что термины «казался разумным» и «чрезмерной костыльности» очень субъективны.


                  Да. Поэтому этот вопрос я вам не задавал :-)
            0
            мимо
              –1
              А у меня тут вопрос к хабраюзерам: у меня в черновиках лежит недописанная статья об аналогичном проекте. Проект живой, активно развивается внутри одной команды, но без меня уже. Ни разу не рекламная — продукта-то все-таки нет в открытом доступе, скорее о том какие шишки набиваются при разработке такого продукта и какие есть хитрые трюки с ним же. Дописывать?)

              Если что — заключение в ней очень простое: такие фреймворки по сути не нужны, если уже есть рендер на серверсайде, нужно вытягивать страницы и заменять их диффом, а клиентская логика должна навешиваться через вебкомпоненты или аналогичный механизм. Если нет рендера на сервере — это браузерное приложение и оно должно работать само по себе жсоном.
                +1
                Пишите, конечно.
                А мы поглядим.
                Больше статей, хороших и разных! :)
                –1
                Выглядит хорошо и работает шустро, но, боюсь, руководство не пойдет на жертву в сторону IE8 :(
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Полностью и безоговорочно поддерживаю! Но бизнес, есть бизнес…
                      +4
                      Бизнес есть бизнес, и он должен понимать, что для поддержки IE8 нам нужно разработать полный аналог такой же системы, только с легаси-поддержкой, и это более чем удваивает (и это в лучшем случае!) стоимость разработки и поддержки. Если бизнес готов на это идти — прекрасно, мы будем поддерживать хоть IE8, хоть Netscape Communicator. Пускай только снабжают сектор разработки соответствующим количеством ресурсов,…
                  0
                  Мне не понятно, где место клиентского кода, который работает с DOM.
                  Можно ли использовать SMP подход c MVC фреймворками, как оно согласуется, или есть другой способ?
                    0
                    Посмотрите в CommitsModule this.$context.isBrowser
                    • НЛО прилетело и опубликовало эту надпись здесь
                      +1
                      Вопрос такой. Сразу скажу: документацию не читал — лишь пробежался по диагонали.
                      Развернул пример, посмотрел его. Все неплохо.
                      Сейчас приложение работает так: main -> pages -> about
                      Это все хорошо, но about тут в первую очередь страница. У нее есть title, например. Сейчас title для about определяются в main. Можно ли определять его все таки в about?
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Плохо это. А как на Конфеттине реализован динамический титл, если не секрет?
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Не совсем понял. По page и serviceId можно получить метатеги и титл, но для этого нужно иметь объект услуги. Он получается в другом модуле, а в модуле mainPage должен быть либо еще один запрос (который я не увидел), либо из одного модуля его нужно как то передать в другой?
                              • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        > а Derby ещё требует Redis

                        Редиска для дерби не обязательна.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Да, только обратил внимание, у вас — «Речь идёт о Rendr, Meteor, Derby, которые привязаны намертво к MongoDB, а Derby ещё требует Redis.»

                            Кроме того, что дерби не «требует» redis, derby не привязан намертво к mongodb, архитектура там модульная, mongodb цепляется при помощи соответствующиего адаптера — livedb-mongo. Кода там не особо много, можно написать подобный адаптер для своей db. Есть по-моему для foundation. Хотя ограничение здесь все же есть — куда проще здесь будет разработать адаптер для документоориентированной db, чем, например, для реляционной.

                            В derby-standalone вообще можно цеплять любую, какую угодно свою базу, но derby-standalone, это, конечно, не изоморфный вариант, а некий аналог andular-а — заточеный на использование только на клиенте.
                          0
                          Вообще, интересный у вас проект. Наводит на мысли.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Ага, позитивные. Мне вообще изоморфные фреймворки интересны, интересно в них копаться, возникают какие-то идеи. Вот например ваша реализация DI мне понравилась с учетом uglifyjs mangle exeptions, документация хорошая. Надо бы на примеры приложений глянуть. Инересно, как у вас модель цепляется.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  А вы не думали случайно, для рендера попробовать ReactJs? По сути готовые web-компоненты, декларативный подход
                                  • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Как было упомянуто здесь, в 2GIS используется ещё один фреймворк для построения изоморфных приложений — Slot. Почему было принято решение написать новый фреймворк, а не использовать и усовершенствовать существующий?
                            • НЛО прилетело и опубликовало эту надпись здесь

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

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