TL;DR: Тропа SPA темна и полна ужасов. Ты можешь бесстрашно сражаться с ними… или выбрать другой путь, который приведёт тебя к нужному месту: современный Rails.
![](https://habrastorage.org/r/w780q1/webt/bm/nz/o6/bmnzo6gdhrpqqdufks3kgylhxjw.jpeg)
Я вспоминаю, как думал, что Rails фокусируется на неправильной цели, когда DHH анонсировали Turbolinks в 2012 году. Тогда я был убеждён в том, что мгновенное время ответа во время взаимодействия с пользователем — это ключ к превосходному UX. Из-за возникающих задержек сети, такая интерактивность возможна, только если вы минимизируете зависимость от сети и вместо сетевых обращений поддерживаете большую часть состояния на клиенте.
Я думал, что это необходимо для приложений, над которыми я работал. И с таким мнением я перепробовал множество подходов и фреймворков для реализации одного и того же шаблона: Single-page applications (SPA). Я верил, что SPA — это будущее. Несколько лет спустя, я не уверен в том, каково же будущее, но я точно хочу найти альтернативу.
Одностраничное приложение — JavaScript приложение, которое, единожды загруженное, получает полный контроль без необходимости перезагружать страницу: рендеринг, получение данных с сервера, обработка взаимодействия с пользователем, обновление экрана…
Такие приложения выглядят более нативно чем традиционные веб-страницы, где приложение зависит от сервера. Например, если вы используете Trello, вы можете заметить насколько быстро создаются карточки.
Конечно же, с большой силой приходит большая ответственность. В традиционных веб-приложениях ваше серверное приложение включает доменную модель и бизнес-правила, технологию доступа к данным для работы с базой данных и слой контроллеров для управления тем, как HTML страницы собираются в ответ на HTTP запросы.
C SPA всё несколько сложнее. Вам всё ещё нужно серверное приложение, включающую вашу доменную модель и правила, веб-сервер, база данных и какая-то технология доступа к данным… и ещё куча дополнительных штук сверху:
Для сервера:
Для нового JavaScript клиента:
Суммируя, с SPA у вас будет ещё одно приложение для поддержки. И новый набор проблем для решения. И заметьте, вы не можете заменить одно приложение другим. Вам всё ещё нужно серверное приложение (теперь оно будет рендерить JSON вместо HTML).
Если вы никогда не работали с SPA, вы можете недооценивать сложности, с которыми вы столкнётесь. Я знаю это, потому что я допустил те же ошибки в прошлом. Отрендерить JSON? Я могу с этим справиться. Богатая модель доменных объектов в JavaScript? Звучит забавно. И, знаешь, этот фреймворк решит все эти проблемы. Отличный пирог!
Неверно.
Обмен данными между вашим новым приложением и сервером — сложная проблема.
Есть две противоположных силы:
Кроме этого, вам нужно подумать о том, что загружать, и когда это делать для каждого их экранов. Я имею в виду, что вам нужен баланс между начальной загрузкой, тем, что нужно сразу, и тем, что может быть загружено лениво, и разработать API, который позволяет это делать.
Некоторые стандарты могут тут помочь. JSON API, чтобы стандартизировать JSON формат; или GraphQL, для выборки только нужных данных, таких сложных как потребуется, одним запросом. Но ни один из них не спасёт вас от:
И оба эти аспекта представляют собой достаточный объём дополнительной работы.
Люди привыкли ассоциировать SPA со скоростью, но правда такова, что заставить их загружаться быстро не так-то уж и просто. Этому есть множество причин:
Это не значит, что невозможно заставить SPA загружаться быстро. Я просто говорю, что это сложно и вам стоит позаботиться об этом, поскольку это не придёт само вместе с архитектурой SPA.
Например, Discourse, SPA на базе Ember, имеет фантастическое время загрузки, но помимо всего прочего, они предзагружают большой объём JSON данных в виде части начального HTML, чтобы не делать дополнительные запросы. И отмечу, что команда Discourse помешаны в хорошем смысле на скорости и их навыки сильно выше среднего. Подумайте об этом перед тем, как с лёгкостью воспроизводить то же самое в вашем SPA.
Амбициозное решение этой проблемы — изоморфный JavaScript: отрендерите вашу начальную страницу на сервере и быстро отдайте клиенту, пока в фоне SPA загружается и получает полный контроль, когда готово.
Этот подход требует исполнения JavaScript рантайма на сервере, и он тоже не без технических проблем. Например, разработчики должны учитывать события загрузки SPA, поскольку процесс загрузки меняется.
Мне нравится возможность переиспользования кода в этой идее, но я не видел реализации, позволившей бы мне идти в противоположном направлении. Кроме того, мне кажется очень забавным такой процесс рендеринга страниц:
— Server: запрос к серверному API
— Server: запрос к базе данных
— Server: сгенерировать JSON
— Server: преобразовать JSON в HTML
— Client: отобразить начальный HTML
— Client: загрузить SPA
— Client: распарсить начальный HTML и подписаться на события DOM
Не могли бы вы просто запросить данные из БД, сгенерировать HTML и начать работать?
Это не совсем честно, потому что вы будете не SPA и потому что большая часть этой магии спрятана за фреймворком, но это всё ещё кажется мне неправильным.
Разрабатывать приложения с богатым пользовательским интерфейсом сложно. Это всё потому, что это одна из проблем, которая вдохновила появление объектно-ориентированного подхода и многих шаблонов проектирования.
Управлять состоянием на клиенте сложно. Традиционные веб-сайты обычно фокусируются на экранах с одной ответственностью, которые теряют состояние, когда перезагружаются. В SPA же приложение отвечает за то, чтобы всё состояние и обновления экрана во время использования были консистентными и проходили гладко.
На практике, если вы начинали писать JavaScript небольшими порциями, чтобы улучшить взаимодействие, то в SPA вам придётся писать тонны дополнительного JavaScript кода. Тут вам стоит убедиться, что вы делаете всё правильно.
Существует столько же разных архитектур, сколько SPA-фреймворков:
Из всех фреймворков, что я видел, Ember — мой любимый. Я обожаю его согласованность и то, что он довольно упрямый. Мне также нравится его модель программирования в последних версиях, сочетающая традиционный MVC, компоненты и роутинг.
С другой стороны, я сильно против Flux/Redux лагеря. Я видел так много умных людей, применяющих их, что приложил все усилия к его изучению и пониманию и ни один раз. Я не могу не трясти головой от разочарования, когда я вижу код. Я не вижу себя счастливым во время написания такого кода.
Наконец, Я не могу смириться со смешением HTML и CSS в компонентах, полных JavaScript логики. Я понимаю какую проблему это решает, но проблемы, которые привносит этот подход, не делают его стоящим этого.
Оставим личные предпочтения, суть в том, что если вы выберете путь SPA, то у вас появится очень сложная проблема: создать архитектуру вашего нового приложения правильно. И индустрия довольно далеко от того, чтобы прийти к согласию по вопросу — как это делать. Каждый год появляются новые фреймворки, шаблоны и версии фреймворков, что слегка меняет модель программирования. Вам потребуется писать и поддерживать тонну кода, основываясь на вашем архитектурном выборе, так что подумайте об этом как следует.
При работе с SPA вероятно вы встретитесь с дублированием кода.
Для вашей SPA логики, вы захотите создать богатую модель объектов, представляющих вашу доменную область, и бизнес логику. И вам все ещё нужно всё то же самое для серверной логики. И это вопрос времени, когда вы начнёте копировать код.
Например, представим, что вы работаете с инвойсами. Вы возможно имеете класс Invoice в JavaScript, который содержит метод total, который суммирует цену всех элементов, чтобы вы могли отрендерить стоимость. На сервере, вам также понадобится класс Invoice с методом total для вычисления этой стоимости, чтобы отправить её по e-mail. Видишь? Клиентский и серверный класс Invoice реализуют одинаковую логику. Дублирование кода.
Как сказано выше, изоморфный JavaScript мог бы нивелировать эту проблему, позволяя проще переиспользовать код. И я говорю нивелировать, потому что соответствие между клиентом и сервером не всегда 1-к-1. Вы захотите быть уверены, что некоторый код никогда не покидает сервер. Большое количество кода имеет смысл только для клиента. А также, некоторые аспекты просто отличаются (например, серверный элемент может сохранять данные в БД, а клиент может использовать удалённый API). Переиспользование кода, даже если это возможно, — это сложная проблема.
Вы можете поспорить, что вам не нужна богатая модель в вашем SPA и что вы вместо этого будете работать с JSON/JavaScript объектами напрямую, распределяя логику по компонентам UI. Теперь у вас есть то же самое дублирование кода, перемешанное с вашим кодом UI, удачи с этим.
И то же самое случится если вы захотите шаблоны для рендеринга HTML между сервером и клиентом. Например, для SEO, как насчёт сгенерировать страницы на сервере для поисковых роботов? Вам потребуется заново написать ваши шаблоны на сервере и убедится, что они синхронизированы с клиентскими. Опять дублирование кода.
Необходимость воспроизводить логику шаблонов на сервере и клиенте, по моему опыту, — источник возрастающего несчастья программистов. Сделать это один раз — это нормально. Когда вы сделаете это в 20ый раз, вы схватитесь за голову. Сделав это в 50ый раз, вы задумаетесь, нужны ли все эти SPA штуки.
По моему опыту, разрабатывать хорошие SPA — это намного более сложная задача, чем написание веб-приложений с генерацией на сервере.
Во-первых, неважно насколько вы осторожны, неважно как много тестов вы пишете. Чем больше кода вы пишете, больше багов у вас будет. И SPA представляет (извините, если я сильно давлю) огромную кучу кода для написания и поддержки.
Во-вторых, как упоминалось выше, разработка богатого GUI это сложно и выливается в сложные системы, состоящие из множества элементов, взаимодействующих друг с другом. Чем более сложную систему вы создаёте, тем больше у вас багов. И по сравнению с традиционными веб-приложениями, использующими MVC, сложность SPA просто безумная.
Например, для сохранения консистетности на сервере вы можете использовать ограничения в БД, валидации модели и транзакции. Если что-то пойдёт не так, вы отвечаете с сообщением об ошибке. В клиенте, всё слегка сложнее. Очень многое может пойти не так просто потому, что слишком много всего происходит. Может быть так, что какая-то запись сохраняется удачно, а какая-то другая запись нет. Возможно, вы перешли в оффлайн посередине какой-то операции. Вы должны убедиться, что UI остаётся консистентным, и что приложение восстанавливается после ошибки. Всё это возможно, конечно, только намного сложнее.
Это звучит глупо, но для разработки SPA, вам нужны разработчики, которые знают, что с этим делать. В то же время, вам не стоит недооценивать сложность SPA, вам не стоит думать, что любой опытный веб-разработчик с правильной мотивацией и пониманием может написать отличное SPA с нуля. Вам нужны правильные навыки и опыт или ожидайте, что важные ошибки будут сделаны. Я знаю это, потому что это в точности мой случай.
Это возможно более важный вызов для вашей компании, чем вы думаете. Подход SPA поощряет команды узкой специализации вместо команд из специалистов общего профиля:
Шансы таковы, что вы окажетесь с людьми, которые не могут работать с SPA, и которые не могут работать на серверной стороне, просто потому что они не знают как.
Эта специализация может идеально подходить для Facebook или Google и их команд, состоящих из нескольких слоев инженерных войск. Но будет ли это хорошо для вашей команды из 6 человек?
Есть 3 вещи, входящих в современный Rails, которые могут изменить ваше мнение о разработке современных веб-приложений:
Трудно понять, какого это применять какой-то подход, не поиграв с ним. Поэтому я сделаю несколько отсылок на Basecamp в следующих разделах. Я не имею никакого отношения к Basecamp, кроме как счастливый пользователь. Что касается этой статьи, это просто хороший живой пример современного Rails, который вы можете попробовать бесплатно.
Идея Turbolinks проста: ускорьте ваше приложение, полностью заменив перезагрузку страниц на AJAX запросы, которые заменяют `` элемент. Внутреннее колдовство, выполняющее эту работу, скрыто. Как разработчик, вы можете сосредоточиться на традиционном серверном программировании.
Turbolinks вдохновлён pjax и прошёл через несколько ревизий.
Раньше я беспокоился о его производительности. Я был неправ. Ускорение огромно. То, что меня убедило, это то, как я использовал его в проекте, но вы можете просто попробовать пробную версию в Basecamp и поиграться с ней. Попробуйте создать проект с некоторыми элементами, а затем перемещайтесь по ним, щелкая по разделам. Это даст вам хорошее представление о том, как выглядит Turbolinks.
Я не думаю, что Turbolinks просто потрясает своей новизной (pjax — 8 лет). Или своей технической утонченностью. Меня поражает то, как простая идея может повысить вашу производительность на порядок по сравнению с альтернативой SPA.
Позвольте мне выделить некоторые проблемы, которые он устраняет:
MVC на сервере, в варианте, используемом Rails и многими другими фреймворками, намного проще, чем любой из шаблонов, используемых для архитектуры богатых графических интерфейсов: получить запрос, поработать с БД для его удовлетворения и отобразить страницу HTML в качестве ответа.
Наконец, ограничение, что всегда заменяется имеет замечательный эффект: вы можете сосредоточиться на первоначальном рендеринге страниц вместо того, чтобы обновлять определенные разделы (или обновлять некоторые состояния в мире SPA). В общем случае он просто все делает.
Заметьте, я говорю не об обозначении проблем, а об их устранении. Например, GraphQL или SPA-регидратация — это суперумные решения для очень сложных проблем. Но что, если вместо того, чтобы пытаться найти решение, вы ставите себя в ситуацию, когда эти проблемы не существуют? Это изменение подхода к проблеме. И мне потребовались годы, чтобы в полной мере оценить способность этого подхода решать проблемы.
Разумеется, Turbolinks не является беспроблемной серебряной пулей. Самая большая проблема заключается в том, что он может сломать существующий JavaScript код:
Помните, когда рендеринг HTML через Ajax был в тренде 15 лет назад? Угадай, что? Это все еще замечательный инструмент, который есть в вашем арсенале:
Вы можете видеть, как этот подход ощущается в Basecamp, открыв меню вашего профиля, нажав на верхнюю правую кнопку:
![](https://habrastorage.org/webt/yc/6b/th/yc6bth2x9on2cvks1r34w937xey.gif)
Открывается мгновенно. Со стороны разработки вам не нужно заботиться о сериализации JSON и клиентской стороне. Вы можете просто отобразить этот фрагмент на сервере, используя все возможности Rails.
Похожий инструмент, который Rails включает в себя в течение многих лет, — это серверные ответы JavaScript (SJR). Они позволяют вам отвечать на запросы Ajax (обычно формировать представления) с JavaScript, который исполняется клиентом. Он дает те же преимущества, что AJAX-рендеринг HTML-фрагментов: исполняется очень быстро, вы можете повторно использовать код на стороне сервера, и вы можете напрямую обращаться к базе данных для создания ответа.
Вы можете увидеть, как это происходит, если вы зайдёте в Basecamp и пытаетесь создать новый todo. После того, как вы нажмете «Добавить todo», сервер сохранит todo и ответит фрагментом JavaScript, который добавляет новый todo в DOM.
Я думаю, что многие разработчики сегодня смотрят на AJAX-рендеринг и SJR-ответы с презрением. Я тоже это помню. Они являются инструментом и, как таковые, могут подвергаться злоупотреблениям. Но при правильном использовании это потрясающее решение. Позвольте вам предложить отличный UX и интерактивность по очень низкой цене. К сожалению, как и Turbolinks, их сложно оценить, если вы ещё не сражались с SPA.
Stimulus — это JavaScript фреймворк, опубликованный несколько месяцев назад. Он не заботится о рендеринге или об управлении состоянием на основе JavaScript. Вместо этого, это просто хороший, современный способ организации JavaScript, который вы используете для добавления HTML:
Если вы примете Rails-путь, ваш JavaScript будет сосредоточен на изменении HTML-кода, созданного на стороне сервера, и улучшении взаимодействия (c небольшим количеством JavaScript). Stimulus предназначен для организации такого кода. Это не система SPA и не претендует на то, чтобы быть таковой.
Я использовал Stimulus в нескольких проектах, и мне это очень нравится. Он избавляет от кучи шаблонного кода, он построен на последних веб-стандартах и читается очень красиво. И что-то, что я люблю особенно: теперь это стандартный способ сделать что-то, что до сих пор приходилось решать в каждом приложении.
Turbolinks обычно продается как «Получите все преимущества SPA без каких-либо неудобств». Я не думаю, что это полностью верно:
Теперь разработка — это игра компромиссов. И в этой игре:
Я считаю, что с Rails вы можете получить 90% того, что предлагает SPA с 10% усилий. Что касается производительности, Rails убивает SPA. Что касается UX, я думаю, что многие разработчики делают ту же ошибку, что и я, предполагая, что SPA UX является непревзойденным. Это не так. Фактически, как обсуждалось выше, вам лучше знать, что вы делаете при создании своего SPA, или UX будет на самом деле хуже.
Я наблюдаю за тем, как компании массово принимают SPA фреймворки, и вижу бесчисленные статьи о том, как делать причудливые вещи в стиле SPA. Я думаю, что существует много «использований неправильного инструмента для работы», поскольку я твердо верю, что типы приложений, которые оправдывают использование SPA, ограничены.
И я говорю, что оправдывают, потому что SPA сложны. Во всяком случае, я надеюсь, что убедил вас в этом. Я не говорю, что невозможно создать великолепные SPA-приложения, или что современные Rails-приложения великолепны по определению, просто один подход суперсложный, а другой намного проще.
Готовя эту статью, я наткнулся на этот твит:
![](https://habrastorage.org/r/w1560/webt/px/54/rc/px54rc9odiplrsororeiozro--c.png)
Это заставило меня посмеяться, потому что я выберу первые варианты, если альтернатива не будет оправдана. Он также является представителем своего рода мышления разработчиков, который обожает сложность и процветает в этом, вплоть до того, что считает сумасшедшими других людей с другими критериями.
По прошествии многих лет я понял, что сложность часто является выбором. Но в мире программирования на удивление сложно выбрать простоту. Мы так высоко ценим сложность, что принятие простоты часто заставляет мыслить по-другому, что по определению — трудно.
Помните, что вы можете избавиться от неприятностей. Если вы выберете путь SPA, убедитесь, что он оправдан и вы понимаете проблемы. Если вы не уверены, экспериментируйте с разными подходами и убедитесь сами. Возможно, Facebook или Google, в их масштабе, не имеют роскоши принимать такие решения, но вы, вероятно, можете.
И если вы разработчик Rails, который покинул Rails много лет назад, я рекомендую вам вернуться к нему. Я думаю, вы будете в восторге.
![](https://habrastorage.org/webt/bm/nz/o6/bmnzo6gdhrpqqdufks3kgylhxjw.jpeg)
Я вспоминаю, как думал, что Rails фокусируется на неправильной цели, когда DHH анонсировали Turbolinks в 2012 году. Тогда я был убеждён в том, что мгновенное время ответа во время взаимодействия с пользователем — это ключ к превосходному UX. Из-за возникающих задержек сети, такая интерактивность возможна, только если вы минимизируете зависимость от сети и вместо сетевых обращений поддерживаете большую часть состояния на клиенте.
Я думал, что это необходимо для приложений, над которыми я работал. И с таким мнением я перепробовал множество подходов и фреймворков для реализации одного и того же шаблона: Single-page applications (SPA). Я верил, что SPA — это будущее. Несколько лет спустя, я не уверен в том, каково же будущее, но я точно хочу найти альтернативу.
Кроличья нора SPA
Одностраничное приложение — JavaScript приложение, которое, единожды загруженное, получает полный контроль без необходимости перезагружать страницу: рендеринг, получение данных с сервера, обработка взаимодействия с пользователем, обновление экрана…
Такие приложения выглядят более нативно чем традиционные веб-страницы, где приложение зависит от сервера. Например, если вы используете Trello, вы можете заметить насколько быстро создаются карточки.
Конечно же, с большой силой приходит большая ответственность. В традиционных веб-приложениях ваше серверное приложение включает доменную модель и бизнес-правила, технологию доступа к данным для работы с базой данных и слой контроллеров для управления тем, как HTML страницы собираются в ответ на HTTP запросы.
C SPA всё несколько сложнее. Вам всё ещё нужно серверное приложение, включающую вашу доменную модель и правила, веб-сервер, база данных и какая-то технология доступа к данным… и ещё куча дополнительных штук сверху:
Для сервера:
- API, удовлетворяющий потребности вашего клиента в данных
- Система сериализации в JSON для обмена данными с клиентом и система кэширования, которая поддерживает это
Для нового JavaScript клиента:
- Система шаблонов для преобразования данных в HTML
- Представление вашей доменной модели и правил
- Слой доступа к данным в клиентском приложении для передачи данных серверу
- Система обновления представлений при изменении данных
- Система для связи URL-адресов и экранов (надеюсь, вы не будете использовать один адрес для всего, это выглядит не совсем как веб-приложение)
- Система для склеивания всех компонентов, необходимых для отображения экранов и получения данных для этого
- Новые шаблоны и архитектура для организации всего когда
- Система для обработки ошибок, логгирования, отслеживания исключений и т.д.
- Система для генерации JSON для серверных запросов
- Фреймворк для автоматизированного тестирования, поддерживающая ваш SPA
- Дополнительный набор тестов для написания и поддержки
- Дополнительный набор инструментов для сборки, упаковки и развёртывания нового приложения
Суммируя, с SPA у вас будет ещё одно приложение для поддержки. И новый набор проблем для решения. И заметьте, вы не можете заменить одно приложение другим. Вам всё ещё нужно серверное приложение (теперь оно будет рендерить JSON вместо HTML).
Если вы никогда не работали с SPA, вы можете недооценивать сложности, с которыми вы столкнётесь. Я знаю это, потому что я допустил те же ошибки в прошлом. Отрендерить JSON? Я могу с этим справиться. Богатая модель доменных объектов в JavaScript? Звучит забавно. И, знаешь, этот фреймворк решит все эти проблемы. Отличный пирог!
Неверно.
API и обмен данными.
Обмен данными между вашим новым приложением и сервером — сложная проблема.
Есть две противоположных силы:
- Вы захотите делать так мало запросов к серверу, как только можно, чтобы улучшить производительность
- Преобразование одной записи в JSON это просто. А вот смешение больших моделей различных записей для минимизации количества запросов — совсем нет. Вам понадобится аккуратно разработать логику сериализации для оптимизации количества запросов к базе данных и сохранения производительности на уровне.
Кроме этого, вам нужно подумать о том, что загружать, и когда это делать для каждого их экранов. Я имею в виду, что вам нужен баланс между начальной загрузкой, тем, что нужно сразу, и тем, что может быть загружено лениво, и разработать API, который позволяет это делать.
Некоторые стандарты могут тут помочь. JSON API, чтобы стандартизировать JSON формат; или GraphQL, для выборки только нужных данных, таких сложных как потребуется, одним запросом. Но ни один из них не спасёт вас от:
- Проработки каждого обмена данными
- Реализации запросов, позволяющих выбрать данные эффективно на сервере
И оба эти аспекта представляют собой достаточный объём дополнительной работы.
Время начальной загрузки
Люди привыкли ассоциировать SPA со скоростью, но правда такова, что заставить их загружаться быстро не так-то уж и просто. Этому есть множество причин:
- Приложению нужны данные перед тем, как отрендерить что-то, и чтобы распарсить достаточно большой объём JavaScript нужно время.
- Сверх начального HTTP запроса для загрузки приложения, обычно нужно сделать один или больше запросов для получения JSON данных, необходимых для рендеринга экрана.
- Клиент должен преобразовать JSON в HTML, чтобы показать хоть что-то. В зависимости от устройства и количества JSON для преобразования это может вносить заметные задержки.
Это не значит, что невозможно заставить SPA загружаться быстро. Я просто говорю, что это сложно и вам стоит позаботиться об этом, поскольку это не придёт само вместе с архитектурой SPA.
Например, Discourse, SPA на базе Ember, имеет фантастическое время загрузки, но помимо всего прочего, они предзагружают большой объём JSON данных в виде части начального HTML, чтобы не делать дополнительные запросы. И отмечу, что команда Discourse помешаны в хорошем смысле на скорости и их навыки сильно выше среднего. Подумайте об этом перед тем, как с лёгкостью воспроизводить то же самое в вашем SPA.
Амбициозное решение этой проблемы — изоморфный JavaScript: отрендерите вашу начальную страницу на сервере и быстро отдайте клиенту, пока в фоне SPA загружается и получает полный контроль, когда готово.
Этот подход требует исполнения JavaScript рантайма на сервере, и он тоже не без технических проблем. Например, разработчики должны учитывать события загрузки SPA, поскольку процесс загрузки меняется.
Мне нравится возможность переиспользования кода в этой идее, но я не видел реализации, позволившей бы мне идти в противоположном направлении. Кроме того, мне кажется очень забавным такой процесс рендеринга страниц:
— Server: запрос к серверному API
— Server: запрос к базе данных
— Server: сгенерировать JSON
— Server: преобразовать JSON в HTML
— Client: отобразить начальный HTML
— Client: загрузить SPA
— Client: распарсить начальный HTML и подписаться на события DOM
Не могли бы вы просто запросить данные из БД, сгенерировать HTML и начать работать?
Это не совсем честно, потому что вы будете не SPA и потому что большая часть этой магии спрятана за фреймворком, но это всё ещё кажется мне неправильным.
Архитектура
Разрабатывать приложения с богатым пользовательским интерфейсом сложно. Это всё потому, что это одна из проблем, которая вдохновила появление объектно-ориентированного подхода и многих шаблонов проектирования.
Управлять состоянием на клиенте сложно. Традиционные веб-сайты обычно фокусируются на экранах с одной ответственностью, которые теряют состояние, когда перезагружаются. В SPA же приложение отвечает за то, чтобы всё состояние и обновления экрана во время использования были консистентными и проходили гладко.
На практике, если вы начинали писать JavaScript небольшими порциями, чтобы улучшить взаимодействие, то в SPA вам придётся писать тонны дополнительного JavaScript кода. Тут вам стоит убедиться, что вы делаете всё правильно.
Существует столько же разных архитектур, сколько SPA-фреймворков:
- Большинство фреймворков отличаются от традиционного шаблона MVC. Ember поначалу был вдохновлён Cocoa MVC, но достаточно сильно поменял свою программную модель в последних версиях.
- Прослеживается тенденция, что разработчики предпочитают компоненты, а не традиционное разделение на контроллер и представление (некоторые фреймворки, такие как Ember и Angular, перешли к такому подходу в последних версиях). Все фреймворки реализуют некоторое подобие одностороннего биндинга данных. Двусторонний биндинг не приветствуется из-за побочных эффектов, которые он может вносить.
- Большинство фреймворков включают систему роутинга, которая позволяет сопоставлять URL-адреса и экраны, и определяет, как создавать экземпляры компонентов для рендеринга. Это уникальный подход веб, который не существует в традиционных настольных интерфейсах.
- Большинство фреймворков отделяют HTML шаблоны от JavaScript кода, но React ставит на смешение HTML-генерации и JavaScript и делает это вполне успешно, учитывая его массовое использование. Сейчас также наблюдается хайп вокруг встраивания CSS в JavaScript. Facebook со своей архитектурой Flux довольно сильно повлиял на индустрию, и контейнеры, такие как Redux, vuex и др., находятся под сильным влиянием от него.
Из всех фреймворков, что я видел, Ember — мой любимый. Я обожаю его согласованность и то, что он довольно упрямый. Мне также нравится его модель программирования в последних версиях, сочетающая традиционный MVC, компоненты и роутинг.
С другой стороны, я сильно против Flux/Redux лагеря. Я видел так много умных людей, применяющих их, что приложил все усилия к его изучению и пониманию и ни один раз. Я не могу не трясти головой от разочарования, когда я вижу код. Я не вижу себя счастливым во время написания такого кода.
Наконец, Я не могу смириться со смешением HTML и CSS в компонентах, полных JavaScript логики. Я понимаю какую проблему это решает, но проблемы, которые привносит этот подход, не делают его стоящим этого.
Оставим личные предпочтения, суть в том, что если вы выберете путь SPA, то у вас появится очень сложная проблема: создать архитектуру вашего нового приложения правильно. И индустрия довольно далеко от того, чтобы прийти к согласию по вопросу — как это делать. Каждый год появляются новые фреймворки, шаблоны и версии фреймворков, что слегка меняет модель программирования. Вам потребуется писать и поддерживать тонну кода, основываясь на вашем архитектурном выборе, так что подумайте об этом как следует.
Дублирование кода
При работе с SPA вероятно вы встретитесь с дублированием кода.
Для вашей SPA логики, вы захотите создать богатую модель объектов, представляющих вашу доменную область, и бизнес логику. И вам все ещё нужно всё то же самое для серверной логики. И это вопрос времени, когда вы начнёте копировать код.
Например, представим, что вы работаете с инвойсами. Вы возможно имеете класс Invoice в JavaScript, который содержит метод total, который суммирует цену всех элементов, чтобы вы могли отрендерить стоимость. На сервере, вам также понадобится класс Invoice с методом total для вычисления этой стоимости, чтобы отправить её по e-mail. Видишь? Клиентский и серверный класс Invoice реализуют одинаковую логику. Дублирование кода.
Как сказано выше, изоморфный JavaScript мог бы нивелировать эту проблему, позволяя проще переиспользовать код. И я говорю нивелировать, потому что соответствие между клиентом и сервером не всегда 1-к-1. Вы захотите быть уверены, что некоторый код никогда не покидает сервер. Большое количество кода имеет смысл только для клиента. А также, некоторые аспекты просто отличаются (например, серверный элемент может сохранять данные в БД, а клиент может использовать удалённый API). Переиспользование кода, даже если это возможно, — это сложная проблема.
Вы можете поспорить, что вам не нужна богатая модель в вашем SPA и что вы вместо этого будете работать с JSON/JavaScript объектами напрямую, распределяя логику по компонентам UI. Теперь у вас есть то же самое дублирование кода, перемешанное с вашим кодом UI, удачи с этим.
И то же самое случится если вы захотите шаблоны для рендеринга HTML между сервером и клиентом. Например, для SEO, как насчёт сгенерировать страницы на сервере для поисковых роботов? Вам потребуется заново написать ваши шаблоны на сервере и убедится, что они синхронизированы с клиентскими. Опять дублирование кода.
Необходимость воспроизводить логику шаблонов на сервере и клиенте, по моему опыту, — источник возрастающего несчастья программистов. Сделать это один раз — это нормально. Когда вы сделаете это в 20ый раз, вы схватитесь за голову. Сделав это в 50ый раз, вы задумаетесь, нужны ли все эти SPA штуки.
Хрупкость
По моему опыту, разрабатывать хорошие SPA — это намного более сложная задача, чем написание веб-приложений с генерацией на сервере.
Во-первых, неважно насколько вы осторожны, неважно как много тестов вы пишете. Чем больше кода вы пишете, больше багов у вас будет. И SPA представляет (извините, если я сильно давлю) огромную кучу кода для написания и поддержки.
Во-вторых, как упоминалось выше, разработка богатого GUI это сложно и выливается в сложные системы, состоящие из множества элементов, взаимодействующих друг с другом. Чем более сложную систему вы создаёте, тем больше у вас багов. И по сравнению с традиционными веб-приложениями, использующими MVC, сложность SPA просто безумная.
Например, для сохранения консистетности на сервере вы можете использовать ограничения в БД, валидации модели и транзакции. Если что-то пойдёт не так, вы отвечаете с сообщением об ошибке. В клиенте, всё слегка сложнее. Очень многое может пойти не так просто потому, что слишком много всего происходит. Может быть так, что какая-то запись сохраняется удачно, а какая-то другая запись нет. Возможно, вы перешли в оффлайн посередине какой-то операции. Вы должны убедиться, что UI остаётся консистентным, и что приложение восстанавливается после ошибки. Всё это возможно, конечно, только намного сложнее.
Организационные вызовы
Это звучит глупо, но для разработки SPA, вам нужны разработчики, которые знают, что с этим делать. В то же время, вам не стоит недооценивать сложность SPA, вам не стоит думать, что любой опытный веб-разработчик с правильной мотивацией и пониманием может написать отличное SPA с нуля. Вам нужны правильные навыки и опыт или ожидайте, что важные ошибки будут сделаны. Я знаю это, потому что это в точности мой случай.
Это возможно более важный вызов для вашей компании, чем вы думаете. Подход SPA поощряет команды узкой специализации вместо команд из специалистов общего профиля:
- SPA фреймворки представляют собой сложные части программного обеспечения, в которых требуется бесчисленное количество часов опыта, чтобы быть продуктивным. Только люди из вашей компании, проводящие эти часы за кодом, смогут поддерживать эти приложения.
- SPA фреймворки требуют продуманных и производительных API. Выполнение этих требований требует совершенно иного набора навыков, чем те, что необходимы работы с SPA.
Шансы таковы, что вы окажетесь с людьми, которые не могут работать с SPA, и которые не могут работать на серверной стороне, просто потому что они не знают как.
Эта специализация может идеально подходить для Facebook или Google и их команд, состоящих из нескольких слоев инженерных войск. Но будет ли это хорошо для вашей команды из 6 человек?
Современный Rails
Есть 3 вещи, входящих в современный Rails, которые могут изменить ваше мнение о разработке современных веб-приложений:
- одна из них — Turbolinks и это взрыв мозга
- другая — старый друг, которого сегодня упускают из вида: SJR ответы и простые AJAX запросы для рендеринга
- и последняя была добавлена недавно: Stimulus
Трудно понять, какого это применять какой-то подход, не поиграв с ним. Поэтому я сделаю несколько отсылок на Basecamp в следующих разделах. Я не имею никакого отношения к Basecamp, кроме как счастливый пользователь. Что касается этой статьи, это просто хороший живой пример современного Rails, который вы можете попробовать бесплатно.
Turbolinks
Идея Turbolinks проста: ускорьте ваше приложение, полностью заменив перезагрузку страниц на AJAX запросы, которые заменяют `` элемент. Внутреннее колдовство, выполняющее эту работу, скрыто. Как разработчик, вы можете сосредоточиться на традиционном серверном программировании.
Turbolinks вдохновлён pjax и прошёл через несколько ревизий.
Раньше я беспокоился о его производительности. Я был неправ. Ускорение огромно. То, что меня убедило, это то, как я использовал его в проекте, но вы можете просто попробовать пробную версию в Basecamp и поиграться с ней. Попробуйте создать проект с некоторыми элементами, а затем перемещайтесь по ним, щелкая по разделам. Это даст вам хорошее представление о том, как выглядит Turbolinks.
Я не думаю, что Turbolinks просто потрясает своей новизной (pjax — 8 лет). Или своей технической утонченностью. Меня поражает то, как простая идея может повысить вашу производительность на порядок по сравнению с альтернативой SPA.
Позвольте мне выделить некоторые проблемы, которые он устраняет:
- Обмен данными. У вас его нет. Не нужно сериализовывать JSON, создавать API-интерфейсы или думать о запросах данных, которые удовлетворяют потребности клиентов с учётом производительности.
- Начальная нагрузка. В отличие от SPA, этот подход стимулирует быстрое время загрузки (by design). Для рендеринга экрана вы можете получить данные, которые вам нужны непосредственно из базы данных. И эффективный запрос данных из реляционных баз данных или кэширование HTML — это хорошо решаемые проблемы.
- Архитектура: Вам не нужна сложная архитектура для организации вашего JavaScript-кода. Вам нужно всего лишь сосредоточиться на правильной архитектуре вашего серверного приложения, что вам всё равно нужно делать при использовании SPA.
MVC на сервере, в варианте, используемом Rails и многими другими фреймворками, намного проще, чем любой из шаблонов, используемых для архитектуры богатых графических интерфейсов: получить запрос, поработать с БД для его удовлетворения и отобразить страницу HTML в качестве ответа.
Наконец, ограничение, что всегда заменяется имеет замечательный эффект: вы можете сосредоточиться на первоначальном рендеринге страниц вместо того, чтобы обновлять определенные разделы (или обновлять некоторые состояния в мире SPA). В общем случае он просто все делает.
- Дублирование кода. Существует только одно представление вашего приложения, которое живет на сервере. Ваша доменная модель, её правила, экраны приложений и т.д. Нет необходимости дублировать концепции в клиенте.
- Хрупкость. По сравнению с SPA, JavaScript для работы на ваших страницах и его сложность сокращены до небольших долей, и поэтому количество ошибок. Кроме того, вы можете полагаться на атомарное выполнение операций на сервере, используя транзакции базы данных, ограничения и валидации.
Заметьте, я говорю не об обозначении проблем, а об их устранении. Например, GraphQL или SPA-регидратация — это суперумные решения для очень сложных проблем. Но что, если вместо того, чтобы пытаться найти решение, вы ставите себя в ситуацию, когда эти проблемы не существуют? Это изменение подхода к проблеме. И мне потребовались годы, чтобы в полной мере оценить способность этого подхода решать проблемы.
Разумеется, Turbolinks не является беспроблемной серебряной пулей. Самая большая проблема заключается в том, что он может сломать существующий JavaScript код:
- Turbolinks поставляется со своим пользовательским событием «загрузка страницы», и существующие плагины, полагающиеся на регулярные загрузки страниц, не будут работать. Сегодня есть лучшие способы добавить поведение к DOM, но устаревшие виджеты не будут работать, если не будут адаптированы.
- JavaScript-код, изменяющий DOM, должен быть идемпотентным, поскольку его могут запускать несколько раз. Опять же, это делает недействительным много существующего JavaScript.
- Скорость отличная, но это не совсем как в SPA, который может обрабатывать некоторые взаимодействия, не загружая сервер. Я расскажу больше о компромиссах позже.
AJAX рендеринг и SJR ответы
Помните, когда рендеринг HTML через Ajax был в тренде 15 лет назад? Угадай, что? Это все еще замечательный инструмент, который есть в вашем арсенале:
- Получение фрагмента HTML с сервера и добавление его в DOM, ощущается супербыстрым (на 100мс быстрым).
- Вы можете рендерить HTML на сервере, что позволяет повторно использовать ваши представления и извлекать необходимые данные непосредственно из базы данных.
Вы можете видеть, как этот подход ощущается в Basecamp, открыв меню вашего профиля, нажав на верхнюю правую кнопку:
![](https://habrastorage.org/webt/yc/6b/th/yc6bth2x9on2cvks1r34w937xey.gif)
Открывается мгновенно. Со стороны разработки вам не нужно заботиться о сериализации JSON и клиентской стороне. Вы можете просто отобразить этот фрагмент на сервере, используя все возможности Rails.
Похожий инструмент, который Rails включает в себя в течение многих лет, — это серверные ответы JavaScript (SJR). Они позволяют вам отвечать на запросы Ajax (обычно формировать представления) с JavaScript, который исполняется клиентом. Он дает те же преимущества, что AJAX-рендеринг HTML-фрагментов: исполняется очень быстро, вы можете повторно использовать код на стороне сервера, и вы можете напрямую обращаться к базе данных для создания ответа.
Вы можете увидеть, как это происходит, если вы зайдёте в Basecamp и пытаетесь создать новый todo. После того, как вы нажмете «Добавить todo», сервер сохранит todo и ответит фрагментом JavaScript, который добавляет новый todo в DOM.
Я думаю, что многие разработчики сегодня смотрят на AJAX-рендеринг и SJR-ответы с презрением. Я тоже это помню. Они являются инструментом и, как таковые, могут подвергаться злоупотреблениям. Но при правильном использовании это потрясающее решение. Позвольте вам предложить отличный UX и интерактивность по очень низкой цене. К сожалению, как и Turbolinks, их сложно оценить, если вы ещё не сражались с SPA.
Stimulus
Stimulus — это JavaScript фреймворк, опубликованный несколько месяцев назад. Он не заботится о рендеринге или об управлении состоянием на основе JavaScript. Вместо этого, это просто хороший, современный способ организации JavaScript, который вы используете для добавления HTML:
- Он использует MutationObserver для привязки поведения к DOM, то есть ему не важно, как HTML появляется на странице. Конечно, это отлично работает с Turbolinks.
- Он сэкономит вам кучу шаблонного кода для привязки поведения к DOM, для привязки обработчиков к событиям и для размещения элементов в указанном контейнере.
- Он нацелен на то, чтобы ваш HTML-код был читабельным и понятным, что приятно, если вы когда-либо сталкивались с проблемой поиска того, какая часть JavaScript действует на этом проклятом элементе.
- Он поощряет сохранение состояния в DOM. Опять же, это означает, что ему не важно, как генерируется HTML, что подходит для многих сценариев, в том числе Turbolinks.
Если вы примете Rails-путь, ваш JavaScript будет сосредоточен на изменении HTML-кода, созданного на стороне сервера, и улучшении взаимодействия (c небольшим количеством JavaScript). Stimulus предназначен для организации такого кода. Это не система SPA и не претендует на то, чтобы быть таковой.
Я использовал Stimulus в нескольких проектах, и мне это очень нравится. Он избавляет от кучи шаблонного кода, он построен на последних веб-стандартах и читается очень красиво. И что-то, что я люблю особенно: теперь это стандартный способ сделать что-то, что до сих пор приходилось решать в каждом приложении.
Игра компромиссов
Turbolinks обычно продается как «Получите все преимущества SPA без каких-либо неудобств». Я не думаю, что это полностью верно:
- Приложения, построенные с использованием современного Rails, выглядят быстрыми, но SPA по-прежнему будет быстрее реагировать на взаимодействия, которые не зависят от сервера.
- Существуют сценарии, в которых SPA имеет больше смысла. Если вам нужно предложить высокий уровень интерактивности, вам нужно управлять большим количеством состояний, выполнять сложную логику на стороне клиента и т.д., SPA фреймворк сделает вашу жизнь проще.
Теперь разработка — это игра компромиссов. И в этой игре:
- Современный Rails позволяет вам создавать приложения, которые достаточно быстры и отлично выглядят.
- Для огромного множества приложений Rails позволяет реализовать те же функции с меньшим количеством кода и меньшей сложностью.
Я считаю, что с Rails вы можете получить 90% того, что предлагает SPA с 10% усилий. Что касается производительности, Rails убивает SPA. Что касается UX, я думаю, что многие разработчики делают ту же ошибку, что и я, предполагая, что SPA UX является непревзойденным. Это не так. Фактически, как обсуждалось выше, вам лучше знать, что вы делаете при создании своего SPA, или UX будет на самом деле хуже.
Заключение
Я наблюдаю за тем, как компании массово принимают SPA фреймворки, и вижу бесчисленные статьи о том, как делать причудливые вещи в стиле SPA. Я думаю, что существует много «использований неправильного инструмента для работы», поскольку я твердо верю, что типы приложений, которые оправдывают использование SPA, ограничены.
И я говорю, что оправдывают, потому что SPA сложны. Во всяком случае, я надеюсь, что убедил вас в этом. Я не говорю, что невозможно создать великолепные SPA-приложения, или что современные Rails-приложения великолепны по определению, просто один подход суперсложный, а другой намного проще.
Готовя эту статью, я наткнулся на этот твит:
![](https://habrastorage.org/webt/px/54/rc/px54rc9odiplrsororeiozro--c.png)
Это заставило меня посмеяться, потому что я выберу первые варианты, если альтернатива не будет оправдана. Он также является представителем своего рода мышления разработчиков, который обожает сложность и процветает в этом, вплоть до того, что считает сумасшедшими других людей с другими критериями.
По прошествии многих лет я понял, что сложность часто является выбором. Но в мире программирования на удивление сложно выбрать простоту. Мы так высоко ценим сложность, что принятие простоты часто заставляет мыслить по-другому, что по определению — трудно.
Помните, что вы можете избавиться от неприятностей. Если вы выберете путь SPA, убедитесь, что он оправдан и вы понимаете проблемы. Если вы не уверены, экспериментируйте с разными подходами и убедитесь сами. Возможно, Facebook или Google, в их масштабе, не имеют роскоши принимать такие решения, но вы, вероятно, можете.
И если вы разработчик Rails, который покинул Rails много лет назад, я рекомендую вам вернуться к нему. Я думаю, вы будете в восторге.