Недавно столкнулся с необходимостью определить, зарегистрирована ли URL Scheme в браузере, чтобы в зависимости от результата показывать либо кнопку загрузки приложения, либо прямой URL на его запуск.
Оказалось, что каких-либо стандартных механизмов для этого не существует. Но поскольку пользователи никак не хотели обращать внимания на кнопку Download и красную надпись о необходимости предварительной установки приложения, пришлось искать варианты. Об этом и пойдет речь ниже.
Сразу хочу предупредить, что не оказалось ни одного способа, сработавшего хотя бы в двух браузерах стабильно одинаково. Поэтому любителей красивых кроссбраузерных решений ждет разочарование — статья полна хаков (просьба не минусовать за это, самому тошно, но задача есть задача).
Для начала создадим наши ссылки.
Теперь создадим общую функцию, которая будет определять браузер и запускать соответствующий обработчик. Ну и еще одну вспомогательную, которая будет выводить запрос на загрузку приложения и загружать его в случае положительного ответа.
Начнем с Firefox и Opera, поскольку они в этом вопросе предоставляют 100% надежный и красивый механизм исключений. Но, к сожалению, реализация все равно отличается.
У Firefox самое простое решение.
Opera не намного отличается от Firefox, за исключением того, что она отлавливает исключение не в момент попытки загрузки во фрейм URL с незарегистрированным протоколом, а в момент попытки обращения к не определенному атрибуту фрейма (contentWindow.location).
Функции createFrame() и deleteFrame() для Firefox и Opera:
Safari под Windows тоже умеет ловить исключения, но со скрытым фреймом этот номер не прошел. Как вариант, можно использовать обычное окно. Решение не элегантное, но рабочее.
Safari под MacOS с исключениями, видимо, не дружит. Поэтому тут придется применить чисто костыльный метод. Фишка заключается в том, чтобы после запуска приложения проверить фокус нашего окна браузера. Если приложение успешно запустилось, то окно потеряло фокус, и в обработчике этого события мы зафиксировали этот факт. Если же приложение не открылось, то окно по-прежнему имеет фокус.
К сожалению, этот способ не на 100% стабилен. Например, если за время таймаута браузер не успел открыть приложение, или наоборот — пользователь успел закрыть приложение до наступления таймаута, и окно опять получило фокус. В результате, такие варианты выливаются в следующую неприятную картину. Всплывает окно с предложением загрузки, а через секунду стартует приложение. Или наоборот — стартует приложение, пользователь его тут же закрывает, и появляется окно с предложением загрузки. Поэтому увеличение или уменьшение таймаута почти одинаково плохо (увеличение все-таки немного лучше, т.к. в большинстве случаев пользователь не станет мгновенно закрывать только что открытое приложение). На практике наиболее приемлемым получился таймаут в одну секунду.
Chrome оказался полностью совместимым с Safari под MacOS, за исключением таймаута (Safari потребовалась почти секунда, чтобы запустить приложение, в то время, как Chrome запустил его меньше, чем за 250 миллисекунд).
(На практике способ с таймаутом очень сильно зависит от загруженности компьютера в момент запуска приложения. Я попадал на ситуации с Chrome и Safari, когда приложение не успевало запуститься, и выводилось окно с предложением о загрузке, после чего запускалось приложение. Опять-таки проблема с таймаутом, описанная выше).
Ну и наконец Internet Explorer не показал стабильного результата при всех стараниях. Для IE технически подходит вариант Safari под Windows (с открытием маленького окна). Но получить хотя бы 50% стабильность так и не удалось. Поэтому IE ушел в ветку «другие браузеры», которая не делает никаких проверок, а просто отображает обе ссылки — на запуск и на загрузку приложения. (Буду признателен, если кто-то подскажет способ для IE).
Теперь нам осталось только проинициализировать наши ссылки.
Примечание: Решение тестировалось в последних версиях Chrome, Firefox, Opera, Safari и IE на платформах Windows и MacOS. В мобильных браузерах тесты не проводились, но вариант с таймаутом вполне может оказаться работоспособным на Android и iOS.
Оказалось, что каких-либо стандартных механизмов для этого не существует. Но поскольку пользователи никак не хотели обращать внимания на кнопку 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.