如何限制iOS Universal Links跳轉(zhuǎn)

如何限制iOS Universal Links跳轉(zhuǎn)

有時(shí)由于產(chǎn)品需求,我們需要使用一個(gè)WKWebView來(lái)呈現(xiàn)第三方平臺(tái)的內(nèi)容。當(dāng)?shù)谌狡脚_(tái)擁有自己的App時(shí),通常都會(huì)在網(wǎng)頁(yè)端引導(dǎo)用戶跳轉(zhuǎn)到第三方App。iOS Universal Links是實(shí)現(xiàn)這種跳轉(zhuǎn)的一種常見(jiàn)方式,已經(jīng)有很多文章討論如何實(shí)現(xiàn)Universal Links,本文則是反其道而行之,討論如何禁用Universal Links觸發(fā)的App跳轉(zhuǎn)。

Universal Links原理

要想禁用Universal Links,首先就需要了解它的原理。本文只是簡(jiǎn)要介紹一下它的基本原理,詳情可以參考下列文檔:

官方文檔:Support Universal Links

Raywenderlich:Universal Links – Make the Connection

官方對(duì)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的實(shí)現(xiàn)

要實(shí)現(xiàn)Universal Links,需要對(duì)網(wǎng)頁(yè)的服務(wù)器端和iOS App端同時(shí)做一些配置:

對(duì)于服務(wù)器:

  • 創(chuàng)建一個(gè)名為apple-app-site-association的JSON文件,用于描述App可以將哪些URL當(dāng)做Universal Link來(lái)處理
  • 將這個(gè)apple-app-site-association文件上傳到HTTPS web服務(wù)器。該文件可以被放置在服務(wù)器的根目錄,或者.well-know子目錄。例如,Bilibili的配置文件就是放置在根目錄,http://www.bilibili.com/apple-app-site-association。有興趣的可以打開(kāi)這個(gè)鏈接,其實(shí)就是一個(gè)JSON文件,具體含義可以參考官方文檔Support Universal Links

對(duì)于App:

  • 創(chuàng)建一個(gè)名為com.apple.developer.associated-domains的entitlement,包含App支持的Universal Link的domain。注意,不同的子域名都會(huì)被當(dāng)做不同的domain,比如www.bilibili.comm.bilibili.com就是兩個(gè)domain。
  • AppDelegate.application:continueUserActivity:restorationHandler:中響應(yīng)WebView傳入的Universal Links。

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

  • 前者配置在App端,后者配置在網(wǎng)頁(yè)端。
  • 前者針對(duì)domain,后者針對(duì)domain下的URL,指明某個(gè)domain下哪些URL可以被當(dāng)做Universal Links。

Universal Links的工作流程

上面一節(jié)介紹的是如何配置服務(wù)器端和App端以便支持Universal Links,那么當(dāng)這一切都部署好之后,用戶在WebView/Safari上點(diǎn)擊了一個(gè)Universal Link后,本地App是如何被打開(kāi)的?

  • App A安裝成功后,iOS會(huì)根據(jù)其com.apple.developer.associated-domains中列出的domain,下載對(duì)應(yīng)的apple-app-site-association文件。
  • 用戶在App B的WebView中點(diǎn)擊一個(gè)URL后,該WebView的webView(:, decidePolicyFor:, decisionHandler:)被觸發(fā)(如果存在),決定是否允許訪問(wèn)該網(wǎng)址。
  • 如果上一步允許訪問(wèn),則系統(tǒng)會(huì)結(jié)合com.apple.developer.associated-domainsapple-app-site-association判斷該URL是否為Universal links,若不是,則直接在網(wǎng)頁(yè)中打開(kāi)。若是,則做出下列判斷:
    • 若手動(dòng)關(guān)閉了Universal Links跳轉(zhuǎn)(見(jiàn)下一節(jié)說(shuō)明),直接在網(wǎng)頁(yè)中打開(kāi)新網(wǎng)址。
    • 若不是用戶手動(dòng)點(diǎn)擊的操作,直接在網(wǎng)頁(yè)中打開(kāi)新網(wǎng)址。
    • 若新舊網(wǎng)址屬于同一域名,直接在網(wǎng)頁(yè)中打開(kāi)新網(wǎng)址。例如,www.bilibili.comm.bilibili.com屬于不同域名,在它們之間切換會(huì)觸發(fā)App跳轉(zhuǎn),但在m.bilibili.com的不同網(wǎng)址間切換并不會(huì)觸發(fā)。
    • 否則,打開(kāi)App A,并調(diào)用它的AppDelegate.application:continueUserActivity:restorationHandler:

Universal Links的坑

突破微信跳轉(zhuǎn)限制-Universal Links那些坑 總結(jié)了Universal Links失效的一些情況:

  • Universal Links will not work if you paste the link into the browser URL field.
  • Universal Links work with a user driven <a href="..."> element click across domains. Example: if there is a Universal Link on google.com pointing to bnc.lt, it will open the app.
  • Universal Links will not work with a user driven <a href="..."> element click on the same domain. Example: if there is a Universal Link on google.com pointing to a different Universal Link on google.com, it will not open the app.
  • Universal Links cannot be triggered via Javascript (in window.onload or via a .click() call on an <a> element), unless it is part of a user action.

除了上述情況外,若用戶通過(guò)Universal Links跳轉(zhuǎn)到App后,又點(diǎn)擊了屏幕右上角的URL(如下圖),iOS會(huì)在網(wǎng)頁(yè)端再次打開(kāi)這個(gè)鏈接。此外,系統(tǒng)還會(huì)認(rèn)為用戶偏向于在網(wǎng)頁(yè)端查看URL,因此用戶再次點(diǎn)擊超鏈時(shí),系統(tǒng)不會(huì)再跳轉(zhuǎn)到App,相當(dāng)于用戶手動(dòng)關(guān)閉了Universal Links。如果想再次啟動(dòng)Universal Links,用戶需要在網(wǎng)頁(yè)端手動(dòng)點(diǎn)擊屏幕右上方的“打開(kāi)

禁用Universal Links


禁用Universal Links

禁用Universal Links


啟用Universal Links

限制Universal Links

上面介紹了Universal Links的基本原理,根據(jù)這些原理,我們有兩個(gè)禁用Universal Links的思路。

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

上面講到,當(dāng)Universal Links被點(diǎn)擊時(shí),我們App的webView(: decidePolicyFor: decisionHandler:)會(huì)首先被觸發(fā),用來(lái)決定是否允許對(duì)該URL的訪問(wèn),如果我們事先知道哪些URL屬于Universal Links,就可以在這個(gè)地方將它們禁掉。

使用這個(gè)方法的好處在于簡(jiǎn)單,如果我們的App只會(huì)訪問(wèn)有限數(shù)量的第三方網(wǎng)站,那么只需要找到每個(gè)網(wǎng)站Universal Links的格式即可。但若我們App可能打開(kāi)任意的網(wǎng)站,那就不能用這個(gè)辦法了。

那么如何找到Univeral Links的網(wǎng)址格式?簡(jiǎn)單的方法就是在webView(: decidePolicyFor: decisionHandler:)中設(shè)置斷點(diǎn),然后點(diǎn)擊某個(gè)會(huì)導(dǎo)致App跳轉(zhuǎn)的鏈接即可。正常的網(wǎng)站通常會(huì)用一個(gè)非常有別于它們主域名的網(wǎng)址來(lái)作為會(huì)導(dǎo)致App跳轉(zhuǎn)的網(wǎng)址,比如搜狐的跳轉(zhuǎn)網(wǎng)址就是形如http://s1.h5.itc.cn/app/phone.html?xxxxx,因此這個(gè)方法基本夠用。

如果想找到最完備的Universal Links列表,可以采用下面的方式,此處以搜狐視頻為例:

  1. 在Mac上使用iTunes下載搜狐視頻。iTunes通常把App保存在/Users/XXX/Music/iTunes/iTunes Media/Mobile Applications目錄下,(把XXX替換為你的用戶名)
  2. 使用解壓工具解壓搜狐視頻的ipa包,生成一個(gè)文件夾,點(diǎn)擊其中的Payload >> SOHUVideo >> 右擊 >> 選擇“Show Package Contents” >> archived-expanded-entitlements.xcent >> 使用文本工具打開(kāi),就可以看到下列內(nèi)容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>development</string>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:m.tv.sohu.com</string>
        <string>applinks:wx.m.tv.sohu.com</string>
        <string>applinks:t.mtv.sohu.com</string>
        <string>applinks:s1.h5.itc.cn</string>
        <string>applinks:tv.sohu.com</string>
    </array>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.com.sohu.SohuVideo</string>
    </array>
</dict>
</plist>

