Для справки: Server-Driven UI
Прежде чем мы углубимся в реализацию Airbnb пользовательского интерфейса, управляемого сервером (server-driven UI, SDUI), важно понять общую идею и то, как он обеспечивает преимущество перед традиционным пользовательским интерфейсом, управляемым клиентом.
В традиционном мире данные управляются бэкендом, а пользовательский интерфейс управляется каждым клиентом (веб, iOS и Android). В качестве примера возьмем страницу с объявлением на Airbnb. Чтобы показать нашим пользователям листинг, мы можем запросить данные листинга у серверной части. Получив эти данные листинга, клиент преобразует эти данные в пользовательский интерфейс.
Такой подход связан с несколькими проблемами. Во-первых, на каждом клиенте нужна логика построения списка, которая преобразует и отображает данные. Эта логика быстро усложняется и становится негибкой, если мы постоянно вносим изменения в способ отображения списков.
Во-вторых, каждый клиент должен поддерживать паритет друг с другом. Как уже упоминалось, логика экранов быстро усложняется, и каждый клиент имеет свои собственные сложности и специфические реализации для обработки состояний, отображения пользовательского интерфейса и т.д. Легко представить ситуацию, в которой клиенты будут отличаться друг от друга.
Наконец, у мобильных устройств есть проблема с версиями. Каждый раз, когда нам нужно добавить новые функции на наш экран со списком, нам нужно выпустить новую версию мобильных приложений, чтобы пользователи могли получить самые новые возможности. Пока пользователи не обновятся, у нас не будет нормальных способов определить, используют ли пользователи эти новые функции и как они реагируют на них.
Время для SDUI
Что, если бы клиентам даже не нужно было бы знать, что они отображают список? Что, если бы мы могли передать пользовательский интерфейс напрямую клиенту и полностью отказаться от данных? По сути, это то, что делает SDUI — мы передаем и пользовательский интерфейс, и данные вместе, и клиент отображает их даже не зная о данных, которые он содержит.
Специальная реализация SDUI Airbnb позволяет нашему бэкенду контролировать данные и то, как эти данные отображаются для всех клиентов одновременно. Все — от макета экрана, того, как разделы расположены в этом макете, данных, отображаемых в каждом разделе, и даже действий, предпринимаемых при взаимодействии пользователей с разделами, в наших веб-приложениях, приложениях для iOS и Android контролируется одним ответом бэкенда.
SDUI в Airbnb: Призрачная платформа (Ghost Platform) 👻
Ghost Platform (GP) — это унифицированная, самостоятельная, управляемая сервером система пользовательского интерфейса, которая позволяет нам быстро выполнять итерации и безопасно запускать функции в Интернете, iOS и Android. Она называется Ghost, потому что наша основной фокус — функции «Гость» (Guest) и «Хозяин» (Host), две стороны наших приложений Airbnb.
GP предоставляет веб, iOS и Android фреймворки на родных языках клиентов (Typescript, Swift и Kotlin соответственно), которые позволяют разработчикам создавать server-driven функции с минимальной настройкой.
Основная особенность GP заключается в том, что такие функции могут совместно использовать библиотеку стандартных секций, макетов и действий, многие из которых имеют обратную совместимость, что позволяет командам быстрее реализовывать фичи и перемещать сложную бизнес-логику на бэкенд.
Стандартизированная схема
Основа Ghost Platform — это стандартизированная модель данных, которую клиенты могут использовать для визуализации пользовательского интерфейса. Чтобы сделать это возможным, GP использует общий уровень данных для серверных сервисов, используя унифицированную сеть сервисов данных, называемую Viaduct.
Ключевым решением, которое помогло нам сделать нашу серверную систему пользовательского интерфейса масштабируемой, было использование единой общей схемы GraphQL для веб, iOS и Android приложений, то есть мы используем одну и ту же схему для обработки ответов и создания строго типизированных моделей данных на всех наших платформах.
Мы уделили время обобщению общих аспектов различных функций и последовательному и продуманному учету особенностей каждой страницы. В результате получилась универсальная схема, способная отображать все функции Airbnb. Эта схема достаточно мощна, чтобы учитывать повторно используемые секции, динамические макеты, подстраницы, действия и многое другое, а соответствующие структуры GP в наших клиентских приложениях используют эту универсальную схему для стандартизации визуализации пользовательского интерфейса.
Ответ GP
Первый фундаментальный аспект GP — это структура общего ответа. Для описания пользовательского интерфейса в ответе GP используются две основные концепции: секции (разделы) и экраны.
1. Как пользователи видят функции Airbnb по сравнению с тем, как GP видит те же функции в виде экранов и секций.
- Секции: разделы являются наиболее примитивным строительным блоком GP. Раздел описывает данные единой группы компонентов UI, содержащие точные данные для отображения — уже переведенные, локализованные и отформатированные. Каждый клиент берет данные секции и преобразует их непосредственно в пользовательский интерфейс.
- Экраны: любой ответ GP может иметь произвольное количество экранов. Каждый экран описывает макет экрана и, в свою очередь, то, где будут отображаться секции из массива sections (называется “места размещения”). Он также определяет другие метаданные, например, как отображать разделы — например, в виде всплывающего, модального или полноэкранного режима — и данные логов.
2. Пример схемы GP Response GraphQL.
Серверная часть функции, созданная с помощью GP, будет реализовывать этот GPResponse (2) и заполнять экраны и разделы в зависимости от варианта их использования. Клиентские платформы GP в веб, iOS и Android предоставляют разработчикам стандартную обработку для получения реализации GPResponse и преобразования ее в пользовательский интерфейс с минимальной работой с их стороны.
Секции
Секции являются основным строительным блоком GP. Ключевой особенностью разделов GP является то, что они полностью независимы от других разделов и экрана, на котором они отображаются.
Отсоединяя секции от окружающего их контекста, мы получаем возможность повторно использовать и перепрофилировать их, не беспокоясь о тесной связи бизнес-логики с какой-либо конкретной функцией.
Схема секции
В схеме GraphQL, секции GP представляют собой объединение всех возможных типов разделов. Каждый тип секции определяет поля, который она предоставляет для рендеринга. Секции принимаются в реализации GPResponse с некоторыми метаданными и предоставляются через обертку SectionContainer, которая содержит подробную информацию о статусе секции, данных логов и актуальной модели данных раздела.
3. Фрагмент схемы GraphQL нашей секции
Одна из важных концепций, которую следует затронуть, — это SectionComponentType. SectionComponentType управляет тем, как рендерится модель данных раздела. Это позволяет при необходимости визуализировать одну модель данных множеством разных способов.
Например, два типа SectionComponentTypes TITLE и PLUS_TITLE могут использовать одну и ту же модель данных TitleSection, но реализация PLUS_TITLE будет использовать логотип Airbnb Plus и стиль заголовка для визуализации TitleSection. Это обеспечивает гибкость функций, использующих GP, при этом обеспечивая возможность повторного использования схемы и данных.
4. Пример отрисовки модели данных TitleSection по-разному с использованием SectionComponentType
Компоненты секции
Данные секции преобразуются в пользовательский интерфейс через «Компоненты секции». Каждый компонент отвечает за преобразование модели данных и SectionComponentType в компоненты пользовательского интерфейса. Компоненты абстрактных разделов предоставляются GP на каждой платформе на их родных языках (например, Typescript, Swift, Kotlin) и могут быть расширены разработчиками для создания новых секций.
Компоненты секции сопоставляют модель данных секции с одной уникальной визуализацией и, следовательно, относятся только к одному типу SectionComponentType. Как упоминалось ранее, разделы отображаются без какого-либо контекста экрана, на котором они находятся, или разделов вокруг них, поэтому каждый компонент раздела не имеет бизнес-логики, зависящей от функции.
Я разработчик Android, поэтому давайте возьмем пример для Android (и потому, что Kotlin великолепен 😄). Для создания секции заголовка у нас есть фрагмент кода, показанный ниже. Web и iOS имеют аналогичные реализации — в Typescript и Swift соответственно — для создания компонентов секции.
5. Пример компонента секции на Kotlin
GP предоставляет множество «основных» компонентов секций, таких как наш пример TitleSectionComponent выше, которые предназначены для конфигурирования, стилизации и обратной совместимости с бэкендом, чтобы мы могли адаптироваться к любому варианту использования функции. Однако разработчики, создающие новые функции на GP, могут добавлять новые компоненты раздела по мере необходимости.
6. GP принимает данные секции, использует компонент секции, чтобы преобразовать его в пользовательский интерфейс (TitleSectionComponent в примере 5), и представляет пользователю созданный пользовательский интерфейс секции
Экраны
Экраны — еще один строительный блок GP, но, в отличие от секций, экраны в основном обрабатываются клиентскими фреймворками GP и более упорядочены в использовании. Экраны GP отвечают за расположение и организацию секций.
Схема экранов
Экраны получаются как тип ScreenContainer. Экраны могут быть запущены в модальном (всплывающем) виде, внизу (bottom sheet) или в полноэкранном режиме, в зависимости от значений, описанных в поле screenProperties.
Экраны позволяют динамически настраивать макет экрана и, в свою очередь, упорядочивать разделы с помощью типа LayoutsPerFormFactor. LayoutsPerFormFactor определяет макет для компактных и широких экранов с помощью интерфейса, называемого ILayout, который будет подробно рассмотрен ниже. Затем фреймворк GP на каждом клиенте использует плотность экрана, поворот и другие факторы, чтобы определить, какой ILayout fromLayoutsPerFormFactor нужно визуализировать.
7. Пример схемы экранов GP
ILayouts
8. Несколько примеров реализации ILayout, которые используются для указания различных мест размещения
ILayouts позволяет экранам изменять макеты в зависимости от ответа сервера. В схеме, ILayout — это интерфейс, в котором каждая реализация ILayout определяет различные размещения. Места размещения содержат один или несколько типов SectionDetail, которые указывают на секции в самом внешнем массиве sections ответа. Мы указываем на модели данных раздела, а не включаем их в ответ. Это уменьшает размеры ответа за счет повторного использования секций в конфигурациях макета (LayoutsPerFormFactor в 7).
9. Пример схемы ILayout GP
Клиентские фреймворки GP обрабатывают ILayout для разработчиков, поскольку типы ILayout более самостоятельны, чем разделы. Каждый ILayout имеет уникальное средство рендеринга в каждом клиентском фреймворке GP. Средство визуализации макета берет каждый SectionDetail из каждого места размещения, находит соответствующий компонент секции для визуализации, создает пользовательский интерфейс секции, используя этот компонент, и, наконец, помещает сделанный UI в макет.
Действия
Последняя концепция GP — это наша инфраструктура обработки действий и событий. Одним из наиболее важных аспектов GP является то, что помимо определения секций и компоновки экрана в сетевом ответе, мы также можем определять действия, предпринимаемые, когда пользователи взаимодействуют с пользовательским интерфейсом на экране, такие как нажатие кнопки или смахивание карточки. Мы делаем это через интерфейс IAction в нашей схеме.
10. Пример схемы GP IAction
Вспомните (6), что компонент секции — это то, что превращает наш TitleSection в пользовательский интерфейс на каждом клиенте. Давайте посмотрим на тот же Android-пример компонента TitleSectionComponent с динамическим действием IAction, запускаемым при нажатии на текст подзаголовка.
11. Пример компонента секции с IAction, запускаемым при клике на заголовке
Когда пользователь нажимает подзаголовок в этой секции, он запускает IAction, переданный для поля onSubtitleClickAction в TitleSection. GP отвечает за маршрутизацию этого действия к обработчику событий, определенному для функции, который будет обрабатывать инициированное действие IAction.
Существует стандартный набор общих действий, которые GP выполняет универсально, например переход к экрану или прокрутка к разделу. Компоненты могут добавлять свои собственные типы IAction и использовать их для обработки уникальных действий своих функций. Поскольку обработчики событий для конкретных функций привязаны к функции, они могут содержать столько бизнес-логики для конкретных функций, сколько они пожелают, что дает свободу использовать настраиваемые действия и бизнес-логику при возникновении конкретных вариантов использования.
Собираем все вместе
Мы рассмотрели несколько концепций, и чтобы связать все вместе, давайте возьмем полный ответ GP и посмотрим, как он рендерится.
12. Пример действительного ответа GP в формате JSON
Создание компонентов секций
Функции, использующие GP, должны будут извлечь ответ, реализующий упомянутый выше GPResponse (2). Получив ответ GPResponse, GP парсит этот ответ и создает секции для разработчика.
Напомним, что каждая секция в нашем массиве разделов имеет SectionComponentType и модель данных секции. Разработчики, работающие с GP, добавляют компоненты секции, используя SectionComponentType в качестве ключа для рендеринга модели данных раздела.
GP находит каждый компонент секции и передает ему соответствующую модель данных. Каждый компонент секции создает UI-компоненты для секции, которые GP вставит в нужное место в макете.
13. Превращение данных секций в UI
Обработка действий
Теперь, когда элементы пользовательского интерфейса каждого компонента секции настроены, нам нужно обработать взаимодействие пользователей с секциями. Например, если они нажимают кнопку, нам нужно обрабатывать действие, выполняемое при нажатии.
Напомним, что GP обрабатывает события маршрутизируя их собственному обработчику. Пример ответа выше (12) содержит два раздела, которые могут запускать действия, toolbar_section и book_bar_footer. При создании обоих этих секций их компоненты просто должны принять IAction и указать, когда его запускать, что в обоих случаях будет при нажатии кнопки.
Мы можем сделать это с помощью обработчиков кликов на каждом клиенте, которые будут использовать фреймворк GP для маршрутизации события клика.
button( onClickListener = { GPActionHandler.handleIAction(section.button.onClickAction) } )
Настройка экрана и макета
Чтобы сделать полностью интерактивный экран для наших пользователей, GP просматривает массив экранов, чтобы найти экран с идентификатором «ROOT» (идентификатор экрана GP по умолчанию). Затем GP ищет правильный тип ILayout в зависимости от экрана и других факторов, относящихся к конкретному устройству, которое использует пользователь. Для простоты мы будем использовать макет из поля compact, SingleColumnLayout.
Затем GP найдет средство визуализации макета для SingleColumnLayout, в котором он обрабатывает макет с помощью верхнего контейнера (размещение nav), прокручиваемого списка (main размещение) и плавающего нижнего колонтитула (размещение footer ).
Это средство рендеринга макета возьмет модели для мест размещения, которые содержат объекты SectionDetail. Эти SectionDetails содержат некоторую информацию о стилях, а также sectionId секции, который нужно обработать. GP будет перебирать эти объекты SectionDetail и заполнять разделы в соответствии с местами размещения, используя компоненты секции, которые мы создали ранее.
14. GP берет встроенные секции с добавленными обработчиками действий, добавляет разделы в места размещения ILayout
Что дальше с GP?
GP существует всего около года, но большинство наиболее часто используемых функций Airbnb (например, поиск, страницы со списками, оформление заказа) построены на GP. Несмотря на критическую массу использования, GP все еще находится в зачаточном состоянии, и многое еще предстоит сделать.
У нас есть планы по созданию более составного пользовательского интерфейса за счет «вложенных секций», улучшающего обнаружение элементов, которые уже существуют, с помощью наших инструментов проектирования, таких как Figma, и WYSIWYG-редактирования секций и мест размещения, позволяющих изменять функции без кода.
Если вы увлечены серверным пользовательским интерфейсом или созданием масштабируемых пользовательских интерфейсов, вам предстоит еще многое сделать. Мы рекомендуем вам подать заявку на открытые должности в нашей команде инженеров.
Re-Engineering Travel tech
Server-driven UI сложен. Бесчисленные часы ушли на создание надежной схемы, клиентских фреймворков и документации для разработчиков, которые позволили GP добиться успеха.
Если вы хотите получить более подробный обзор SDUI и GP, недавно у меня была возможность выступить на конференции Airbnb «Re-Engineering Travel tech», представляя GP. Я бы посоветовал вам ознакомиться с общим обзором серверного пользовательского интерфейса и GP (перейдите к отметке 31 минуту, если у вас мало времени).