概要
- 在 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 下面是 apps 和 details。
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ū)分大小寫的模式匹配,值也一樣。
下圖是蘋果內置的一些常見的替代變量

下面我們來看看替代變量如何使用?
首先,在 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)內置證書存儲的信任)。

如何在設備上啟用替代模式呢?
蘋果要求用戶在 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 文件。
最佳實踐
- 優(yōu)雅地失敗 可能會向你提供表示過期、無效或不存在內容的 URL,如果你確定一個通用鏈接不能被你的 App 打開,你可以嘗試在 Safari 視圖控制器中打開它,這可以讓用戶參與你的 App。如果 Safari 視圖控制器不是選項,則考慮在 Safari 中打開 URL,或者至少提示有關問題的詳細信息,避免將用戶發(fā)送到空白屏幕。
- 如果有人訪問你的網站,請使用 Smart App Banner 提供到 App Store 或你的內容的鏈接。Smart App Banner 與 Safari 無縫集成,不需要 JavaScript 或 自定義 URL Scheme 來支持它。