什麼是網頁轉 App 跳轉及其實現方式? 網頁轉 App 跳轉 (Web to App redirection) 透過 Universal Links、App Links 或自訂協議 (fallback schemes) 將行動網站訪客路由至原生應用程式。它會執行使用者觸發的 JavaScript,以啟動已安裝的 App,或將未安裝的訪客引導至應用程式商店。
網頁轉 App 跳轉是一種自動化路由機制,將行動網頁訪客從登陸頁面直接轉送至指定的手機原生 App 頁面。透過評估裝置平台、瀏覽器功能及啟動結果,動態跳轉框架能為現有使用者執行深度連結 (Deep Link) 喚醒,並在支援的歸因系統下,將未安裝 App 的訪客連同上下文參數一同引導至應用程式商店。
| 術語 | 定義 | 相關技術 | 搜尋意圖角色 |
|---|---|---|---|
| 網頁轉 App (Web to App) | 將網頁瀏覽器訪客路由至原生手機 App 的架構流程。 | 行動深度連結 | 資訊型 / 商業型 |
| Apple 智慧 App 橫幅 (Smart App Banner) | 透過 apple-itunes-app meta 標籤設定的 Safari 原生推廣橫幅。 | Safari 網頁導航 | 資訊型 |
| 自訂網頁轉 App 橫幅 | 一種跨瀏覽器的 HTML 與 JavaScript 元件,可呈現動態的 App 啟動或下載行動呼籲 (CTA)。 | 網頁轉 App 跳轉 | 資訊型 |
| 自訂 URL Scheme | 一種專屬 URI 協定,允許外部網頁啟動原生應用程式。 | 深度連結路由 | 技術型 / 資訊型 |

