iOS Universal link 入门指南

2,171 阅读9分钟

1.1 Universal link 是什么

Universal Link是苹果在WWDC上提出的iOS9的新特性之一。此特性类似于深层链接,并能够方便地通过打开一个Https链接来直接启动您的客户端应用(手机有安装App)。对比起以往所使用的URL Scheme,这种新特性在实现web-app的无缝链接时能够提供极佳的用户体验。

当你的应用支持Universal Link(通用链接),当用户点击一个链接是可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接。在苹果开发者中可以看到对它的介绍是:

Seamlessly link to content inside your app, or on your website in iOS 9 or later. With universal links, you can always give users the most integrated mobile experience, even when your app isn’t installed on their device.

1.2 Universal link 的应用场景

使用Universal Link(通用链接)可以让用户在Safari浏览器或者其他APP的webview中拉起相应的APP,也可以在APP中使用相应的功能,从而来把用户引流到APP中。

这具体是一种怎样的情景呢?举个例子,你的用户safari里面浏览一个你们公司的网页,而此时用户手机也同时安装有你们公司的App;而Universal Link能够使得用户在打开某个详情页时直接打开你的app并到达app中相应的内容页面,从而实施用户想要的操作(例如查看某条新闻,查看某个商品的明细等等)。比如在Safari浏览器中进入淘宝网页点击打开APP则会使用Universal Link(通用链接)来拉起淘宝APP。

1.3 Universal link 跳转的好处

  • 唯一性: 不像自定义的URL Scheme,因为它使用标准的HTTPS协议链接到你的web站点,所以一般不会被其它的APP所声明。另外,URL scheme因为是自定义的协议,所以在没有安装 app 的情况下是无法直接打开的(在Safari中还会出现一个不可打开的弹窗),而Universal Link(通用链接)本身是一个HTTPS链接,所以有更好的兼容性;

  • 安全: 当用户的手机上安装了你的APP,那么系统会去你配置的网站上去下载你上传上去的说明文件(这个说明文件声明了当前该HTTPS链接可以打开那些APP)。因为只有你自己才能上传文件到你网站的根目录,所以你的网站和你的APP之间的关联是安全的;

  • 可变: 当用户手机上没有安装你的APP的时候,Universal Link(通用链接)也能够工作。如果你愿意,在没有安装你的app的时候,用户点击链接,会在safari中展示你网站的内容;

  • 简单: 一个HTTPS的链接,可以同时作用于网站和APP;

  • 私有: 其它APP可以在不需要知道你的APP是否安装了的情况下和你的APP相互通信。

2. Universal link配置和运行

2.1 配置App ID支持Associated Domains

登录developer.apple.com/ 苹果开发者中心,找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了。

