Знайомимось з Backend-driven UI — підходом до мобільної розробки
Більшість розробників, яким доводилося завантажувати мобільні застосунки на App Store чи Play Market, найімовірніше стикались із ситуацією, коли після релізу нової версії програми вилізла якась дрібна бажина, яку магічним чином раніше ніхто не помітив. А для її усунення потрібно робити новий білд з фіксом, знову проходити перевірки від платформ, чекати апруву і тільки після цього релізити як нову версію у вищезгаданих платформах цифрової дистрибуції. На жаль, весь цей процес займає чимало часу. Як же мені в аналогічній ситуації вдалося скоротити час розробки? Рішення — використати підхід Backend-driven UI.
Привіт, мене звати Любомир і я Full-Stack Developer в ІТ-компанії ORIL. За час своєї роботи я мав досвід з мобільними застосунками в різних сферах. І оптимізувати процес розробки мені часто допомагав такий підхід як Backend-driven UI. У цій статті я познайомлю вас детальніше з цим підходом, його перевагами та особливостями використання.
І App Store, і Play Market мають фактично ідентичні правила та рекомендації щодо змін, які можна вносити динамічно без повторного подання заявки.
Наведу реальний приклад, коли зміни дозволено ввести динамічно: у мене є сторінка, на якій відображається список продуктів із загальною інформацією про них; але згодом перед мною постає задача додати, наприклад, серійний номер продукту. Така зміна не потребує додатково подавати заявку і може бути втілена за допомогою підходу Backend Drive UI.
Інформацію про рекомендації App Store можна дізнатися за цим посиланням App Review Guidelines.
Що таке Backend-driven User Interface (BDUI)
Це підхід розробки, який передбачає перенесення бізнес-логіки та функціональності компонентів на серверну частину, а клієнтська частина своєю чергою будуватиметься за вказівками з сервера.
Основними перевагами цієї концепції я відзначу такі:
- Спрощення оновлень інтерфейсу користувача. Цей підхід дозволяє нам адаптувати та оновлювати інтерфейс користувача без необхідності додатково оновлювати мобільний застосунок на платформах цифрової дистрибуції. Це відповідно дозволяє дуже швидко реагувати на запити та задовольняти потреби користувачів.
- Простота реалізації нових фіч. Будь-які зміни наявних чи реалізація нових фіч розгортатимуться швидше, що, зі свого боку, також пришвидшить процес тестування.
- Масштабованість і гнучкість. UI значною мірою залежить від сервера, який за допомогою інструкцій керує вмістом та виглядом інтерфейсу. І саме це дозволяє управляти, наприклад, різними версіями застосунку для окремих сегментів користувачів. Також це знадобитися для A/B-тестування.
- Уніфікація контенту. Серверна частина динамічно керує та адаптує наповнення і вигляд контенту для кожного окремого користувача. Завдяки цьому, у поєднанні з даними про поведінку та уподобання користувача, можна створювати дуже персоналізований інтерфейс.
- Менші витрати на підтримку. Зі зменшенням часу на розробку, розгортання, тестування та реліз на App Store i Play Market мобільних застосунків прямо пропорційно зменшуються і витрати. Що не менш важливо, з полегшенням контролю версій також зменшується шанс виникнення помилок при оновленнях у користувачів.
Втім, такий підхід також має ряд недоліків:
- Навантаження на сервери. Оскільки бізнес-логіка та інструкції побудови користувацької частини також знаходяться на сервері, то відповідно більшим стає і навантаження на нього. Тож, щоб забезпечити стабільну роботу, знадобиться і збільшення ресурсів сервера.
- Збільшення залежності від інтернет-з’єднання. Знову ж таки, відштовхуючись від того, що бізнес-логіка та інструкції побудови інтерфейсу користувача знаходяться на сервері застосунку, можуть виникнути проблеми із продуктивністю при поганому інтернет-з’єднанні.
- Ускладнення архітектури сервера. Використання концепції BDUI несе за собою ускладнення архітектури, що певною мірою може створювати труднощі у застосуванні загальновідомих архітектурних підходів та створить нагромадження коду.
- Затримки при оновленні контенту. Спираючись на попередні пункти, можна зрозуміти: якщо сервер сильно навантажений, час затримки при оновленні контенту користувачем на інтерфейсі застосунку збільшиться. Це відбувається тому, що для будь-яких змін в UI потрібні інструкції з сервера.
Реальні застосунки, які використовують Backend-driven UI:
Мені здається, всі знають Facebook, Airbnb, Amazon, Netflix, Spotify, Uber тощо. На перший погляд, вони всі дуже різні, абсолютно відрізняються функціями та потребами, і мають мало спільного. Але всі вони так чи інакше використовують в різній мірі BDUI-концепцію.
Якщо говорити про застосунок Uber, йдеться про відображення динамічної інформації про поїздку. Тобто зміст і вигляд динамічних даних, які бачить користувач, контролює сервер. Spotify своєю чергою застосовує BDUI для відображення списків відтворення, рекомендацій та іншого музичного вмісту. Це дозволяє програмі використовувати дані з сервера для зміни вмісту та відображення інтерфейсу в реальному часі.
Airbnb, своєю чергою, використали UI-систему під назвою Ghost Platform. Ця система пришвидшує ітерації розробки та запуску функцій на всіх платформах тим, що вона надає фреймворки на нативних мовах платформи клієнта.
Порівняння з Server Side Rendering (SSR)
Коли я вперше почув про Backend-driven UI, то був переконаний, що це те саме, що і Server Side Rendering. Ці підходи об’єднує одна концепція — перенести бізнес-логіку на серверну частину, та максимально спростити клієнтську частину. Але між ними також є дуже важлива відмінність.
У BDUI сервер контролює наповнення та вигляд інтерфейсу за допомогою інструкцій, які клієнтська частина використовує при побудові інтерфейсу. Натомість SSR повністю самостійно контролює процес формування інтерфейсу та відправляє користувацькій частині готовий HTML-макет.
Нижче наведена порівняльна таблиця за основними критеріями, між цими підходами:
Критерій |
Server Side Rendering (SSR) |
Backend-driven UI (SDUI) |
Принцип |
Рендеринг макета відбувається на серверній частині перед відправленням до клієнта |
Сервер визначає вигляд та структуру макета шляхом генерації інструкцій |
Рендеринг |
HTML рендериться на сервері |
Рендеринг відбувається на клієнтській частині |
Перформанс |
Швидший при першому рендерингу, але надалі швидкість просідає |
Швидший при динамічних змінах UI |
SEO |
Весь контент відразу доступний для пошукових алгоритмів, що добре для SEO |
Менш ефективний через динамічний рендеринг |
Кешування |
Легко кешуються повноцінні відрендерені сторінки |
Кешування ускладнене через динамічність інтерфейсу |
Динамічність |
Обмежена |
Дуже гнучкий |
Підтримка |
Простий для підтримки статичного контенту |
Складніша підтримка через необхідність детальної синхронізації клієнтської та серверної частин |
Приклад
Тепер, ознайомившись з принципом дії концепції BDUI, перейдемо до практики. Припустимо, що у нас є уже зарелізений мобільний застосунок, який відображає список елементів на продаж, у моєму випадку це взуття.
Нижче я наведу часткові приклади коду, а весь код ви можете переглянути у GitHub-репозиторії.
Для початку я створив три хуки.
Перший: useFetchData — для отримання інструкцій з серверної частини.
export const useFetchData = () => { const [instractions, setInstractions] = useState<IItemInstractions[]>([]); useEffect(() => { axios .get("http://localhost:3000/") .then((response: { data: IItemInstractions[] }) => { setInstractions(response.data); }); }, []); return { instractions }; };
Другий: useRenderComponent — універсальний хук рендерингу попередньо створених в корені проєкту компонент.
export const useRenderComponent = () => { const render = useCallback(({ name, props, children }: IInstractions) => { const Component = lazy(() => import(`../components/${name}`)); if (children) { return <Component {...props}>{children}</Component>; } return <Component {...props} />; }, []); return render; };
Третій: useRenderItemsList — для рендерингу безпосередньо елементу зі списку.
export const useRenderItemsList = (instractions: IItemInstractions[]) => { const [components, setComponents] = useState<ReactNode[]>([]); const render = useRenderComponent(); const renderListItem = useCallback( (itemInstructions: IItemInstractions) => { const images = itemInstructions.images.map((image) => render(image)); const texts = itemInstructions.texts.map((text) => render(text)); const actions = itemInstructions.actions.map((action) => render(action)); return ( <StyledItem> <StyledRow> {images} <StyledTextContainer>{texts}</StyledTextContainer> </StyledRow> <StyledActions>{actions}</StyledActions> </StyledItem> ); }, [render] ); useEffect(() => { if (instractions) { setComponents( instractions.map((instraction) => renderListItem(instraction)) ); } }, [instractions, renderListItem]); return { components }; };
А також створив три компоненти: Button
, Image
, TypographyText
. Наведу приклад коду тільки для кнопки:
const StyledButton = styled(AntdButton)<IButtonProps>` &.ant-btn { display: flex; align-items: center; justify-content: center; height: ${(props) => props.$height || "100%"}; color: ${(props) => props.$color || "white"}; width: ${(props) => props.$width || "100%"}; min-width: ${(props) => props.$minWidth}; padding: ${(props) => props.$padding || "8px 0"}; margin: ${(props) => props.$margin}; font-size: ${(props) => props.$fontSize || "16px"}; line-height: ${(props) => props.$lineHeight || "20px"}; font-weight: ${(props) => props.$fontWeight || 400}; background: ${(props) => props.$background || "black"}; border: ${(props) => props.$border || "1px solid"}; border-color: ${(props) => props.$borderColor || "transparent"}; cursor: pointer; } &:focus { border-color: transparent; } &.ant-btn[disabled] { background: #efefef; color: #606060; cursor: not-allowed; } `; const Button = ({ children, ...props }: IButtonProps) => { return <StyledButton {...props}>{children}</StyledButton>; }; export default memo(Button);
І для повноти картини ось так виглядає мій App.ts:
function App() { const { instractions } = useFetchData(); const { components } = useRenderItemsList(instractions); return ( <Container> <Suspense fallback={<div>Loading...</div>}>{components}</Suspense> </Container> ); } export default App;
Отже, як це працює, в App.ts я викликаю хук для отримання інструкцій з серверу, так хук котрий з рендерить створенні шаблони компонентів за інструкціями. Це виглядатиме наступним чином:
Серверна частина потребує сервісу котрий генеруватиме інструкції, а також шаблони компонентів по яких відбуватиметься генерація.
getItems(): IItemListInstraction[] { // fetch data const products = data.items; // generate instructions for products const instructions = products.map((prodcut) => ProductComponent(prodcut)); return instructions; }
А так виглядатиме приклад компоненти котру ми генеруємо:
export const ProductComponent = ( prodcut: IProduct, isDisabled?: boolean, ): IItemListInstraction => { return { images: [ { name: 'Image', props: { src: prodcut.img, width: '145px', height: '72px', }, }, ], texts: [ { name: 'TypographyText', props: {}, children: prodcut.name, }, { name: 'TypographyText', props: {}, children: prodcut.size, }, ], actions: [ { name: 'Button', props: { $width: '150px', disabled: isDisabled, }, children: 'Buy now', }, ], }; };
Отже, ми отримуємо дані, в моєму випадку вони захардкоджені, а після чого по кожному елементу створюємо компоненту, котра в собі зберігає інформацію про картинки, текст та дії, котрі можна буде вчинити з кожним елементом.
А тепер, до нас прийшла вимога, наприклад, для деяких певних елементів, додати кнопку Add to cart. При звичайному підході розробки, нам доведеться робити зміни на клієнтській частині, знову проходити процес релізу і вже після цього користувачі зможуть побачити зміни. Але оскільки ми використовуємо BDUI це можна зробити наступним чином:
const instructions = products.map((prodcut) => { const component = ProductComponent(prodcut); if (prodcut.id === '2' || prodcut.id === '5') { component.actions.push({ name: 'Button', props: { $width: '150px', $margin: '0 0 0 20px', $background: 'firebrick', $color: 'greenyellow', $fontWeight: 'bold', }, children: 'Add to cart', }); } return component; });
Після чого наша клієнтська частина автоматично оновить свій вигляд:
Або, наприклад, прийшла вимога стилізувати disabled-стан, для конкретної кнопки, і додати персоналізовані стилі для назви продукту:
Backend-driven UI для веброзробки як аналог CMS
BDUI також може стати у пригоді у веброзробці, щоб зробити сторінки та макети програми більш адаптивними. Але спочатку сформулюємо проблему: я жорстко закодував макет сторінок сайту, наприклад, за допомогою будь-якої CMS, що містить багато різних полів, тексту та зображень.
Згодом мені надійшла вимога внести зміни, скажімо, для кожного умовного блоку додати нову кнопку чи поле. Або потрібно додати повністю новий розділ чи нові варіації A/B-тестування. Тоді доведеться створити купу нових сторінок, компонентів, і модифікувати наявні, що несе за собою заповнення багатьох полів і так далі. А це все монотонна робота, що потребує багато часу і зусиль.
Натомість, якщо ж використовувати BDUI підхід, мені просто необхідно створити компоненти на клієнтській частині, а їхнє наповнення, розміщення та специфікація надійде з інструкцій за допомогою API. Це відповідно сильно спростить та прискорить виконання подібних завдань.
Висновок
На мою думку, концепція Backend-driven UI заслуговує як мінімум вашої уваги, як максимум — часткового використання у майбутніх проєктах. Адже вона відкриває багато нових можливостей для розробників, надаючи інструмент для динамічного керування наповненням інтерфейсу користувача та не потребуючи регулярного оновлення шляхом створень нових версій на платформах цифрової дистрибуції. І варто пам’ятати, що перенесення всієї бізнес-логіки на сторону сервера забезпечує нам змогу швидкого реагування на тенденції ринку або потреби користувачів.
BDUI активно використовують гіганти IT-індустрії, чим демонструють успішне застосування підходу в сучасних умовах ринку та покращують для нас, як для користувачів, досвід взаємодії з динамічними інтерфейсами. Це ще одна причина звернути увагу на такий потужний інструмент, як Backend-driven UI, та додати його у свій перелік освоєних навичок. Адже за умілого застосування цей підхід зможе покращити досвід і розробки, і використання будь-якого застосунку.
13 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів