Мобильная версия SPA приложения

Возможны 2 подхода:

  • Одно адаптивное приложение
  • Два отдельных приложения

Одно приложение

Adaptive Web Design использует медиа выражения и несколько макетов в зависимости от ширины экрана (мобильные устройства, планшеты, настольные компьютеры). На каждом из макетов скрывается/показывается определённая часть разметки. Без использования JS может иметь несколько дублирующих элементов в HTML разметке, например переключатель языка перемещается из подвала (desktop) в пункт меню в шапке (mobile).

JS позволяет отрисовывать разные компоненты на одной странице (маршруте). В Angular можно использовать сервис определяющий breakpoints и наработки material. Доклад, демонстрирующих подобный подход - Reactive Responsive Design - Michael Madsen. А далее либо заменять всю конфигурацию router`a (resetConfig) и тогда будут уникальные маршруты с привязанными специфичными компонентами. Либо создавать компоненты обёртки, которые будут выбирать какой компонент требуется отрисовать в конкретном месте.

Преимущества:

  • Нет необходимости поддерживать отдельные приложения (веб-сайты) для разных устройств
  • При переходе между версиями приложение не загружается с самого начала
  • Каждой странице единый и уникальный URL
  • Нельзя ошибиться в выборе версии. На мобильных устройствах всегда мобильная версия (Про User-Agent)

Недостаток:

  • Основной - загружаемый размер данных. В основном это проблема мобильных устройств. В мобильной версии не нужны библиотеки для настольной версии. Трафик не везде быстрый и дешёвый.
  • В коде "каша" из настольных и мобильных компонентов.

Если приложение (сайт) достаточно простой то одно приложение, наверное, лучший выбор. Дополнительный плюс: статистика совместного использования в счетчиках и в социальных сетях (Facebook Likes, Tweets) считается без разделения, поскольку версии страниц для мобильных и настольных компьютеров используют один уникальный URL-адрес.

Два приложения

Полнофункциональная мобильная версия SPA не всегда нужна.

Преимущества:

  • Основное - загружаемый размер данных. Загружается только то, что нужно.
  • Можно использовать разные технологии (языки) в настольной и мобильной версии.
  • Код относительно независим, проще вести разработку отдельными командами.

Недостатки:

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

Определение мобильного устройства

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

Как определять мобильное устройство?

По User-Agent на сервере или клиенте.

Технически это регулярное выражение сопоставляемое с User-Agent.

На сервере реализуется средствами nginx (предпочтительно по скорости) или сервером приложения. Примеры:

PHP device-detector и проекты основанные на нем имеют ~ 10 000 тестов, потому что определение по User-Agent даёт заметное количество ошибок на непопулярных устройствах/браузерах, которым требуется имитировать популярные аналоги.

Пример для nginx:

set $mobile_rewrite do_not_perform;

if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
  set $mobile_rewrite perform;
}

if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
  set $mobile_rewrite perform;
}

if ($mobile_rewrite = perform) {
  rewrite ^ http://m.site.ru redirect;
  break;
}

По ширине устройства на клиенте

В браузере доступно свойство окна (window.innerWidth), показывающее ширину документа без вычета полосы прокрутки. То есть на мониторе 1920 innerWidth будет равно 1920.

Устройства с выбранной шириной, например 640px, считаются мобильными.

Размещение мобильной версии

Два вопроса. Где размещать мобильную версию и как между ними переключаться?

На одном домене с настольной версией

Теоритически можно организовать для обычных сайтов, но практически доступно только для SPA приложений. Проблема с обычными сайтами - определение происходит по User-Agent на сервере. Если опредление версии ошибочное, то обычный клиент (наркоман всегда знает как подменить User-Agent) не сможет получить правильную версию. Поэтому отдельную мобильную версию размещают на поддомене, чаще всего m.site.ru.

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

Переключение осуществляется через query параметр, например ?force=mobile или ?force=desktop и обновление страницы.

На поддомене m.site.ru или по отдельному маршруту /mobile

Переключение осуществляется через редирект на соответствующую версию. Из примера выше:

if ($mobile_rewrite = perform) {
  # rewrite ^ http://site.com/mobile redirect;
  rewrite ^ http://m.site.ru redirect;
  break;
}

При необходмости на мобильную версию можно попасть просто по ссылке.

Имеется SEO проблема. Индексирование мобильной версии сайта на поддомене. Чтобы сообщить роботу о наличии мобильной версии сайта на поддомене, необходимо указать на страницах основного сайта соответствующие URL мобильной версии при помощи атрибута rel="alternate" тега link , например

<link rel="alternate" media="only screen and (max-width: 640px)" href="http://m.site.ru/dir/" />

Собственный опыт

В январе 2020 в Vepp добавили мобильную карточку сайта. Сделано второе Angular приложение специально для мобильных устройств. Соответственно имеется 2 Angular приложения. Размещается на одном домене. Определение происходит на клиенте по ширине окна. Мобильными считаются устройства с шириной экрана менее 768 пикселей.

Для вставки требуемых ресурсов в новый index.html документ используется следующий скрипт:

function loadStyles(styles) {
  styles.forEach(style => {
    let linkTag = document.createElement('link');
    linkTag.rel = style.rel;
    if (linkTag.type) {
      linkTag.type = style.type;
    }
    linkTag.href = style.href;
    document.head.append(linkTag);
  });
}
function loadScripts(scripts) {
  scripts.forEach(script => {
    let scriptTag = document.createElement('script');
    scriptTag.src = script.src;
    if (script.type) {
      scriptTag.type = script.type;
    } else {
      scriptTag.noModule = true;
    }
    if (script.defer) {
      scriptTag.defer = true;
    }
    document.body.append(scriptTag);
  });
}

const links = {
  client: ['{{clientLinks}}'],
  mobile: ['{{mobileLinks}}'],
};

const scripts = {
  client: ['{{clientScripts}}'],
  mobile: ['{{mobileScripts}}'],
};

let version = null;
if (window.location.href.match('force=mobile')) {
  version = 'mobile';
} else if (window.location.href.match('force=client')) {
  version = 'client';
} else {
  version = window.innerWidth < 768 ? 'mobile' : 'client';
}

loadStyles(links[version]);

,где {{clientLinks}}, {{clientScripts}} - массивы стилей и скриптов (runtime, polyfills, main), полученных из артефактов сборки путем парсинга index.html файлов. Особенность: скрипты загрузки полифилов должны быть вставлены первыми.

Похожие записи

RxJS Pipeable Operators

Начиная с версии rxjs 5.5 операторы вместо цепочки вызовов применяются как параметры функции pipe.

Добавить css link и js script динамически

const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css';
link.integrity = 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO'; // необязательно
link.crossOrigin = 'anonymous'; // необязательно
document.head.appendChild(link);

const script = document.createElement('script');
script.src = 'https://code.jquery.com/jquery-3.3.1.slim.min.js';
script.integrity = 'sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo'; // необязательно
s...