Весной 2017 года Eric Simons, со-основатель учебного проекта Thinkster, анонсировал проект «RealWorld»демо приложение и спецификация к нему. Проект объявил своей целью выйти за рамки привычных «todo»-демок для более прикладного сравнения и изучения возможностей различных фреймворков и технологий, а также подходов к разработке и способов решения задач.

image


Введение


О проекте RealWorld


Автор описывает идею и смысл проекта следующим образом:
So about a year ago, I had this realization about a problem I had:

Mastering the core concepts & ideology of a new framework is unnecessarily frustrating.

You read the docs, run a contrived example in a codepen, rip apart the “todo” example app & put it back together again, get their CLI installed locally… and then you’re off to the races!

Except you’re not. Not even close. Because when you start actually trying to build out your own app, that’s when Murphy’s law hits you.


— Eric Simons

По сути RealWorld — это клон блого-социальной платформы, как Medium или Хабр, названный «Conduit», который разрабатывается энтузиастами с использованием различных frontend и backend (да-да, фуллстек) технологий по одной и той же спецификации и макетам. Любому желающему поучаствовать в проекте нужно создать новое issue в репозитории на GitHub, в котором описать желаемый технологический стек, форкнуть starter-kit проекта и начать разработку. Кроме того, разработчик имеет возможность увидеть конечный результат в действии с помощью демо-приложения, написанного Эриком на AngularJS.

Фактически, итогом каждой новой реализации должно стать точно такое же приложение, но написанное с использованием других технологий или подходов. Уже реализованы и опубликованы форки для React/Redux, Elm, Angular2, React/MobX, Svelte/Sapper и других фреймворков. На подходе также Vue, Ember и другие.



Среди backend технологий проект реализован на Node/Express, Laravel, Django, ASP.NET Core, Rails и еще куче всего.



В конце 2017 года проект подготовил сравнение 9-ти наиболее популярных frontend реализаций по 3 критериям: производительность (first meaningful paint), размер бандла(-ов) (gzip) и кол-во строк кода, которые требуются для реализации проекта (loc).

На текущий момент, RealWorld — один из лучших демо-проектов по веб-разработке. Включает в себя решения наиболее распространенных задачи на примере достаточно реального веб-приложения и актуального стека технологий. Проект имеет более 11k звезд на гитхабе и, как мне кажется, является довольно полезным и интересным. Однако, как это часто бывает, на Хабре практически нет информации о нем. Лишь парочка упоминаний в дайджестах.



О чем это я


Так вот, к чему я все это пишу. Последние несколько лет, я фактически являюсь евангелистом изоморфного (по-другому, «универсального») подхода к написанию веб-приложений. В нашей компании также практикуется подход, который мы называем «environment-agnostic apps», т.е. приложения, которые могут работать в любом окружении (точнее каком-то списке окружений) без изменений. Для нас это особенно важно, потому что компания занимается разработкой под широчайший список платформ от веба до разнообразных IoT.

«Евангелист» все же наверное слишком громкий термин, однако за это время я участвовал в немалом количестве обсуждений и конференций, а также успел несколько раз прочитать собственный доклад на эту непростую и неоднозначную тему. Кроме того, меня давно и сильно волнуют проблемы «accessibility», «progressive enhancement» и «SEO» современных веб-приложений, также приложений основанных на веб-стеке.

Дисклеймер по поводу изоморфного подхода и споров о нем.
Я прекрасно осознаю тот факт, что изоморфный (универсальный) подход к разработке — это довольно сложная и даже холиварная тема. Множество статей написано, много копей сломано. Достаточно большое количество разработчиком не принимает этот поход, некоторые выступают как откровенные хейтеры.

Уважаемые коллеги разработчики, если по какой-то причине, вы считаете, что данный подход к разработке не имеет право на жизнь, прошу вас сначала посмотреть хотя бы вступительную часть моего доклада на YouTube, например этого. Возможно это сэкономит мне время на аргументацию, а вас убережет от желания устроить холивар на эту тему в комментариях к этой или другим статьям данного туториала.

