iOS 通用鏈接(Universal Links)

概要

  • 在 Web 和 App 中表示我們的內容的一個 URL
  • 通用鏈接允許用戶在 App 中而不是在 Web 瀏覽器中打開內容,從而讓你提供更豐富的體驗
  • 在 iOS、tvOS 和 macOS App 中可用
  • 在 App 和網站之間安全關聯

什么是通用鏈接

通用鏈接是 HTTP 或 HTTPS URL,無論是在網上還是在我們的 App 中,Apple 的操作系統(tǒng)將其識別為指向網絡或 App 中的資源。這意味著,無論用戶是否安裝了 App,一個 URL 都可以表示該內容,它是提高用戶在 App 中參與度的好方法。

iOS 9、tvOS 10 和 macOS 10.15 中引入了通用鏈接。通用鏈接在我們的 App 和網站之間是安全關聯的,App 中有一個權限文件,這個權限文件指示它可以代表哪些 domain;Web 服務器上則有一個 JSON 文件,該文件包含了關于我們的 App 中可以表示其域的哪些部分的更多細節(jié)。這種雙向安全握手確保沒有人可以將用戶重定向到他們的 App,蘋果建議我們把使用自定義 URL 方案(custom URL schemes) 的地方遷移到通用鏈接,自定義 URL 方案本質上是不安全的,并且可能被惡意開發(fā)人員濫用,強烈建議不要使用自定義 URL 方案。

怎么創(chuàng)建通用鏈接

1. 配置你的網絡服務器

  • 安裝一個有效的 HTTPS 證書
    由于 HTTP 不安全,不能用于驗證 App 和網站之間的關聯,所以,我們的 Web 服務器必須有一個有效的 HTTPS 證書,并且用于簽名 HTTPS 證書的根證書必須被操作系統(tǒng)識別,不支持自定義根證書。

  • 添加 apple-app-site-association 文件
    生成證書并配置到服務器之后,還需要添加 apple-app-site-association JSON 文件。當你的 App 安裝到 Apple 設備上時,操作系統(tǒng)將下載該文件,以確定服務器將允許 App 使用哪些服務,系統(tǒng)還會定期下載此文件的更新,通用鏈接就是這個文件中可能包含的許多服務之一。蘋果推薦我們服務器把這個文件放置到 https://example.com/.well-known/apple-app-site-association 路徑,不推薦使用其它路徑,注意不要對 apple-app-site-association 進行簽名。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appID": "ABCDE12345.com.example.app",
                "paths": [ "/path/*/filename" ]
             }
         ]
    }
}

這個文件頂層是一個字典,它的 key 是服務類型。對于通用鏈接,key 是 applinks,在頂層的 key 下面是 appsdetails。

apps:對于通用鏈接,apps 的值總是一個空數組。在 iOS13、tvOS 13 和 macOS 10.15 及以上系統(tǒng),我們可以刪除 apps 字段,如果需要支持 iOS 12、tvOS 12或更早版本,則仍需保留 apps 字段。

details: 包含一個字典數組,每個字典代表一個特定 App 的通用鏈接配置。在 iOS 12、tvOS 12或更早版本,details 的值支持使用字典結構(現在是數組)。

appID:它的值是App 標識符App 標識符是一個由 Apple 提供的10位字母數字前綴和 bundleId 組成。這個前綴可能等于也可能不等于你的團隊標識符,檢查開發(fā)人員門戶網站 (https://developer.apple.com) 以確認你的App 標識符。如果有多個相同通用鏈接配置的 App,且 target 是 iOS13、tvOS 13 和 macOS 10.15 及以上系統(tǒng),可以使用 ** appIDs** 來減小該文件的大小,該鍵的值是一個App 標識符數組。如果需要支持 iOS 12、tvOS 12或更早版本,則應該繼續(xù)為每個 App 使用appID。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
                "paths": [ "/path/*/filename" ]
             }
         ]
    }
}

paths:它的值是包含路徑的數組。路徑使用模式匹配,且模式匹配與在終端中執(zhí)行相同,星號用于表示多個通配符,而問號只匹配一個字符。

components:從 iOS13、tvOS 13 和 macOS 10.15 開始,蘋果使用 components 替換 paths,它的值是一個字典數組,其中每個字典都包含 0 個或多個 URL 組件以進行模式匹配。如果需要支持 iOS 12、tvOS 12或更早版本,可以保留paths。 iOS13、tvOS 13 和 macOS 10.15 及以上系統(tǒng),如果 components 存在,將忽略 paths
你可以匹配 URL 的 path 組件,該組件的 key 是 "/"。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
                "components": [ 
                    {
                       "/": "/path/*/filename",
                    }
                ]
             }
         ]
    }
}

你可以匹配 URL 的 fragment 組件,它的 key 是 "#";你還可以匹配 URL 的 query 組件,它的 key 是"?"。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
                "components": [ 
                    {
                       "/": "/path/*/filename",
                       "#": "*fragment",
                       "?": "widget=?*"
                    }
                ]
             }
         ]
    }
}

現在很多 URL 將 query 組件分成鍵值對,稱為 query item。對于 query 組件,可以指定字典 (而不是字符串) 作為它的值,從而模式匹配單個 query item。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
                "components": [ 
                    {
                       "/": "/path/*/filename",
                       "#": "*fragment",
                       "?": { "widget": "?*", "grommet": "please" }
                    }
                ]
             }
         ]
    }
}

URL 可以重復 query item 名稱,并且操作系統(tǒng)將要求給定 query item 名稱的所有實例都匹配模式,沒有值的 query item 和沒有 query item 字段都由操作系統(tǒng)自動處理,就好像它們的值等于空字符串一樣。

要使用 components 字典匹配待篩選 URL,所有指定的 component 必須匹配。如果不指定 component,操作系統(tǒng)的默認行為就是忽略那個 component。

例如,你的 App 不關心 URL 的 fragment 組件,你就不需要指定它。此外,我們的網站可能有一些部分還不能在 App 中顯示,可以使用 true 設置 exclude 來排除這些部分。component 中的 exclude 與在 paths 中使用 not 關鍵詞具有相同的行為。

// Your apple-app-site-association File
{
    "applinks": {
         "apps": [],
         "details": [
             {
                "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
                "components": [ 
                    {
                       "/": "/path/*/filename",
                       "#": "*fragment",
                       "?": { "widget": "?*", "grommet": "please" }
                       "exclude": true
                    }
                ]
             }
         ]
    }
}

模式匹配的增強:從 iOS13.5、macOS 10.15.5 開始,支持不區(qū)分大小寫的模式匹配,在 components 中新增 caseSensitive,將它的值設置為 false 以禁用區(qū)分大小寫 (即不區(qū)分大小寫)。

"components": [{ "/": "/sourdough/?*", "caseSensitive": false }]

Unicode 模式的增強:從 iOS14、macOS 11 開 始,支持 Unicode 編碼,即禁用百分比編碼(比如:URL 中的中文字符;俄語中的音標等),禁用后使用 32bit Unicode 碼位序列,而不是 7 位 ASCII 字符。

"components": [{ "/": "/螞蟻上樹/?*", "percentEncoded": false }]

上面兩種增強功能可以同時使用,并支持添加默認值 ** defaults,默認值是應用于所有模式的值的字典,除非某個模式顯式地覆蓋它。如果 defaults 是與 components 同一級的組件,它將應用于components** 數組中的所有模式;如果 defaults 是與 details 同一級的組件,它將應用于這個 domain 下的所有通用鏈接。

"defaults": { "percentEncoded": false,  "caseSensitive": false },
"components": [
{ "/": "/螞蟻上樹/?*", "percentEncoded": false },
{ "/": "/sourdough/?*", "caseSensitive": false },
{ "/": "/canat onbed/?*", "caseSensitive": false, "percentEncoded": false},
]

下面是一些 URL 示例,我們需要它們進行模式匹配。

// Pattern-Matching Examples

"appIDs": [ "ABCDE12345.com.example.app" ],
"components" : [
   {
     "/": "/*/order/"    https://example.com/taco/order/
   },                           https://example.com/salad/order/
   {
     "/": "/taco/*",    https://example.com/taco/?cheese=panela
     "?": { "cheese" : "?*" }
   },
   {
     "#": "coupon-1???",    https://example.com#coupon-1234
     "exclude": true
   },
   {
      "/": "",    https://example.com#coupon-5678
      "#": "coupon-????"
   }
 ]

替代變量(Substitution variables):在 macOS 10.15.6 和 iOS 13.5 以上系統(tǒng)版本可用。它們是可以匹配的字符串的命名列表,這些變量出現在模式匹配字符串中代表你指定的所有值,它們的名字幾乎可以包含任意字符(除了 "$"、"("、")"),在模式中遇到變量名時總是區(qū)分大小寫。另外,你指定替代變量的值可以包含用于通配符匹配的問號和星號,但不能引用替代變量。默認情況下,如果模式匹配區(qū)分大小寫,則值也會區(qū)分大小寫。如果你啟用了不區(qū)分大小寫的模式匹配,值也一樣。

下圖是蘋果內置的一些常見的替代變量


image.png

下面我們來看看替代變量如何使用?

首先,在 applinks 下添加一個新的鍵值對 substitutionVariables,它的值是字典,而字典中的 key 是變量名,value 則是包含要匹配的子字符串的數組。

{
    "applinks": {
        "substitutionVariables": {
            "food": [ "burrito", "shawarma", "sushi", "curry-pad-thai" ]
        },
        "details": [{
            "appIDs": [ "ABCDE12345.com.example.restaurant" ],
            "components": [
                { "/": "/$(lang)_CA/$(food)", "exclude": true  },  // 這一行代碼為了排查不想要的匹配
                { "/": "/$(lang)_$(region)/$(food)" }
            ]
        }]
    }
}

如何處理 ** substitutionVariables** 中的特殊要求呢?比如,加拿大地區(qū)的 food 與其它地區(qū)的 food 有一些差異。
我們可以在 substitutionVariables 字典中添加新的變量 "Canadian food"。如果這些變量名中存在 Unicode 編碼,我們還可以在 substitutionVariables 字典中添加 "percentEncoded" 字段并設置其值為 false 來禁用百分比編碼,從而使用 Unicode 編碼。

{
    "applinks": {
        "substitutionVariables": {
            "food": [ "burrito", "shawarma", "sushi", "curry-pad-thai" ],
            "Canadian food": [ "burrito", "poutine", "butter-tart", "fiddlehead" ],
             "percentEncoded": false
        },
        "details": [{
            "appIDs": [ "ABCDE12345.com.example.restaurant" ],
            "components": [
                { "/": "/$(lang)_CA/$(Canadian food)" },
                { "/": "/$(lang)_CA/$(food)", "exclude": true  },  // 這一行代碼為了排查不想要的匹配
                { "/": "/$(lang)_$(region)/$(food)" }
            ]
        }]
    }
}

進入 App 配置之前,先聊聊國際化

URL 始終使用 ASCII 編碼,所以模式匹配也是使用 ASCII 編碼來完成的。當你創(chuàng)建 JSON 時,如果需要匹配當前的 Unicode 字符,需要對其進行編碼。你可能想為支持的每個國家提供特定國家的模式,這大大增加了 JSON 的大小。如果國家之間的模式匹配是一致的,則可以通過使用通配符 ?? 簡化 JSON 來減少服務器之間的流量。例如,如果你使用兩個字母的國家碼來分隔內容,那么只需使用"??"。如果遇到帶有無效國家碼或特定語言環(huán)境標識符的 URL,則將其視為用戶的當前語言環(huán)境。

從 iOS13、tvOS 13 和 macOS 10.15 開始,操作系統(tǒng)將根據用戶最可能瀏覽的位置對 apple-app-site-association 文件下載進行優(yōu)先級排序,蘋果仍然會在安裝 App 時下載它們,但是優(yōu)先級不同,頂級域名 .com、.net 和 .org 是高優(yōu)先級域,國家代碼 TLD(也稱為 ccTLD),如果國際化的 TLD 與用戶當前的語言環(huán)境設置匹配,那么它們也會被優(yōu)先化。

