Это история о нашем фреймворке JS, который скоро будет упразднен, который используется в мобильной сети Traveloka. Нет, это не печальная история, а, скорее, путешествие, полное познаний и познаний.

История началась в начале 2015 года, когда мы решили написать собственный JavaScript-фреймворк для одностраничного приложения с рендерингом на стороне сервера.

Почему?

Вам может быть интересно, почему мы решили создать собственный JS-фреймворк.

Помните, это было в начале 2015 года, когда Angular все еще набирал обороты, jQuery был популярен как никогда, а React? Ну, давайте просто скажем, что люди до сих пор не могут справиться с HTML-in-JS.

Фронтенд-инжиниринг все еще был относительно тихим, не было усталости от JavaScript, все были довольны своим техническим выбором в то время.

Мы используем jQuery, встроенный в структуру приложения с тяжелым ООП через Google Closure Compiler. Оболочка нашего HTML-приложения была создана монолитным Java-приложением, обслуживаемым в EC2. Мы создали компоненты, используя что-то похожее на директиву Angular (не спрашивайте меня, почему мы не используем Angular). Наша мобильная сеть отлично работала как одностраничное приложение.

Так было до тех пор, пока нам не пришлось задуматься о SEO.

Поисковая оптимизация

Нравится вам это или нет, но Google - это выход для многих людей, заходящих в Интернет. Когда они ищут что-то неизвестное, первое, что им приходит в голову, - это Google.

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

Если что-то кажется слишком хорошим, чтобы быть правдой, вероятно, это так.

Наше исследование показало обратное. Использование метода оболочки приложения означает, что при первоначальном рендеринге почти не отображается никакого контента. Роботу Googlebot необходимо загрузить и проанализировать большой фрагмент JS, прежде чем что-либо увидеть, и, как вы, возможно, знаете, с большинством SPA (включая наши), ожидание может занять много времени. Краулер, не любит ждать.

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

Рендеринг на стороне сервера (SSR)

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

Мы никогда раньше не делали ничего подобного, ресурсов по этой теме было мало, а предлагаемая в то время структура не давала нам того, чего мы действительно хотели. Это либо клиентские SPA, либо классические прогрессивные улучшения с небольшим количеством JS.

Затем мы сделали то, что сделал бы любой любопытный инженер, мы открыли множество сайтов и проанализировали, как их сайты работают. Мы хотели знать, есть ли на сайте хотя бы такие же требования, как и у нас. Наконец, мы наткнулись на Twitter.com (старый твиттер, а не новый PWA Twitter Lite, построенный с использованием RNW 😉), и мы улыбнулись.

Гибридное одностраничное приложение

К нашему большому удивлению, Twitter уже реализовал именно то, что мы хотели (позже мы узнали, что Google и Facebook также сделали то же самое). При более внимательном рассмотрении с помощью сетевой панели Chrome DevTools мы увидели, как они это реализовали:

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

Помните наш стек Java-jQuery? Оказалось, что наш стек отлично подошел при таком подходе. Мы уже визуализировали HTML (хотя и только оболочку приложения) на Java, и уже есть клиентская навигация с хеш-историей. Нам нужен был способ получить полезную нагрузку JSON при навигации на стороне клиента. Именно тогда мы начали создавать собственный JS-фреймворк.

Мы называем это блоками.

Блоки Фреймворка

Да, я знаю, это глупо, лениво и неоригинально. К настоящему времени вы, должно быть, устали слышать о лего, блоке, кирпиче. Модуляризация бла-бла-бла. По правде говоря, название произошло от того, как мы структурировали наши страницы и компоненты.

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

Мы предоставили набор заполнителей, в которые может отображаться компонент. Каждый компонент будет иметь определенный заполнитель (например, наиболее распространенными заполнителями были заголовок, контент и нижний колонтитул). Заполнитель может содержать несколько компонентов, но компонент может иметь только один заполнитель).

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

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

JSON API?

Другой важной частью правильной работы этой платформы было предоставление JSON API, который возвращал частичный HTML и список необходимых JS и CSS. Мы сделали это, используя ту же концепцию заполнителя, о которой говорилось ранее.