可以看到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都是主站的域名,我們顯然也不可能禁用主站的域名,否則會(huì)導(dǎo)致網(wǎng)頁(yè)無(wú)法訪問(wèn)的情況。有人可能有點(diǎn)不放心,覺(jué)得好多域名沒(méi)有被禁用,是否可能出現(xiàn)App跳轉(zhuǎn)的情況?萬(wàn)一出現(xiàn)從m.tv.sohu.com跳wx.m.tv.sohu.com的情況怎么辦?一般來(lái)說(shuō),對(duì)于一個(gè)正常的網(wǎng)站,比如搜狐視頻,在上線Universal Links時(shí),肯定會(huì)避免跨域名跳轉(zhuǎn)的情況,否則若用戶安裝了搜狐視頻的App,然后使用Safari在瀏覽搜狐的網(wǎng)頁(yè),點(diǎn)著點(diǎn)著,突然毫無(wú)征兆的跳到了App,這絕對(duì)是很差的一個(gè)用戶體驗(yàn)。

當(dāng)然,有些網(wǎng)站就是喜歡不走平常路,所有的Universal links的domain都是主域名,而且還有跨域名跳轉(zhuǎn)的情況,這時(shí)候你可能只能使用下面的思路2了。

對(duì)于上面的domain,我們可以通過(guò)拼接的方式找到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:無(wú)法訪問(wèn)

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

https://tv.sohu.com/apple-app-site-association:跳轉(zhuǎn)到首頁(yè)

{
"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:用戶點(diǎn)擊產(chǎn)生的URL變化才能觸發(fā)App跳轉(zhuǎn)

Universal Links觸發(fā)App跳轉(zhuǎn)的一個(gè)很重要的前提是,這個(gè)URL的變化一定是用戶點(diǎn)擊造成的!直接粘貼或者通過(guò)JavaScript方式修改的Universal Links都是無(wú)效的。因此我們可以考慮向WebView注入JavaScript,監(jiān)聽(tīng)用戶URL的點(diǎn)擊事件,當(dāng)點(diǎn)擊發(fā)生時(shí),我們截?cái)嗍录膫鞑?,并使用定時(shí)器延時(shí)修改WebView的URL,讓系統(tǒng)誤以為這是一個(gè)純JavaScript調(diào)用,與用戶點(diǎn)擊無(wú)關(guān)。

這個(gè)思路的好處在于可以做出比較通用的,適合所有站點(diǎn)的方案。但弊端在于,首先,延時(shí)更新URL會(huì)導(dǎo)致用戶體驗(yàn)上的卡頓;其次,這個(gè)思路要求在用戶點(diǎn)擊的那一刻,我們必須獲取真實(shí)的目的URL,如果<a>的格式為<a href='javascript:;'>,那我們就沒(méi)辦法了,因?yàn)檎鎸?shí)的URL是通過(guò)一段JavaScript動(dòng)態(tài)計(jì)算出來(lái)的,點(diǎn)擊時(shí)拿不到。

這里以Bilibili為例。首先,參考思路1中獲取Universal Links domain的方法,拿到它的archived-expanded-entitlements.xcent。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>development</string>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:bangumi.bilibili.com</string>
        <string>applinks:live.bilibili.com</string>
        <string>applinks:www.bilibili.com</string>
        <string>applinks:m.bilibili.com</string>
        <string>applinks:space.bilibili.com</string>
        <string>applinks:d.bilibili.com</string>
    </array>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.tv.danmaku.bilianime</string>
    </array>
    <key>keychain-access-groups</key>
    <array>
        <string>746845GC96.tv.danmaku.bilianime</string>
    </array>
</dict>
</plist>

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

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

全是主域名。。。按下面的操作步驟,也確實(shí)出現(xiàn)了在Safari中點(diǎn)著點(diǎn)著就跳到Bilibili的App的情況。

iOS Safari中打開(kāi)m.bilibili.com >> 點(diǎn)擊上方Tab的“番劇” >> 點(diǎn)擊某個(gè)“連載動(dòng)畫(huà)” >> 跳轉(zhuǎn)到Bilibili App。

之所以會(huì)這樣,是因?yàn)锽站一般的域名都是m.bilibili.com,但番劇的某些域名是bangumi.bilibili.com。對(duì)于這個(gè)站點(diǎn),顯然是無(wú)法使用思路1中的方法了。

按照思路2,我們先創(chuàng)建下面的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 <a> 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);
    };
})();

上面的代碼有以下重點(diǎn):

  • addEventListener必須添加到body上面,才能確保監(jiān)聽(tīng)到所有<a>element的點(diǎn)擊事件。
  • eventHandler()用于找到有效的<a>.href,并將值傳遞給updateHref()
  • updateHref()首先截?cái)帱c(diǎn)擊事件的傳遞,然后使用假URL和timer讓系統(tǒng)誤以為這次URL變化是純粹的javascript調(diào)用。

JavaScript的代碼到此為止,接下編寫(xiě)App代碼。代碼很簡(jiǎn)單,創(chuàng)建一個(gè)WKWebView,并在. atDocumentEnd時(shí)加載上面的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")!))
    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容