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

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

Используйте навигацию, если не хотите читать статью полностью:

Как определить подход к разработке приложения

В России публикация и монетизация в популярных магазинах приложений в последние годы, мягко говоря, осложнилась. Google объявил о приостановке выплат для аккаунтов с российскими банковскими реквизитами, а ряд приложений был удален из магазинов. Многие команды ищут альтернативные пути дистрибуции, чтобы прийти напрямую к пользователю.

Например, есть нативные приложения. Их пишут отдельно под каждую платформу: для iOS — на Swift или Objective-C, для Android — на Kotlin или Java. Такой подход дает максимальную производительность и полный доступ к системным возможностям — камере, сенсорам, уведомлениям. Он скорее оправдан для сложных проектов с жесткими требованиями к UX и скорости.

Есть также No-code и low-code платформы. Они используют визуальные редакторы и преднастроенные компонент��. Приложения на основе WebView, по сути, представляют собой обертку, внутри которой отображается веб-страница из интернета или локального файла. 

WebView — компонент, позволяющий отображать веб-страницы или HTML внутри мобильного или десктопного приложения.

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

Нативная «обертка» — это, например, нижнее меню и экран загрузки. Но в сердце этого приложения большое пустое окно браузера. А значит работает оно с теми же технологиями, ограничениями и безопасностью. 

Когда приложение запускается, оно загружает в этом браузере сайт — например, если это сервис по заказу еды, он загружает онлайн-меню своего ресторана. Это меню, по сути, является сайтом — с HTML, JavaScript, бэкендом, куками и прочим.

Главные минусы приложений с WebView — зависимость от интернета и ненативный интерфейс. Это значит, что веб-контент может не соответствовать ожиданиям пользователей, привыкших к нативным приложениям. 

Также, меньший доступ к API и ограничения платформы. Последний тезис про то, что разные ОС имеют различные версии WebView и это приводит к несогласованности.

Есть еще React Native и Flutter. Они занимают промежуточное место между WebView и чистой нативной разработкой. React Native использует JavaScript и создает мост к нативным компонентам, что позволяет быстро переиспользовать веб-логику и иметь общую кодовую базу для iOS и Android. При этом иногда требуется доработка нативных частей и оптимизация. 

Flutter основан на Dart и рендерит интерфейс через движок Skia, поэтому дает очень высокую производительность и одинаковый внешний вид (консистентный UX) на разных платформах. Это выгодно для сложных анимаций и кастомного дизайна. Но тут другая проблема — нужно отдельно решать вопросы для веб-версии, помимо того, что изучение Dart потребует времени. 

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

В чем плюсы WebView

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

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

Когда стоит использовать это решение

Выбор зависит от конкретных задач и ресурсов. Тогда почему бы это просто не обернуть в PWA? 

PWA — это веб-приложение, которое работает внутри браузера операционной системы, используя возможности Service Worker и кэширования.

Сейчас скажу попроще. PWA — это когда вы открываете браузер (Safari или Google Chrome) и заходите на сайт-оригинал и сохраняете его на экране в качестве приложения. Тогда можно нажать на значок и откроется веб-сайт в формате веб-приложения. По сути он открывается в браузере, который изначально был установлен для PWA. 

Оно занимает в среднем до мегабайта памяти. Информация в таком приложении кэшируется, а значит, PWA будет работать в офлайн. Большим плюсом является возможность изменять все в несколько кликов (офферы и тексты внутри PWA).


Но в чем загвоздка. Такое приложение нельзя опубликовать в сторе, потому что это, по сути, ссылка. Поэтому разработчики пошли дальше и придумали TWA (Trusted Web Activity). По сути, PWA обернут в самостоятельный браузер через Cordova. 

В случае с Cordova вы создаете как бы свой браузер без интерфейса, в котором есть только WebView вашего сайта и вот уже его можно выложить в стор. Потому что это именно приложение (браузер у которого одна зада��а — отображать ваш веб-сайт). Как это сделать — рассказали в статье.