Давайте уважать друг друга. Также отдельно прошу уважить тот труд, который я планирую вложить на пользу сообщества путем разработки данного проекта и написания подробного туториала по ходу. Прошу не воспринимать данный цикл статей как принуждение вас или кого бы то ни было к использованию данных подходов. Мир!

В рамках своего доклада я реализовал изоморфное демо-приложение в виде небольшого блога на основе одного из шаблонов Material Desing Lite. Приложение прям скажем маленькое, фактически без бекенда — его наличие лишь эмулируется с помощью таких штук как JSON Placeholder.

К своему стыду, сам я узнал о проекте RealWorld лишь в конце прошлого года, но тогда не нашел достаточно времени, чтобы поучаствовать в нем. Сейчас со временем стало немного проще и я хочу попробовать реализовать RealWorld приложение (взамен текущему), отвечающее следующим характеристикам:

«Манифест» проекта:


  1. Соответствовать спецификации проекта RealWorld;
  2. Полностью поддерживать работу на сервере (SSR и все прочее);
  3. На клиенте работать как полноценное SPA;
  4. Индексироваться поисковиками;
  5. Работать с выключенным JS на клиенте;
  6. 100% изоморфного (общего) кода; *
  7. Для реализации НЕ использовать «полумеры» и «костыли»; **
  8. Использовать максимальной простой и общеизвестный стек технологий. ***


* - о том как считать общий код.
Это немного спорный вопрос. Я много думал о том, как именно следует считать общий (изоморфный) код и пришел к выводу, что таким кодом является непосредственно код самого приложения написанный мной для его реализации. Иными словами тот код, который реализует функционал текущего проекта, т.е. проекто-зависимый код.

Та часть кода, которая является лишь основой приложения (например код веб-сервера и т.п.) и может быть без изменений использована в другом проекте, не будет учитываться в качестве кода приложения. Можно назвать этот код «starter-kit» или «boilerplate» или как угодно. В любом случае этот код не является непосредственной частью конкретной бизнес-задачи и реализует обобщённую базу, на основе которой пишется приложение.

Еще раз, кодом приложения будет считаться код написанные лично мной для реализации конкретного проекта.

** - мое понимание костылей и полумер.
Изучая примеры изоморфных приложений, можно обратить внимание что часто используются не совсем обоснованные вещи, такие как: (1) отдельные точки входа для серверного и клиентского приложения; (2) всевозможные условные операторы, проверяющие текущее окружение (аля «isServer» и т.п.) и отдельные ветки кода в рамках общих файлов; (3) серверный роутинг и фетчинг данных выполняется не так, как клиентский и т.д. и т.п.

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

*** - про стек.
Часто, когда речь заходит об изоморфных приложениях, у людей возникает мысль, что необходимо использовать какие-то специальные стартер-киты, топ-левел-фреймверки или иные специальные штуки, меняющие привычный стек технологий.

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

Как видите, список довольно амбициозный. Тут вам и спорная «изоморфность» и всеми желанный, но такой недостижимый «progressive enhancement». И блог-платформа, работающая в стиле SPA, но при этом поддерживающая SEO. Все это на единой кодовой базе поверх существующего backend и по ТЗ, в которые нельзя внести изменения.

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

Также довольно часто встречается мнение, что изоморфный код намного сложнее и вообще писать изоморфные приложения это капец как трудно, отнимает много времени и экономические не выгодно. Последние три пункта моего «манифеста», в том числе, произрастают из этих заблуждений.

Короче говоря, для меня данный проект является еще и возможностью полностью или частично опровергнуть все это, а также повеселиться. Очень надеюсь что у меня все получится. Если вам интересна тема и хотелось бы пройти этот путь со мной по шагам — не переключайтесь!

Выбор стека


Уверен вы уже поняли, но если нет, тогда уточняю — писать будем frontend часть приложения. Для этого проект RealWorld предоставляет Frontend спецификацию, которая включает:

  • Готовый backend с rest api поддерживающий всю необходимую функциональность;
  • Кастомная тема для Bootstap 4;
  • HTML разметка всех страниц приложения;
  • Гайдлайны по роутингу.

Полный список инструкций можно посмотреть тут.