為什麼網頁轉 App 跳轉對行動漏斗轉化至關重要
轉化差距:比較行動網頁跳出率與原生 App 的終身價值 (LTV)
行動網站是一個廣泛的獲客管道,能捕捉來自搜尋廣告、社交發現、展示廣告與自然引薦的漏斗頂端流量。然而,與原生 App 使用者相比,行動網頁訪客通常表現出較低的轉化率、較短的會話時間與較高的跳出率。相比之下,原生手機 App 提供優化的使用者介面、本地會話持久性、硬體加速渲染及推播通知支援,能促進更高的長期參與度與終身價值 (LTV)。
網頁轉 App 跳轉的核心營運目標是彌合此商業差距。透過將行動網頁訪客直接轉送至原生 App 體驗,成長團隊能在摩擦導致會話中斷前,充分把握使用者的購買意圖。當行動訪客在手機瀏覽器探索促銷優惠時,將該使用者直接路由至對應的 App 內商品頁面,可維持高意圖的互動動能,並消除重複的認證或導航步驟。
技術困境:為什麼靜態連結無法提供統一的使用者旅程
僅依賴基礎的靜態超連結,會在現代行動作業系統中引入系統性摩擦。僅指向應用程式商店的靜態連結,會為已安裝該 App 的現有使用者帶來即時障礙;點擊連結會啟動商店頁面而非打開已安裝的應用程式。相反地,若配置靜態的自訂 URI 協定(例如 myapp://promo),則僅在應用程式已安裝時有效;若使用者沒有該 App,桌面與行動瀏覽器會顯示令人困惑的錯誤訊息,例如 Safari 的「無法打開該頁面,因為位址無效」的視窗,或在嵌入式容器中出現未處理的協定錯誤。
此外,若沒有延遲路由系統,靜態商店連結通常無法在安裝後保留應用程式路由上下文。若將未安裝 App 的使用者直接引導至應用程式商店,且缺乏上下文參數的串接,使用者在下載並安裝後進入 App 時,通常只會落在通用首頁。使用者必須手動搜尋最初促使下載的促銷活動或商品。這種上下文斷層會加劇引導流程的流失,並降低行銷投資回報。
上下文參數傳遞在維護促銷意圖中的角色
自動化網頁轉 App 跳轉框架透過執行動態參數傳輸克服了靜態連結的限制。當使用者與行動登陸頁面上的行動呼籲 (CTA) 元件互動時,客戶端路由腳本會捕捉上下文參數——包括活動標籤、引薦代幣、優惠券識別碼與目標場景路由。
Openinstall 作為行動歸因與深度連結平台,賦能前端開發人員將動態查詢參數綁定至客戶端跳轉函數。若應用程式已安裝,系統會將使用者路由至原生 App 並透過作業系統委派傳遞負載。若未安裝該 App,路由層會在歸因伺服器上捕捉上下文參數,將使用者引導至正確的應用程式商店,並在支援延遲深度連結系統且符合平台隱私限制的前提下,於應用程式安裝後首次啟動時還原原始參數。
現代瀏覽器導航如何影響網頁轉 App 路由
使用者手勢指令:為什麼現代瀏覽器封鎖程式自動跳轉
過去,行動網頁曾試圖透過在頁面載入時執行程式化跳轉來自動喚醒原生應用程式,例如注入指向自訂 URI 協定的動態 iframe 元素,或在自動化計時器迴圈中執行 window.location.href = "myapp://"。現代行動瀏覽器(包括 Apple Safari、Google Chrome 與 Mozilla Firefox)會主動限制未經提示的程式化導航。
瀏覽器安全政策根據路由機制執行不同的使用者觸發要求:Chrome Intent URIs:Google Chrome 要求明確的使用者手勢(例如明確的點擊事件)來觸發 intent:// 協定處理;若無事先使用者互動,自動化腳本或計時器執行的任務將被忽略。
- 彈出式視窗與外部協定視窗:Safari 與 Firefox 會抑制缺乏直接使用者互動的視窗確認或協定交接,以防止濫用的背景跳轉與點擊劫持 (Clickjacking)。
- 驗證過的 HTTPS 連結:雖然 Universal Links 與 App Links 遵循系統級的 URL 解析規則,但若透過沒有互動觸發的自動化腳本迴圈來調用它們,常會導致瀏覽器預設為標準網頁導航。

Safari 同域名限制:跨越 Universal Link 跨域邊界
Apple 的 Universal Links 為 iOS 應用程式提供加密的域名驗證。然而,Apple Safari 強制執行嚴格的同域名導航限制,如 Apple 關於允許應用程式與網站連結至您的內容的開發者文件所述。如果使用者瀏覽託管於 https://example.com/promo 的網頁,並點擊指向相同域名 (https://example.com/app/open) 的 Universal Link,Safari 會認為使用者意圖繼續瀏覽該網站,並執行標準的網頁導航,而非啟動原生 App。
當 Safari 的期望行為是離開當前瀏覽頁面並打開 App 時,使用不同的關聯子域名是 Apple 建議的模式。網頁登陸頁面應位於不同的域名或專用子域名(例如 https://www.example.com 或 https://link.example.com),同時 Universal Link 的關聯域名則透過獨立的目的地主機(例如 https://app.example.com)路由。點擊指向跨越不同子域名邊界的連結,即可滿足 Safari 的導航規則並啟動原生應用程式。
Google Chrome Intent URIs:在 Android 上配置降級封裝
在執行 Google Chrome 的 Android 裝置上,網頁開發人員可以利用 Android Intent URIs 啟動原生 App 喚醒,並在單一 URI 字串內指定自動化的瀏覽器降級目標。Android Intent URI 使用 intent:// 語法,編碼目的地路由、目標應用程式套件名稱與瀏覽器降級 URL:
intent://product/detail/1024#Intent;scheme=myapp;package=com.example.app;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end
當使用者在 Chrome 中點擊 Intent URI 時:
- 活動解析:Chrome 嘗試將 Intent 解析為符合已宣告套件與協定的合格活動 (Activity)。
- 原生啟動:若存在合格活動,作業系統將啟動目標應用程式並傳遞 Intent 負載。
- 降級執行:若未安裝對應應用程式,Chrome 會捕捉解析失敗,並導航至
S.browser_fallback_url中定義的 URL,將使用者引導至 Google Play 或指定的網頁登陸頁面,而不會顯示未處理的協定錯誤對話框。
透過 W3C Page Visibility API (document.hidden) 偵測 App 喚醒
當使用標準自訂 URL 協定或 Universal Links 時,網頁會評估 App 是否啟動,以便停止二次降級計時器。由於瀏覽器 JavaScript 無法直接查詢原生作業系統的處理程序表來確認應用程式狀態,前端架構依賴 W3C Page Visibility Level 2 規範。
當瀏覽器成功將執行權交接給外部應用程式時,瀏覽器標籤頁會進入背景,改變其可見性狀態。客戶端腳本會監聽 visibilitychange 事件並檢查 document.hidden 屬性:
document.addEventListener("visibilitychange", function() {
if (document.hidden) {
// 瀏覽器失去前景可見性;清除待處理的商店降級計時器
clearTimeout(fallbackTimer);
}
});
使用 visibilitychange 提供了一個訊號,表示瀏覽器在啟動嘗試後失去了前景焦點。雖然它本身無法證明特定目標應用程式已成功啟動,但它作為一個有用的抑制訊號,能在使用者離開瀏覽器時防止不必要的商店跳轉。
直接喚醒與延遲商店降級的技術機制

多層次跳轉級聯:Universal Links、自訂協定與商店降級
生產級的網頁轉 App 架構會部署多層次跳轉級聯,在多樣的作業系統、瀏覽器廠商與安裝狀態下平滑過渡:
- 第一層:作業系統驗證深度連結 (Universal Links 與 App Links):主要路由採用 HTTPS 域名驗證連結。在 iOS 9+ 與 Android 6.0+ 上,這些連結在安裝 App 時提供直接、無需對話框的啟動方式,未安裝時則降級至網頁導航。
- 第二層:自訂 URI 協定與 Intent 協定:當驗證連結不可用或在舊版瀏覽器中執行時,路由引擎會降級至由使用者明確點擊觸發的自訂 URI 協定 (
myapp://) 或 Android Intent URIs。 - 第三層:上下文商店重定向:若原生協定均無法啟動 App 會話,降級層會將瀏覽器路由至 Apple App Store、Google Play Store 或指定的下載頁面,並在支援的情況下將歸因參數傳遞至後端。
避免現代瀏覽器中使用舊版隱藏 iframe 探測
過去自訂協定的一種變通方法是在 DOM 中注入隱藏的 iframe 元素,以在不離開頁面的情況下探測 App 是否已安裝。在現代行動瀏覽器中,此方法已被棄用且會產生負面效果:
- 現代版本的 Apple Safari 與 Google Chrome 會主動封鎖源自動態注入 iframe 的自訂協定調用。
- 在 Safari 中,嘗試透過 iframe 載入未安裝的協定仍可能觸發令人困擾的「位址無效」警示。
- 隱藏的 iframe 探測會繞過使用者手勢追蹤,從而觸發瀏覽器安全警告。
現代架構不依賴靜默的 iframe 探測,而是利用驗證過的 Universal Links 與 App Links。當必須支援自訂協定作為降級選項時,應從使用者發起的點擊處理器中直接調用,並伴隨明確且由使用者控制的降級 CTA。
基於計時器與可見性的降級時機策略
為了將未安裝的使用者引導至應用程式商店頁面且不干擾原生 App 的啟動,客戶端腳本會部署一種結合可見性檢查與執行計時器的經驗法則。當使用者點擊網頁轉 App 的 CTA 按鈕時:
- 啟動嘗試:腳本嘗試透過 Universal Link 或自訂協定觸發原生 App。
- 計時器配置:設定 JavaScript 超時(通常是特定於實現的 1500ms 到 2500ms 經驗視窗)。
- 心跳計算:腳本記錄觸發導航前的時間戳記,並在計時器執行時評估經過的時間。若 App 已成功啟動,作業系統的掛起動作會延遲計時器的執行;當使用者最終回到瀏覽器時,經過的時間會顯著超過設定的間隔 (
),允許腳本取消商店重定向。 - 降級跳轉:若
document.hidden保持為 false 且經過時間與設定間隔接近 (),表示啟動嘗試未在設定視窗內產生前景丟失訊號,腳本隨即執行明確的商店導航。
[使用者點擊網頁 CTA 按鈕]
│
▼
[瀏覽器與 OS 偵測層]
┌─────────┴─────────┐
▼ ▼
[iOS Safari] [Android Chrome / Firefox]
│ │
▼ ▼
[Universal Link] [Verified App Link / Intent]
│ │
┌──┴─────────┐ ┌──┴─────────┐
▼ ▼ ▼ ▼
Eligible Web Path Eligible Web Path
│ │ │ │
▼ ▼ ▼ ▼
[啟動 App] [Web] [啟動 App] [Web]
│ │
▼ ▼
[可見性檢查] [Intent 降級]
│ │
▼ ▼
[App Store 跳轉] [Play Store 跳轉]
如何使用 JavaScript SDK 實現網頁轉 App 跳轉
透過 Openinstall Web JS SDK 初始化客戶端跳轉
Openinstall Web JS SDK 提供跨瀏覽器路由功能,抽象化了 User-Agent 解析、Universal Link 路由與降級計時的複雜性。開發人員在網頁登陸頁面初始化 SDK,配置其唯一的應用程式識別碼與路由參數。
腳本應以非同步方式載入,並在初始 DOM 建構期間進行初始化。請查看 Web SDK 整合文件以獲取完整的介面參數與 API 規格。
將使用者觸發的點擊綁定至 m.wakeupOrInstall() 方法
為符合現代瀏覽器的使用者手勢限制,跳轉必須直接綁定至實體觸控或點擊事件。Openinstall SDK 暴露了 wakeupOrInstall() 方法,該方法可協調跳轉級聯:
- 它評估客戶端裝置類型(iOS、Android、桌面)。
- 它即時產生適當的 Universal Links 或 Chrome Intent URIs。
- 它將上下文參數附加至啟動請求中。
- 它自動管理可見性監聽器與降級計時器。
在網頁轉 App 過渡中傳遞動態查詢參數
網頁跳轉期間傳遞的動態參數可包括促銷代碼、邀請代幣、行銷頻道 ID 或 App 內路由目的地。前端腳本從 URL 查詢字串中捕捉這些參數,針對長度與字元限制進行輸入清理,並將其綁定至跳轉負載。
下方的技術實作展示如何在 H5 登陸頁面上配置 Openinstall Web JS SDK、附加經過清理的活動參數,並將執行動作綁定至互動式按鈕點擊:
// Web: 使用 Openinstall Web JS SDK 的 H5 登陸頁面實作
// 參考整合範例;請根據當前生產環境中部署的 Openinstall Web SDK 版本驗證腳本 URL、建構函數、回調生命週期、參數架構與 wakeupOrInstall() 簽章。
(function() {
// Openinstall Web SDK 的動態腳本注入
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://web.cdn.opoinstallcloud.com/openinstall.js";
script.onload = function() {
// 初始化 OpenInstall Web SDK 實例
var openInstall = new OpenInstall({
appKey: "YOUR_OPOINSTALL_APPKEY",
onready: function() {
var m = this;
var openAppButton = document.getElementById("openAppBtn");
// 直接綁定跳轉至使用者點擊事件 (符合瀏覽器使用者手勢要求)
openAppButton.addEventListener("click", function() {
var urlParams = new URLSearchParams(window.location.search);
// 輸入清理:根據嚴格的白名單與長度限制驗證參數
var rawTarget = urlParams.get("target") || "product_detail";
var rawId = urlParams.get("id") || "";
var rawSource = urlParams.get("utm_source") || "web_campaign";
var rawChannel = urlParams.get("channelCode") || "web_landing";
var allowedScenes = ["product_detail", "promo_hub", "category_view"];
var targetScene = allowedScenes.indexOf(rawTarget) !== -1 ? rawTarget : "product_detail";
// 強制執行識別碼的英數與長度限制
var idRegex = /^[A-Za-z0-9_-]{1,64}$/;
var targetId = idRegex.test(rawId) ? rawId : "DEFAULT_ID";
var channelRegex = /^[A-Za-z0-9_-]{1,32}$/;
var channelCode = channelRegex.test(rawChannel) ? rawChannel : "web_landing";
var campaignSource = rawSource.replace(/[^A-Za-z0-9_-]/g, "").substring(0, 32);
var dynamicPayload = {
targetScene: targetScene,
targetId: targetId,
campaignSource: campaignSource
};
// 執行多層次跳轉級聯:
// 1. 嘗試 Universal Link / App Link 直接喚醒
// 2. 若未偵測到啟動訊號,則降級至商店下載
m.wakeupOrInstall({
data: dynamicPayload,
channelCode: channelCode
});
});
}
}, document.getElementById("openAppBtn"));
};
document.head.appendChild(script);
})();
// Android: Custom WebViewClient 覆寫 shouldOverrideUrlLoading,實作頁框邊界與路由安全驗證
// 攔截自訂協定、Intent 協定與內部 App Links,並進行嚴格的目標驗證
package com.example.app.webview
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
data class ValidatedAppRoute(val path: String, val queryParams: Map<String, String>)
object AppLinkRouteValidator {
private val allowedPrefixes = setOf("/open/", "/product/", "/promo/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): ValidatedAppRoute? {
val path = uri.path ?: ""
if (allowedPrefixes.none { path.startsWith(it) }) {
return null
}
val sanitizedParams = mutableMapOf<String, String>()
for (name in uri.queryParameterNames) {
// 嚴格安全機制:若存在未知的查詢鍵,拒絕請求
if (name !in allowedQueryKeys) {
return null
}
val value = uri.getQueryParameter(name) ?: ""
if (value.length <= 64 && value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
sanitizedParams[name] = value
} else {
return null
}
}
return ValidatedAppRoute(path, sanitizedParams)
}
}
object CustomSchemeValidator {
private val allowedSchemes = setOf("myapp")
private val allowedHosts = setOf("open", "product", "event")
private val allowedPathPrefixes = setOf("/detail/", "/view/", "/main/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): Uri? {
val scheme = uri.scheme?.lowercase() ?: return null
val host = uri.host?.lowercase() ?: return null
val path = uri.path ?: ""
if (!allowedSchemes.contains(scheme) || !allowedHosts.contains(host)) {
return null
}
if (allowedPathPrefixes.none { path.startsWith(it) }) {
return null
}
for (name in uri.queryParameterNames) {
if (name !in allowedQueryKeys) return null
val value = uri.getQueryParameter(name) ?: ""
if (value.length > 128 || !value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
return null
}
}
return uri
}
}
class CustomWebViewClient(
private val context: Context,
private val internalRouter: (ValidatedAppRoute) -> Unit
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request ?: return false
// 安全要求:限制外部 App 路由僅在主頁框導航時運作
if (!request.isForMainFrame) {
return false // 子頁框與 iframe 無法觸發外部 Intent 或自訂協定
}
val url = request.url.toString()
return handleUrlNavigation(url)
}
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url == null) return false
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
// 舊版回調缺乏可靠的 isForMainFrame 中繼資料;請勿路由外部協定
return if (scheme == "http" || scheme == "https") {
false // 允許載入標準網頁內容
} else {
Log.w("WebViewClient", "在舊版回調上封鎖外部協定:$scheme")
true // 抑制未處理的協定錯誤,而不啟動外部 Intent
}
}
private fun handleUrlNavigation(url: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase() ?: return false
// 1. 處理 HTTP / HTTPS 與內部 App Links
if (scheme == "http" || scheme == "https") {
// 檢查 URL 是否符合主應用程式自身的 App Link 域名
if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
val validatedRoute = AppLinkRouteValidator.validate(uri)
if (validatedRoute != null) {
Log.i("WebViewClient", "內部 App Link 已驗證。分派至內部路由器。")
internalRouter(validatedRoute)
} else {
Log.w("WebViewClient", "拒絕格式錯誤或未經授權的內部 App Link:$uri")
}
return true // 內部處理;取消 WebView 載入
}
return false // 允許 WebView 原生載入標準網頁內容
}
// 2. 根據嚴格的協定、主機、路徑與查詢白名單驗證自訂協定
val validatedUri = CustomSchemeValidator.validate(uri)
if (validatedUri != null) {
return launchSafeIntent(Intent(Intent.ACTION_VIEW, validatedUri))
}
// 3. 處理具備安全重建的 Chrome Intent URIs
if (scheme == "intent") {
return try {
val parsed = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
val targetPackage = parsed.`package`
val allowedPackages = setOf("com.example.app", "com.example.partner")
if (targetPackage != null && allowedPackages.contains(targetPackage)) {
val rawDataUri = parsed.data
if (rawDataUri != null) {
val validatedDataUri = CustomSchemeValidator.validate(rawDataUri)
if (validatedDataUri != null) {
// 重建一個乾淨、專用的 ACTION_VIEW Intent,而非執行原始解析物件
val safeIntent = Intent(Intent.ACTION_VIEW, validatedDataUri).apply {
`package` = targetPackage
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
launchSafeIntent(safeIntent)
} else {
Log.w("WebViewClient", "拒絕含有無效資料 URI 的 Intent:$rawDataUri")
true
}
} else {
Log.w("WebViewClient", "拒絕缺少資料 URI 負載的 Intent")
true
}
} else {
Log.w("WebViewClient", "拒絕鎖定未經驗證套件的 Intent:$targetPackage")
true // 取消未經驗證套件的 Intent
}
} catch (e: Exception) {
Log.e("WebViewClient", "解析 Intent URI 失敗:$url", e)
true // 抑制未處理的協定錯誤
}
}
// 拒絕未知協定以避免 net::ERR_UNKNOWN_URL_SCHEME 錯誤
Log.w("WebViewClient", "封鎖未經驗證的協定導航:$scheme")
return true
}
private fun launchSafeIntent(intent: Intent): Boolean {
return try {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
true
} catch (e: Exception) {
Log.w("WebViewClient", "找不到可處理 Intent 的 Activity:${intent.data}", e)
true // 防止 WebView 當機
}
}
}
// iOS: WKNavigationDelegate 實作,用於 Universal Links 與自訂協定攔截
// 攔截 WKWebView 內部的導航動作,並執行域名、路徑與查詢安全邊界驗證
import UIKit
import WebKit
struct ValidatedAppRoute {
let path: String
let queryParams: [String: String]
}
class AppRouteValidator {
static let allowedPrefixes = ["/open/", "/product/", "/promo/"]
static let allowedQueryKeys = ["target", "id", "promo", "utm_source"]
static func validate(url: URL) -> ValidatedAppRoute? {
let path = url.path
guard allowedPrefixes.contains(where: { path.hasPrefix($0) }) else {
return nil
}
var sanitizedParams: [String: String] = [:]
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems {
let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
for item in queryItems {
let name = item.name
// 關閉失敗驗證機制:若存在未知的查詢鍵,拒絕 URL
guard allowedQueryKeys.contains(name) else { return nil }
let value = item.value ?? ""
if value.count <= 64 && value.rangeOfCharacter(from: validChars.inverted) == nil {
sanitizedParams[name] = value
} else {
return nil
}
}
}
return ValidatedAppRoute(path: path, queryParams: sanitizedParams)
}
}
class CustomSchemeValidator {
static let allowedSchemes = ["myapp"]
static let allowedHosts = ["open", "product", "event"]
static let allowedPathPrefixes = ["/detail/", "/view/", "/main/"]
static let allowedQueryKeys = ["target", "id", "promo", "utm_source"]
static func validate(url: URL) -> URL? {
guard let scheme = url.scheme?.lowercased(), allowedSchemes.contains(scheme) else {
return nil
}
guard let host = url.host?.lowercased(), allowedHosts.contains(host) else {
return nil
}
let path = url.path
if !path.isEmpty && !allowedPathPrefixes.contains(where: { path.hasPrefix($0) }) {
return nil
}
// 以關閉失敗的姿勢驗證查詢參數鍵與值
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems {
let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
for item in queryItems {
guard allowedQueryKeys.contains(item.name) else { return nil }
let value = item.value ?? ""
if value.count > 128 || value.rangeOfCharacter(from: validChars.inverted) != nil {
return nil
}
}
}
return url
}
}
class CustomWebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override fun viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.navigationDelegate = self
view.addSubview(webView)
}
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let scheme = url.scheme?.lowercased() ?? ""
// 1. 處理主應用程式關聯域名 (自身的 Universal Link)
// Apple 文件備註:透過 UIApplication.open() 打開 App 自身的 Universal Link
// 不會路由回 App。必須先驗證後直接分派至內部路由器。
if let host = url.host?.lowercased(), host == "app.example.com" {
NSLog("[WKWebView] 偵測到主機關聯域名:%@", url.path)
if let validatedRoute = AppRouteValidator.validate(url: url) {
AppInternalRouter.shared.navigate(to: validatedRoute)
} else {
NSLog("[WKWebView] 抑制未經授權或格式錯誤的內部路由:%@", url.absoluteString)
}
decisionHandler(.cancel)
return
}
// 2. 處理具備嚴格白名單與路由驗證的自訂協定
if scheme != "http" && scheme != "https" && scheme != "about" {
if let validatedURL = CustomSchemeValidator.validate(url: url) {
UIApplication.shared.open(validatedURL, options: [:]) { success in
if !success {
NSLog("[WKWebView] 無法啟動已授權的自訂協定:%@", validatedURL.absoluteString)
}
}
} else {
NSLog("[WKWebView] 抑制未經授權的協定或路由:%@", url.absoluteString)
}
decisionHandler(.cancel)
return
}
// 3. 允許標準網頁導航
decisionHandler(.allow)
}
}
克服 WebView 與 App 內瀏覽器沙盒
App 內瀏覽器挑戰:瀏覽社交 App 中的限制型 WebView
很大一部分的行動網頁流量來自於在社交媒體平台與通訊軟體(如微信、Line、Instagram 與 Facebook Messenger)中共享的連結。當使用者在這些 App 中點擊連結時,平台會在嵌入式的App 內瀏覽器 (WebView) 中渲染登陸頁面,而非打開系統預設瀏覽器(Safari 或 Chrome)。
由於社交 App 內瀏覽器強制執行特定廠商的沙盒,其功能差異巨大:
- 某些環境會完全攔截並抑制自訂 URI 協定 (
myapp://)。 - Universal Link 交接可能會受到限制,導致使用者被困在社交 App 的 WebView 中。
- 直接 APK 檔案下載或外部商店跳轉可能會根據平台政策與 App 版本而被封鎖。
網頁轉 App 架構不假設存在通用的自動化跳轉突破機制,而是偵測這些執行環境,並提供明確的視覺化指引,指導使用者在系統預設瀏覽器中打開連結(例如透過原生的「在 Safari 中打開」或「在 Chrome 中打開」選單)。

Android WebView 處理:以嚴格白名單覆寫 shouldOverrideUrlLoading
當行動應用程式在自身原生介面內託管嵌入式 WebView 時,開發人員必須配置適當的導航攔截。預設情況下,Android 的 WebView 僅識別標準網頁協定 (http:// 與 https://)。當 WebView 內的網頁嘗試導航至自訂 URI 協定 (myapp://) 或 Android Intent URI (intent://) 時,WebView 會拋出未處理的協定錯誤:net::ERR_UNKNOWN_URL_SCHEME。
如 Android 開發者關於 WebViewClient shouldOverrideUrlLoading 的參考資料所述,開發人員必須繼承 WebViewClient 並覆寫 shouldOverrideUrlLoading。根據 OWASP 行動應用安全測試指南關於不安全深度連結的指導,開發人員必須執行嚴格的協定白名單,驗證導航事件是否源自於主頁框 (request.isForMainFrame) 以防止 iframe 濫用,並重建安全的 ACTION_VIEW Intent,而非向作業系統分派未經限制的原始 Intent 物件。
iOS WKWebView 處理:實作 decidePolicyForNavigationAction 與自域名邊界
在 iOS 上,嵌入 WKWebView 元件的應用程式必須透過 Apple 開發者關於 WKNavigationDelegate 的指導管理導航政策。
關於 Universal Links 存在一個關鍵的架構差異:正如 Apple 所記載,若應用程式對指向其自身關聯域名的 Universal Link 呼叫 UIApplication.shared.open(),iOS 將不會將此連結作為 Universal Link 在主機 App 中打開。因此:
- 主機 App 關聯域名:若 URL 符合主機 App 自身的關聯域名 (
app.example.com),應用程式必須取消 WebView 載入 (decisionHandler(.cancel)),對照嚴格的白名單驗證路由路徑與參數,並直接調用 App 的內部導航路由器。 - 已授權的外部協定:若 URL 包含鎖定外部 App 的已授權自訂協定,在委派至
UIApplication.shared.open之前,請對照明確的白名單驗證協定、主機、路徑與查詢格式。
區分 Apple 智慧 App 橫幅與自訂網頁轉 App 橫幅
在全頁跳轉會破壞使用者體驗的情境下(如內容部落格、新聞入口網站或技術文件),成長團隊會部署推廣橫幅。工程團隊應區分兩種不同的橫幅架構:
- Apple 智慧 App 橫幅 (Smart App Banners):透過
<meta name="apple-itunes-app" content="app-id=myAppID">宣告的原生 Safari 功能。此橫幅由 Safari 在 iOS 裝置上原生渲染,無需執行 JavaScript,但不會在 Android 或第三方瀏覽器上顯示。 - 自訂網頁轉 App CTA 橫幅:由 JavaScript 渲染的 HTML 元件,可在主要行動瀏覽器上呈現,而打開 App 的行為則依賴瀏覽器與平台。這些橫幅會顯示動態 App 圖示、標題、評分與上下文 CTA,並觸發
m.wakeupOrInstall()將執行階段參數直接傳遞至原生 App。開發人員可直接從 OpoInstall SDK 下載中心取得客戶端函式庫。
跨瀏覽器網頁轉 App 跳轉相容性矩陣
評估行動瀏覽器環境中的跳轉協定
由於瀏覽器廠商持續更新導航安全政策,工程團隊必須針對特定的瀏覽器執行階段評估跳轉協定,以維持可靠的使用者漏斗。
瀏覽器環境技術能力與行為限制
下表總結了跨瀏覽器技術相容性、使用者手勢限制與降級機制:
| 瀏覽器 / 環境 | 主要支援協定 | 使用者觸發限制 | App 喚醒行為 | 未安裝降級機制 |
|---|---|---|---|---|
| iOS Safari | Universal Links | 自訂協定需使用者手勢;Universal Links 建議採用跨子域名模式 | 若符合資格且已驗證則直接啟動 App | 在瀏覽器中載入網頁 URL |
| Android Chrome | Android App Links / Intent URIs | intent:// 與自訂協定需直接的使用者手勢 |
若為主機已驗證則繞過選擇對話框 | 導航至 S.browser_fallback_url 或 Play Store |
| Android Firefox | HTTPS 連結 / 符合資格的 OS 支援 App 路由 | 取決於瀏覽器版本;請對照當前 Android 版 Firefox 行為驗證 | 可能隨 Firefox 版本與 Android 連結處理器狀態而異 | 遵循標準網頁連結 |
| App 內社交 WebView | 隨 App 與版本而異 | 受社交容器沙盒與點擊攔截影響 | 經常被攔截或限制 | 引導使用者至系統預設瀏覽器 (Safari / Chrome) |
| 嵌入式 Android WebView | 透過 shouldOverrideUrlLoading 攔截 |
主機 App 實作控制觸發規則 | 需要明確的 WebViewClient 覆寫 |
除非覆寫,否則會拋出 net::ERR_UNKNOWN_URL_SCHEME |
常見問題 (FAQ)
網頁轉 App 跳轉如何處理未安裝該 App 的使用者?
為什麼沒有使用者點擊事件時程式化跳轉會失敗?
如何避免 Android WebView 中的 net::ERR_UNKNOWN_URL_SCHEME 錯誤?
總結與決策框架
自動化網頁轉 App 跳轉需要彌合網頁瀏覽器沙盒與原生行動作業系統之間的差距。僅依賴基礎靜態連結或未驗證的自訂協定會產生摩擦、觸發瀏覽器警告,並增加使用者旅程中的流失率。
部署多層次跳轉管線——結合驗證過的 Universal Links、App Links、使用者手勢觸發的 JavaScript 與平滑的商店降級機制——有助於減少瀏覽器級的路由失敗,並支援更平順地過渡至原生應用程式。透過將客戶端 Web SDK 與具備彈性、白名單強制執行功能的 WebView 委派相結合,工程團隊能降低轉化摩擦,並支援使用者生命週期互動目標。
欲了解如何實現穩健的網頁轉 App 跳轉流程,請諮詢 Web SDK 整合文件,從 OpoInstall SDK 下載中心取得客戶端函式庫,探索 行動歸因實作參考,或在 OpoInstall 開發者控制台註冊您的應用程式。
相關資料
-
概念:網頁轉 App 跳轉、Universal Links、Android App Links、自訂 URL 協定、Apple 智慧 App 橫幅
-
技術:W3C Page Visibility API、Android WebViewClient、iOS WKNavigationDelegate、Intent URIs
-
標準:IETF RFC 3986 統一資源識別碼、Apple 關聯域名規範、Android 數位資產連結協定、OWASP 行動應用安全測試指南 (MASTG)
-
API:OpoInstall Web JS SDK
wakeupOrInstallAPI、AndroidshouldOverrideUrlLoading、iOSdecidePolicyForNavigationAction -
官方文件與參考:
Share this article



