Post

Universal Links iOS 开发攻略|设定技巧与本地测试环境搭建

掌握 iOS Universal Links 设定与更新机制,解决 Apple CDN 快取延迟问题,并透过本地 nginx + ngrok 环境快速模拟测试,提升多域名与复杂路径匹配准确度,确保 APP 与网站无缝串接体验。

Universal Links iOS 开发攻略|设定技巧与本地测试环境搭建

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


iOS 13, iOS 14 Universal Links 新鲜事&建立本地测试环境

Photo by [NASA](https://unsplash.com/@nasa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by NASA

前言

对于一个有网站又有 APP 的服务, Universal Links 的功能对于使用者体验来说无比的重要,能达到 Web 与 APP 之间的无缝接轨;但一直以来都只有简单设置,没有太多的著墨;前阵子刚好又遇到花了点时间研究了一下,把一些有趣的事记录下来。

常见考量

经手过的服务,对于实作 Universal Links 的考量都是 APP 上并没有实作完整的网站功能,Universal Links 认的是域名,只要域名匹配到就会开启 APP;关于这个问题可以下 NOT 排除 APP 上没有相应功能的网址,若网站服务网址很极端,那干脆新建一个 subdomain 用来做 Universal Links。

apple-app-site-association 何时更新?

  • iOS < 14,APP 在第一次安装、更新时会去询问 Universal Links 网站的 apple-app-site-association。

  • iOS ≥ 14 ,则是由 Apple CDN 做快取定期更新 Universal Links 网站的 apple-app-site-association;APP 在第一次安装、更新时会去跟 Apple CDN 拿取;但这边就会有个问题,Apple CDN 的 apple-app-site-association 可能还是旧的。

关于 Apple CDN 的更新机制,查了一下文件,没有提到;查了下 讨论 ,官方也只回应「会定期更新」细节之后会发布在文件…但至今依然还没看到。

我自己觉得应该最慢 48 小时,就会更新吧。。。所以下次有更改到 apple-app-site-association 的话建议在 APP 上架更新前几天就先改好 apple-app-site-association 上线。

apple-app-site-association Apple CDN 确认:

1
2
Headers: HOST=app-site-association.cdn-apple.com
GET https://app-site-association.cdn-apple.com/a/v1/你的网域

可以取得当前 Apple CDN 上的版本长怎样。(记得加上 Request Header Host=https://app-site-association.cdn-apple.com/

iOS ≥ 14 Debug

因前述的 CDN 问题,那我们在开发阶段该如何 debug 呢?

还好这部分苹果有给解决方法,不然没办法即时更新真的要吐血了;我们只需要再 applinks:domain.com 加上 ?mode=developer 即可,另外还有 managed(for 企业内部 APP) , or developer+managed 模式可设定。

加上 mode=developer 后,APP 在模拟器上每次 Build & Run 时都会直接跟网站拿最新的 app-site-association 来用。

如果要 Build & Run 在实机则要先去「设定」->「开发者」-> 打开「Associated Domains Development」选项即可。

⚠️ 这边有个坑 ,app-site-association 可以放在网站根目录或是 ./.well-known 目录下;但在 mode=developer 下他只会问 ./.well-known/app-site-association ,害我以为怎么没效。

开发测试

如果是 iOS <14 记得有更改过 app-site-association 的话要删掉再重 Build & Run APP 才会去抓最新的回来,iOS ≥ 14 请参考前述方法加上 mode=developer。

app-site-association 内容的修改,好一点的话可以自行修改伺服器上的档;但对于有时候碰不到伺服器端的我们来说,如果要做 universal links 的测试会非常的麻烦,要不停的麻烦后端同事帮忙,变成要很确定 app-site-association 内容后一次上线,一直改来改去会把同事逼疯。

在本地建一个模拟环境

为了解决上述问题,我们可以在本地起一个小服务。

首先在 mac 上安装 nginx:

1
brew install nginx

如果没安装过 brew 可先安装:

1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装完 nginx 后,前往 /usr/local/etc/nginx/ 打开编辑 nginx.conf 档案:

1
2
3
4
5
6
7
8
9
10
11
...
server {
        listen       8080;
        server_name  localhost;
#charset koi8-r;
#access_log  logs/host.access.log  main;
location / {
            root   /Users/zhgchgli/Documents;
            index  index.html index.htm;
        }
...

大概在第 44 行的位置将 location / 里的 root 换成你想要的目录位置(这边以 Documents 为例)。

listen on 8080 port ,如果没有冲突则不需要修改。

储存修改完后,下指令启动 nginx:

1
nginx

若要停止时,则下:

1
nginx -s stop

停止。

如果有更改 nginx.conf 记得要下:

1
nginx -s reload

重新启用服务。

建立一个 ./.well-known 目录在刚设定的 root 目录内,并将 apple-app-site-association 档案放到 ./.well-known 内。

⚠️ .well-known 建立后若消失,请注意 Mac 要打开「显示隐藏资料夹」功能:

在 terminal 下:

1
defaults write com.apple.finder AppleShowAllFiles TRUE

再下 killall finder 重启所有 finder,即可。

⚠️ apple-app-site-association 看起来没有副档名,但实际还是有 .json 副档名:

在档案上按右键 -> 「取得资讯 Get Info」->「Name & Extension」-> 检查有无副档名&同时可取消勾选「隐藏档案类型 Hide extension」

没问题后,打开浏览器测试以下连结是否正常下载 apple-app-site-association:

1
http://localhost:8080/.well-known/apple-app-site-association

如果能正常下载代表本地环境模拟成功!

如果出现 404/403 错误则请检查 root 目录是否正确、目录/档案是否有放入、apple-app-site-association 是否不小心带了副档名( .json)。

注册&下载 Ngrok

[ngrok.com](https://dashboard.ngrok.com/get-started/setup){:target="_blank"}

ngrok.com

解压缩出 ngrok 执行档

解压缩出 ngrok 执行档

进入 [Dashboard 页面](https://dashboard.ngrok.com/get-started/setup){:target="_blank"} 执行 Config 设定

进入 Dashboard 页面 执行 Config 设定

1
./ngrok authtoken 你的TOKEN

设定好之后,下:

1
./ngrok http 8080

因我们的 nginx 在 8080 port。

启动服务。

这时候我们会看到一个服务启动状态视窗,可以从 Forwarding 中取的此次分配到的公开网址。

⚠️ 每次启动分配到的网址都会变,所以仅能作为开发测试使用。

这边以此次分配到的网址 https://ec87f78bec0f.ngrok.io/ 为例

回到浏览器改输入 https://ec87f78bec0f.ngrok.io/.well-known/apple-app-site-association 看看能不能正常下载浏览 apple-app-site-association 档案,如果没问题则可继续下一步。

将 ngrok 分配到的网址输入到 Associated Domains applinks: 设定中。

记得带上 ?mode=developer 方便我们测试。

重新 Build & Run APP:

打开浏览器输入相应的 Universal Links 测试网址(EX: https://ec87f78bec0f.ngrok.io/buy/123 )查看效果。

页面出现 404 不要理他,因为我们实际没有那一页;我们只是要测 iOS 对网址匹配的功能符不符合我们预期;如果上方有出现 「Open」代表匹配成功,另外也可以测 NOT 反向的状况。

点击「Open」后开启 APP -> 测试成功!

开发阶段都测试 OK 后,将确认修改过之后的 apple-app-site-association 档案再交给后端上传到伺服器就能确保万无一失啰~

最后记得将 Associated Domains applinks: 改为正试机网址。

另外我们也可以从 ngrok 运行状态视窗中看到每次 APP Build & Run 有没有跟我们要 apple-app-site-association 档案:

iOS < 13 之前:

设定档较简单,只有以下内容可设定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "applinks": {
      "apps": [],
      "details": [
           {
             "appID" : "TeamID.BundleID",
             "paths": [
               "NOT /help/",
               "*"
             ]
           }
       ]
   }
}

TeamID.BundleId 换成你的专案设定 (ex: TeamID = ABCD , BundleID = li.zhgchg.demoapp => ABCD.li.zhgchg.demoapp )。

如果有多个 appID 则要重复加入多组。

paths 部分则为匹配规则,能支援以下几种语法:

  • * :匹配 0~多个字元,ex: /home/* (home/alan…)

  • ? :匹配 1 个字元,ex: 201? (2010~2019)

  • ?* :匹配 1 个~多个字元,ex: /?* (/test、/home. . )

  • NOT :反向排除,ex: NOT /help (any url but /help)

更多玩法组合可自己依照实际情况决定,更多资讯可参考 官方文件

- 请注意,他不是 Regex,不支援任何 Regex 写法。

- 旧版不支援 Query (?name=123)、Anchor ( #title)。

- 中文网址须先转成 ASCII 后才能放在 paths 中 (所有url 字元均要是 ASCII)。

iOS ≥ 13 之后:

强化了设定档内容的功能,多增加支援 Query/Anchor、字符集、编码处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"applinks": {
  "details": [
    {
      "appIDs": [ "TeamID.BundleID" ],
      "components": [
        {
          "#": "no_universal_links",
          "exclude": true,
          "comment": "Matches any URL whose fragment equals no_universal_links and instructs the system not to open it as a universal link"
        },
        {
          "/": "/buy/*",
          "comment": "Matches any URL whose path starts with /buy/"
        },
        {
          "/": "/help/website/*",
          "exclude": true,
          "comment": "Matches any URL whose path starts with /help/website/ and instructs the system not to open it as a universal link"
        },
        {
          "/": "/help/*",
          "?": { "articleNumber": "????" },
          "comment": "Matches any URL whose path starts with /help/ and that has a query item with name 'articleNumber' and a value of exactly 4 characters"
        }
      ]
    }
  ]
}

转贴自官方文件,可以看到格式有所改变。

appIDs 为阵列,可放入多组 appID,这样就不用像以前一样只能整个区块重复输入。

WWDC 有提到与旧版兼容, 当 iOS ≥ 13 有读到新的格式就会忽略旧的 paths

匹配规则改放在 components 中;支援 3 种类型:

  • / : URL

  • ? :Query,ex: ?name=123&place=tw

  • # :Anchor,ex: #title

并且可以搭配使用,假设今天 /user/?id=100#detail 才需要跳到 APP 则可写成:

1
2
3
4
5
{
  "/": "/user/*",
  "?": { "id": "*" },
  "#": "detail"
}

其中匹配语法同原本语法,也是支援 * ? ?*

新增 comment 注解栏位,可输入注解方便辨识。(但请注意这是公开的,别人也看得到)

反向排除则改为指定 exclude: true

新增 caseSensitive 指定功能,可指定匹配规则是否对大小写敏感, 预设:true ,有这需求的话可以少写许多规则。

新增 percentEncoded 前面说到的,旧版需要先将网址转为 ASCII 放到 paths 中(如果是中文字会变得很丑无法辨识);这个参数就是是否要帮我们自动 encode, 预设是 true 。 假设是中文网址就能直接放入了(ex: /客服中心 )。

详细官方文件可 参考此

预设字符集:

这算是这次更新蛮重要的功能之一,新增支援字符集。

系统帮我们定义好的字符集:

  • $(alpha) :A-Z 和 a-z

  • $(upper) :A-Z

  • $(lower) :a-z

  • $(alnum) :A-Z 和 a-z 和 0–9

  • $(digit) :0–9

  • $(xdigit) :十六进制字符,0–9 和 a,b,c,d,e,f,A,B,C,D,E,F

  • $(region) :ISO 地区编码 isoRegionCodes ,Ex: TW

  • $(lang) :ISO 语言编码 isoLanguageCodes ,Ex: zh

假设我们的网址有多语系,我想要支援 Universal links 时,可以这样设定:

1
2
3
"components": [        
     { "/" : "/$(lang)-$(region)/$(food)/home" }      
]

这样不管是 /zh-TW/home/en-US/home 都能支援,非常方便,不用自己写一整排规则!

自订字符集:

除了预设字符集之外,我们也能自订字符集,增加设定档复用、可读性。

applinks 中加入 substitutionVariables 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "applinks": {
    "substitutionVariables": {
      "food": [ "burrito", "pizza", "sushi", "samosa" ]
    },
    "details": [{
      "appIDs": [ ... ],
      "components": [
        { "/" : "/$(food)/" }
      ]
    }]
  }
}

范例中自订了一个 food 字符集,并在后续 components 中使用。

以上范例可匹配 /burrito , /pizza , /sushi , /samosa

细节可参考 此篇 官方文件。

没有灵感?

如果对设定档内容没有灵感,可偷偷参考其他网站福的内容,只要在服务网站首页网址加上 /app-site-association/.well-known/app-site-association 即可读取他们的设定。

例如: https://www.netflix.com/apple-app-site-association

补充

在有使用 SceneDelegate 的情况下,open universal link 的进入点是在SceneDelegate 中:

1
func scene(_ scene: UIScene, continue userActivity: NSUserActivity)

而非 AppDelegate 的:

1
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool

延伸阅读

参考资料

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.