如何限制iOS Universal Links跳转

如何限制iOS Universal Links跳转

有时由于产品需求,我们需要使用一个WKWebView来呈现第三方平台的内容。当第三方平台拥有自己的App时,通常都会在网页端引导用户跳转到第三方App。iOS Universal Links是实现这种跳转的一种常见方式,已经有很多文章讨论如何实现Universal Links,本文则是反其道而行之,讨论如何禁用Universal Links触发的App跳转。

Universal Links原理

要想禁用Universal Links,首先就需要了解它的原理。本文只是简要介绍一下它的基本原理,详情可以参考下列文档:

官方文档:Support Universal Links

Raywenderlich:Universal Links – Make the Connection

官方对Universal Links的描述如下:

When you support universal links, iOS users can tap a link to your website and get seamlessly redirected to your installed app without going through Safari. If your app isn’t installed, tapping a link to your website opens your website in Safari.

Universal links give you several key benefits that you don’t get when you use custom URL schemes. Specifically, universal links are:

  • Unique. Unlike custom URL schemes, universal links can’t be claimed by other apps, because they use standard HTTP or HTTPS links to your website.
  • Secure. When users install your app, iOS checks a file that you’ve uploaded to your web server to make sure that your website allows your app to open URLs on its behalf. Only you can create and upload this file, so the association of your website with your app is secure.
  • Flexible. Universal links work even when your app is not installed. When your app isn’t installed, tapping a link to your website opens the content in Safari, as users expect.
  • Simple. One URL works for both your website and your app.
  • Private. Other apps can communicate with your app without needing to know whether your app is installed.

Universal Links的实现

要实现Universal Links,需要对网页的服务器端和iOS App端同时做一些配置:

对于服务器:

  • 创建一个名为apple-app-site-association的JSON文件,用于描述App可以将哪些URL当做Universal Link来处理
  • 将这个apple-app-site-association文件上传到HTTPS web服务器。该文件可以被放置在服务器的根目录,或者.well-know子目录。例如,Bilibili的配置文件就是放置在根目录,http://www.bilibili.com/apple-app-site-association。有兴趣的可以打开这个链接,其实就是一个JSON文件,具体含义可以参考官方文档Support Universal Links。

对于App:

  • 创建一个名为com.apple.developer.associated-domains的entitlement,包含App支持的Universal Link的domain。注意,不同的子域名都会被当做不同的domain,比如www.bilibili.com和m.bilibili.com就是两个domain。
  • AppDelegate.application:continueUserActivity:restorationHandler:中响应WebView传入的Universal Links。

com.apple.developer.associated-domains v.s. apple-app-site-association

  • 前者配置在App端,后者配置在网页端。
  • 前者针对domain,后者针对domain下的URL,指明某个domain下哪些URL可以被当做Universal Links。

Universal Links的工作流程

上面一节介绍的是如何配置服务器端和App端以便支持Universal Links,那么当这一切都部署好之后,用户在WebView/Safari上点击了一个Universal Link后,本地App是如何被打开的?

  • App A安装成功后,iOS会根据其com.apple.developer.associated-domains中列出的domain,下载对应的apple-app-site-association文件。
  • 用户在App B的WebView中点击一个URL后,该WebView的webView(:, decidePolicyFor:, decisionHandler:)被触发(如果存在),决定是否允许访问该网址。
  • 如果上一步允许访问,则系统会结合com.apple.developer.associated-domainsapple-app-site-association判断该URL是否为Universal links,若不是,则直接在网页中打开。若是,则做出下列判断:
    • 若手动关闭了Universal Links跳转(见下一节说明),直接在网页中打开新网址。
    • 若不是用户手动点击的操作,直接在网页中打开新网址。
    • 若新旧网址属于同一域名,直接在网页中打开新网址。例如,www.bilibili.com和m.bilibili.com属于不同域名,在它们之间切换会触发App跳转,但在m.bilibili.com的不同网址间切换并不会触发。
    • 否则,打开App A,并调用它的AppDelegate.application:continueUserActivity:restorationHandler:

Universal Links的坑

突破微信跳转限制-Universal Links那些坑 总结了Universal Links失效的一些情况:

除了上述情况外,若用户通过Universal Links跳转到App后,又点击了屏幕右上角的URL(如下图),iOS会在网页端再次打开这个链接。此外,系统还会认为用户偏向于在网页端查看URL,因此用户再次点击超链时,系统不会再跳转到App,相当于用户手动关闭了Universal Links。如果想再次启动Universal Links,用户需要在网页端手动点击屏幕右上方的“打开

禁用Universal Links


禁用Universal Links

禁用Universal Links


如何限制iOS Universal Links跳转_第2张图片
启用Universal Links

限制Universal Links

上面介绍了Universal Links的基本原理,根据这些原理,我们有两个禁用Universal Links的思路。

思路1:webView(: decidePolicyFor: decisionHandler:)

上面讲到,当Universal Links被点击时,我们App的webView(: decidePolicyFor: decisionHandler:)会首先被触发,用来决定是否允许对该URL的访问,如果我们事先知道哪些URL属于Universal Links,就可以在这个地方将它们禁掉。

使用这个方法的好处在于简单,如果我们的App只会访问有限数量的第三方网站,那么只需要找到每个网站Universal Links的格式即可。但若我们App可能打开任意的网站,那就不能用这个办法了。

那么如何找到Univeral Links的网址格式?简单的方法就是在webView(: decidePolicyFor: decisionHandler:)中设置断点,然后点击某个会导致App跳转的链接即可。正常的网站通常会用一个非常有别于它们主域名的网址来作为会导致App跳转的网址,比如搜狐的跳转网址就是形如http://s1.h5.itc.cn/app/phone.html?xxxxx,因此这个方法基本够用。

如果想找到最完备的Universal Links列表,可以采用下面的方式,此处以搜狐视频为例:

  1. 在Mac上使用iTunes下载搜狐视频。iTunes通常把App保存在/Users/XXX/Music/iTunes/iTunes Media/Mobile Applications目录下,(把XXX替换为你的用户名)
  2. 使用解压工具解压搜狐视频的ipa包,生成一个文件夹,点击其中的Payload >> SOHUVideo >> 右击 >> 选择“Show Package Contents” >> archived-expanded-entitlements.xcent >> 使用文本工具打开,就可以看到下列内容:




    aps-environment
    development
    com.apple.developer.associated-domains
    
        applinks:m.tv.sohu.com
        applinks:wx.m.tv.sohu.com
        applinks:t.mtv.sohu.com
        applinks:s1.h5.itc.cn
        applinks:tv.sohu.com
    
    com.apple.security.application-groups
    
        group.com.sohu.SohuVideo
    


可以看到com.apple.developer.associated-domains包含了以下domain:

  • m.tv.sohu.com
  • wx.m.tv.sohu.com
  • t.mtv.sohu.com
  • s1.h5.itc.cn
  • tv.sohu.com

除了s1.h5.itc.cn外,其他domain都是主站的域名,我们显然也不可能禁用主站的域名,否则会导致网页无法访问的情况。有人可能有点不放心,觉得好多域名没有被禁用,是否可能出现App跳转的情况?万一出现从m.tv.sohu.com跳wx.m.tv.sohu.com的情况怎么办?一般来说,对于一个正常的网站,比如搜狐视频,在上线Universal Links时,肯定会避免跨域名跳转的情况,否则若用户安装了搜狐视频的App,然后使用Safari在浏览搜狐的网页,点着点着,突然毫无征兆的跳到了App,这绝对是很差的一个用户体验。

当然,有些网站就是喜欢不走平常路,所有的Universal links的domain都是主域名,而且还有跨域名跳转的情况,这时候你可能只能使用下面的思路2了。

对于上面的domain,我们可以通过拼接的方式找到apple-app-site-association的下载地址,有兴趣的可以试试。

https://m.tv.sohu.com/apple-app-site-association

https://wx.m.tv.sohu.com/apple-app-site-association

https://t.mtv.sohu.com/apple-app-site-association:无法访问

https://s1.h5.itc.cn/apple-app-site-association

https://tv.sohu.com/apple-app-site-association:跳转到首页

{
"applinks":{
        "apps":[],
        "details":[
             {
                "appID":"X3XWZ5HCGK.com.sohu.iPhoneVideo",
                "paths":[
                    "/app/*"
                ]
            },
            {
                "appID":"VB2VQ6GKB2.com.sohu.inhouse.iphonevideo",
                "paths":[
                    "/app/*"
                ]
            },
            {
                "appID":"4AW78593E8.com.sohu.mobile.iPhoneVideo",
                "paths":[
                    "/app/*"
                ]
            
           },
           {
                "appID":"89DSCLLV97.com.sohu.SohuVideo",
                "paths":[
                    "/app/*"
                ]
            },
            {
                "appID":"VB2VQ6GKB2.com.sohu.inhouse.sohuvideoipad",
                "paths":[
                    "/app/*"
                ]
            },
            {
                "appID":"4AW78593E8.com.sohu.mobile.SohuVideo",
                "paths":[
                    "/app/*"
                ]
            }
        ]
    }
}

思路2:用户点击产生的URL变化才能触发App跳转

Universal Links触发App跳转的一个很重要的前提是,这个URL的变化一定是用户点击造成的!直接粘贴或者通过JavaScript方式修改的Universal Links都是无效的。因此我们可以考虑向WebView注入JavaScript,监听用户URL的点击事件,当点击发生时,我们截断事件的传播,并使用定时器延时修改WebView的URL,让系统误以为这是一个纯JavaScript调用,与用户点击无关。

这个思路的好处在于可以做出比较通用的,适合所有站点的方案。但弊端在于,首先,延时更新URL会导致用户体验上的卡顿;其次,这个思路要求在用户点击的那一刻,我们必须获取真实的目的URL,如果的格式为,那我们就没办法了,因为真实的URL是通过一段JavaScript动态计算出来的,点击时拿不到。

这里以Bilibili为例。首先,参考思路1中获取Universal Links domain的方法,拿到它的archived-expanded-entitlements.xcent。





    aps-environment
    development
    com.apple.developer.associated-domains
    
        applinks:bangumi.bilibili.com
        applinks:live.bilibili.com
        applinks:www.bilibili.com
        applinks:m.bilibili.com
        applinks:space.bilibili.com
        applinks:d.bilibili.com
    
    com.apple.security.application-groups
    
        group.tv.danmaku.bilianime
    
    keychain-access-groups
    
        746845GC96.tv.danmaku.bilianime
    


com.apple.developer.associated-domains包括以下domains:

  • bangumi.bilibili.com
  • live.bilibili.com
  • www.bilibili.com
  • m.bilibili.com
  • space.bilibili.com
  • d.bilibili.com

全是主域名。。。按下面的操作步骤,也确实出现了在Safari中点着点着就跳到Bilibili的App的情况。

iOS Safari中打开m.bilibili.com >> 点击上方Tab的“番剧” >> 点击某个“连载动画” >> 跳转到Bilibili App。

之所以会这样,是因为B站一般的域名都是m.bilibili.com,但番剧的某些域名是bangumi.bilibili.com。对于这个站点,显然是无法使用思路1中的方法了。

按照思路2,我们先创建下面的JavaScript代码:

(function() {
    var url = window.location.host;
    if (url.search(".bilibili") >= 0) {
        function updateHref(href, event) {
            event.preventDefault();
            event.stopImmediatePropagation();
            event.stopPropagation();

            // use timer and fakeURL to fool the system
            var fakeURL = 'javascript:;';
            setTimeout(function () {
                window.location.href = fakeURL;
                setTimeout(function () {
                    window.location.href = href;
                }, 20);
            }, 20);
        };

        function eventHandler(event) {
            var element = event.target;
            while (element) {
                if (element.tagName == 'A') {
                    break;
                };
                element = element.parentElement;
            };
            if (!element || element.tagName != 'A') {
                return;
            };

            if (element.href == undefined || element.href.length == 0) {
                return;
            };
            if (element.href.search('javascript') == 0) {
                return;
            };
            var hrefAttr = element.href;
            if (!hrefAttr.includes('bilibili')) {
              return;
            };

            updateHref(hrefAttr, event);
        };

        // Some  elements are added after this JavaScript is injected, so add event to body to make sure all element events could be handled.  
        document.body.addEventListener('click', eventHandler, true);
    };
})();

上面的代码有以下重点:

JavaScript的代码到此为止,接下编写App代码。代码很简单,创建一个WKWebView,并在. atDocumentEnd时加载上面的JavaScript代码即可。

class ViewController: UIViewController {

    lazy var webView: WKWebView = {
        let webView = WKWebView(frame: .zero)

        let js = try! String(contentsOfFile: Bundle.main.path(forResource: "appJump", ofType: "js")!)
        let appJumpScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        webView.configuration.userContentController.addUserScript(appJumpScript)

        return webView
    }()

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        webView.frame = view.bounds
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(webView)

        webView.load(URLRequest(url: URL(string: "http://m.bilibili.com")!))
    }

}

你可能感兴趣的:(如何限制iOS Universal Links跳转)