前言:为什么需要自定义协议唤起应用?
在现代Web应用中,我们经常需要实现类似百度网盘、微信客户端的"网页唤起本地应用"功能。这种技术通过注册自定义协议(如weixin://
)实现深层链接(Deep Linking),但不同浏览器存在兼容性问题。本文将提供一套完整的跨浏览器解决方案。
一、技术原理剖析
1. 核心机制
- 自定义URL协议 :通过
<a href="appname://">
或location.href
触发 - 浏览器事件监听 :利用
blur
事件检测应用是否成功唤起 - 降级方案:针对不同浏览器提供多种备选方案
2. 兼容性矩阵
方案 | Chrome | Firefox | Safari | IE/Edge | 移动端 |
---|---|---|---|---|---|
直接location跳转 | ✓ | ✓ | ✓ | 部分 | ✓ |
隐藏iframe方案 | ✓ | ✓ | ✓ | ✓ | ✓ |
msLaunchUri(IE专属) | ✗ | ✗ | ✗ | ✓ | ✗ |
二、原生JavaScript实现(生产级方案)
scss
(function (f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}
g.protocolCheck = f();
}
})(function () {
var define, module, exports;
return (function e(t, n, r) {
function s(o, u) {
if (!n[o]) {
if (!t[o]) {
var a = typeof require == "function" && require;
if (!u && a) return a(o, !0);
if (i) return i(o, !0);
var f = new Error("Cannot find module '" + o + "'");
throw ((f.code = "MODULE_NOT_FOUND"), f);
}
var l = (n[o] = { exports: {} });
t[o][0].call(
l.exports,
function (e) {
var n = t[o][1][e];
return s(n ? n : e);
},
l,
l.exports,
e,
t,
n,
r
);
}
return n[o].exports;
}
var i = typeof require == "function" && require;
for (var o = 0; o < r.length; o++) s(r[o]);
return s;
})(
{
1: [
function (require, module, exports) {
function _registerEvent(target, eventType, cb) {
if (target.addEventListener) {
target.addEventListener(eventType, cb);
return {
remove: function () {
target.removeEventListener(eventType, cb);
},
};
} else {
target.attachEvent(eventType, cb);
return {
remove: function () {
target.detachEvent(eventType, cb);
},
};
}
}
function _createHiddenIframe(target, uri) {
var iframe = document.createElement("iframe");
iframe.src = uri;
iframe.id = "hiddenIframe";
iframe.style.display = "none";
target.appendChild(iframe);
return iframe;
}
function openUriWithHiddenFrame(uri, failCb, successCb) {
var timeout = setTimeout(function () {
failCb();
handler.remove();
}, 1000);
var iframe = document.querySelector("#hiddenIframe");
if (!iframe) {
iframe = _createHiddenIframe(document.body, "about:blank");
}
var handler = _registerEvent(window, "blur", onBlur);
function onBlur() {
clearTimeout(timeout);
handler.remove();
successCb();
}
iframe.contentWindow.location.href = uri;
}
function openUriWithTimeoutHack(uri, failCb, successCb) {
var timeout = setTimeout(function () {
failCb();
handler.remove();
}, 1000);
//handle page running in an iframe (blur must be registered with top level window)
var target = window;
while (target != target.parent) {
target = target.parent;
}
var handler = _registerEvent(target, "blur", onBlur);
function onBlur() {
clearTimeout(timeout);
handler.remove();
successCb();
}
window.location = uri;
}
function openUriUsingFirefox(uri, failCb, successCb) {
var iframe = document.querySelector("#hiddenIframe");
if (!iframe) {
iframe = _createHiddenIframe(document.body, "about:blank");
}
try {
iframe.contentWindow.location.href = uri;
successCb();
} catch (e) {
if (e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
failCb();
}
}
}
function openUriUsingIEInOlderWindows(uri, failCb, successCb) {
if (getInternetExplorerVersion() === 10) {
openUriUsingIE10InWindows7(uri, failCb, successCb);
} else if (
getInternetExplorerVersion() === 9 ||
getInternetExplorerVersion() === 11
) {
openUriWithHiddenFrame(uri, failCb, successCb);
} else {
openUriInNewWindowHack(uri, failCb, successCb);
}
}
function openUriUsingIE10InWindows7(uri, failCb, successCb) {
var timeout = setTimeout(failCb, 1000);
window.addEventListener("blur", function () {
clearTimeout(timeout);
successCb();
});
var iframe = document.querySelector("#hiddenIframe");
if (!iframe) {
iframe = _createHiddenIframe(document.body, "about:blank");
}
try {
iframe.contentWindow.location.href = uri;
} catch (e) {
failCb();
clearTimeout(timeout);
}
}
function openUriInNewWindowHack(uri, failCb, successCb) {
var myWindow = window.open("", "", "width=0,height=0");
myWindow.document.write("<iframe src='" + uri + "'></iframe>");
setTimeout(function () {
try {
myWindow.location.href;
myWindow.setTimeout("window.close()", 1000);
successCb();
} catch (e) {
myWindow.close();
failCb();
}
}, 1000);
}
function openUriWithMsLaunchUri(uri, failCb, successCb) {
navigator.msLaunchUri(uri, successCb, failCb);
}
function checkBrowser() {
var isOpera =
!!window.opera || navigator.userAgent.indexOf(" OPR/") >= 0;
var ua = navigator.userAgent.toLowerCase();
return {
isOpera: isOpera,
isFirefox: typeof InstallTrigger !== "undefined",
isSafari:
(~ua.indexOf("safari") && !~ua.indexOf("chrome")) ||
Object.prototype.toString
.call(window.HTMLElement)
.indexOf("Constructor") > 0,
isIOS:
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
!window.MSStream,
isChrome: !!window.chrome && !isOpera,
isIE: /*@cc_on!@*/ false || !!document.documentMode, // At least IE6
};
}
function getInternetExplorerVersion() {
var rv = -1;
if (navigator.appName === "Microsoft Internet Explorer") {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
if (re.exec(ua) != null) rv = parseFloat(RegExp.$1);
} else if (navigator.appName === "Netscape") {
var ua = navigator.userAgent;
var re = new RegExp("Trident/.*rv:([0-9]{1,}[.0-9]{0,})");
if (re.exec(ua) != null) {
rv = parseFloat(RegExp.$1);
}
}
return rv;
}
module.exports = function (uri, failCb, successCb, unsupportedCb) {
function failCallback() {
failCb && failCb();
}
function successCallback() {
successCb && successCb();
}
if (navigator.msLaunchUri) {
//for IE and Edge in Win 8 and Win 10
openUriWithMsLaunchUri(uri, failCb, successCb);
} else {
var browser = checkBrowser();
if (browser.isFirefox) {
openUriUsingFirefox(uri, failCallback, successCallback);
} else if (browser.isChrome || browser.isIOS) {
openUriWithTimeoutHack(uri, failCallback, successCallback);
} else if (browser.isIE) {
openUriUsingIEInOlderWindows(
uri,
failCallback,
successCallback
);
} else if (browser.isSafari) {
openUriWithHiddenFrame(uri, failCallback, successCallback);
} else {
unsupportedCb();
//not supported, implement please
}
}
};
},
{},
],
},
{},
[1]
)(1);
});
三、Vue专属优化方案
javascript
export function openUrlWithInputTimeoutHack(url, failCb, successCb) {
let target = document.createElement('input')
target.style.width = '0'
target.style.height = '0'
target.style.position = 'fixed'
target.style.top = '0'
target.style.left = '0'
document.body.appendChild(target)
target.focus();
var handler = _registerEvent(target, "blur", onBlur);
console.log('focus')
function onBlur() {
console.log('blur')
successCb && successCb()
handler.remove()
clearTimeout(timeout)
document.body.removeChild(target)
};
//will trigger onblur
location.href = url
// Note: timeout could vary as per the browser version, have a higher value
var timeout = setTimeout(function () {
console.log('setTimeout')
failCb && failCb()
handler.remove()
document.body.removeChild(target)
}, 1000);
}
function _registerEvent(target, eventType, cb) {
if (target.addEventListener) {
target.addEventListener(eventType, cb);
return {
remove: function () {
target.removeEventListener(eventType, cb);
}
};
} else {
target.attachEvent(eventType, cb);
return {
remove: function () {
target.detachEvent(eventType, cb);
}
};
}
}
四、关键问题解决方案
1. 浏览器拦截处理
javascript
// 添加用户手势触发
button.addEventListener('click', () => {
protocolCheck('app://', fail, success);
});
2. 移动端适配方案
javascript
// 区分处理移动端
if (/Mobile|Android|iPhone/i.test(navigator.userAgent)) {
// 使用Universal Links/App Links
window.location.href = 'https://example.com/app/route';
} else {
protocolCheck('app://', fail, success);
}
3. 协议检测增强版
ini
function checkProtocolSupported(protocol) {
return new Promise(resolve => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${protocol}//check`;
iframe.onload = () => {
document.body.removeChild(iframe);
resolve(true);
};
iframe.onerror = () => {
document.body.removeChild(iframe);
resolve(false);
};
document.body.appendChild(iframe);
});
}
五、安全与最佳实践
- HTTPS要求:现代浏览器要求页面必须为HTTPS才能使用自定义协议
- 用户确认:重要操作应先获取用户确认再唤起应用
- 降级处理:始终提供下载或网页版备选方案
- 协议白名单:只允许唤起预定义的安全协议
ini
const ALLOWED_PROTOCOLS = ['weixin', 'alipay'];
function validateProtocol(uri) {
const protocol = uri.split(':')[0];
return ALLOWED_PROTOCOLS.includes(protocol);
}
六、现代替代方案
- Web Share API:适合分享场景
- PWA应用:通过Service Worker实现更深度集成
- Electron/CEF:桌面应用的更优选择
结语
本文提供的解决方案已在多个生产环境验证,支持:
- Chrome/Firefox/Safari/Edge全系列
- IE10+兼容支持
- 响应式设计适配移动端
互动问题:你在实现应用唤起时遇到过哪些棘手问题?欢迎在评论区分享你的实战经验!