Deep Linking(深層連結)指的是一條連結直接指向網站或 App 裡的某個特定內容,例如某個商品頁、某篇文章,而不是只能開到首頁再自己找。在網頁上,這就是一般帶有路徑、查詢參數或 # 片段的網址;在手機 App 上,則要靠作業系統把連結交給正確的 App 處理,Android 與 iOS 各有一套機制。本文先從網址結構講起,再說明網頁、Android 與 iOS 三種情境怎麼運作。
網頁端用 history.pushState 改網址的細節,可前往〈JavaScript 變更網址不刷新頁面與 history API 用法〉觀看,本文只說明它和 Deep Linking 的關係。
一、Deep Linking 的網址長什麼樣子
不論目標是網頁還是 App,深層連結都是一個 URL,常見的組成如下:
| 部分 | 範例 | 用途 |
|---|---|---|
| scheme | https、example | 決定用什麼方式處理這個連結 |
| host | www.example.com | 網域,App Links 與 Universal Links 靠它驗證關聯 |
| path | /products/123 | 指向哪個內容 |
| query | ?ref=ad | 附帶參數 |
| fragment | #reviews | 指向該內容的某個部分,由瀏覽器自己處理 |
用 JavaScript 的 URL 解析兩種連結,可以看到差異。解析方式的完整說明見〈JS 解析 URL:new URL 與 URLSearchParams 屬性對照〉。
for (const s of [
'example://products/123?ref=ad',
'https://www.example.com/products/123?ref=ad#reviews',
]) {
const u = new URL(s);
console.log(s);
console.log({ protocol: u.protocol, host: u.host, pathname: u.pathname, search: u.search, hash: u.hash });
}
在 Node.js v22 執行的輸出如下:
example://products/123?ref=ad
{
protocol: 'example:',
host: 'products',
pathname: '/123',
search: '?ref=ad',
hash: ''
}
https://www.example.com/products/123?ref=ad#reviews
{
protocol: 'https:',
host: 'www.example.com',
pathname: '/products/123',
search: '?ref=ad',
hash: '#reviews'
}
自訂 scheme 的 example://products/123 被拆成 host 為 products、pathname 為 /123,和 https 連結「host 是網域、path 是內容路徑」的直覺不同。設計自訂 scheme 連結時,要先決定好 host 與 path 各放什麼,App 端解析時才不會對不上。
二、網頁上的 Deep Linking:路徑要能直接開
網頁的深層連結需求,主要出現在單頁應用程式(SPA):畫面是用 JavaScript 切換的,網址卻必須能獨立開啟。下面的範例用 history.pushState 改網址,並在載入時依 location.pathname 決定要顯示的內容:
<nav>
<a href="/" data-link>首頁</a>
<a href="/products/42" data-link>商品 42</a>
<a href="/products/7" data-link>商品 7</a>
</nav>
<div id="view"></div>
<script>
function render() {
const m = location.pathname.match(/^\/products\/(\d+)$/);
const view = document.getElementById('view');
view.textContent = m ? `商品頁:id=${m[1]},hash=${location.hash || '(無)'}` : '首頁';
}
document.addEventListener('click', (e) => {
const a = e.target.closest('a[data-link]');
if (!a) return;
e.preventDefault();
history.pushState(null, '', a.getAttribute('href'));
render();
});
window.addEventListener('popstate', render);
render();
</script>
直接用 /products/42?ref=ad#reviews 當第一個網址開啟時,頁面載入後立刻顯示對應內容:

