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

в 10:43, , рубрики: javascript, javascript hacks, JS, url scheme, Веб-разработка, Программирование, метки: ,

Недавно столкнулся с необходимостью определить, зарегистрирована ли 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.

Автор: socioniq

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js