Каждая страница была реализована как класс Java, расширяющий этот абстрактный класс:

Чтобы создать страницу, мы расширили этот класс и предоставили базовый шаблон для передачи в super (). В конструкторе класса мы могли бы зарегистрировать наши компоненты и файл JS, необходимые для рендеринга этой страницы. Когда пришел запрос, мы уже знали, какие компоненты существуют на этой странице, и мы могли сгенерировать полную HTML-строку, выполнив метод renderHtml.

Мы сделали то же самое в методе renderJson. Поскольку список компонентов был известен заранее, мы могли создать определенный тип ответа, в котором перечислены компоненты, которые должны были отображаться в конкретном заполнителе, если произошла навигация на стороне клиента.

Как мы узнали разницу между первоначальной визуализацией и навигацией на стороне клиента? Мы помещаем в наш URL специальную строку запроса. Контроллер может определить, какой ответ будет отправлен, только на основе строки запроса. Это может быть так просто:

Initial load
GET https://m.traveloka.com/en-id/faq
Content-Type: text/html
Client side navigation
GET https://m.traveloka.com/en-id/hotel?type=json
Content-Type: application/json

Обработка ошибок

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

Когда в середине навигации происходила ошибка, мы просто обновляли страницу.

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

Устаревание

Перенесемся на 3 года, и почти ни одна страница больше не использует этот фреймворк. Остаток все еще можно увидеть в разных местах, но это уже не та структура, которую мы строили раньше. Сегодня мы используем обычный набор приложений для реагирования, которые вы можете найти в сети: react-router, react-loadable, next.js и другие.

Если этот фреймворк решает большую часть нашего варианта использования, почему мы отказались от него?

Одна из причин частично связана с миграцией с монолита Java на микросервисы Node.js (для веб-серверов) в конце 2015 года.

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

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

Другая причина - сложность интеграции кода JS внутри компилятора закрытия Google с современной экосистемой JS вокруг npm. Мы даже построили транспиллер с GCC-ES5 на Flowtype-ES6, но его так и не подобрали. Учитывая, что React набирает обороты, мы не уверены, что хотим сохранить наши спагетти jQuery в будущем.

Заключение

Трудно обеспечить хорошую базовую структуру, которая хорошо работает на устройствах наших пользователей, при этом поддерживая хороший DX для наших инженеров. Это требует постоянных усилий. Еще сложнее было то, что нам пришлось создать это с ограниченными знаниями, которые у нас были (когда мы писали фреймворк, мы проработали в компании меньше года).

Но написание этого фреймворка дало нам много знаний. Мы изучили сложности между мобильными браузерами, особенно в части клиентской навигации (Blackberry и Windows Mobile взяли на себя корону за это). Мы узнали, как работает Интернет, мы могли косвенно учиться у других людей, очищая их сайты с помощью DevTools. Мы также узнали, что мы делали разбиение кода на основе маршрутов еще до того, как это стало круто!

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

Так что да, мы создали JS-фреймворк не для развлечения. Если вы слышали, что нам нравится создавать фреймворк самим, это потому, что в экосистеме с открытым исходным кодом нет ничего, что могло бы у нас работать. Удовольствие - это только один из возможных побочных эффектов, которые случаются, когда вы сами создаете фреймворк. Страх - другой побочный эффект 😆.

На данный момент мы активно разрабатываем еще одну среду пользовательского интерфейса на основе React, решающую некоторые проблемы, связанные с универсальным отрисованным приложением реагирования (SSR + гидратация на стороне клиента).

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

Нам не терпится поделиться нашими выводами после того, как мы закончим эксперименты с этой новой структурой.

Экосистема JavaScript значительно выросла с тех пор, как мы написали фреймворк в 2015 году, и многие люди экспериментируют с замечательными вещами, которым мы можем научиться сегодня. Отчасти поэтому мы с оптимизмом смотрим на то, что строим в настоящее время. Теперь, когда нам доступно так много инструментов, сейчас действительно прекрасное время для работы в Интернете.