🏆 Рейтинг ІТ-работодателей 2019: уже собрано более 5000 анкет. Оцените свою компанию!
×Закрыть

Как мы строим платформу для интеграций, или «iFramе is not a sh*t»

Тема внутренних маркетплейсов очень популярна в продуктовом мире. Благодаря App Store и Play Market встроенные приложения стали частью нашей повседневной жизни. В этой статье я расскажу, как мы используем iFrame, чтобы дать внешним разработчиками возможность качественно встраиваться в нашу систему. Объясню, с какими сложностями уже успели столкнуться и какие инсайты получили.

Статья будет полезна тем, кто работает в продуктовых компаниях и строит платформу для своего продукта. Если ты человек продукта, запрыгивай на борт.

Немного о том, что мы делаем в Poster

Poster — это облачная система автоматизации для кафе, баров и ресторанов. Для непосвященных звучит пугающе, а если проще — кушали когда-то в Пузатой хате? Когда рассчитываешься, кассир пробивает заказ в системе учета. Такая система учета (ее еще называют POS-система или просто «касса») делает много важных и полезных вещей: ведет учет остатков на складе, печатает чеки, рассчитывает затраты на приготовление блюда, выручку и т.д.

Большинство POS-систем начинали свой путь со времен Windows 98 и выглядят вот так:

Работа с заказом в стационарной системе R_Keeper

Мы первые в СНГ сделали это по-другому:

Работа с заказом в Poster

Касса на iPad вместо Windows-моноблока. Админка в облаке вместо сервера под паркетом заведения. Сегодня с нами работают 8000 активных заведений в 75 странах мира.

Для чего нам открытый API

С самого начала мы знали, что у нашего продукта будет открытый API. Мы всегда верили, что за открытыми системами будущее. Для нас API это прежде всего:

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

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

Дополнительный канал заработка. Как правило, каталоги берут комиссию с продаж приложений. Наша комиссия 30%, но бизнес-модель компании не строится вокруг маркетплейса. В нашем случае, мы ставим перед маркетплейсом задачу быть самоокупаемым.

Как пришли к iFrame

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

Встраивать приложение в код ядра. Изначально так и делали, но нагородили кучу костылей, от которых даже сейчас сложно избавиться. Не подходит.

Хостить JS/HTML на нашем сервере и загружать в админку. Таким подходом получим бесшовную интеграцию, но кто-то бесшовно поломает всю систему. Не катит.

Встроить в админку iFrame с приложением.
— Не, ребят, iFrame — это говно! Еще мой дед их юзал.

Это первое, что мы услышали про iFrame, но серьезных аргументов «против» не нашли, поэтому решили попробовать 😀

Почему разработчики думают, что iFrame говно? Честно, не знаю. Я и сам так думал, пока не нырнул в эту кроличью нору. Когда писал статью, собрал мнения, почему не нужно юзать iFrame. Вот, что услышал:

  • «iFrame небезопасен» → Да, так и было, лет 10 назад.
  • «У меня на прошлом проекте все было написано на iFrame, чтобы прокинуть параметр из одной части сайта в другую, нужно потратить день» → Если неправильно использовать и городить костыли, то так и получится.
  • «Он очень медленный» → Согласен, если 5 штук добавить на страницу, начнет тупить. Тогда все, что нужно сделать — не добавлять 5 штук.

В общем, все это, на мой взгляд, стереотип, главное как iFrame использовать.

Как мы используем iFrame

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

  1. Manage платформу как возможность встроить веб-страницу в админ-панель;
  2. POS платформу для расширения функций кассы;
  3. Device платформу — единый хаб для управления устройствами в заведении.

В этой статье я расскажу самое интересное: как мы используем iFrame в POS-платформе. Начну с нескольких примеров, которые уже сделали на этой технологии:

  • Оплатить заказ криптовалютой, emoji;
  • Отправить заказ из кассы на доставку;
  • Вызвать официанта, просканировав QR-код на столе;
  • Оплатить заказ бонусами из приложения по лояльности.

Вот так система лояльности может расширить работу кассы:

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

Платформа под капотом

Касса Poster — это Single Page Application, которое открыто в браузере в нативном приложении. Внутри SPA под каждое приложение создается iFrame, в котором мы загружаем страницу с таким HTML:

<html manifest="/platform_157.appcache">
<head>
    <script src="/js/pos/platform/bundle.1545155099.js"></script>
    <link rel="stylesheet" href="/css/pos/platform/main.1545155225.css">
</head>
<body>
     <div id="app-container"></div>
</body>
</html>

На этой странице подгружается JS, который связывает Poster и iFrame. Как только страница полностью загружена, iFrame отправляет Poster сообщение о том, что он готов к работе. В примере я буду использовать упрощенный код, но в реальности все обернуто в дополнительный уровень абстракции:

window.top.sendMessage({ action: "loaded", msgHash: "Ha2s7m" })

Poster слушает сообщения от iFrame и в ответ говорит, что нужно загрузить ещё один JS с кодом, который написал уже внешний разработчик:

// Слушатель на стороне Poster
window.addEventListener("message", (msg) => {
    If (msg.action === "loaded") {
       // Отвечаем приложению
       window.top.sendMessage({ 
          action: "init",
          data: "http://localhost:8080/bundle.js"
       })
    }
});

Разработчик пишет свой SPA на основе нашего шаблона на github. JS собирается Webpack-ом в один bundle файл. В процессе разработки bundle раздается через Webpack Dev Server и касса грузит его с локальной машины. При этом можно использовать фишки webpack, такие как live reload.

Как только приложение закончено, разработчик загружает bundle к нам на сервер одной командой — npm run deploy. С этого момента любой клиент Poster может подключить приложение и пользоваться им.

Зачем загружать код к нам на сервер? В работе заведения часто случаются перебои с интернетом, поэтому Poster работает оффлайн и, если отключить интернет и перезагрузить Poster, все ресурсы загружаются из кэша. Для этого мы используем AppCache, но у нее есть ограничение — кэшируемые файлы должны быть на одном домене. Таким образом, чтобы внешние приложения могли работать оффлайн, их код должен быть на нашем сервере.

API платформы

Мы даем простой JS API, с которым можно управлять кассой. Например, чтобы установить на открытый заказ скидку, нужно написать следующий код:

let result = await window.Poster.orders.getActive(); // Получаем текущий заказ
await window.Poster.orders.setOrderBonus(result.order.id, 10) // Ставим скидку 10 грн

window.Poster — это глобальная переменная со всеми методами API. Под капотом каждый метод отправляет сообщение на Poster через postMessage. Каждому сообщению присваивается уникальный хеш. По этому хешу в локальную переменную сохраняется callback, который нужно вызвать, как только закончится выполнение метода. Когда Poster заканчивает выполнение, он отправляет на iFrame postMessage c результатом и хешом сообщения, на которое отвечает. По нему мы находим сохраненный callback и вызываем его.

Таким образом, вся коммуникация между iFrame и Poster построена на postMessage, при этом весь этот процесс двухсторонний и может инициироваться как со стороны Poster, так и со стороны фрейма.

Для удобства разработки все функции API возвращают Promise. Код процессится через babel поэтому можно использовать async. Это упрощает написание кода и помогает избежать callback hell.

Интерфейс и общение с пользователем

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

По умолчанию, iFrame скрыт от пользователя. Чтобы показать его, нужно вызвать метод Poster.interface.popup, который просто меняет CSS у iFrame и показывает его поверх основного интерфейса. В попапе разработчик рендерит страницу любым удобным для него способом. Например, мы используем React, но можно подключить другие фреймворки: Angular, Vue.js.

Некоторые ивенты могут менять обычный алгоритм работы Poster. Такие ивенты называются блокирующими и ждут ответа от приложения. Например, ивент закрытия заказа: в один из аргументов обработчика приходит callback-функция, которую нужно вызвать, чтобы продолжить закрытие заказа. Эту функцию нужно вызвать в течение 5 секунд или отобразить свой интерфейс, иначе мы перезагрузим iFrame и продолжим стандартный алгоритм работы Poster.

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

// Подписываемся на ивент перед закрытием заказа
Poster.on('beforeOrderClose', (data, next) => {
    // Если к заказу уже привязан гость, продолжаем обычный алгоритм работы Poster
    if (data.order.clientId) {
        next();
    } else {
        // Показываем окно с напоминанием
        Poster.interface.popup({title: 'Спроси про дисконтную карту'});
    }
});