Итак, сразу постараюсь выполнить п.8 манифеста и выберу максимально простой стек:

  • Ractive + плагины для приложения;
  • Express + плагины для веб-сервера;
  • Webpack + плагины для сборки.
  • Axios для http запросов;

Как видите, пока выглядит довольно просто. Никаких супер-пупер специальных решений с приставкой «isomorphic-» нет и не будет.

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

Server-side


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

Дисклеймер
Данный туториал предназначен прежде всего для frontend разработчиков среднего и выше уровня. Которые знакомы с современными инструментами разработки и в курсе, что такое SPA и изоморфность.

В рамках туториала НЕ будут раскрываться вопросы установки npm-модулей, вводного знакомства с webpack, работы с командной строкой или иных базовых вещей на сегодняшний день. Исхожу из того, что для большинства читатетей рутинные операции по настройки дев-среды и работы с инструментами разработки и т.п. уже знакомы и отлажены.

Про архитектуру




Примерно вот так будет выглядеть общая архитектура приложения. Несколько ключевых моментов:

  1. Fontend часть разделена на клиент-серверное приложение на Ractive и на чисто серверную основу на Express/Node;
  2. RealWorld backend располагается позади frontend сервера;
  3. HTTP запросы к REST API проксируются через frontend сервер на backend.

Workflow


  1. Пользователь вводит URL в адресную строку;
  2. Клиент делает синхронный запрос на сервер;
  3. Сервер обрабатывает входящий запрос и запускает основной код приложения;
  4. В момент, когда приложение готово, сервер рендерит текущее состояние приложения в HTML и возвращает ответ;
  5. Клиент интерпритирует полученный HTML и начинает загружать сопутствующие ресурсы (CSS, JS, картинки, шрифты и т.п.) ;
  6. Пользователь получает контент;
  7. Код приложения скачивается в фоне и инициализируется;
  8. Пользователь взаимодействует с интерфейсом;
  9. Клиентский код приложения обрабатывает действия, делает асинхронные запросы на сервер и обновляет состояние.

Процесс совершенно обычный для любых изоморфных веб-приложений и точно также будет работать в данном случае.

Структура проекта


Я буду использовать совершенно привычную для себя структуру подобного проекта:


  • /assets — статические ресурсы;
  • /config — json-конфиги приложения;
  • /dist — output-директория вебпака;
  • /middleware — директория серверных скриптов;
  • /src — исходный код приложения;
  • /tests — тесты;
  • /tools — утилиты;
  • /views — чисто серверные шаблоны HTML;
  • server.js — код веб-сервера на Express;



Ну и конечно же целая куча всевозможные package.json, webpack.config.js и других конфигурационных файлов, описание которых выходит за рамки туториала.

Пишем код


Основной код веб-сервера будет располагаться в файле ./server.js. Кроме самого Express я также буду использовать некоторые из его плагинов («express-middleware») для решения утилитарных задач. Предварительно все необходимые модули, естественно, необходимо прописать в package.json установить с помощью команды npm i.

Сначала подключим 3rd-party модули и сам Express:

const express = require('express'),
      helmet = require('helmet'),
      compress = require('compression'),
      serveStatic = require('serve-static'),
      cons = require('consolidate');

Все используемые расширения Express совершенно обычные и не имеют отношения к изоморфности. Большая часть из них вполне себе стандартная. Вот их краткое описание и предназначение:

  • helmet — поможет защитить наш frontend от некоторых распространенных атак с помощью установки определенных HTTP заголовков;
  • compression — gzip сжатие для ответов сервера;
  • serve-static — помогает раздавать статику через Express;
  • consolidate — модуль шаблонизации для Express.

Далее, создадим несколько пока ничего не делающих middleware и подключим их:

const app = require('./middleware/app'),
      api = require('./middleware/api'),
      req = require('./middleware/req'),
      res = require('./middleware/res'),
      err = require('./middleware/err');


  • app — здесь будет располагаться код запускающий приложение на сервере;
  • api — это прокси http запросов на бекенд апи;
  • req — пред-обработка всех HTTP запросов;
  • res — отправка HTML ответов;
  • err — обработка серверных ошибок.

