Недавно мне нужно было перенаправить мобильную веб-версию приложения в установленное приложение.

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

У Apple и Android есть свой особый способ обработки пользовательских схем, который я не буду рассматривать в этой статье. По этим темам существует множество ресурсов, и, как всегда, хорошее место для начала — это документы:

Для Android они обрабатываются как фильтры намерений с категорией BROWSABLE в файле AndroidManifest.xml. Вот документация по фильтрам намерений.
В iOS они обрабатываются в файле Info.plist вашего приложения. Вот документы для реализации пользовательских схем URL.

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

TL:DR;

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

(function(doc,win){
 // success/failure functions
 var success = function(){
 // found the app
 alert(‘Redirected to the app’)
 }
 var failure = function(){
 // app not installed
 alert(‘Failed to find app’)
 }
 var cookieName = “myapp_install_check”
 var domainRegex = /myhostname/i
 var scheme = “myapp”
 if(domainRegex.test(document.referrer) && new RegExp(cookieName + ‘=’,’i’).test(document.cookie)) return
 var __onload = win.onload
 var done = function(found){
 doc.cookie = cookieName + ‘=yes;path=/;’
 ;(found ? success : failure)()
 }
 win.onload = function(){
 var iframe = doc.createElement(“iframe”)
 iframe.style.border = “none”
 iframe.style.width = “1px”
 iframe.style.height = “1px”
 var t = setTimeout(function() {
 done(false)
 }, 1000);
 iframe.onload = function () { clearTimeout(t); done(true) }
 iframe.src = scheme + ‘://’ + (doc.location.pathname.replace(/^\//,’’) || ‘/’)
 doc.body.appendChild(iframe)
 if(__onload) __onload.apply(this,arguments)
 }
 })(document,window)
  • замените myapp_install_check именем файла cookie, чтобы не перенаправлять пользователя, которого мы уже пытались перенаправить, пока он не откроет новое окно.
  • замените myhostname на ваше имя хоста. Это будет что-то вроде mysite\.com
  • замените myapp своей пользовательской схемой - тем, что вы установили в своем мобильном приложении для ответа, за исключением ://.
  • добавьте любой код, который вы хотели бы выполнить, если у пользователя есть приложение (и мы делаем перенаправление на него) в функцию success. Например, если вы не хотите оставлять окно открытым, это было бы хорошим местом для window.close().
  • добавьте любой код, который вы хотите выполнить, если у пользователя не установлено приложение, в функцию failure. Например, это было бы хорошим местом для показа всплывающего окна с просьбой установить ваше приложение.

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

Бэкенд

Для этого требуется очень мало серверной части — однако, поскольку это аппаратное изменение, это один из немногих случаев, когда я считаю допустимым использовать заголовок пользовательского агента для включения или исключения фрагмента javascript. В моем случае у нас уже есть обнаружение UA, что-то вроде этого должно помочь (это непроверенный псевдокод узла, но вы можете сделать это в любом бэкэнде):
var ua = request.headers['User- Agent']
var mobile = /(ip(hone|od|ad)|android)/i.test(ua)
// mobile теперь верно для устройств.

Если вам так хочется, вы также можете выполнить эту проверку на клиенте, протестировав window.navigator.userAgent.

Сторона клиента

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

 var __onload = window.onload
 window.onload = function(){
 // this is where we'll attempt to open the app.
 if(__onload) __onload.apply(this,arguments)
 }

Далее мы хотим написать обработчик, который предотвратит запуск этого скрипта более одного раза в одном сеансе. В моем случае я определяю сеанс как открытую вкладку в браузере. Я справляюсь с этим, заключая свой сценарий в самоисполняемую анонимную функцию (SEAF) и запуская проверку файлов cookie, прежде чем мы добавим обработчик onload, чтобы убедиться, что мы не добавляем его более одного раза в одном сеансе. Мы также проверим, посещает ли пользователь нелокальный домен или пустой домен. Если хоть одно из этих утверждений верно, это новый сеанс, поэтому мы попытаемся перенаправить его в приложение.

Для этого мне понадобится быстрый метод управления файлами cookie и проверка. Вот как теперь выглядит наш код:

(function(){ var cookieName = 'my_app_tried_redirect' var hostRegex = /myhostname/ // cookie reading method var readCookie = function(){ var parts = document.cookie.split(';') var i=0,len=parts.length for(i;i var cName = cookieName + '=' var part = parts[i] // trim leading whitespace while(part.charAt(0) == ' ') part = part.substring(1,part.length) if(part.indexOf(cName)) return part.substring(cName.length,part.length) } return null } // don't fire this twice in the same session if(hostRegex.test(document.referrer) && readCookie()) return // redirect on load var __onload = window.onload window.onload = function(){ if(/Android/i.test(navigator.userAgent)){ // this is where we'll do android specific stuff } // this is where we'll do stuff that works elsewhere if(__onload) __onload.apply(this,arguments) } })()

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

var appFound var complete = function(found){ appFound = found ? true : appFound // set a cookie so we don't redirect again in this session doc.cookie = cookieName + '=yes;path=/;' if(appFound){ // user has app installed } else { // user does not have app } }

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

var injectiFrame = function(path,callback){ var iframe = document.createElement("iframe") iframe.style.border = "none" iframe.style.width = "1px" iframe.style.height = "1px" var t = setTimeout(function() { callback(false) }, 1000); iframe.onload = function () { clearTimeout(t); callback(true) } iframe.src = path document.body.appendChild(iframe) }

По сути, через 1 секунду, если кадр не сообщил об успехе, мы предполагаем, что произошел сбой. Этот подход чище, чем другие, потому что он в основном невидим для пользователя (за пределами предостережения iOS) и оставит его в покое, чтобы продолжить сеанс просмотра (или, например, вы можете вывести всплывающий баннер «Установите приложение!») без дальнейшего беспокойства.

Теперь мы добавим вызовы к нашим различным схемам внутри ранее созданного обработчика window.onload. Вот как это выглядит:

// redirect on load var __onload = window.onload window.onload = function(){ // attempt to open a custom scheme url injectiFrame('yourcustomscheme://' + (document.location.pathname.replace(/^\//,'') || 'home'),complete) if(__onload) __onload.apply(this,arguments) }

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

Наконец, я хочу убедиться, что я не запускаю условный блок appFound более одного раза, поскольку мы потенциально можем раздражать пользователя несколькими всплывающими окнами или подобным, если мы попробуем несколько схем. Итак, я добавлю глобальную переменную appChecks и буду увеличивать ее при каждом вызове injectIFrame. Затем я добавлю строку вверху функции complete:

if(--appChecks > 0) return

Это пропустит тело complete, когда у нас все еще есть один или несколько ожидающих загрузки iFrames.

Вот пример полного кода для чистого перенаправления в приложение:

(function(doc,win){ // local vars var cookieName = 'my_app_tried_redirect' var domainRegex = /myhostname/ var scheme = "mycustomscheme" var appFound = false var appChecks = 0 // functions var injectiFrame = function(path,callback){ appChecks++ var iframe = doc.createElement("iframe") iframe.style.border = "none" iframe.style.width = "1px" iframe.style.height = "1px" var t = setTimeout(function() { callback(false) }, 1000); iframe.onload = function () { clearTimeout(t); callback(true) } iframe.src = path doc.body.appendChild(iframe) } var complete = function(found){ appFound = found ? true : appFound // wait for all checks to complete if(--appChecks > 0) return // completed all checks // set a cookie so we don't redirect again in this session doc.cookie = cookieName + '=yes;path=/;' if(appFound){ // user has app installed } else { // user does not have app } } var readCookie = function(name) { var nameEQ = name + "=" var ca = document.cookie.split(';') for(var i = 0; i < ca.length; i++) { var c = ca[i] while (c.charAt(0) == ' ') c = c.substring(1, c.length) if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length) } return null } // don't fire this twice in the same session if(domainRegex.test(document.referrer) &amp;&amp; readCookie(cookieName)) return // add some iframes to attempt to pop open the app var __onload = win.onload win.onload = function(){ injectiFrame(scheme + '://' + (doc.location.pathname.replace(/^\//,'') || 'home'),complete) if(__onload) __onload.apply(this,arguments) } })(document,window)

Предостережения:

Это должно быть несколько простым и легко модифицируемым для ваших нужд. Единственное предостережение, которое я знаю об использовании этого подхода, заключается в том, что при использовании в iOS и у пользователя не установлено приложение, пользователь получит всплывающее окно с сообщением «Не удается открыть страницу — Safari не может открыть страницу, потому что адрес недействителен». Единственный вариант, который есть у пользователя, — это отклонить сообщение, и как только он это сделает, он сможет продолжить сеанс без перерыва. Однако это может сбивать с толку некоторых пользователей, и я ожидаю, что с этим будет связано определенное количество проблем с CS. Это можно решить с помощью всплывающего окна, когда у пользователя нет приложения, обращающегося к тому факту, что он только что увидел это уведомление. Некоторые приложения, такие как quora, просто не подтверждают, что вы получили сообщение, но предлагают загрузить приложение. Это раздражает, но, к сожалению, неизбежно в iOS на момент написания этой статьи.

Первоначально опубликовано на сайте jesseditson.com 29 июля 2013 г.