TWA отличается от обычного WebView тем, что работает как самостоятельное приложение. Он основывается на протоколе Digital Asset Links, который устанавливает доверительные отношения между вашим доменом и приложением, подтверждая право собственности и позволяя запускать веб-контент как часть нативного приложения.

Webview-приложение проходит модерацию (например, в Google Play) и требует установки на телефон. Приложение может быть арендовано, и одновременно его могут использовать для размещения трафика сотни вебов.

По факту тот же PWA и TWA — это не WebView, но они тесно связаны. И это все — далеко не no-code. Настройка манифеста, Service Worker, HTTPS и Digital Asset Links требует не только технических знаний, но и работы. Если не хотите с этим разбираться, понадобится конструктор. 

Значит, для простого мобильного приложения часто достаточно обернуть готовый сайт в WebView и получить установочный APK. Именно этим займемся дальше.

Мобильная ферма Selectel

Начните тестировать на реальных устройствах за 2 минуты – откуда угодно.

Попробовать →

Конструктор веб-приложений

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

Для примера возьмем бесплатный AppsGeyser. У него простой интерфейс, но он работает и делает все, что нам нужно. У бесплатной версии есть два важных ограничения — в приложении может показываться реклама платформы и такую сборку нельзя напрямую опубликовать в Google Play. Для быстрой проверки идеи это не проблема, а если нужна публикация без брендинга, всегда можно перейти на платный план.

Заходим на главную страницу сервиса и кликаем Login для регистрации — без этого сервис не отдаст готовый файл. Дальше все стандартно: вводим почту, придумываем пароль и нажимаем кнопку Sign up:

Теперь можно переходить к созданию приложения. Нажимаем Создать приложение (Create app) в правом верхнем углу.

Мы сразу попадаем на главный экран. Платформа предложит указать источник контента: URL, загрузку HTML или ZIP. 

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

Что мы знаем про людей из IT и программистов? У них нет времени. Точнее, время есть, но не на рутинные дела. Поэтому для теста я написал небольшое веб-приложение для кофейни под названием — «Завтрак программиста».

Создание веб-приложения

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

Начинаем с того, что создаем стандартный HTML-документ с базовыми мета-тегами, чтобы страница хорошо отображалась в WebView, особенно на мобильных устройствах с вырезами и узкими экранами:

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
  <meta name="theme-color" content="#0b5cff">
  <title>Завтрак программиста — мобильная версия</title>

Указываем ключевой тег viewport с параметром viewport-fit=cover. Он важен для корректного отображения на смартфонах с «челками» и вырезами, чтобы контент не прятался за ними. Цвет темы задает оттенок синего для системного интерфейса вокруг WebView (например, статус-бара).

Переходим к стилям. Мы определяем CSS-переменные для основных цветов — это удобно, чтобы позже менять всю цветовую схему за пару строчек:

:root {
  --accent:#0b5cff; --bg:#f7f9ff; --card:#fff; --muted:#6b7280;
}

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

body {
  margin: 0; font-family: system-ui,-apple-system,Segoe UI,Roboto;
  background: var(--bg); color: #111;
  -webkit-tap-highlight-color: transparent;
}

-webkit-tap-highlight-color: transparent; — убирает нежелательный синий оттенок при нажатии по элементам в мобильном WebView, улучшая визуальный опыт.

Оборачиваем все содержимое в контейнер .wrap с ограниченной шириной 480px, чтобы на телефоне страница выглядела максимально удобно и не растягивалась на большие экраны:

.wrap {
  max-width:480px; margin:0 auto; padding:12px;
}

Делаем шапку нашего приложения — она синяя (я конечно не дальтоник, но это и мой любимый цвет) и содержит логотип-эмодзи и заголовок с подзаголовком:

<header>
  <div class="logo">☕</div>
  <div>
    <h1>Завтрак программиста</h1>
    <p class="lead">Быстро и офлайн</p>
  </div>
</header>

В CSS это оформлено так, чтобы шапка выглядела современно и сжато, а эмодзи была ровно по центру и в круглом фоне с небольшим прозрачным слоем:

header {
  background: linear-gradient(90deg,var(--accent),#2c6eff);
  color: #fff; border-radius:12px; padding:10px;
  display:flex; align-items:center; gap:10px;
}
.logo {
  width:42px; height:42px; border-radius:10px;
  background:rgba(255,255,255,0.15);
  display:flex; justify-content:center; align-items:center;
  font-weight:700;
}
Получается вот так.
Получается вот так.

Основной контент идет в теге <main>. Первый — это популярные позиции из меню, оформленные в виде карточек. Для каждого продукта задается иконка-эмодзи, название и цена. 

Динамическое наполнение происходит в скрипте — мы описываем товары в массиве PRODUCTS, а потом уже программно создаем карточки. Такой способ хорош тем, что можно легко менять меню без правки HTML:

<main>
  <section class="card">
    <strong>Популярное сегодня</strong>
    <div class="products" id="products"></div>
  </section>

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

<section class="card" style="display:flex; gap:8px; align-items:center;">
  <input type="email" id="email" placeholder="email для промокода" style="flex-grow:1; min-width:0; padding:9px; border-radius:8px; border:1px solid #e6e8f5; font-size:11px;">
  <button id="subscribe" class="btn-ghost" style="padding:8px 12px; font-size:13px; white-space:nowrap;">OK</button>
</section>
</main>
Появилось дополнительное поле.
Появилось дополнительное поле.

Для предотвращения перекрытия поля подписки с фиксированной панелью корзины добавил нижний отступ в main:

main {
  margin-top:12px;
  padding-bottom:90px; /* защита от перекрытия нижней панелью */
}

Нижняя панель закреплена внизу экрана, показывая кнопку корзины. Там отображается иконка и значок с количеством выбранных товаров:

<div class="bottom">
  <div class="bar" id="cartBar">
    ? Буфер <span id="badge" class="badge" style="display:none">0</span>
  </div>
</div>

И ее стили:

.bottom {
position:fixed; left:0; right:0; bottom:0;
background:rgba(255,255,255,0.92);
backdrop-filter:blur(8px);
padding:10px;
display:flex; justify-content:center;
box-shadow:0 -3px 12px rgba(0,0,0,0.05);
}
.bar {
background:var(--accent); color:#fff;
border-radius:14px; padding:10px 16px;
font-weight:700; display:flex; align-items:center; gap:6px;
}
.badge {
background:#ff3b30; border-radius:999px;
padding:2px 8px; font-size:12px;
}

Модальное окно корзины скрыто по умолчанию и появляется поверх контента, когда пользователь открывает корзину:

<div class="modal" id="modal" aria-hidden="true">
  <h3>Ваш заказ</h3>
  <div id="rows"></div>
  <div style="display:flex;gap:8px;margin-top:10px">
    <button id="send">В продакшн</button>
    <button id="close" class="btn-ghost">Откатить</button>
  </div>
</div>

Его базовые стили:

.modal {
  position:fixed; left:12px; right:12px; bottom:18%;
  background:var(--card); border-radius:14px;
  box-shadow:0 10px 40px rgba(0,0,0,0.2);
  padding:12px; display:none;
  max-height:65vh; overflow:auto;
  animation: fadein .2s ease;
}
Вот так это выглядит.
Вот так это выглядит.

Логика приложения

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

Вначале у нас есть массив PRODUCTS, где каждому товару присвоены id, имя, цена и даже эмодзи-иконка. Это маленькая база данных внутри страницы:

const PRODUCTS = [
  {id:'c',name:'Фикспучино',price:250,icon:'☕'},
  {id:'f',name:'Сеньор-латте',price:270,icon:'?'},
  {id:'s',name:'Фулл-стек сендвич',price:220,icon:'?'}
];

Для хранения корзины используется localStorage — так оффлайн‑заказы не потеряются, ��аже если пользователь закроет приложение:

const cartKey='zp_cart';
const load=()=>JSON.parse(localStorage.getItem(cartKey)||'[]');
const save=data=>localStorage.setItem(cartKey,JSON.stringify(data));

Далее, функция renderProducts отвечает за создание карточек товаров. Она перебирает PRODUCTS и для каждого продукта создает HTML-разметку — блок с иконкой, названием, ценой и кнопкой «Добавить». Элементы динамически добавляются в контейнер .products:

function renderProducts(){
  productsEl.innerHTML=''; 
  PRODUCTS.forEach(p=>{
    const el=document.createElement('div');
    el.className='prod';
    el.innerHTML=`<div class="icon">${p.icon}</div>
      <div class="meta">${p.name}</div>
      <div class="price">${p.price} ₽</div>
      <button data-id="${p.id}">Добавить</button>`;
    productsEl.appendChild(el);
  });
}

Функция renderBadge подсчитывает количество товаров в корзине и обновляет значок бейджа. Если корзина пустая — бейдж прячется:

function renderBadge(){
  const total=load().reduce((s,i)=>s+i.qty,0);
  if(total){ badge.textContent=total; badge.style.display='inline-block'; }
  else badge.style.display='none';
}

События кликов обработаны через делегирование на весь документ. При нажатии кнопки с атрибутом data-id происходит добавление товара в корзину. Логика простая: если товар уже есть, его количество увеличивается, иначе товар добавляется с количеством 1:

document.addEventListener('click',e=>{
  const btn=e.target.closest('button[data-id]');
  if(btn){
    const cart=load();
    const p=PRODUCTS.find(x=>x.id===btn.dataset.id);
    const item=cart.find(i=>i.id===p.id);
    if(item) item.qty++;
    else cart.push({...p,qty:1});
    save(cart); renderBadge(); return;
  }
  if(e.target.id==='subscribe') alert('Подписка оформлена');
});

Это позволяет удобно обрабатывать добавление товаров в корзину без привязки к конкретным кнопкам.

document.getElementById('cartBar').addEventListener('click',()=>{
  const cart=load();
  rows.innerHTML=cart.length ? cart.map(i=>`
    <div class="order-row"><div>${i.name} × ${i.qty}</div><div>${i.price*i.qty} ₽</div></div>`).join('')
    : <div style="color:var(--muted)">Корзина пуста</div>;
  modal.style.display='block'; modal.setAttribute('aria-hidden','false');
});

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

document.getElementById('close').addEventListener('click',()=>{
  modal.style.display='none'; modal.setAttribute('aria-hidden','true');
});
document.getElementById('send').addEventListener('click',()=>{
  const cart=load(); if(!cart.length)return alert('Корзина пуста');
  if(navigator.onLine){
    setTimeout(()=>{
      alert('Залили на прод!');
      localStorage.removeItem(cartKey);
      renderBadge();
      modal.style.display='none';
    },500);
  } else {
    alert('Оффлайн — заказ сохранен');
    modal.style.display='none';
  }
});

В конце страницы вызываются функции начальной отрисовки продуктов и обновления индикатора:

renderProducts();
renderBadge();

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

 

Вынес отображение уведомления, поэтому скрин немного растянулся.
Вынес отображение уведомления, поэтому скрин немного растянулся.
Полноценный код:
<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
  <meta name="theme-color" content="#0b5cff">
  <title>Завтрак программиста — мобильная версия</title>
  <style>
    :root {
      --accent:#0b5cff; --bg:#f7f9ff; --card:#fff; --muted:#6b7280;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0; font-family: system-ui,-apple-system,Segoe UI,Roboto;
      background: var(--bg); color: #111;
      -webkit-tap-highlight-color: transparent;
    }
    .wrap { max-width:480px; margin:0 auto; padding:12px; }
    header {
      background: linear-gradient(90deg,var(--accent),#2c6eff);
      color: #fff; border-radius:12px; padding:10px;
      display:flex; align-items:center; gap:10px;
    }
    .logo {
      width:42px; height:42px; border-radius:10px;
      background:rgba(255,255,255,0.15);
      display:flex; justify-content:center; align-items:center;
      font-weight:700;
    }
    h1 { font-size:17px; margin:0; }
    p.lead { font-size:13px; margin:4px 0 0; opacity:0.9; }
    main {
      margin-top:12px;
      padding-bottom:90px; /* защита от перекрытия нижней панелью */
    }
    .card {
      background: var(--card);
      border-radius:12px;
      padding:10px; margin-bottom:12px;
      box-shadow:0 4px 14px rgba(0,0,0,0.04);
    }
    .products {
      display:flex; flex-wrap:wrap; gap:10px; margin-top:8px;
    }
    .prod {
      flex:1 1 calc(50% - 10px);
      background:#fff; border:1px solid #f1f3fa;
      border-radius:10px; padding:12px 8px; text-align:center;
      display:flex; flex-direction:column; align-items:center; justify-content:center;
    }
    .icon { font-size:32px; margin-bottom:6px; }
    .prod .meta { font-size:14px; color:#111; }
    .price { color:var(--accent); font-weight:700; margin-top:4px; font-size:13px; }
    button {
      appearance:none; border:0; padding:8px 12px; border-radius:8px;
      background:var(--accent); color:#fff; font-weight:600;
      margin-top:8px; font-size:13px;
    }
    .btn-ghost {
      background:transparent; color:var(--accent);
      border:1px solid rgba(11,92,255,0.2);
    }
    input {
      flex:1; padding:9px; border-radius:8px;
      border:1px solid #e6e8f5; font-size:14px;
    }
    .bottom {
      position:fixed; left:0; right:0; bottom:0;
      background:rgba(255,255,255,0.92);
      backdrop-filter:blur(8px);
      padding:10px;
      display:flex; justify-content:center;
      box-shadow:0 -3px 12px rgba(0,0,0,0.05);
    }
    .bar {
      background:var(--accent); color:#fff;
      border-radius:14px; padding:10px 16px;
      font-weight:700; display:flex; align-items:center; gap:6px;
    }
    .badge {
      background:#ff3b30; border-radius:999px;
      padding:2px 8px; font-size:12px;
    }
    .modal {
      position:fixed; left:12px; right:12px; bottom:18%;
      background:var(--card); border-radius:14px;
      box-shadow:0 10px 40px rgba(0,0,0,0.2);
      padding:12px; display:none;
      max-height:65vh; overflow:auto;
      animation: fadein .2s ease;
    }
    .order-row {
      display:flex; justify-content:space-between;
      border-bottom:1px solid #f1f2f8;
      padding:6px 0; font-size:14px;
    }
    @keyframes fadein { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
  </style>
</head>
<body>
  <div class="wrap" id="app">
    <header>
      <div class="logo">☕</div>
      <div>
        <h1>Завтрак программиста</h1>
        <p class="lead">Быстро и офлайн</p>
      </div>
    </header>
    <main>
      <section class="card">
        <strong>Популярное сегодня</strong>
        <div class="products" id="products"></div>
      </section>
    <section class="card" style="display:flex; gap:8px; align-items:center;">
  <input type="email" id="email" placeholder="email для промокода" style="flex-grow:1; min-width:0; padding:9px; border-radius:8px; border:1px solid #e6e8f5; font-size:11px;">
  <button id="subscribe" class="btn-ghost" style="padding:8px 12px; font-size:13px; white-space:nowrap;">OK</button>
</section>
    </main>
  </div>
  <div class="bottom">
    <div class="bar" id="cartBar">
      ? Буфер <span id="badge" class="badge" style="display:none">0</span>
    </div>
  </div>
  <div class="modal" id="modal" aria-hidden="true">
    <h3>Ваш заказ</h3>
    <div id="rows"></div>
    <div style="display:flex;gap:8px;margin-top:10px">
      <button id="send">В продакшн</button>
      <button id="close" class="btn-ghost">Откатить</button>
    </div>
  </div>
  <script>
    const PRODUCTS = [
      {id:'c',name:'Фикспучино',price:250,icon:'☕'},
      {id:'f',name:'Сеньор-латте',price:270,icon:'?'},
      {id:'s',name:'Фулл-стек сендвич',price:220,icon:'?'}
    ];
    const cartKey='zp_cart';
    const productsEl=document.getElementById('products');
    const badge=document.getElementById('badge');
    const modal=document.getElementById('modal');
    const rows=document.getElementById('rows');
    const load=()=>JSON.parse(localStorage.getItem(cartKey)||'[]');
    const save=data=>localStorage.setItem(cartKey,JSON.stringify(data));
    function renderProducts(){
      productsEl.innerHTML='';
      PRODUCTS.forEach(p=>{
        const el=document.createElement('div');
        el.className='prod';
        el.innerHTML=`<div class="icon">${p.icon}</div>
          <div class="meta">${p.name}</div>
          <div class="price">${p.price} ₽</div>
          <button data-id="${p.id}">Добавить</button>`;
        productsEl.appendChild(el);
      });
    }
    function renderBadge(){
      const total=load().reduce((s,i)=>s+i.qty,0);
      if(total){ badge.textContent=total; badge.style.display='inline-block'; }
      else badge.style.display='none';
    }
    document.addEventListener('click',e=>{
      const btn=e.target.closest('button[data-id]');
      if(btn){
        const cart=load();
        const p=PRODUCTS.find(x=>x.id===btn.dataset.id);
        const item=cart.find(i=>i.id===p.id);
        if(item) item.qty++;
        else cart.push({...p,qty:1});
        save(cart); renderBadge(); return;
      }
      if(e.target.id==='subscribe') alert('Промокод активирован');
    });
    document.getElementById('cartBar').addEventListener('click',()=>{
      const cart=load();
      rows.innerHTML=cart.length ? cart.map(i=>`
        <div class="order-row"><div>${i.name} × ${i.qty}</div><div>${i.price*i.qty} ₽</div></div>`).join('')
        : <div style="color:var(--muted)">Корзина пуста</div>;
      modal.style.display='block'; modal.setAttribute('aria-hidden','false');
    });
    document.getElementById('close').addEventListener('click',()=>{
      modal.style.display='none'; modal.setAttribute('aria-hidden','true');
    });
    document.getElementById('send').addEventListener('click',()=>{
      const cart=load(); if(!cart.length)return alert('Корзина пуста');
      if(navigator.onLine){
        setTimeout(()=>{
          alert('Залили в прод!');
          localStorage.removeItem(cartKey);
          renderBadge();
          modal.style.display='none';
        },500);
      } else {
        alert('Оффлайн — заказ сохранен');
        modal.style.display='none';
      }
    });
    renderProducts();
    renderBadge();
  </script>
</body>
</html>

Продолжаем создавать приложение через конструктор

Прописываем название: «Завтрак программиста» и подгружаем почту, на которую будут приходить уведомления о работе приложения. 

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

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

Также в настройке приложения можно добавить интеграцию с push-уведомлениями через OneSignal. Для этого потребуется создать проект в OneSignal, получить уникальный App ID и вставить его в соответствующее поле конструктора.

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

Также можн�� посмотреть, как выглядит сайт приложения, доступное для скачивания.

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

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

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

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

Что проверять в первую очередь

Сфокусируйтесь на реальных сценариях: установка и запуск, навигация по основным разделам, формы и кнопки, попадание в оффлайн-режим, оформление заказа/отправка формы, получение push-уведомления. 

Проверьте корректность отображения на узких экранах, кликабельность элементов, отсутствие горизонтального скролла и плавность прокрутки. Не забудьте про права (permissions) и обработку их отказа пользователем.

Перед массовыми прогонами сформируйте матрицу из 6–8 репрезентативных устройств (низкий/средний/флагман, разные вендоры, несколько версий Android/iOS). Автоматизируйте базовые smoke-тесты в CI и оставляйте ручные сессии для нестандартных сценариев — так вы экономите время и деньги.

Что в итоге

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

Комментарии (0)