- PVSM.RU - https://www.pvsm.ru -

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

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

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

Впервые термин изоморфные приложения лично я увидел в блоге компании airbnb [1], перевод этой статьи можно прочитать на Хабре [2], хотя этот термин начал звучать немного раньше, например в блоге nodejitsu [3]. Поэтому немного сложно сказать, кто это придумал, но факт в том, что в наше время существует целый класс веб-приложений, которые принято называть изоморфными. На Хабре этот термин в основном упоминался в статьях о фреймворках от gritzko [4] и zag2art [5].

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

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

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

Возникновение класса изоморфных приложений обосновано желаниями разработчиков, например:

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

Лично я все это хочу столько, сколько занимаюсь веб-разработкой, и, очевидно, не я один, так как в мире радуги, единорогов и изоморфизма уже есть множество фреймворков для разработки изоморфных приложений:

  • Rendr [6] (Airbnb);
  • Kraken [7] (PayPal);
  • Mojito [8] (Yahoo);
  • Meteor [9];
  • Derby [10];
  • наверное, ещё с пару десятков менее известных решений.

Ещё есть такой подход как MEAN [11] (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 [12] (АСУ ТП) или приложений для совместной работы над чем угодно. Однако если нужно реализовать обычный сайт, как это делают разработчики на RoR или на PHP-фреймворке — это лишние сложности.

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

Сatberry.js

Называется этот фреймворк Catberry.js, и сейчас он уже в версии 3.0. Не то чтобы он стар, но для версионирования используется Semantic Versioning 2.0.0 [13], а фреймворк пару раз претерпел изменения без обратной совместимости. 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 [14] с активной моделью, она должна оповещать о своих изменениях все заинтересованные View, как это делается, например, в Meteor. В Catberry ничего подобного нет.
  • есть ещё MVC с пассивной моделью, но в таком случае Controller должен отслеживать изменения модели и обновлять View, ничего подобного в Catberry тоже нет;
  • вместо этого обновления происходят только от действия пользователя, когда он меняет URL или отправляет данные формы. Разумеется, никто не мешает вам дополнительно обновлять Placeholder вызовом запроса на обновление в коде Module, например по событиям или используя long-polling. Но контент плейсхолдера будет зависеть от состояния приложения, описанного в URL. В этом смысл подхода — возможность на сервере и в браузере по URL полностью восстановить состояние приложения, чтобы отрендерить идентичные страницы.

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

Catberry-приложение работает именно так, как я ранее описывал изоморфные приложения:

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

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

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

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

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

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

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

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

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

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

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

Когда мы разрабатываем веб-приложение, нам часто нужно воспользоваться действиями, которые зависят от реализации в определенной среде, то есть работают по-разному в браузере и на сервере. Для это в Catberry есть изоморфные реализации таких действий. Они внешне работают идентично, имеют одинаковый программный интерфейс, но внутри реализованы, используя средства текущей среды. Вот перечень таких реализаций:

  • получение Location;
  • получение Referrer;
  • получение User Agent;
  • очистка URL fragment (hash);
  • получение или установка Cookie;
  • Redirect;
  • HTTP/HTTPS запросы;
  • кеш данных, которые использовались для последнего рендеринга каждого плейсхолдера.

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

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

Service Locator и DI

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

Например,

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 [17].

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

Каждый модуль — это директория с файлом 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 методов.
Пример реализации модуля можно посмотреть на Гитхабе [18].

Promises

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

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

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

С чего начать

Если это, довольно беглое, описание фреймворка вас заинтересовало, то можно начать с этих двух строк в терминале:

npm -g install catberry-cli
catberry init example

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

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

Подробную документацию и примеры можно найти на GitHub [27].
Ну и, конечно, сама страница репозитория [28] на GitHub и Twitter-аккаунт catberryjs [29], в котором всегда самые свежие новости о фреймворке.

Автор: pragmadash

Источник [30]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/74196

Ссылки в тексте:

[1] блоге компании airbnb: http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/

[2] прочитать на Хабре: http://habrahabr.ru/post/203444/

[3] блоге nodejitsu: http://blog.nodejitsu.com/scaling-isomorphic-javascript-code/

[4] gritzko: http://habrahabr.ru/company/swarm/blog/238785/

[5] zag2art: http://habrahabr.ru/post/206526/

[6] Rendr: https://github.com/rendrjs/rendr

[7] Kraken: http://krakenjs.com/

[8] Mojito: https://github.com/yahoo/mojito

[9] Meteor: https://www.meteor.com/

[10] Derby: http://derbyjs.com/

[11] MEAN: http://mean.io

[12] SCADA: http://ru.wikipedia.org/wiki/SCADA

[13] Semantic Versioning 2.0.0: http://semver.org

[14] описанию MVC: https://ru.wikipedia.org/wiki/Model-View-Controller

[15] Service Locator: http://en.wikipedia.org/wiki/Service_locator_pattern

[16] Dependency Injection: http://en.wikipedia.org/wiki/Dependency_injection

[17] UglifyJS2: https://github.com/mishoo/UglifyJS2

[18] Гитхабе: https://github.com/catberry/catberry-cli/blob/master/templates/example/catberry_modules/search/SearchModule.js

[19] Promises: https://www.promisejs.org

[20] статья: http://habrahabr.ru/post/242767/

[21] уже есть в браузере: http://caniuse.com/#search=promises

[22] библиотека-полифил: https://github.com/then/promise

[23] Конфеттин: http://konfettin.ru

[24] Flamp: http://flamp.ru

[25] CLI-утилитой: https://github.com/catberry/catberry-cli

[26] Runnable: http://runnable.com/U_RHWYh4VjkgAeCl/example-catberry-js-application-that-works-with-github-api

[27] найти на GitHub : https://github.com/catberry/catberry/blob/master/docs/index.md

[28] страница репозитория: https://github.com/catberry/catberry

[29] catberryjs: https://twitter.com/catberryjs

[30] Источник: http://habrahabr.ru/post/242909/