2.配置你的 App

  • 添加 Associated Domains Entitlement
    在 Xcode 中打開項目,并導航到 project settings,添加 Associated Domains 功能,這將向選定的 target 添加一個新的權限——關聯域權限。
    關聯域權限的值是 "<類型>:<域名>" 的字符串數組,對于通用鏈接服務類型是 applinks,這個數組中的值的順序會被系統(tǒng)忽略。
<array>
        <string>applinks:www.example.com</string>
</array>

在這里,我們聲明 App 支持通用鏈接,例如 www.example.com。當 App 被安裝時,操作系統(tǒng)將訪問 www.example.com,尋找 apple-app-site-association 文件。如果它存在,并且包含這個 App 的 App 標識符 信息,那么關聯就被確認了。還可以指示對給定 domain 的 sub-domain 的通配符支持,如下所示,在通用鏈接查找期間,精確域比通配符域具有更高的優(yōu)先級。

<array>
        <string>applinks:www.example.com</string>
        <string>applinks:*.example.com</string>
</array>

最后,來看一個國際化 URL 的例子,國際化 domain 需要使用 Punycode 進行編碼。

假設我們的 App 聲明了對某些 domain 的支持,我們需要在 URL 進入時解析它們。通用鏈接是基于 Foundation 的 NSUserActivity 類的,由 AppDelegate 處理,我們將需要一個處理程序來處理傳入的用戶活動。我們的 AppDelegate 中可能已經有了這個方法,該方法返回一個 bool,如果你能夠成功打開用戶活動則返回 true,否則返回 false。如果你使用 UIScene,可以使用類似的委托方法。

// Configuring Your App
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    // 將通用鏈接與可能支持的其它用戶活動區(qū)分開來
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
    // 從用戶活動對象獲取網絡頁面 URL,對于一個通用鏈接它永遠不會是 nil
    let url = userActivity.webpageURL,
    // 從 URL 構建一個 URL 組件結構體,應該始終使用 URL 組件解析 URL,使用正則表達式或手動解析 URL 字符串可能會使你容易受到安全問題的影響
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
         return false
    }
    
    // 比如,選擇你感興趣的組件來校驗
    for queryItem in components.queryItems ?? [] {
        ...
    }

    // 當你支持來自多個域的通用鏈接時,不要忘記檢查 host 組件

  return true
}

// Opening universal links in other applications
let url = /* ... */
UIApplication.shared.open(url, options: [:]) { success in /* ... */ }

apple-app-site-association 文件是如何進入用戶設備的?這一流程存在什么問題?蘋果又是如何改善的呢?

當我們打開 App Store,選擇想要下載的 App,下載并安裝 App 后,操作系統(tǒng)會檢查其應用權限(這里是關聯域權限),發(fā)現它需要一個或多個 apple-app-site-association 文件數據,此時,設備將打開到承載該文件的 Web 服務器的鏈接,以便下載該文件。

現在,設備的帶寬有限,如果需要從多個 Web 服務器下載多個文件時,設備需要一次下載多個文件。當下載完一個文件后,apple-app-site-association 文件從 Web 服務器到進入設備,由關聯域守護程序解析,App 的通用鏈接就會活躍起來,然后設備移動到下一個排隊的服務器去下載 apple-app-site-association 文件,依次類推。

如果下載有問題怎么辦?

讓我們再次嘗試下載那幾個文件,首先設備嘗試建立到服務器的連接,假設 Wi-Fi 中斷或者服務器崩潰,又或者根本無法從設備訪問服務器,那么下載的程度取決于故障的確切性質,但數據不會傳到設備上,最終設備不得不放棄下載,然后轉到下一個服務器下載另一個文件。這會使設備處于不一致的狀態(tài),即雖然安裝了 App,但它的通用鏈接和其關聯域數據不可用,這種狀態(tài)可能持續(xù)數小時或數天,直到系統(tǒng)下次嘗試更新該 App 的數據。

蘋果的改進

