Установление нового стандарта для интерфейсных служб

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

Тогда можно спросить: «Почему мы должны взяться за этот проект, чтобы переписать нашу службу WordPress для интерфейса?»

Результат для нас, описанный в одной строке: «Создание нового стандарта для интерфейсных сервисов».

Установление новых стандартов

Установление нового стандарта здесь предполагает кардинальное изменение в нашей организации того, как мы смотрели на наши интерфейсные приложения, и необходимость существенной реформы, чтобы вызвать волну изменений к лучшему. Когда мы предлагали переписать приложение, команды Viki уже опробовали различные инструменты. Одним из таких инструментов был ReactJS, который использовался на основном сайте Viki, и, в большей степени, Soompi Awards, ответвление от Soompi, которое использовало React-router для создания одностраничного приложения (SPA).

Со временем мы обнаружили, что React лучше других инструментов, которые мы использовали для интерфейсных компонентов. Например, одним из преимуществ, которые мы обнаружили, было то, что React можно было использовать для создания компонентов с очень небольшой связью с основным техническим стеком. Этот потенциал повторного использования был лучше, чем использование нами компонентов CoffeeScript, которые были связаны с другим нашим приложением Ruby on Rails (RoR).

Вышеупомянутое подчеркивает понятие построения для повторного использования между несвязанными услугами непосредственной команды и, в более широком смысле, компании. Создавая больше шаблонов в том же ключе, что и выше, мы стремимся создать «шаблон» инструментов, который мы можем использовать для всех интерфейсных сервисов. Это приносит большую пользу команде, так как переключение контекста сводится к минимуму, когда члены команды переносятся между разными проектами.

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

Стандартный вид клиента - SPA + изоморфный

Мы начали нашу переработку с изучения визуализированного представления для пользователей. Наше приложение WordPress отображает контент в основном на стороне сервера. Таким образом, клиентская сторона остается неиспользованной кладовой драгоценных ресурсов. С этой целью мы решили изучить принципы архитектуры оболочки приложения или, более точный подход, SPA.

SPA определяется следующим образом - приложение, которое пересматривает часть представления для различных комбинаций «контекста». «Контекст» здесь может относиться к полному доменному имени (FQDN), пути и состоянию (например, куки-файлы для использования сеанса или параметры GET, которые обозначают параметры для отображения текущей страницы).

Для этого переписывания SPA будет переводить на реализацию логики на стороне клиента, которая изменяет веб-страницу при изменении контекста страницы. Это резко контрастирует с нашим приложением WordPress, где каждый HTTP-запрос, сформированный из одного и того же «контекста», приводит к новому отображению представления с сервера.

Можно спросить: «Как перенос ответственности за рендеринг со стороны сервера на сторону клиента улучшает взаимодействие с пользователем?» Ответ на этот вопрос заключается в оптимизации поведения браузеров при загрузке клиентских веб-страниц. Обычно при загрузке страницы возвращается HTML-ответ серверной части службы, который, в свою очередь, предоставляет браузеру инструкции по синтаксическому анализу. За этим следует загрузка различных ресурсов (JavaScript, CSS и шрифт) и создание элементов DOM с соответствующими обратными вызовами событий (не обязательно в этом порядке).

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

Реализация

Мы использовали React-Router для создания собственного SPA. Это было сделано путем ссылки на текущий контекст страницы на различные компоненты, ориентированные на маршрут. Например, URL-адрес с путем / будет использовать компонент домашнего маршрута, а URL-адрес с путем / article /: id будет использовать компонент маршрута статьи. Изменения в URL-адресе путем нажатия на внутренние ссылки на сайте вызовут обновление компонента маршрута, который в настоящее время используется.

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

Компоненты уровня страницы показаны на рис. 1 выше, через элемент «Navbar». Можно с уверенностью предположить, что панель навигации не отличается между компонентами маршрута для одного веб-приложения. Следовательно, этот тип функций должен отображаться независимо от маршрутов, как показано в «renderRoutes» выше.

Компоненты уровня служебных программ являются основными строительными блоками приложения. Одним из таких примеров является компонент «Ввод», который выдает предупреждение, если введенные символы превышают лимит, переданный через свойства компонента. Этот компонент можно повторно использовать при создании новой учетной записи пользователя или написании комментария к статье. Следовательно, было бы разумно отделиться от маршрута, предоставив повторно используемые компоненты в различных компонентах маршрута или даже компонентах уровня страницы.

