Между дизайн-системой и Server Driven UI
- среда, 22 июля 2020 г. в 00:28:45
Всем привет! Меня зовут Владимир Касаткин, и я работаю бэкенд-разработчиком в компании ivi.ru, в команде "UX". Цель этой статьи — показать, как мы уменьшили объём клиентской разработки, но при этом увеличили количество проводимых A/B-тестов.
Раньше вся продуктовая разработка была разбита на большие направления ("платформы"): бэкенд, Smart TV, iOS, Android, веб. При этом фичи пилились достаточно долго (по полгода), а побочным эффектом были заметные различия внешнего вида и функционала одной и той же фичи на разных платформах.
Потом нас разбили по маленьким кросс-функциональным командам. Разработка пошла быстрее, костылей и платформенных различий на клиентах становилось всё больше.
На тот момент вся компания занималась глобальным редизайном всего продукта, и нам были озвучены следующие требования:
Это очень справедливые требования, так как на тот момент вся логика по расположению блоков была зашита на клиентах. Например промо-блок нужно прибить к верху экрана, избранное поместить на 3 строчку, и так далее.
Заодно мы смогли побороть старую проблему на главной странице, где рекомендации были не согласованы друг с другом.
Структура страницы была зашита на клиентах, и загрузка содержимого была разбита на несколько этапов:
Схема запросов
Из-за этого рекомендательная система старалась сделать каждую подборку максимально релевантной, и на первых позициях оказывался один и тот же контент. Так происходит из-за большого пересечения контента в подборках между собой. Ведь любой новый фильм может входить сразу в несколько подборок, например: "Новинки 2020", "Претенденты на Оскар", "Лучшая режиссура" и так далее. То есть пользователь мог видеть несколько новых фильмов почти во всех блоках на одной странице.
Мы рассмотрели несколько возможных вариантов реализации. Каждый из вариантов сводился к тому, чтобы клиенты получали некий скелет страницы, содержащий в себе указание какие сущности рисовать, какими данными их заполнять. Разница лишь в детализации и скорости работы такой системы.
Вариант 1 — внедрить GraphQL, и пусть клиенты сами разбираются что куда рисовать. Главный минус: мы никогда не работали с GraphQL, все команды могут потратить уйму времени на выяснение нюансов. А ещё всё API придётся строить с нуля и при этом бесконечно поддерживать старое API для старых клиентов.
Вариант 2 — отдавать полотно, содержащие в себе все ответы всех команд, которые клиенты запрашивали раньше по отдельности, с минимальной обёрткой. Это почти как GraphQL, но без GraphQL.
Мы начали разработку второго варианта и столкнулись с тем, что бекенд не справлялся. Клиентам требуется такой объём информации, что собрать её всю в одной команде крайне тяжело. Время ответа новый команды API составляло 1.5 сек для страницы из 15 блоков по 20 единиц контента в каждой.
Есть и нюанс производительности клиентов. Если отдавать всё сразу, то размер ответа для одной страницы из 15 блоков будет весить примерно 3 Мб. Старые Smart TV не могут переваривать столько данных за раз "by design", а пагинация страницы пачками по 1-2 блока приводит к схеме "отдельный запрос на каждый блок".
Вариант 3 — Server Driven UI. В предыдущем варианте у нас есть конечное описание фичей, которые понимает клиент. Что если мы захотим добавить новый абзац текста, или какую-нибудь хитрую кнопку в левом верхнем углу? Мы не управляем вёрсткой, мы не можем внедрить ещё один "такой же" объект, но в другое место на экране. У нас есть только конечный набор вариаций.
Об этом есть хорошие выступления от OZON: (1) и (2). Но мы не пошли этим путём, потому что у нас были жёсткие временные ограничения.
В итоге мы остановились на усреднённом варианте. В новой команде API мы отдаём скелет страницы, а содержимое для блоков клиенты запрашивают отдельно из старых команд API. Весь механизм построен на трёх новых сущностях:
Мы назвали этот механизм pages.
Страница соответствует какому-либо экрану с вертикальным скроллом в приложении и содержит в себе последовательность блоков. Идентификаторы базовых страниц (главная, каталоги, поиск) захардкожены в приложениях. Все дальнейшие манипуляции с выдачей происходят на бекенде исходя из базовых страниц.
У блока есть некий тип (type
), который указывает клиенту на используемую структуру данных. Каждому типу блока соответствует конкретная команда API, которую клиент должен запросить, чтобы отобразить содержимое блока. Блок содержит в себе необходимые параметры для запроса этой команды API (request_params
). И тут же есть данные для аналитики (groot_params
), которые необходимо затрекать при отображении блока.
Пример такого блока:
{
"id": 4121,
"type": "collection",
"title": "Фильмы в 4K UHD",
"hru": "movies-uhd4k",
"groot_identifier": "collection",
"version": 1,
"groot_params": {
"ui_type": "collection",
"ui_id": "collection"
},
"request_params": {
"sort": "priority_in_collection",
"id": 8239,
"withpreorderable": true,
}
}
Вообще блок как сущность может содержать в себе следующие свойства:
Визуальный пример сложного блока:
Таргетинг — внутренняя сущность, служит для описания условий, в которых может отображаться блок. У нас есть таргетинги по территориям, по типам приложений, по идентификаторам A/B-тестов. Таргетинги можно объединять и пересекать друг с другом. Они нужны для выполнения требований правообладателей, а так же для соответствия здравому смыслу (например, нет смысла показывать подборку с боевиками в детском приложении или детском профиле).
При этом хардкодить новые сущности на бэкенде было не интересно, и мы сразу разрешили управлять ими через админку. На скриншоте показана форма редактирования одного блока. На одной странице таких блоков может быть несколько сотен.
Возвращаясь к примеру с главной страницей сервиса, порядок работы стал следующим:
После этого мы перенесли и другие страницы на новый механизм:
После "расхардкоживания" выдачи мы сделали следующий шаг: создали механизм A/B-тестирования внутри одной "страницы".
Там всё достаточно просто:
Ниже примеры A/B-тестов, которые мы запустили таким образом:
Создание абсолютно нового дизайна блока всё равно требует полной разработки. И в нашем случае это будет выглядеть так:
Рассмотрим несколько сценариев.
Итого: тут никакой разработки.
Итого: тут только backend-разработка.
Итого: в этом сценарии участвует вся разработка.
Ранее у нас в компании уже был большой этап унификации дизайна и вёрстки компонентов. Это вылилось в создание дизайн-системы: статья об этом, запись выступления.
Сейчас же произошла унификации работы с API. Каждой из клиентских платформ пришлось полностью переписать своё приложение чтобы "расхардкодить" то что было раньше, и иметь возможность рисовать что угодно где угодно, переиспользовать блоки-компоненты.
Нам удалось одновременно получить несколько плюшек: