Pull to refresh

Отличия в адаптации сайта и AJAX веб-приложения для iOS

Website developmentJavaScriptDevelopment for iOS
Есть сейчас такая тенденция — делать в сайтах поддержку планшетов iPad и других устройств на iOS: iPhone, iPod. Но если для сайтов это достаточно просто, при хорошей верстке, можно добавить пару тегов в head и готово, то для веб-приложений, где есть сессии с использованием Cookies, все обстоит сложнее и есть подводные камни. Итак, возможно, еще не все знают, что в мобильном Safari можно нажать кнопку меню (со стрелкой, как на рисунке) и выбрать там «Добавить в Домой» / «Add to Home Screen», тогда для сайта появится иконка на рабочем столе. Но иконка будет просто запускать Safari с этим сайтом, а вот если добавить пару известных тегов (см. ниже), то все элементы управления Safar будут скрыты и приложение будет работать на полный экран, как обычные нативные приложения iOS. Так вот основная выявленная проблема в том, что в этом режиме сессия все время сбрасывается. Стоит переключится на другое приложение или рабочий стол, даже просто перейти по ссылке, и опять вернуться в веб-приложение, как страница перегрузится и сессионной Cookie уже не будет, нужно логиниться заново. Эту проблему то мы и решим.

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

Рецепт для простого сайта


Решение это известно, я приведу его только потому, что ниже дополню его отличиями для веб-приложения.
Тут документация по дополнительным мета-тегам Safari, а вот нужные нам мета-теги для вставки в head:

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<link rel="apple-touch-icon-precomposed" href="/favicon.png">

Картинка /favicon.png станет иконкой на рабочем столе. Про размеры иконок подробнее написано в документации, их может быть несколько, например.

Полноэкранный режим


Для перехода приложения в полноэкранный режим, присовокупим к тем двум тегам еще два:

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black" />

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

Решение для WebApplication


Как я вскользь упоминал, для веб-приложений нужно выдержать дополнительные условия: приложение не должно переходить по ссылкам a href, это приводит к открытию ссылок в браузере и выходу из фулскрин режима. Но вот делать window.location.reload(true); можно и даже window.location = "/demo/path"; вполне разрешен из JavaScript. При этих переходах кукизы не теряются и все хорошо.

Следующий код позволит сохранить сессионный cookie в localStorage, и когда кукиз будет потерян при переходе между приложениями в iOS, то этот же код восстановить из кукиз и перегрузит страницу, чтобы сервер отдал ее в том виде, как должен получить залогиненый пользователь.

function PersistCookie(SessionCookieName) {
  if (localStorage && (
    navigator.userAgent.match(/iPhone/i) ||
    navigator.userAgent.match(/iPod/i) ||
    navigator.userAgent.match(/iPad/i)
  )) {
    var CookieSession = document.cookie.match(new RegExp(SessionCookieName + "=[^;]+"));
    var LocalSession = localStorage.getItem(SessionCookieName);
    if (CookieSession) {
      CookieSession = CookieSession[0].replace(SessionCookieName + "=", "");
      if (LocalSession!=CookieSession) {
        localStorage.setItem(SessionCookieName, CookieSession);
      }
    } else if (LocalSession && LocalSession!=CookieSession) {
      document.cookie = SessionCookieName + "=" + LocalSession + "; path=/";
      window.location.reload(true);
    }
  }
}

Как видно из кода, у нас есть два места хранения сессионной переменной: document.cookie и localStorage, мы читаем из обоих, а пишем туда, где кода небыло. В случае, если код есть в обоих местах, то предпочтение отдается document.cookie, т.к. может так случиться, что сервер заменит сессионную переменную и нам ее нужно записать поверх той, что уже есть в localStorage. Пример вызова: PersistCookie(«SID»); В параметрах передается имя сессионной куки. Вызов нужно делать при загрузке страницы, но оборачивать в событие «onload» или в jQuery.ready() не обязательно. Для PHP имя сессионной куки «PHPSESSID», для ASP.NET «ASP.NET_SessionId» и т.д. но может меняться в настройках сервера или программно. При отлогинивании пользователя нужно не забыть сделать if (localStorage) localStorage.clear(); чтобы кукиз не вернулся. Еще можно отключить проверку navigator.userAgent, чтобы код работал не только в iOS, но я не исследовал, будет ли это полезно или вредно.

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

UPD: Обнаружилась приятная особенность, localStorage сквозной для всего домена, то есть, залогинившись в Safari, сессия распространяется на установленное «псевдо-приложение» и наоборот, если в приложении залогиниться, то потом в сафари кукиз добавляется, если страницу обновить.

