紀錄工作經驗、相關知識,解決技術相關問題。

新手 Jamp Holes, 網站相關

Deep Linking 是什麼?深層連結在網頁與 App 的用途和運作方式

Deep Linking 是什麼?深層連結在網頁與 App 的用途和運作方式 特色圖片

Deep Linking(深層連結)指的是一條連結直接指向網站或 App 裡的某個特定內容,例如某個商品頁、某篇文章,而不是只能開到首頁再自己找。在網頁上,這就是一般帶有路徑、查詢參數或 # 片段的網址;在手機 App 上,則要靠作業系統把連結交給正確的 App 處理,Android 與 iOS 各有一套機制。本文先從網址結構講起,再說明網頁、Android 與 iOS 三種情境怎麼運作。

網頁端用 history.pushState 改網址的細節,可前往〈JavaScript 變更網址不刷新頁面與 history API 用法〉觀看,本文只說明它和 Deep Linking 的關係。

一、Deep Linking 的網址長什麼樣子

不論目標是網頁還是 App,深層連結都是一個 URL,常見的組成如下:

部分範例用途
schemehttps、example決定用什麼方式處理這個連結
hostwww.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 無頭模式直接開啟 /products/42?ref=ad#reviews 的畫面,顯示「商品頁:id=42,hash=#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、https scheme。在 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,官方文件列出的處理順序是:

  1. 開啟使用者已指定的預設 App。
  2. 只有一個 App 能處理時,直接開啟它。
  3. 否則顯示選擇視窗(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,需要兩邊都設定,建立雙向關聯:

  1. 在網站放一個名為 apple-app-site-association(沒有副檔名)的 JSON 檔,網址為 https://<網域>/.well-known/apple-app-site-association。官方要求必須用 https://、有效憑證,且不得有重新導向。App 安裝時,系統會下載這個檔案驗證。
  2. 在 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 LinksiOS 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 以上手動觸發驗證的步驟,裝置需連上網路。請依序執行,每個指令獨立複製:

  1. 先重設裝置上這個 App 的 App Links 狀態,把 PACKAGE_NAME 換成你的套件名稱:
adb shell pm set-app-links --package PACKAGE_NAME 0 all
  1. 重新觸發網域驗證:
adb shell pm verify-app-links --re-verify PACKAGE_NAME
  1. 等幾分鐘讓驗證完成,再查看結果:
adb shell pm get-app-links PACKAGE_NAME

輸出中,網域狀態為 verified 代表驗證成功;none 表示可能還沒驗證完,官方建議多等幾分鐘後重新執行第 2 步。

補充說明:以上指令與狀態說明整理自 Android 官方文件,需要實體裝置或模擬器才能執行。

Q3:# 後面的內容會傳給伺服器嗎?

不會。片段識別由瀏覽器自己處理,伺服器收不到。需要讓伺服器知道的參數,要放在 path 或 query。

Q4:App 沒裝的時候,深層連結會怎樣?

Universal Links 是標準網址,沒有安裝 App 時,系統會用預設瀏覽器開啟,由網站接手。因此網站本身也要能顯示該網址對應的內容。

參考資料與延伸閱讀

發佈留言