![](https://static001.geekbang.org/infoq/9a/9aeea1dfab91b136fbfb114ed4fb792f.png)

2.2 配置iOS App工程

Xcode 11.0版本

工程配置中相应功能:targets->Signing&Capabilites->Capability->Associated Domains,在其中的Domains中填入你想支持的域名,也必须必须以applinks:为前缀。

具体步骤如下图:

![](https://static001.geekbang.org/infoq/6a/6ad0b18feb32811edc6c11f5f86e4680.png)
![](https://static001.geekbang.org/infoq/f5/f5c2afa52c14eb521265af60e8373e77.png)
![](https://static001.geekbang.org/infoq/41/41e65450d1623f28530881686de2d8ee.png)

Xcode 11.0以下版本

工程配置中相应功能:targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀。563513413,不管你是大牛还是小白都欢迎入驻

配置项目中的Associated Domains:

![](https://static001.geekbang.org/infoq/f4/f4abafb3154822c7ec3b2262e24d110c.png)

2.2 配置和上传apple-app-association

究竟哪些的url会被识别为Universal Link,全看这个apple-app-association文件Apple Document UniversalLinks.html

  • 你的域名必须支持Https

  • 域名 根目录 或者 .well-known 目录下放这个文件apple-app-association,不带任何后缀

  • 文件为json保存为文本即可

  • json按着官网要求填写即可

apple-app-site-association模板:

{    "applinks": {        "apps": [],        "details": [            {                "appID": "9JA89QQLNQ.com.apple.wwdc",                "paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]            },            {                "appID": "ABCD1234.com.apple.wwdc",                "paths": [ "*" ]            }        ]    }}

说明:

**appID:**组成方式是 teamId.yourapp’s bundle identifier。如上面的 9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID。

**paths:**设定你的app支持的路径列表,只有这些指定的路径的链接,才能被app所处理。星号的写法代表了可识 别域名下所有链接。

上传指定文件:上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,自己先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件。

2.4 如何验证 Universal link 生效

  • 可以使用iOS自带的备忘录程序,输入链接,长按链接,如果弹出菜单中有”在‘xxx’中打开”,即表示配置生效。

  • 或者将要测试的网址在Safari中打开,在出现的网页上方下滑,可以看到有在”xxx”应用中打开, 出现菜单:

![](https://static001.geekbang.org/infoq/eb/ebff709967c1261aa6e1ed80686885c8.png)

当点击某个链接,直接可以进我们的app了,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容。

AppDelegate里中实现代理方法,官方链接:Handling Universal Links

Objective-C:

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb])    {        NSURL *url = userActivity.webpageURL;        if (url是我们希望处理的)        {            //进行我们的处理        }        else        {            [[UIApplication sharedApplication] openURL:url];        }    }         return YES;}

Swift:

func application(_ application: UIApplication,                 continue userActivity: NSUserActivity,                 restorationHandler: @escaping ([Any]?) -> Void) -> Bool{    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,        let incomingURL = userActivity.webpageURL,        let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),        let path = components.path,        let params = components.queryItems else {            return false    }        print("path = \(path)")        if let albumName = params.first(where: { $0.name == "albumname" } )?.value,        let photoIndex = params.first(where: { $0.name == "index" })?.value {                print("album = \(albumName)")        print("photoIndex = \(photoIndex)")        return true            } else {        print("Either album name or photo index missing")        return false    }}

3. Universal link遇到的问题和解决方法

3.1 跨域

前端开发经常面临跨域问题,恩Universal Link也有跨域问题,但不一样的是,Universal Link,必须要求跨域,如果不跨域,就不行,就失效,就不工作。(iOS 9.2之后的改动,苹果就这么规定这么设计的)

这也是上面拿知乎举例子的时候重点强调的一个问题,知乎为什么使用oia.zhihu.com做Universal Link?

  • 假如当前网页的域名是 A

  • 当前网页发起跳转的域名是 B

  • 必须要求 B 和 A 是不同域名,才会触发Universal Link

  • 如果B 和 A 是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App

是不是不太好理解,那直接拿知乎举例子

有心人可能看到,知乎的Universal Link配置的是 oia.zhihu.com 这个域名,并且对这个域名下比如/answers /questions /people 等urlpath进行了识别,也就是说,知乎的universal link,只有当你访问 https://oia.zhihu.com/questions/xxxx,在移动端会触发Universal Link,而知乎正经的Urlhttps//www.zhihu.com/questions/xxx是不会触发Universal Link的,知乎为什么制作,为什么不把他的主域名配置Universal Link,就是由于Universal Link的跨域的原因。

知乎的一般网页URL都是www.zhihu.com域名,你在微信朋友圈看到了知乎的问题分享,如果copy url 你就能看到这样的链接

www.zhihu.com/question/22…

微信里其实是屏蔽Schema的,但是你依然能看到大大的一个按钮App内打开,这确实就是通过Universal Link来实现的,但如果知乎把Universal Link 配在了www.zhihu.com域名,那么即便已经安装了App,Universal Link也是不会生效的。

一般的公司都会有自己的主域名,比如知乎的www.zhihu.com,在各处分享传播的时候,也都是直接分享基于主域名的url,但为了解决苹果强制要求跨域才生效的问题,Universal Link就不能配置在主域名下,于是知乎才会准备一个oia.zhihu.com域名,专为Universal Link使用,不会跟任何主动传播分享的域名撞车,从而在任何活动WAP页面里,都能顺利让Universal Link生效。

跨域的另外一个好处是可以突破微信跳转限制,支持微信无缝跳转到App.

简单一句话

只有当前webview的url域名,与跳转目标url域名不一致时,Universal Link 才生效

3.2 更新

apple-app-association的更新时机有以下两种:

  • 每次App安装后的第一次Launch,会拉取apple-app-association

  • Appstore每次App的版本更新后的第一次Launch,也会更新apple-app-association

所以反复重新杀APP重开完全没用,删了App重装确实有用,但不可能让用户这么去做。也就是说,一旦不小心因为意外apple-app-association,想要挽回又让那部分用户无感,App再发一个版本就好了

3.3 Universal Link用户行为

Universal Link 触发后打开App,这时候App的状态栏右上角会有文字提示来自XXApp,可以点状态栏的文字快速返回原来的AP

如果用户点了返回微信,就会被苹果记住,认为用户并不需要跳出原App打开新App,因此这个App的Universal Link会被关闭,再也无效。

想要开启也不是不行,让用户重新用safari打开,universal link的页面,然后会出现很像苹果smart bar的东西,那个东西点了后就能打开

4. H5端的Universal Link业务部署

H5端的Universal Link跳转,从产品经理的角度看,需要满足以下2个需求:

  • 如果已安装App,跳转对应界面

  • 如果没安装App,跳转App下载界面

H5端部署 Universal Link示例:

router.use('/view', function (req, res, next) {    var path = req.path;    res.redirect('https://www.xxx.com/view' + path + '?xxx=xxx');});

整个效果就是

  • 跳转https://www.xxx.com/view/*

  • 已安装App

  • 打开App触发handleUniversalLink

  • 走到/view/分支,拼接阅读页路由跳转

  • 未安装AppWebView

  • 原地跳转https://``www.xxx.com``/view/*

  • 命中服务器的重定向逻辑

  • 重定向到https://``www.xxx.com``/view/*

  • 打开相应的H5页面

5. 附:打开App过渡页.html示例源码

<!DOCTYPE html"><html><head>    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />    <meta name="viewport" content="width=device-width,minimum-scale=1.0">    <body bgcolor = "#FFEEDD">    <input type="text" id="acti">        <p>下载页面<a href="javascript:;" onclick="open_iOS_App()">ios 点击链接</a></p>     </body> <a href="" id="aaa"></a> <script type="text/javascript">        var timeout;        function open_iOS_App() {            if (isWeiXin()) {                open_App();            }else{                open_App();                timeout = setTimeout('open_itunes()', 3000);            };        }        function open_Android_App() {            if (isWeiXin()) {                open_android_weixin();            }else{                open_App_Android();            };        }                function open_android_weixin() {           var acti = document.getElementById("acti").value;           var linkUrl = "xxxxxxxxxxxxx://utils?action=sendIntent¶ms=" + acti;           var yingyongbaoUrl = "http://a.app.qq.com/o/simple?pkgname=com.xxxx.xxxxx&android_schema="             + encodeURI(linkUrl);           window.location = yingyongbaoUrl;         }        function open_App() {                     var ver = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);                       ver = parseInt(ver[1], 10);                       if(ver<9)                       {                               if (isWeiXin()) {                                open_weixin_App();                             }else{                                open_itunes();                              }                      } else{                               var activity = document.getElementById("acti").value;                                 document.getElementById("aaa").href = "https://joeychang.me/s.html?params="+ encodeURI(activity);                               //  alert(document.getElementById("aaa").href);                               // alert(activity);                               document.getElementById("aaa").click();                      }         }        function open_App_Android() {            var acti = document.getElementById("acti").value;            window.location = "intent://xxxxxxxxx?params="+ acti +"#Intent;package=com.xxxx.xxxxx;scheme=xxxxxxx;launchFlags=268435456;end;";            }        function open_itunes() {/* 打开app store */            window.location="http://itunes.apple.com/cn/app/idxxxxxxxx";         }                function open_weixin_App() {/* 打开腾讯应用宝 间接跳转 */                var acti = document.getElementById("acti").value;            window.location="http://a.app.qq.com/o/simple.jsp?pkgname=com.xxxx.xxxxx&activity=" + acti;         }        /*            判断是否是微信浏览器        */        function isWeiXin(){            var ua = window.navigator.userAgent.toLowerCase();            if(ua.match(/MicroMessenger/i) == 'micromessenger'){                return true;            }else{                return false;            }        }        function linktoApp() {                        var queryStr = decodeURI(window.location.search.substr(1));                        var ua = navigator.userAgent.toLowerCase();                         if (queryStr.indexOf("}") >= 0 && queryStr.indexOf("{") >= 0) {                              var activity = queryStr.substring(queryStr.indexOf("{"), queryStr.lastIndexOf("}") + 1);                              document.getElementById("acti").value = activity;                              if (/iphone|ipad|ipod/.test(ua)) {                         // IOS                         } else if (/android/.test(ua)) {                        // Android                                open_Android_App();                         } else {                                // 其他浏览器                                window.location="http://a.app.qq.com/o/simple.jsp?pkgname=com.xxxxxxx.xxxxxxxx";                             }                         } else {                              document.getElementById("acti").value = "参数格式错误";                          }                }                // 用js实现在加载完成一个页面后自动执行一个方法        /*用window.onload调用myfun()*/        window.onload=linktoApp;//不要括号    </script></head><body></body></html>