UPD2: Есть и неприятная особенность, каждый раз после возвращения к «псевдо-приложению», кроме сброса кукизов еще и страница перегружается, то есть, если выдать форму и пользователь ее начинает заполнять, потом переключился куда-то, вернулся и все пропало, и форма и все, что ввел. Так что, еще до поста нужно все сохранять в localStorage. Скорее всего, нужно сделать универсальное решение для сохранения форм с любыми полями, и вообще, сохранения «состояния» приложения на момент переключения, чтобы восстановить что там было. Состояние же может содержать состояние навигации внутри приложения, состояние контролов (например закладок, списков), состояние прокрутки, состояние динамических изменений html и css на странице. Все же сбрасывается на пол пути. Кстати, страница при сбросе не перегружается с сервера, а берется из кеша.

UPD3: Для тех, кто хочет минимальными усилиями сделать обычный сайт псевдо-приложением в фулскрин режиме, но не имеет желания переписывать все на AJAX, можно перехватывать все ссылки и делать переходы между страницами через window.location. При этом, как уже говорилось, Safari не выбьет вас в режим браузера, если только ссылка не будет вести на другой домен. Вот решение на jQuery:

$('a').live('click', function(e) { e.preventDefault(); window.location = $(this).attr('href'); });

Но остается проблема со сбросом сайта на первую страницу при переключении между приложениями. Это лечится так же, как и с кукизами — сохраняем в localStorage. Конечно же, нужно определаять, куда ведут ссылки и сохранять в localStorage только ссылки в пределах нашего домена, все другие и так будут открываться в Safari. Вот все наработки собраны в расширение для jQuery:

(function($) {

$.platform = {
  iPhone: navigator.userAgent.match(/iPhone/i),
  iPod: navigator.userAgent.match(/iPod/i),
  iPad: navigator.userAgent.match(/iPad/i),
  Android: navigator.userAgent.match(/Android/i)
};

$.platform.iOS = $.platform.iPhone || $.platform.iPod || $.platform.iPad;
$.platform.Mobile = $.platform.iOS || $.platform.Android;

$.extend({

  fixLinks: function(persist) {
    if ($.platform.iOS) {
      if (persist == null) persist = true;
      persist = persist && localStorage;
      if (persist) {
        var CurrentLocation = window.location.pathname + window.location.search;
      	var StoredLocation = localStorage.getItem("location");
        if (StoredLocation && StoredLocation !== CurrentLocation) {
          window.location = StoredLocation;
        }
      }
      $('a').live('click',function(e) {
        e.preventDefault();
        if (persist && this.host === window.location.host)
        localStorage.setItem("location", this.pathname + this.search);
        window.location = this.href;
      });
    }
  },

  fixCookie: function (SessionCookieName) {
    if (localStorage && $.platform.iOS) {
      var CookieSession = document.cookie.match(new RegExp(SessionCookieName + "=[^;]+"));
      var LocalSession = localStorage.getItem(SessionCookieName);
      if (CookieSession) {
        CookieSession = CookieSession[0].replace(SessionCookieName + "=", "");
        if (LocalSession!=CookieSession) {
          localStorage.setItem(SessionCookieName,CookieSession);
        }
      } else if (LocalSession && LocalSession !== CookieSession) {
        document.cookie = SessionCookieName + "=" + LocalSession + "; path=/";
        window.location.reload(true);
      }
    }
  }

});

})( jQuery );

Использовать расширение очень просто, подключите js файл с библиотекой и при загрузке страницы вставьте вызовы:
  • Исправить проблему с нежелательным выходом в Safari по ссылкам для iOS полноэкранного приложения и сохранять/восстанавливать текущий URL в пределах домена в localStorage: $.fixLinks();
  • Исправить проблему с выходом в Safari для iOS, но не запоминать URL в localStorage: $.fixLinks(false);
  • Исправить проблему со сбросом сессионных Cookie: $.fixCookie(«SID»); где «SID» имя сессионной куки.
Tags:веб-приложениеcookiessessioniOSiPadiPodiPhone
Hubs: Website development JavaScript Development for iOS
Total votes 27: ↑27 and ↓0 +27
Views13K

Popular right now

Разработчик iOS/iPhone
from 125,000 to 250,000 ₽Golf PadНовосибирск
Tech Lead (Development)
from 180,000 ₽Game InsightRemote job
iOS developer
from 200,000 to 300,000 ₽anonym.networkRemote job
IOS разработчик
from 100,000 to 150,000 ₽ЭР-Телеком ХолдингRemote job
iOS Software Engineer
from 200,000 ₽NUTSonМоскваRemote job