URL Scheme: Проверка наличия установленного приложения в Javascript

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

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



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

    Для начала создадим наши ссылки.

    <a class="runlink" href="myapp://command_line_parameters">Run</a>
    <a class="downloadlink" style="display:none;" href="http://mysite.com/download/app.exe">Download</a>
    


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

    function initApplicationLink(runlink, downloadlink) {
    	// Проверяем браузер
    	var func = null;
    	if (navigator.userAgent.indexOf('Firefox')>=0 ) func = checkFirefox;
    	else if (navigator.userAgent.indexOf('Opera')>=0 ) func = checkOpera;
    	else if (navigator.userAgent.indexOf('Chrome')>=0 ) func = checkChrome;
    	else if (navigator.userAgent.indexOf('Safari')>=0 ) func = checkSafari;
    	if ( func!=null ) {
    		// Скрываем ссылку Download
    		$(downloadlink).hide();
    		// Вешаем выбранную функцию на клик по ссылке запуска приложения 
    		$(runlink).on('click', function(){
    			func(runlink, downloadlink);
    			// Отменяем нажатие ссылки, чтобы она не открылась в окне браузера
    			return false;
    		});
    	}
    	else {
    		// Для всех других браузеров просто показываем ссылку Download
    		$(downloadlink).show();
    	}
    }
    
    function downloadConfirmation(downloadlink) {
    	if ( confirm('You need to install our application first. Do you really want to download it now?') )
    		document.location = $(downloadlink).attr('href');
    }
    


    Начнем с Firefox и Opera, поскольку они в этом вопросе предоставляют 100% надежный и красивый механизм исключений. Но, к сожалению, реализация все равно отличается.

    У Firefox самое простое решение.

    function checkFirefox(runlink, downloadlink) {
    	// Создаем скрытый фрейм, в котором пытаемся открыть наш URL
    	var f = createFrame();
    	try {
    		f.contentWindow.location = $(runlink).attr('href');
    	}
    	catch (e) {
    		// Если URL открыть не удалось, выводим запрос на загрузку приложения
    		downloadConfirmation(downloadlink);
    	}
    	// Удаляем наш временный фрейм
    	deleteFrame(f);
    }
    


    Opera не намного отличается от Firefox, за исключением того, что она отлавливает исключение не в момент попытки загрузки во фрейм URL с незарегистрированным протоколом, а в момент попытки обращения к не определенному атрибуту фрейма (contentWindow.location).

    function checkOpera(runlink, downloadlink) {
    	var f = createFrame();
    	f.contentWindow.location = $(runlink).attr('href');
    	setTimeout(function (){
    		try {
    			// Пытаемся поработать с не определенным атрибутом фрейма
    			// (вместо something можно использовать что-угодно)
    			if ( f.contentWindow.location!='something' ) {}
    		}
    		catch (e) {
    			downloadConfirmation(downloadlink);
    		}
    		deleteFrame(f);
    	}, 0);
    }
    


    Функции createFrame() и deleteFrame() для Firefox и Opera:

    function createFrame() {
    	var f = document.createElement('iframe');
    	f.style.display = 'none';
    	return document.body.appendChild(f);
    }
    
    function deleteFrame(f) {
    	document.body.removeChild(f);
    }
    


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

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

    К сожалению, этот способ не на 100% стабилен. Например, если за время таймаута браузер не успел открыть приложение, или наоборот — пользователь успел закрыть приложение до наступления таймаута, и окно опять получило фокус. В результате, такие варианты выливаются в следующую неприятную картину. Всплывает окно с предложением загрузки, а через секунду стартует приложение. Или наоборот — стартует приложение, пользователь его тут же закрывает, и появляется окно с предложением загрузки. Поэтому увеличение или уменьшение таймаута почти одинаково плохо (увеличение все-таки немного лучше, т.к. в большинстве случаев пользователь не станет мгновенно закрывать только что открытое приложение). На практике наиболее приемлемым получился таймаут в одну секунду.

    function checkSafari(runlink, downloadlink) {
    	if ( navigator.userAgent.indexOf('Windows')>=0 ) {
    		// Открываем маленькое окошко с нашим не стандартным URL
    		var w = window.open($(runlink).attr('href'), '', 'width=50, height=50');
    		setTimeout(function(){
    			try {
    				// Пытаемся поработать с не определенным атрибутом окна
    				if ( w.location!='about:blank' ) {}
    				w.close();
    				 window.focus();
    			}
    			catch (e) {
    				w.close();
    				window.focus();
    				downloadConfirmation(downloadlink);
    			}
    		}, 1000);
    	}
    	else {
    		// Под MacOS и теоретически под iOS
    		document.location = $(runlink).attr('href');
    		setTimeout(function(){
    			// Если окно по-прежнему в фокусе, значит приложение не запустилось
    			if ( window.isFocused ) downloadConfirmation(downloadlink);
    		}, 1000);
    	}
    }
    
    // Обработчики событий получения и потери фокуса окна браузера
    window.isFocused = true;
    $(window).on('focus', function(){
    	window.isFocused = true;
    })
    .on('blur', function(){
    	window.isFocused = false;
    });
    


    Chrome оказался полностью совместимым с Safari под MacOS, за исключением таймаута (Safari потребовалась почти секунда, чтобы запустить приложение, в то время, как Chrome запустил его меньше, чем за 250 миллисекунд).

    (На практике способ с таймаутом очень сильно зависит от загруженности компьютера в момент запуска приложения. Я попадал на ситуации с Chrome и Safari, когда приложение не успевало запуститься, и выводилось окно с предложением о загрузке, после чего запускалось приложение. Опять-таки проблема с таймаутом, описанная выше).

    function checkChrome(runlink, downloadlink) {
    	document.location = $(runlink).attr('href');
    	setTimeout(function(){
    		if ( window.isFocused ) downloadConfirmation(downloadlink);
    	}, 1000);
    }
    


    Ну и наконец Internet Explorer не показал стабильного результата при всех стараниях. Для IE технически подходит вариант Safari под Windows (с открытием маленького окна). Но получить хотя бы 50% стабильность так и не удалось. Поэтому IE ушел в ветку «другие браузеры», которая не делает никаких проверок, а просто отображает обе ссылки — на запуск и на загрузку приложения. (Буду признателен, если кто-то подскажет способ для IE).

    Теперь нам осталось только проинициализировать наши ссылки.

    initApplicationLink('.runlink', '.downloadlink');
    


    Примечание: Решение тестировалось в последних версиях Chrome, Firefox, Opera, Safari и IE на платформах Windows и MacOS. В мобильных браузерах тесты не проводились, но вариант с таймаутом вполне может оказаться работоспособным на Android и iOS.
    Share post

    Similar posts

    Comments 17

      –3
      Вы используете jQuery, почему бы тогда для проверки браузера не использовать $.browser? Как ни как упрощает код на таких мелочах. Мое ИМХО, если использовать что-то в проекте, то на 100%.
        +5
        Потому что, начиная с версии 1.9:

        This property was removed in jQuery 1.9 and is available only through the jQuery.migrate plugin. Please try to use feature detection instead.


        (jQuery в примере используется только для уменьшения объема кода)
          +3
          В jQuery 1.9, например, $.browser уже нет. В качестве альтернативы можно использовать Modernizr.
          0
          Не существует !== не нашел, ибо:
          www.whatwg.org/specs/web-apps/current-work/#custom-handlers

          + статья.
            0
            Не существует — значит не существует. Ни один браузер пока не реализовал этот стандарт.
              +3
              Кроме того, этот стандарт позволяет регистрировать в качестве обработчиков URL Scheme только веб-приложения, но не локальные. А это большая разница.
                0
                Статья в блоге разработчиков Opera начинается с того, что они это реализовали у себя. Еще спецификация по сему делу не позволяет печь пирожки и заваривать кофеек, вот только вы не об этом у себя писали, а о том, что нет возможности определить зарегистрирована ли определенная URL Scheme в браузере, аль нет, а это как раз спека и описывает. Ну да ладно, буквоедством заниматься нет желания, я лишь указал на возможное решение, ибо у вас все равно для той же Opera отдельное решение.
                  0
                  Я на досуге проверю в Opera, может ли isProtocolHandlerRegistered() определить схемы, зарегистрированные локальными приложениями, или же она определяет только схемы, зарегистрированные с помощью registerProtocolHandler(). Если может, внесу в статью еще один вариант для Opera, раз уж они впереди планеты всей.
                    +1
                    Проверил isProtocolHandlerRegistered(). Функция проверяет, зарегистрирован ли конкретный URL как обработчик протокола (название функции говорит само за себя — функция проверяет именно обработчик протокола, а не сам протокол). Например, если зарегистрировать собственный протокол вот таким образом:

                    navigator.registerProtocolHandler('web+myproto', 'http://mysite.com/?uri=%s', 'My Proto');
                    

                    то потом можно сделать вот так:

                    navigator.isProtocolHandlerRegistered('web+myproto', 'http://mysite.com/?uri=%s');
                    

                    и получить в ответ 'registered'. Но если сделать вот так:

                    navigator.isProtocolHandlerRegistered('web+myproto', null);
                    

                    получим 'undefined'.

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

                    И еще момент, раз уж заговорили об этом. Зарегистрировать собственный протокол без префикса 'web+' тоже нельзя. Можно только повесить свой обработчик на уже зарегистрированные протоколы (так называемые whitelisted schemes, например: mailto, ssh, tel). Это значит, что зарегистрировать, скажем, протокол 'myproto' через registerProtocolHandler не получится.

                    Но есть и хорошая новость. Стандарт поддерживает уже не только Opera, но и последние версии Chrome и Firefox. Так что, если кому эта фича полезна, уже вполне можно пользоваться.
                0
                Я столкнулся с такой же задачей, нашел вот такие костыли www.rajeshsegu.com/2012/09/browser-detect-custom-protocols/comment-page-1/. Интересно продвинулись ли вы с тех пор в этом направлении? Огорчают всплывающие алерты, хотелось бы чтобы проверка проходила молча.
                  0
                  Спасибо за ссылку, есть полезные моменты там. Что касается алертов «supported => true/false», то они гасятся в 28 строке их кода:

                  alert(getProtocol() + " supported => " + isSupported);

                  В остальном — те же проблемы. Например, благодаря try...catch в Firefox и Opera есть возможность провести 100% проверку и все корректно обработать. А вот в Chrome всегда будут негативные срабатывания, особенно при первой проверке или перегруженной памяти. При чем, если протокол зарегистрирован, то приложение успешно стартанет в конечном итоге. Но ветка false уже пойдет на выполнение, и будет некрасиво. Ну и главная проблема — нельзя просто проверить наличие протокола (в отличие от web-handlers). Сделать можно только проверку боем, попытавшись открыть ссылку/загрузить фрейм. Мне, по крайней мере, других решений не попадалось.
                    0
                    Да опера и лиса радуют — 100% проверка, еще сто процентов на IE10+ метод msLaunchUri (не реализован в версии под windows 7 — емае!), можно добиться 100% на всех ослах, если включить эмуляцию IE8 (начиная с 10 версии условные комментарии не работают — рука-лицо) и прописать Version Vectors в реестре инсталлером, а условным каментом можно проверить наличие ключа.

                    Насколько я понял проверка в сафари и хроме основана на событиях фокуса, что конечно печально.

                    Под алертами я имел ввиду, что для проверки надо попытаться открыть ссылку — и получить запрос на разрешение, соотв. это нельзя делать при загрузке страницы, например, только по нажатию на ссылку.
                      0
                      Все верно. Поэтому для себя я сделал выбор — убрал все эти костыли и оставил на форме обе кнопки — маленькую серую на установку приложения и большую зеленую на запуск + краткий вступительный комментарий. В моем сценарии это лучше, чем вводить пользователей в ступор странным поведением программы, в попытках сделать их жизнь проще. В других сценариях, к сожалению, приходится извращаться.
                        0
                        Отказались в итоге от прямолинейной проверки, прилага после запуска отправляет подтверждение через сервер в браузерный клиент. Решение конечно не 100% надежное, но лучше чем яваскриптовые костыли…

                        Был еще один способ это сделать — флешовый LocalConnection(у нас прилага на Аире и клиент на Флексе), но хромой и тут нагадил, у него флешплеер в сендбоксе, не разрешает…

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

                        Т.о. майкрасофт продвигает, добавил даже msLaunchUri, а остальные под предлогом безопасности ничего не сделали, а оставили так как есть (в том числе и с дырами).
                          0
                          Тоже вариант. Но опять придется ловить вменяемый таймаут ожидания сообщения от проги, чтобы на медленных компах не уйти на false раньше времени. Но тут хоть нет проблемы, что кто-нибудь руками фокус уведет из браузера за время таймаута. Так, глядишь, и скоро выйдем на вполне приемлемое решение :)
                  +1
                  В мобильных браузерах тесты не проводились, но вариант с таймаутом вполне может оказаться работоспособным на Android и iOS.
                  За Android не скажу, а в iOS есть Smart App Banners.

                  В разделе <head> вставляете тег с айди приложения и параметрами для него:

                  <meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">

                  Safari показывает над содержимым страницы такой баннер:
                  пример Smart App Banner

                  Если приложение установлено, то по нажатию на кнопку «Open» оно запустится с переданными параметрами.
                  Если приложение не установлено, то кнопка будет называться «View», и по тапу откроется его страница в App Store.

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

                Only users with full accounts can post comments. Log in, please.