В итоге, в платформе можно нарисовать любой интерфейс, но он ограничен рамками iFrame и не может поломать работу кассы. К сожалению, не у всех разработчиков есть дизайнеры, которые смогут продумать UI/UX до деталей. Большинство просто юзает Bootstrap и не заморачивается над качеством. Это привело нас к новому вызову — сделать интерфейсы внешних приложений простыми и красивыми. Глобально, мы еще не решили эту задачу, но план уже есть:

  1. Выпустить свою библиотеку с компонентами интерфейса, чтобы разработчики могли использовать ее вместе с Bootstrap.
  2. Сформировать UI/UX гайдлайны.
  3. Сделать шаблон приложения с готовым интерфейсом.

Все наши планы по разработке платформы мы опубликовали на доске Trello, где видно какие задачи и когда мы планируем брать. Там же внешние разработчики могут голосовать за фичи или предлагать новые.

Инсайты в процессе разработки

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

Кастомные доработки под клиента

В ядре продукта мы стараемся не делать фичи под клиента. Раньше приходилось отказывать в кастомных доработках, например, запретить гостям пользоваться бонусами, пока сумма покупок не превысит 500 грн.

Сейчас рекомендуем нанять разработчика, который сделает эту доработку для кассы. Так внешний разработчик сети кофеен RedCup решил проблему с мошенничеством со стороны гостей, когда гость покупает сим-карту за 50 грн, регистрирует карту лояльности и получает бонус 200 грн.

Упрощение выхода на зарубежные рынки

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

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

Улучшение архитектуры кода в ядре

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

Итог

За полтора года работы POS-платформы iFrame зарекомендовал себя как отличный инструмент, и мы планируем использовать его в дальнейших проектах. С его помощью мы качественно решаем проблемы наших клиентов и приводим новых клиентов партнерам. Уже 1200 заведений из 8000 пользуются внешними разработками.

Если эта статья будет интересна читателям, в следующей я поделюсь опытом создания Device-платформы — хаба, который управляет девайсами в заведении.

LinkedIn

15 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.
открыто в браузере в нативном приложении
с локальной машины

У вас же столько точек где можно расставить приёмку того или иного плагина. Зачем было городить iFrame? Вы же сами знаете что это адски ресурсо-затратно:

если 5 штук добавить на страницу, начнет тупить. Тогда все, что нужно сделать — не добавлять 5 штук

И ладно если нужно отрендерить форму оплаты от PCI-Compliant processing’а — тут и вариантов особо то нет — чтоб не проходить compliance самим нужно рендерить форму «на стороне процессинга». Но зачем вы сами такое навешиваете? Почему не проставить просто html в div из того же бандла и не таскать iFrame?

кто-то бесшовно поломает всю систему

Нет же, если делать приёмку. Если прогонять html/js/css lint тучи которых существуют, рендерить на стороне приёмки и считать сколько места оно занимает, и только после этого enable’ить bundle к загрузке пользователям на локалку (очень оценил такой ход — смелые вы) и, соответственно, интеграцию. Ещё можно трейсить в нативе и штрафовать/disable’ить bundle’ы за ошибки js/css/выход за разрешенное пространство.
Кастомеры явно же захотят превратить вашу панель в брата-близнеца панели iBox’а или чего подобного с миллионом плагинов. Если бизнес принимается за интеграции — их уже не остановить.
А что предлагает ваша платформа? Не боле 4х интеграций? Потому что с пятой начинаем тупить? И это на таком сильном (относительно мобильного рынка) девайсе как яблоПад?

функцию нужно вызвать в течение 5 секунд или отобразить свой интерфейс, иначе мы перезагрузим iFrame и продолжим стандартный алгоритм работы

Что это за костыль? А если приложение тупит т.к. там уже 4 интеграции от нерадивых разработчиков? Думаете такого не будет? Так что, заказ вообще нельзя будет закрыть? Как можно завязываться на абсолютное значение по времени in-process? Вы же не по сети ходите. У вас же не может быть потери пакетов. Натива либо работает либо нет.

работаем над системой, которая будет показывать владельцу заведения, что рестораторы в его регионе закупают морковку или другие ингредиенты по 25 гривен, а он по 30