Chrome(無頭模式)、Windows 11:直接開啟深層連結的畫面,頁面依路徑顯示商品 42,並讀到 #reviews。
同一個範例執行時,本機伺服器只收到 /products/42?ref=ad 這個請求,沒有 #reviews。這符合 RFC 3986 對 fragment 的說明:片段識別會在取得資源之前從 URI 分離,只由 user agent 處理。因此:
path與query會送到伺服器,伺服器必須對/products/42回傳這個 SPA 的頁面,直接開啟或按 F5 才不會出現 404。#後面的內容只存在瀏覽器端,伺服器看不到,要用location.hash讀取。location.hash的用法見〈Javascript 取得網址後面 #(錨點)名稱:location.hash 用法〉。- 在範例中點擊「商品 7」,只是用
pushState換網址並重新繪製,沒有再向伺服器發出請求。Next.js 的對應做法見〈Next.js URL 更新不刷新網頁〉。
三、Android:Deep Link 與 App Links
Android 官方文件說明,Deep Linking 是 Android 平台的通用能力,利用 Intent 系統把連結交給有興趣的 App;想處理某個 URI 的 App,要在 manifest 宣告對應的 intent filter。
3.1 三種深層連結
官方文件列出三種類型:
- Custom deep links:使用自訂 URI scheme(例如
example://products/123),不是標準網頁連結,若另一個 App 註冊相同 scheme,仍可能出現選擇視窗。 - Web links:使用標準
http、httpsscheme。在 Android 12 以上,一般的網頁連結多半由瀏覽器處理,不一定進到你的 App。 - App Links:Android 6.0(API 23)起可用,是經過網站關聯驗證的網頁連結。驗證通過後,系統會直接把該網域的連結導到你的 App,不會出現選擇視窗。
3.2 宣告 intent filter
要讓 App 接住連結,官方文件要求 intent filter 包含 ACTION_VIEW、BROWSABLE 與 DEFAULT 類別,<data> 至少要有 android:scheme。下面是官方文件的範例,兩個 filter 分別接 http://www.example.com/gizmos 與 example://gizmos:
<activity
android:name="com.example.android.GizmosActivity"
android:label="@string/title_gizmos" >
<intent-filter android:label="@string/filter_view_http_gizmos">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"
android:host="www.example.com"
android:pathPrefix="/gizmos" />
</intent-filter>
<intent-filter android:label="@string/filter_view_example_gizmos">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="example"
android:host="gizmos" />
</intent-filter>
</activity>
官方文件特別提醒:同一個 intent filter 內的多個 <data> 會被合併計算所有組合。例如同時宣告 https://www.example.com 與 app://open.my.app,實際上也會接住 app://www.example.com 與 https://open.my.app,想宣告不同網址時要分成不同的 filter。
Activity 啟動後,可以用 Intent 的 getData() 取得連結的 URI;官方建議在 onCreate() 或 onStart 等早期回呼讀取。
3.3 連結由哪個 App 處理
同一個 URI 可能有多個 App 宣告了符合的 filter,官方文件列出的處理順序是:
- 開啟使用者已指定的預設 App。
- 只有一個 App 能處理時,直接開啟它。
- 否則顯示選擇視窗(disambiguation dialog),讓使用者挑選。
所以光有 intent filter,不保證連結一定進到你的 App。官方也說明,Android 12(API 31)起,若 App 沒有被核准處理該網域,一般的網頁 intent 會改由預設瀏覽器處理。
3.4 App Links 的驗證
要讓網頁連結直接進 App,需要 App Links 驗證。官方文件的流程是:intent filter 設有 android:autoVerify="true",在 Android 6.0 以上安裝 App 時,系統會對 filter 內每個網域,查詢 https://<網域>/.well-known/assetlinks.json 這個 Digital Asset Links 檔。檔案內容格式請依〈Verify Android App Links〉撰寫。
用 adb 測試連結是否解析到正確的 Activity,官方給的語法是:
adb shell am start -W -a android.intent.action.VIEW -d "example://gizmos" com.example.android
四、iOS:Universal Links
Apple 的 Universal Links 是標準的 HTTP 或 HTTPS 連結,同一個網址對網站與 App 都有效。使用者點到連結時,系統會直接導向 App,不經過預設瀏覽器或網站;沒有安裝 App 時,系統會用預設瀏覽器開啟該網址,由網站處理。
要啟用 Universal Links,需要兩邊都設定,建立雙向關聯:
- 在網站放一個名為
apple-app-site-association(沒有副檔名)的 JSON 檔,網址為https://<網域>/.well-known/apple-app-site-association。官方要求必須用https://、有效憑證,且不得有重新導向。App 安裝時,系統會下載這個檔案驗證。 - 在 App 的 Xcode 專案加入 Associated Domains capability,網域格式為
applinks:<網域>,只寫網域,不含路徑、查詢字串或結尾斜線。
官方文件的簡化範例如下,components 用來比對 URL 的各個部分,exclude 表示該模式不當成 Universal Link:
{
"applinks": {
"details": [
{
"appIDs": [ "ABCDE12345.com.example.app" ],
"components": [
{ "/": "/buy/*", "comment": "Matches any URL with a path that starts with /buy/." },
{ "/": "/help/website/*", "exclude": true }
]
}
]
}
}
appIDs 的格式是「Application Identifier Prefix」加「Bundle Identifier」。若網站有多個子網域,每個子網域都要有自己的 apple-app-site-association 檔。
五、三種做法的比較
| 項目 | 自訂 scheme(Android) | Android App Links | iOS Universal Links |
|---|---|---|---|
| 連結格式 | example://... | https://... | https://... |
| 是否需要網站關聯檔 | 否 | 是,assetlinks.json | 是,apple-app-site-association |
| 驗證通過後 | 仍可能出現選擇視窗 | 直接進 App,不出現選擇視窗 | 直接進 App |
常見問題
Q1:點連結為什麼跳出「選擇要用哪個 App 開啟」的視窗?
這是 Android 的 disambiguation dialog。多個 App 宣告了符合的 intent filter,又沒有已指定的預設 App 時,系統會讓使用者選擇。自己的網域想避免這個視窗,要走 App Links 驗證,見 3.4。
Q2:怎麼確認 App Links 驗證有沒有通過?
官方文件提供 Android 12 以上手動觸發驗證的步驟,裝置需連上網路。請依序執行,每個指令獨立複製:
- 先重設裝置上這個 App 的 App Links 狀態,把
PACKAGE_NAME換成你的套件名稱:
adb shell pm set-app-links --package PACKAGE_NAME 0 all
- 重新觸發網域驗證:
adb shell pm verify-app-links --re-verify PACKAGE_NAME
- 等幾分鐘讓驗證完成,再查看結果:
adb shell pm get-app-links PACKAGE_NAME
輸出中,網域狀態為 verified 代表驗證成功;none 表示可能還沒驗證完,官方建議多等幾分鐘後重新執行第 2 步。
補充說明:以上指令與狀態說明整理自 Android 官方文件,需要實體裝置或模擬器才能執行。
Q3:# 後面的內容會傳給伺服器嗎?
不會。片段識別由瀏覽器自己處理,伺服器收不到。需要讓伺服器知道的參數,要放在 path 或 query。
Q4:App 沒裝的時候,深層連結會怎樣?
Universal Links 是標準網址,沒有安裝 App 時,系統會用預設瀏覽器開啟,由網站接手。因此網站本身也要能顯示該網址對應的內容。
參考資料與延伸閱讀
- Create deep links to app content | Android Developers
- Android App Links | Android Developers
- Verify Android App Links | Android Developers
- Allowing apps and websites to link to your content | Apple Developer Documentation
- Supporting associated domains | Apple Developer Documentation
- RFC 3986 第 3.5 節 Fragment
- Location: hash property | MDN
- JavaScript 變更網址不刷新頁面與 history API 用法
- JS 解析 URL:new URL 與 URLSearchParams 屬性對照
- Javascript 取得網址後面 #(錨點)名稱:location.hash 用法
發佈留言