Подключим основной серверный конфиг common.json, инициализируем Express и расширения:

const config = require('./config/common');

const server = express();

server.use(helmet());
server.use(compress({ threshold: 0 }));
server.use(serveStatic('dist'));

server.engine('html', cons.mustache);
server.set('view engine', 'html');

Сразу сообщаем что хотим сжимать также всю статитку и что статика располагается в папке "./dist" (туда же будут генерироваться бандлы вебпаком).

Далее устанавливаем mustache в качестве шаблонизатора для Express. Почему именно его? Просто Ractive использует mustache-синтаксис для своих шаблонов и таким образом мы добьемся единообразия.

Далее финальный штрих:

server.use(req());

server.all('/api/*', api());
server.get('*', app());

server.use(res());
server.use(err());

server.listen(config.port);

Код не требующий особых пояснений. Ключевой момент здесь в следующем:

  • Все запросы на /api/* будут проксироваться на REST API (api middleware);
  • Все иные GET запросы будут запускать наше приложение (app middleware).

Итоговый код веб-сервера
const express = require('express'),
      helmet = require('helmet'),
      compress = require('compression'),
      serveStatic = require('serve-static'),
      cons = require('consolidate');

const app = require('./middleware/app'),
      api = require('./middleware/api'),
      req = require('./middleware/req'),
      res = require('./middleware/res'),
      err = require('./middleware/err');

const config = require('./config/common');

const server = express();

server.use(helmet());
server.use(compress({ threshold: 0 }));
server.use(serveStatic('dist'));

server.engine('html', cons.mustache);
server.set('view engine', 'html');

server.use(req());

server.all('/api/*', api());
server.get('*', app());

server.use(res());
server.use(err());

server.listen(config.port);


Как мне кажется, даже чисто frontend-разработчик, не имеющий особого опыта с Express, легко разберется в столь примитивном коде. Если конечно понимает что такое «middleware» и вообще эту концепцию.

Пока данный код практически ничего не делает. Во всяком случае не делает того, что требуется сделать в рамках проекта. В следующей статье мы допишем чисто серверный код и приступим к коду самого приложения.

Спасибо за внимание и хорошего времени суток!

Комментарии (5)


  1. drwatson1
    15.02.2018 01:30

    Интересно будет поглядеть, что получится. А пока вопрос: зачем проксировать запросы на бэкэнд?


    1. PaulMaly Автор
      15.02.2018 02:24

      Спасибо, хороший вопрос!

      В целом, практика показывает что данный подход является наиболее гибким и, в конечном счете, удобным для подобных проектов. Позволяет избежать всевозможных костылей во время реализации (п.7 манифеста).

      Более того, в данном случае, этот подход является неизбежным, потому что мы хотим обеспечить работу без JS на клиенте.

      Кроме всего прочего, проксирование открывает некоторые дополнительные возможности:

      • Перехват клиентских атак, типа XSS/CSRF/etc.;
      • Сокрытие бекенд сервера + нет необходимости включать CORS на бекенде;
      • Возможности изоморфного кэширования данных и запросов;
      • Более безопасная работа с сессиями/токенами/итп;
      • Перехват запросов/ответов и внесение точечных изменений. Часто пригождается,
        когда необходимо интегрироваться с «legacy» или неподконтрольным бекендом;
      • Изменение способа авторизации. Например довольно удобно навешивать OAuth поверх существующего бекенда;
      • Упрощенная реализация асинхронного «stateful» функционала поверх синхронных «stateless» бекендов. Реальный кейс: websocket'ы для бекенда на PHP.

      На самом деле полезных кейсов еще больше.


      1. PaulMaly Автор
        15.02.2018 02:38

        А вот еще, чуть не забыл. Был такой кейс: интеграция с несколькими бекендами. Например, один собственный и парочка 3rd-party. Ну либо вообще микросервисы.


      1. drwatson1
        15.02.2018 07:43

        Я нашёл ваш issue и репо, но там пока пусто :)

        Исходнички будут?


        1. PaulMaly Автор
          15.02.2018 12:00

          Конечно и даже демо по каждому этапу.)))