我們再次在 App Store 上選擇一個 App,然后下載到設備上,該設備會看到 App 具有關聯域權限,但還沒有連接到關聯的 Web 服務器,而是連接到管理關聯域數據的內容傳遞網絡 (即 Apple CDN)。CDN 是一種功能強大的工具,可以緩存大量數據,因此它可能已經存儲了來自該 Web 服務器的數據。我們假設它沒有存儲,它可以代表該設備連接到服務器。CDN 很強大,因此它可以同時連接該設備上所有 App 的所有服務器,它可以同時下載所有這些域的 apple-app-site-association 文件,然后緩存它們,并通過單個網絡連接將數據發(fā)送到設備。

蘋果使用 CDN 的原因

蘋果已經構建了一個可專門用于關聯域和 apple-app-site-association 文件的 CDN ,可以對它進行微調,然后為用戶提供最好的體驗。由于 CDN 緩存了來自多個 Web 服務器的數據,所以蘋果使用 HTTP/2 連接來請求所需的所有數據,而不是每個 Web 服務器使用單獨的連接。緩存可以減少服務器上的總負載,從每天可能的數百萬次請求降低到僅有的幾次請求。而且 CDN 以順暢快速的連接著稱,總體上讓用戶擁有更安心的 App 體驗。

在 iOS14 和 macOS 11 系統(tǒng)之后,Web 服務器只需要接收來自 Apple CDN 對 apple-app-site-association 的文件請求。該 CDN 位于公網上,但并不是所有的服務器都能訪問。

如果 Web 服務器無法從公網訪問 Apple CDN 該怎么辦?如何繼續(xù)支持這些場景?

無法訪問的原因可能有:服務器是用于部署前測試的 Web 服務器或者是僅供連接到內網的 Web 服務器。

Apple CDN 有一種替代模式(Alternate modes),允許你繞過 CDN ,直接連接到我們控制的 domain。有 2 種替代模式,它們的區(qū)別在于何時使用它們。開發(fā)者模式是在你將 App 部署到 TestFlight 或最終用戶之前專為構建和測試 App 而設計的;托管模式用于使用 MDM 配置文件安裝 App 期間。開發(fā)者模式可以在 Web 服務器上使用任意有效的 SSL 證書(即使它不受操作系統(tǒng)內置證書存儲的信任)。


image.png

如何在設備上啟用替代模式呢?
蘋果要求用戶在 iOS、watchOS 和 tvOS 上選擇加入開發(fā)者模式。在 iOS 設備上打開設置 App,選擇開發(fā)者設置,它們將在我們的設備第一次連接到運行 Xcode 的 Mac 之后出現,在 "開發(fā)者設置"下啟用 "Associated Domains Development" 選項就能把該設備置為開發(fā)者模式。在 Mac 設備上,這個過程稍有不同,打開終端,輸入 "swcutil developer-mode -e true" 命令,系統(tǒng)將提示你輸入管理員密碼或TouchID,授予權限后,將啟用開發(fā)者模式,這是針對每個用戶的操作。

由于開發(fā)者模式是全局切換的,我們不想為所有的 App 都啟用它,所以它只對使用開發(fā)配置文件簽名的 App 生效。在 App Store 或 TestFlight 上簽名發(fā)布的 App 或已經簽名和公證的 Mac App,不能與這個替代模式一起使用。

最后,開發(fā)者模式和托管模式要求我們在 .well-known 目錄中 (不是 domain 的根目錄) 托管 apple-app-site-association 文件。

最佳實踐

  1. 優(yōu)雅地失敗 可能會向你提供表示過期、無效或不存在內容的 URL,如果你確定一個通用鏈接不能被你的 App 打開,你可以嘗試在 Safari 視圖控制器中打開它,這可以讓用戶參與你的 App。如果 Safari 視圖控制器不是選項,則考慮在 Safari 中打開 URL,或者至少提示有關問題的詳細信息,避免將用戶發(fā)送到空白屏幕。
  2. 如果有人訪問你的網站,請使用 Smart App Banner 提供到 App Store 或你的內容的鏈接。Smart App Banner 與 Safari 無縫集成,不需要 JavaScript 或 自定義 URL Scheme 來支持它。

參考

WWDC2019 session-717
WWDC2020 session-10098

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容