Знайомимось з Backend-driven UI — підходом до мобільної розробки

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Більшість розробників, яким доводилося завантажувати мобільні застосунки на 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)

Це підхід розробки, який передбачає перенесення бізнес-логіки та функціональності компонентів на серверну частину, а клієнтська частина своєю чергою будуватиметься за вказівками з сервера.

Основними перевагами цієї концепції я відзначу такі:

  1. Спрощення оновлень інтерфейсу користувача. Цей підхід дозволяє нам адаптувати та оновлювати інтерфейс користувача без необхідності додатково оновлювати мобільний застосунок на платформах цифрової дистрибуції. Це відповідно дозволяє дуже швидко реагувати на запити та задовольняти потреби користувачів.
  2. Простота реалізації нових фіч. Будь-які зміни наявних чи реалізація нових фіч розгортатимуться швидше, що, зі свого боку, також пришвидшить процес тестування.
  3. Масштабованість і гнучкість. UI значною мірою залежить від сервера, який за допомогою інструкцій керує вмістом та виглядом інтерфейсу. І саме це дозволяє управляти, наприклад, різними версіями застосунку для окремих сегментів користувачів. Також це знадобитися для A/B-тестування.
  4. Уніфікація контенту. Серверна частина динамічно керує та адаптує наповнення і вигляд контенту для кожного окремого користувача. Завдяки цьому, у поєднанні з даними про поведінку та уподобання користувача, можна створювати дуже персоналізований інтерфейс.
  5. Менші витрати на підтримку. Зі зменшенням часу на розробку, розгортання, тестування та реліз на App Store i Play Market мобільних застосунків прямо пропорційно зменшуються і витрати. Що не менш важливо, з полегшенням контролю версій також зменшується шанс виникнення помилок при оновленнях у користувачів.

      Втім, такий підхід також має ряд недоліків:

      1. Навантаження на сервери. Оскільки бізнес-логіка та інструкції побудови користувацької частини також знаходяться на сервері, то відповідно більшим стає і навантаження на нього. Тож, щоб забезпечити стабільну роботу, знадобиться і збільшення ресурсів сервера.
      2. Збільшення залежності від інтернет-з’єднання. Знову ж таки, відштовхуючись від того, що бізнес-логіка та інструкції побудови інтерфейсу користувача знаходяться на сервері застосунку, можуть виникнути проблеми із продуктивністю при поганому інтернет-з’єднанні.
      3. Ускладнення архітектури сервера. Використання концепції BDUI несе за собою ускладнення архітектури, що певною мірою може створювати труднощі у застосуванні загальновідомих архітектурних підходів та створить нагромадження коду.
      4. Затримки при оновленні контенту. Спираючись на попередні пункти, можна зрозуміти: якщо сервер сильно навантажений, час затримки при оновленні контенту користувачем на інтерфейсі застосунку збільшиться. Це відбувається тому, що для будь-яких змін в 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
      До обраногоВ обраному10
      LinkedIn
      Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
      Ctrl + Enter
      Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
      Ctrl + Enter

      Дякую, що окрім тексту статті ви ще не полінились розробити Github-репозиторій.
      За концепцією це дуже схоже на розмітку markdown, чому б не використати, наприклад, react-native-markdown-display та додати кастомні компоненти?

      Ще не побачив де знаходиться код того що відбувається при натисканні на кнопку «Buy now», на клієнті чи на сервері? Скоріш за все, баги будуть саме у лозіці роботи застосунку, а не у UI, отже якщо логіка все ще на клієнті, то автор статті так і не досяг своєї мети.

      Доречі, чому б не використовувати OTA оновлення? Це ж кіллер-фіча React Native, гріх цим не користатись

      Вітаю, дякую за коментар, стосовно колбеків, дуже влучно підмічено, я не додавав їх у розроблений код, щоб максимально спростити приклад, але ця логіка також переходить на серверну частину. Наприклад додавши в інструкції компоненти такий філд:

      "action": {
              "url": "/products/buy",
              "method": "POST",
              "payload": {
                "productField": "product",
                "quantityField": "quantity"
               }
      }

      Ну а на фронті просто додати обробку на клік, і пройтися по ключах і значеннях action.payload, де key це буде key обʼєкту який летітиме в баді, а value айдішка елементу з якого тягнути значення для цього обʼєкту.

      const payload = {};
                for (const [key, value] of Object.entries(element.action.payload)) {
                  payload[key] = document.getElementById(value).value;
                }
      fetch(element.action.url, {
                  method: element.action.method,
                  headers: {
                    'Content-Type': 'application/json'
                  },
                  body: JSON.stringify(payload)
                })

      Знову ж таки, навів банальний приклад, сподіваюсь з нього буде зрозуміло, що саме я намагався донести

      От ми замість «фу-фу» веб-апки пишемо модну нативну. І в цю нативну інтегруємо власну мову розмітки і взаємодії. Перевинаходимо html. Тобто наша апка — лиш кастомний браузер, котрий відображає кастомно-розмічену інформацію.

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

      тебя в дизайн забанили? иди у вас секта которых забанили? научись в разделение отвественности

      Чесно, не дуже розумію коментар, що ви мали на увазі?

      дизайн (system design) это в первую очередь про разделение ответственности. тут это явно нарушается.

      может ли это работать? конечно, все что угодно может работать, вопрос как — на этот вопрос мало кто может ответить так как не очень понимают что хорошо/плохо и почему

      про разделение ответственности. тут это явно нарушается.

      яким чином?

      Ну как сказать, один модуль (бекенд) знает и возвращает сущности которые в другом модуле (фронтенд)

      так тойво. весь інторнет так працює — бекенд (вебсайт) знає і повертає те, що рендерить браузер.

      Ну может кто то так и делает но 99% фреймворков этого не делают. Модель возвращаемая с контроллера не содержит юай, по крайне мере не должна

      а, то пан реакціоніст. І як тоді браузер знає, що йому малювати? звідки він цю інформацію отримує?

      Підписатись на коментарі