В дополнение к вышесказанному, компоненты, ориентированные на маршруты, должны охватывать модель оболочки приложения. Это означает, что каждый компонент маршрута должен быть связан с маршрутом на стороне сервера, который обеспечивает ответ (часто JSON), который информирует соответствующий компонент о том, что должно быть отображено. Также могут быть независимые от маршрута идентификаторы, такие как идентификаторы в URL-адресе (например, «/ article / 1», где «1» - это идентификатор, а «article» ссылается на компонент статьи), которые передаются в запросе на сторону сервера. для индивидуального ответа. На рисунке 2 выше мы можем увидеть, как это представлено, где маршрут может существовать в разных состояниях до тех пор, пока контент не будет извлечен.

SEO и изопморфизм

Однако сервис, реализующий только SPA, столкнется с проблемами с поисковой оптимизацией (SEO). Это связано с тем, что исходный HTML-вывод страницы содержит только «оболочку» SPA и не содержит содержимого, как показано на рис. 2 выше. Если какие-либо поисковые роботы, связанные с поисковой оптимизацией, не поддерживают загрузку, синтаксический анализ и выполнение ресурсов JavaScript, сканирование страницы не приведет к получению каких-либо значимых результатов поиска только из «оболочки».

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

Стандартизация инфраструктуры - Google Cloud

Давайте рассмотрим нашу инфраструктуру перед тем, как перейти на Google App Engine. Наше приложение WordPress размещалось на нескольких виртуальных машинах, подготовленных вручную (в то время как в контейнерах с использованием Docker) от облачного провайдера, за балансировщиком нагрузки. Конвейер непрерывной интеграции + непрерывной доставки (CICD) был реализован с использованием варианта Jenkins с ручным развертыванием с использованием внутренних сценариев. Что касается статических ресурсов, таких как скомпилированные JS и CSS, они обслуживались Amazon CloudFront в качестве сети доставки контента (CDN).

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

Часть 1. Интеграция CICD с Cloud Build и IAM

Для обновлений конвейера CICD мы использовали Cloud Build, прежде всего для масштабируемой CI. Это было важно, поскольку наша внутренняя CI размещалась на одной машине и страдала от снижения производительности или даже сбоев, когда ее использовали большее количество конечных пользователей (разработчиков).

Еще одним плюсом было то, что, используя Cloud Build, мы могли использовать преимущества App Engine, предоставленные тем же партнером. Одним из важнейших преимуществ было использование Cloud IAM, где мы могли использовать схему привилегий для систематизации рабочего процесса с помощью учетных записей служб, используемых различными инструментами Google Cloud, как показано на рис. 3. Это также означало, что в силу нахождения в одной экосистеме , нет необходимости записывать учетные данные в наш конвейер CICD для учетной записи службы, которая выполняет доставку или развертывание.

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

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

На рис. 5 выше показан рабочий процесс для нашей промежуточной среды. Cloud Build позволяет нам запускать задачи параллельно, и мы можем извлекать зависимости подмодулей, а также одновременно устанавливать модули тестовых узлов. После выполнения вышеуказанных требований выполняется запуск нашего набора тестов. После успешного завершения тестов ресурсы компилируются и выгружаются с настройкой образа производственной среды и развертыванием в нашем промежуточном приложении App Engine.

Часть II: App Engine Flex и Stackdriver для мониторинга и ведения журнала

Затем мы перейдем к обновлениям для приложения, которое будет обслуживаться через App Engine. При оценке имеющихся у нас вариантов мы не хотели чего-то вроде Compute Engine из-за схожести использования IaaS в нашей предыдущей конфигурации. Kubernetes был бы лучшим выбором из-за более высокого уровня абстракции по сравнению с Compute Engine и им подобными. Однако, глядя на предложения в отрасли, мы обнаружили, что PaaS, в данном случае App Engine, был намного более желательным из-за более простого подхода, который мы могли использовать для масштабирования приложения. Наша команда могла сосредоточиться на интерфейсных функциях, не тратя слишком много времени на разработку и поддержку инфраструктуры.

В App Engine были представлены два варианта: Standard Vs Flex. App Engine Flex предлагает большую гибкость в языке программирования и библиотеках, где не требуется использование библиотек поставщика для выполнения простых задач, что позволяет нам предотвратить блокировку поставщика. С другой стороны, App Engine Standard обещает более быстрое масштабирование времени отклика, время развертывания и более низкие затраты. В конце концов, мы выбрали App Engine Flex из соображений гибкости, упомянутых выше. Это решение также было принято, чтобы не повторять те же ошибки, что и в нашей нынешней головоломке WordPress, которая заключалась в сильной связи кода с плагинами, которые эффективны только в их собственной экосистеме.

Чтобы развернуть новую версию вашего приложения в App Engine для NodeJS, требуются файлы app.yaml и package.json. app.yaml будет содержать конфигурацию используемых машин и логику масштабирования для приложения, а package.json будет содержать зависимости, версию узла и методологию для запуска приложения.

Из оболочки мы можем выдавать команды для развертывания с использованием двух файлов, указанных выше, и заменять текущую версию, обслуживающую наших пользователей. Однако, как объяснялось в предыдущей главе, мы хотели, чтобы только Cloud Build имел права на развертывание или доставку приложения, чтобы свести к минимуму человеческую ошибку. В то время как развертывание для промежуточной обработки не требует ручного вмешательства, доставка для производственной среды требует, чтобы инженер по развертыванию проверил правильные условия до того, как будет выполнено переключение живых служб. Изменения для производства выполняются с помощью консоли, как показано здесь:

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

Таким образом, мы создали конвейер CICD в Cloud Build и использовали App Engine, чтобы использовать решения масштабирования с консолью развертывания, чтобы завершить рабочий процесс CICD для доставки от стадии подготовки к работе.

Создание стандарта производительности

Еще одна проблема, которую мы хотели решить, - производительность. Единственная ответственность за производительность на стороне сервера - это концепция прошлого. Современные веб-браузеры способны оптимизировать производительность, позволяя веб-службам в целом масштабировать усилия по оптимизации на стороне клиента.

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

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

Концепция №1: отложить загрузку синхронных скриптов, асинхронные скрипты, не зависящие от загрузки

Сложность интеграции: ⅕ (⅖ если вы полагаетесь на `$ (document) .ready ()` из jQuery)

Улучшение задержки: ⅗

Улучшение пропускной способности: - (Страница по-прежнему загружает то же содержание)

Как видно на рис. 8 выше, включение асинхронной загрузки или отложенной загрузки во внешние сценарии включает только атрибут HTML в элементе DOM «сценария», что делает его очень простой оптимизацией поверх уже написанных шаблонов HTML или представлений. Но как эти свойства помогают при загрузке страницы?

На рис. 8 выше мы видим, что и defer, и async не блокируют рендеринг страницы, позволяя событию DOMContentLoaded в объекте документа запускаться намного раньше, чем если бы в страница. Для пользователей нашего сайта это означает, что им не нужно смотреть на белый экран в течение длительного времени во время загрузки страницы, что приводит к лучшему восприятию задержки страницы, даже если время полной загрузки остается прежним. Как сообщает KissMetrics в 2011, задержка ответа страницы в 1 секунду может привести к 7% конверсий. Это особенно актуально для нас, как для сайта публикации, где мы можем гораздо раньше привлечь внимание пользователей и снизить показатель отказов.

Для «асинхронных» сценариев это сценарии, которые следует использовать, если у вас есть независимые компоненты, поскольку время загрузки + синтаксического анализа + выполнения в совокупности не является детерминированным и не ожидает другого содержимого или потенциальных зависимостей. «Отложенные» сценарии предлагают нестрогую версию «асинхронного», предлагая анализировать и выполнять сценарии в том порядке, в котором они объявлены на странице, независимо от того, когда сценарии получены. Это позволяет нам предотвратить блокировку отрисовки страницы, сохраняя при этом зависимости между извлеченными активами скрипта.

Для существующих баз кода, которые прочно укоренились в популярной библиотеке JavaScript jQuery и при использовании $(document).ready(), реализация «defer» в элементе сценария jQuery будет немного более сложной задачей. Это потому, что $ не определено на момент синтаксического анализа HTML-документа.

Ссылаясь на рис. 9 выше, это можно изменить, чтобы использовать событие «DOMContentLoaded» для объекта документа, которое мы вкратце обсудили. Используя это событие, можно воспроизвести то же поведение инициализации, что и при использовании $(document).ready(), без необходимости прибегать к блокировке времени рендеринга DOM от загрузки библиотеки jQuery.

Концепция №2: отложенная загрузка изображений, адаптивные изображения и прогрессивная загрузка изображений

Сложность интеграции: ⅖

Улучшение задержки: ⅘

Улучшение пропускной способности: ⅗

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

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

Предоставляя сопоставление размеров порта просмотра с URL-адресами изображений для атрибута «srcset», браузер гарантирует загрузку изображения с оптимальным разрешением экрана, получая те же преимущества в экономии полосы пропускания, что и при отложенной загрузке.

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

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

Бонус: Еще одна оптимизация изображений, хотя и более зависимая от серверной части, заключается в использовании типа изображения, который уже оптимизирован для загрузки на веб-страницы, и для этого у нас есть webp. В случае с Soompi мы использовали встроенный сервис для обслуживания изображений webp. При использовании в сочетании с браузером, поддерживающим изображения в формате webp, мы смогли добиться экономии размеров файлов для загрузки изображений, дополнительно сэкономив пропускную способность как на стороне сервера, так и на стороне клиента.

Не верьте нам на слово. Цитата из исследования webp: WebP обычно достигает в среднем на 30% большего сжатия, чем JPEG и JPEG 2000, без потери качества изображения

Концепция №3: разделение кода

Сложность интеграции: ⅘

Улучшение задержки: ⅘

Улучшение пропускной способности: ⅘

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

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

Реализация разделения кода поверх SPA с рендерингом на стороне сервера может быть сложной задачей для всех, кто плохо знаком с этими концепциями. Например, создание логики для инструментального построения компонентов с разделением кода в сборщике модулей (например, Webpack), а затем обеспечение того, чтобы эти компоненты были надлежащим образом связаны с правильными маршрутами. Одно это сродни созданию чего-то похожего на плагин, чтобы будущие разработчики могли реализовать его интуитивно.

К счастью для нас, есть публичные библиотеки, которые уже решили эту проблему.

Как показано на рис. 12 выше, мы используем React-Loadable для создания компонентов маршрута, которые динамически импортируются как на стороне сервера, так и на стороне клиента.

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

Общие улучшения производительности

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

Ссылаясь на концепции улучшения - вышеупомянутые улучшения производительности можно разделить на три категории: воспринимаемая задержка, задержка и экономия полосы пропускания.

Из приведенной выше диаграммы предполагаемая задержка будет связана со временем загрузки содержимого DOM, индексом скорости и временем начала рендеринга (ответ сервера), где мы увидели улучшения в диапазоне от 35% до 71% по сравнению со старым приложением. Индекс скорости, по определению Speedcurve, инструмента, используемого для вышеуказанного измерения, является важным показателем взаимодействия с пользователем при загрузке страницы. Хотя время начала рендеринга не связано с нашими оптимизациями, приведенными выше, оно отражает то, как улучшения других связанных серверных служб для WordPress, сделанные параллельно с этим проектом, могут быть встроены в пользовательский интерфейс страницы.

С другой стороны, задержка страницы может быть представлена ​​временем загрузки страницы и временем полной загрузки, что привело к увеличению времени загрузки на 56% и 42% соответственно.

Для экономии полосы пропускания он может быть представлен общим размером загруженного содержимого различного файла. Исходя из этих показателей, мы наблюдали 65% -ную экономию полосы пропускания от общего размера файла при сохранении того же удобства для пользователей!

Двигаться вперед

Пока что мы установили новые стандарты для интерфейсных приложений, переписав интерфейсную службу Soompi.

Это конец?

Учитывая наше предыдущее осознание того, что React более дружественен к повторно используемым компонентам, такая же реализация была сделана для теперь осужденных компонентов CoffeeScript, фактически всего несколько лет назад! Это было связано с тем, что в то время RoR обычно использовался на стороне сервера. Поскольку Ruby синтаксически похож на CoffeeScript, он упростил перевод связанного кода между кодом Ruby на стороне сервера и CoffeeScript на стороне клиента, уменьшив переключение контекста для разработчиков и, таким образом, сделав разработку намного удобнее.

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

Другой контент Soompi

Ого, это было давно! Надеюсь, вам понравилось путешествие, которое мы прошли до сих пор. Другие главы нашего обучения подробно описаны ниже:

Подтверждение

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

Айша (github) - принцесса-воин, возглавлявшая атаку в этом переписывании.

Amiel (github) - наш собственный гений WordPress, а также ReactJS pro

Weiyuan (github | linkedin) - какой-то случайный парень, который также занимался серверной частью

Также выражаем признательность следующим инженерам / менеджерам по продукту, которые принимали участие в переписывании этого приложения.

Эрик (linkedin) - технический менеджер веб-команды!

Джонатан (linkedin) - продукт-менеджер по классу.