1. Universal link 介绍

1.1 Universal link 是什么

Universal Link 是苹果在 WWDC 上提出的 iOS9 的新特性之一。此特性类似于深层链接,并能够方便地经过翻开一个 Https 链接来直接发动您的客户端运用(手机有装置 App)。对比起以往所运用的 URLScheme,这种新特性在完成 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 就能够了。

iOS Universal link

2.2 装备 iOS App 工程

Xcode 11.0 版别

工程装备中相应功用:targets->Signing&Capabilites->Capability->Associated Domains,在其间的 Domains 中填入你想支持的域名,也有必要有必要以 applinks:为前缀。

详细步骤如下图:

iOS Universal link

iOS Universal link

iOS Universal link

Xcode 11.0 以下版别

工程装备中相应功用:targets->Capabilites->Associated Domains,在其间的 Domains 中填入你想支持的域名,有必要以 applinks:为前缀。

装备项目中的 Associated Domains:

iOS Universal link

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"https://juejin.im/post/7233626231579689019/: {        "apps"https://juejin.im/post/7233626231579689019/: [],        "details"https://juejin.im/post/7233626231579689019/: [            {                "appID"https://juejin.im/post/7233626231579689019/: "9JA89QQLNQ.com.apple.wwdc"https://juejin.im/post/7233626231579689019/,                "paths"https://juejin.im/post/7233626231579689019/: [ "/wwdc/news/"https://juejin.im/post/7233626231579689019/, "/videos/wwdc/2015/*"https://juejin.im/post/7233626231579689019/]            },            {                "appID"https://juejin.im/post/7233626231579689019/: "ABCD1234.com.apple.wwdc"https://juejin.im/post/7233626231579689019/,                "paths"https://juejin.im/post/7233626231579689019/: [ "*" ]            }        ]    }}

仿制代码

阐明:

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”运用中翻开, 呈现菜单:

iOS Universal link

当点击某个链接,直接能够进咱们的 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…

iOS Universal link

微信里其实是屏蔽 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="https://juejin.im/post/7233626231579689019/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="https://juejin.im/post/7233626231579689019/javascript:;" onclick="https://juejin.im/post/7233626231579689019/open_iOS_App()">ios 点击链接</a></p>     </body>
 <a href="https://juejin.im/post/7233626231579689019/" 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 = "https://juejin.im/post/7233626231579689019/xxxxxxxxxxxxx://utils?action=sendIntentms=" + 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>