вашим ребятам, конечно, виднее, но смысл от фичи?

  1. Откуда данные по закупкам? Как проверить достоверность? Вы с 1С (или что там на ваших рынках, Salesforce сотоварищи?) интегрировать собрались? Аппу, которая бегает локально на ипаде?
  2. Как узнать у какого поставщика закупки дешевле?
  3. И самое главное: как сравнить качество?
Тут наоборот бы фичу — показывать какие дорогие ингридиенты (тоже забивая на то что это не влияет на качество этих самых ингридиентов) тем клиентам заведений, что считают дорогопа багатому === качественно. Типа сыроеды и иже с ними. Только это клиентам надо показывать — в аппе с QR-кодом для лояльности. Саму аппу о которой в статье ведь персонал только видит.

1.
Саша, ты написал две цитаты: «открыто в браузере в нативном приложении» и «с локальной машины» вместе, хотя они взяты из разных частей статьи.

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

Открыто в браузере в нативном приложении именно SPA. SPA это и есть ядро Poster. Выбрали веб приложение, потому что легче поддерживать одно приложение в вебе чем писать 3 нативных интерфейса для каждой платформы.

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

Когда я говорил про локальную машину, я имел ввиду что bundle грузится с компьютера разработчика только в момент разработки плагина. У клиентов JS грузится с нашего сервера.

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

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

Насчет 5 интеграций, я еще не видел ни одного клиента, у которого бы стояло 5 интеграций на одной кассе.

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

3.
Задержка 5 секунд выделяется на каждое приложение и сформирована чисто эмпирическим путем. Опять же, возможно в статье это не достаточно детально описано, но при задержке в 5 секунд мы перезагружаем именно iFrame, а Poster продолжит свою работу как будто плагин не подключен, и пока приложение загружается, официант сможет спокойно закрыть заказ.

Что ты понимаешь под фразой «Вы же не по сети ходите. У вас же не может быть потери пакетов.»? Касса постоянно общается с сервером и при плохом интернете могут быть сбои и задержки.

4.
Фича по сравнению закупочных цен еще в разработке. Данные мы планируем брать анонимизированные по клиентам Poster, то есть это агрегированная неперсонализированная статистика.

Владельцу заведения в его админ панели будут отображаться данные по его региону и по похожему размеру заведения.

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

У вас счёт выставлен на 270 рашкоублей, вы списываете 150 чего-то (сумма без указания валюты не имеет смысла) и получаете к оплате 120грн. Как это вообще работает? Какой клиент на это согласился? Тот, что платит за капучино 110USD? Но то ладно, демо цены. Но как так — в разных валютах и без курсов?

В Poster валюта аккаунта не привязана к реальной валюте или ее курсу. Острой потребности в этом нет, так как один аккаунт Poster всегда работает в рамках одной страны. Тут я не корректно записал демо и не посмотрел на валюту тестового аккаунта.

У реальных клиентов таких проблем не возникает.

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

Плюсую — iframe отличное решение для встраивания контента кастомеров. А главное секьюрно. Написал скрипт обертку который посылает/слушает мессаджи и регистрирует коллбеки. Отдал кастомеру, и все. А он подключил его себе на страницу и юзает как с обычным API сервером. Например:
wrapper.call(’getPromotions’, (data) => initPromotions(data))

Согласен, сейчас пробуем еще одним способом использовать iFrame:

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

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

И так можно делать со всеми сущностями по проекту, которые зависят от других от друга.

Отличная статья, спасибо.

IFRAME нормальна внучка нормального дєдушкі. У меня в SeekTable тоже простой ембеддинг репортов так сделан, с апишкой через postMessage — хотя есть и web API c потенциальной возможностью встроить без ифреймов, по факту в этом случае требуется дев и гораздо больше затрат на интеграцию.

В наш век всяких гуляров, оверхед от ифреймов уже и не оверхед вовсе. И даже если их 5 штук.

Да, главное чтобы визуально все смотрелось органично и iFrame не казался инородным телом в продукте

Я все понимаю, что комментарий больше политический, но

Мы первые в СНГ сделали это по-другому

Украина не входит в СНГ.

ru.wikipedia.org/...​во_Независимых_Государств

да, имел ввиду на постсоветском пространстве

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

Спасибо, посмотрим как у них все оформлено

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