從零收拾一個hybrid框架(二)-- WebView容器基礎功能設計思路

從零收拾一個hybrid容器(一)-- 從選擇JS通訊方案開始javascript

上一篇文章介紹了若是本身親自開發一個hybrid容器,應該怎麼去選擇JS通訊方案,而且重點強調了如今市面上流傳最廣的WebViewJavascriptBridge 他存在的致命問題,以及不該該由於所謂的看似寫法統一而在api選型上作功能妥協的思想前端

前言

這篇文章拖到年後纔開始寫是有點罪過罪過,不過在寫以前我想說幾點,這系列文章我想表達的並非在推廣什麼我本身的新Bridge輪子,也不是針對某個開源 Bridge 框架進行深度的源碼分析。咱們從看開源框架輪子如何設計,如何使用,源碼如何工做的思惟方式中跳出來,換一種模式去從目的出發,從需求出發,思考當你什麼都沒有的時候,你要從零思考構建一個 hybrid 框架的時候,你都要考慮哪些方面?這些方面採用怎樣的設計思想能作到將來在使用中靈活自如。java

單純去作一個純hybrid通訊框架,網上有的各種 bridge 輪子,可是通訊只是 hybrid 這個話題下的一個底層基礎,上層的玩法還有不少,跳出通訊框架看看如今各類前端+客戶端的混合開發模式下,他們是否有啥共通點,而後汲取裏面的設計思想,而後靈活的在本身業務中發揮出來,這樣你面臨的就歷來不是一個選擇題,我要不要用RN?我要不要用新玩具 Flutter?(這玩意槽點不少也離我這篇文章的主題有點遠,我就提一嘴不打算多說)你面臨的只是一個問答題:新出來個重型大輪子,我能不能看看這個思想借鑑到現有業務裏的?android

Hybrid

狹義Hybridgit

也是如今你們廣泛認知的,Hybrid就是一種給 WebView 增長一些js通訊能夠調用原生API的方式github

廣義Hybridweb

我可否認爲,只要是前端的開發思路與客戶端原生的開發思路相結合,就認爲他是一種 Hybrid?數據庫

我可否認爲,經過原生的配合,把本來js or 前端開發作不到的事情作到了,用原生的方式加強了本來的前端技術能力,是否就是一種 Hybrid?編程

我可否認爲,不管是 WebView+Bridge 也好,RN相似的原生渲染框架也好,小程序也好,某種意義上講,他們都算 Hybrid?json

由於Hybrid本是一個面向業務服務的東西,若是業務的野心足夠大,WebView 容器的想象空間應該是在能力上與RN/小程序看齊的,沒錯,WebView 在 Hybrid 的支持下,不單純是設計幾個 Bridge 調用幾個原生 API 的事,我其實在很多聊天羣裏深度聊過這個話題,泛前端動態化這個方向上,各類技術輪子都是一脈相承通着的,因此你看RN or 小程序是一個大廠新作的重型輪子?在我看來他們都是一回事,徹底能夠拆解RN中的每一個環節,把RN號稱比 WebView 好的原生渲染/原生組件拆解融入 WebView,我也能夠學習小程序保持 Html/CSS/JS 的開發方式(固然我知道小程序是WXML/WXSS),而非RN那樣統一用JSX開發。我甚至還能把RN與小程序都沒有的動態bridge融入到Hybrid容器中去,What's more? 還有更多能夠開放的腦洞。

這種拆解不是說能夠作到把全部框架優勢塞在一個大而全的框架裏就完事的,各類優化方案的選擇背後必定帶來的是一些取捨。誰來決定取捨,業務決定,若是本身能深度把握這裏面的設計思想,就不用在意什麼新的輪子新的框架,取其設計優勢(優勢必定帶來取捨,若是選擇這個有點意味着也要選擇他的取捨),融入本身的業務之中。

WebView容器 - 基本功能

前邊把話題聊得有點大,有點虛,那麼收回來,我後面的內容仍是圍繞着 WebView 來設計咱們想要的 Hybrid 框架,本系列第二篇文章我會傳統點,先只從傳統的對 WebView 容器上的功能需求出發說一些基本的功能的設計方案與思路,可是第三篇就會從一些「黑腦洞」的功能層面,在 WebView 的基礎上擴展出一些很是規的能力。本文就基於 WKWebView 說了,UIWebView 若是說真理解設計思想的話,其實沒區別,同理雖然我全部的解釋說明都是 iOS 的,但對於安卓來講設計思想徹底適用。

  • 選擇合適的 JS 通訊方案(第一篇)

  • 實現基本的 WebView 容器能力(第二篇 本篇)

  • 嘗試拓展 WebView 容器的額外能力(第三篇 待續)

一個標準的WebView容器要具有哪些基礎的功能需求,來知足常規的 hybrid-webview 開發呢?

  • 良好的 JS 與原生通訊交互能力

  • 靈活的業務模塊擴展能力

  • UserAgent 管理

  • Cookies 管理

  • 本地加載 JS 管理

通訊交互設計

<!--more-->

上一篇文章其實介紹了好幾種JS/OC通訊方案,若是涉及相關的代碼,我只會以WKWebView的 messageHandler(異步) + Prompt彈框攔截(同步)evaluatingJavaScript 的方式進行一些展現代碼介紹,但其實用啥方案並沒有區別,思考理解設計思路,而後在任何的通訊方案下遷移運用

我說的只是一種設計思路,並非惟一設計思路

一個好的交互通訊設計應該考慮哪幾個方面?這幾個方面的考慮都是出於什麼目的?

  • JS主動調用原生:

最基礎功能,WebView 各類想要調用原生能力都經過這個設計來通知原生,不管是打開新頁面新路由,仍是彈個 Tips 框,仍是執行 IAP 購買,仍是打開攝像頭等等。

  • JS主動調用原生後回調:

仍是在基礎功能之上,若是 WebView 是想要獲取一些只有原生纔有的數據,好比讀原生數據庫,查看原生設備網絡/磁盤等硬件情況,須要在上面的功能下還額外回調給 WebView

  • 原生主動調用JS:

有什麼業務場景須要原生主動調用JS呢?舉個例子H5開發的時候特別想知道不少事件與時機,好比在H5界面下用戶home/鎖屏了,用戶回到 APP 了,H5都想捕獲這個時機用來開發業務需求,好比App開發的 viewWillDisappear /viewDidAppear 等前端開發也想得到當前頁面進入屏幕,離開屏幕等事件需求,從而執行對應的業務邏輯。

和主動調用後回調相比,直接主動調用JS,在底層執行 API 的時候確定同樣都是 evaluatingJavaScript 但畢竟表明着2種功能形式與場景,所以在設計思想上也會帶來必定差別。

  • 原生主動調用JS後回調:

在原生的事件發起後若是不只僅想要通知JS,而且還但願從JS獲取數據,那麼就須要直接封裝好回調(總好過,先經過原生主動調用JS,在經過JS主動調用回原生傳遞數據,這樣方便的多)

  • 同步通訊 

在上一篇文章中我提出過同步返回對於JS的意義,一樣的代碼若是能用 = 直接在JS中同步+順序的方式處理數據,則開發複雜度會遠低於異步callback式的設計(哪怕異步callback能夠被封裝成promise,但其方便程度,也遠遠無法和 = 同步返回對比)  

//同步JS調用Native  JS這邊能夠直接寫 =  !!!
var nativeNetStatus = nativeObject.getNetStatus();
//異步JS調用Native JS只能這麼寫
nativeObject.getNetSatus(callback(net){
    console.log(net)
}) 複製代碼
  • 通訊編碼:

JS與OC,在運行環境上畢竟是差別很大的2個環境,相互之間進行通訊,必定是按着必定的通訊協議來進行的,在協議的處理過程當中必定伴隨着通訊編碼,在一個 WebView 容器體系下,通訊編碼也須要注意不少細節,從而保證數據傳輸過程當中的健壯性。

上面介紹的四塊後面會有針對性的伴隨着代碼示例進行詳解

接下來也會介紹一些有必定的設計價值,但不必定要必備,可選的兩個功能,我就不深刻詳解了

  • 鑑權設計(可選):

App 內打開 WebView 的時候,這麼多原生能力都提供給了網頁端,但網頁端是否都是可信而且安全的呢,雖然鑑權設計視業務需求而定,能夠作的相對簡單,也能夠作的複雜一些,是可選的。

簡單的作那麼就是廣告類的頁面一概用常規 WebView 打開,只有本身業務所在域名的網頁,纔會用帶有 Hybrid 能力的WebView進行打開,而且全部 Bridge 通訊在執行前先檢查判斷所在域名是否合法,只有本身業務域名下的網頁,才容許bridge通訊,其餘的一概拒絕。

想要作複雜點?其實微信服務號就是一個典型的例子,全部微信服務號JSSDK提供的能力,其實都是微信 WebView 的 Hybrid 能力,但你若是想調用你必須配置 AppId 與 AppSecret ,而後在 JSSDK 內部與微信原生內部進行權限認證功能的開發,這我就不深刻展開了,通常業務也用不到,只是這個環節我擴展提一下,若是想作複雜也是有必要而且有業務場景的。

  • 批量發送(可選):

JS與OC通訊,必然面臨着上下文通訊開銷,開銷試通訊頻繁次數而定,大部分的傳統 Hybrid WebView 設計,不太須要考慮這一點,所以無需專門設計批量發送,但若是特殊的業務需求致使必須頻繁通訊,那麼批量發送經過加大數據吞吐量減小通訊次數,從而減少上下文通訊開銷,這一個環節也是要考慮,可是可選的。

就像我說的通常來講用不上,常規 Hybrid WebView 開發裏那通訊調用頻率,一點壓力都沒有,但何時會用上呢?

若是還記得我上篇文章說過的 JS與OC 假跳轉方式的通訊會吃消息?那麼若是你真的在處理 UIWebView 的時候堅持想使用假跳轉,那麼吃消息這個事情怎麼解決呢?沒錯,隊列批量發送,全部JS的通訊在發起調用的時候都暫存進入一個數組隊列,全局起 timer 心跳,以不會吃消息的頻次大約300ms一次,檢測消息隊列是否含有內容,而後經過合併消息,一次通訊發送給客戶端。

RN也是有批量發送功能的,能夠關注一下 RN 的 RCTBatchBridge 這個類,RN爲何須要批量消息隊列呢?RN並非假跳轉的方式通訊,而是最穩健功能強大的 JSContext,根本不會發生丟消息的問題,但RN依然選擇了批量通訊發送,是由於RN不一樣於WebView,RN 的渲染層依賴 JS 告知原生進行組件貼圖,那麼多界面元素,每次Dom變化,均可能帶來的複雜高頻次渲染消息的發出,所以 RN 總體設計了批量發送功能,思路也是一致,在 JS 這邊每次執行通訊,都暫存在隊列,而後以心跳方式 flush 整個隊列。 WebView的渲染都在 WebKit 內核裏,其實瀏覽器內核也面臨所謂的 Dom 環境與 JS 環境的通訊,但這個已經在內核裏深度優化了,咱們就先無論啦,WebView的渲染涉及的高頻詞通訊,徹底不在咱們的 Hybrid 框架設計的考慮範圍內(前面我說過了一個腦洞,讓WebView也像RN同樣能夠渲染原生貼圖,那麼批量發送就應該在考慮範圍內了)

詳細設計思路 — JS Call OC 無回調

聲明:我會沿着思路一步步給出示例代碼,會隨着思路推翻或者修正前面給出過的示例代碼

JS 發送消息

首先在 JS 側咱們把每個調用原生的消息對象設計一下都須要涵蓋什麼內容?先只考慮 JS Call OC 無回調

var msgBody = {};
msgBody.handler = 'common';
msgBody.action = 'nativeLog';
msgBody.params = params; //任意json對象,用於傳參.複製代碼

非得設計這幾個字段目的是啥? handler 和 action 其實主要是給每一條通訊消息肯定惟一的名字,params用於數據傳參。

有的人說想要惟一識別每條消息,用一個 name 字段,或者乾脆用個消息號數字就行了,反正發到客戶端,客戶端還得一一識別而後不管是註冊式分發執行或是switch式分發執行。沒錯,因此我說了,並非惟一設計思路,均可以靈活根據本身的業務與想法任意調整。

我爲何用2個呢?主要的緣由在於想對大量的通訊消息有一個整理,對於相近類似能夠歸類的消息,先用 handler 來命名消息所在的模塊,在用 action 來命名消息的具體名字,好處是將來在進行模塊化擴展,不管是在 JS 側,仍是在 OC 側,均可以根據模塊名,把大量的消息處理代碼,分割到不一樣模塊的代碼類之中去,仍是本着模塊擴展與管理的想法,來把消息體用 handler 和 action 2個字段來進行描述

sendMessage: function (data) {
    if (this.isIOS) {
        try {
            window.webkit.messageHandlers.WKJSBridge.postMessage(data);
        }
        catch (error) {
            console.log('error native message');
        }
    }
​
    if (this.isAndroid) {
        try {
            prompt(JSON.stringify([data]));
        }
        catch (error) {
            console.log('error native message');
        }
    }
​
},複製代碼

定義完消息體咱們就須要進行通訊了,這個函數其實就是抽象出一層消息發送層,將咱們剛剛建立的消息體,當作 data 傳入 sendMessage 函數,這就是我在第一篇文章中提到的發送層來隔離平臺差別以及通訊 API 差別,不要爲了追求所謂的前端代碼統一來選擇一個有天生缺陷的假跳轉通訊,就算選擇徹底不同的通訊方式,設計這樣一箇中間層,同樣能夠作到前端 JS 代碼的統一

從擴展性的角度來說,若是將來蘋果可能出了更新更好的 API ,安卓也有通訊 API 的調整,直接處理這個中間層就行了,別管是分平臺適配,分安卓/iOS系統版本號適配,甚至同時兼容UIWebView/WKWebView,計算機領域裏沒有什麼問題不是加一箇中間層解決不了的(笑~)

若是之後打算擴展 批量消息,通訊隊列,那麼其實也是同樣的思路,設計一個 sendBatchMessage的Api

通訊編碼

由於本文采用的是 WKWebView 的 messageHandler 方法,在 JS Call OC 的時候會自動處理編碼序列化與解碼反序列化,因此你能夠看到 isIOS 分支沒有任何額外處理,就直接 Call 了,但我們既然講思路,那也得看看須要手動處理通訊編碼的場景。(UIWebView 的 JSContext 通訊同理,能夠傳遞對象,系統自動處理)

android 的 prompt 通訊,prompt() 函數原本就是瀏覽器彈出一個輸入框,輸出一串字符串,因此只接受純字符輸入,那麼編碼方式就簡單了,直接把 data 的 json 對象,用 JS 的 json序列化變成 json 字符串輸出,等客戶端收到攔截後把 json 字符串,反序列化成字典對象,(WKWebView若是也採用彈框攔截,同理)

UIWebView假跳轉方式通訊,由於假跳轉本意是跳轉到一個非法url,天然數據傳遞必須依靠url的參數規則去定製協議去拼接,那麼就得拼接成相似 xxx:xxx/xxx?handler=xx&action=xx&params="json string" 的url,而後在客戶端攔截到url後,按着一樣的規則反解

通訊編碼問題,會根據通訊方案的選取/通訊協議的設計,有着大相徑庭實現與方案,以及各自面臨的坑和問題。帶着一個準則,不管最終採用了什麼樣的數據協議設計,JS 怎麼編碼的,OC 怎麼反向解碼還原,就必定沒問題,必定能解決。(就拿假跳轉來講,當你跳進 encodeURIComponent 與 UrlEncode 的坑裏的時候,一旦數據結構複雜起來,那就有的玩了,稍有不慎,可能編解碼中間就出現歧義了就得踩坑了,不過這種事情不是啥大問題,最終必定能解決就是了)

OC接收消息

須要提早介紹一下我會在OC端設計幾種對象,後面還會在靈活擴展的環節詳細解釋

消息體對象:包含單次消息的全部信息

bridge對象:個人 Hybrid WebView 設計理念是組合,而不是繼承,所以我設計的不是一個 XXWebView / XXWebViewController 基類,使用者不須要在業務代碼中使用 WebView 必須從我這裏繼承。我設計的是一個 NSObject 的 bridge 對象,使用者只須要跟本身業務中用的任意一種 WKWebView 的業務本身的類進行綁定,就能夠擁有 Hybrid 的能力

業務web對象:業務方的 webview 對象 or webviewVC對象,業務方能夠自由寫本身的代碼,規劃本身的基類,不受任何限制,綁上bridge後,能夠調用 bridge api

首先咱們在OC也定義一個消息體對象

@interface msgObject : NSObject
    
@property (nonatomic, copy, readonly) NSString * handler;
@property (nonatomic, copy, readonly) NSString * action;
@property (nonatomic, copy, readonly) NSDictionary * parameters;
​
- (instancetype)initWithDictionary:(NSDictionary *)dict;
​
@end
​複製代碼

而後咱們這裏設計一套 block 註冊式的消息體處理函數管理(放棄if else / switch 式的消息分發吧,哈哈),由於消息體是 handler / action 2層定義,因此 handlerMap 是個二維字典

// 這段代碼屬於 bridge 對象
// self.handlerMap 是 bridge對象的內部字典屬性,保存着全部外部註冊的各類通訊的處理block代碼
​
-(void)registerHandler:(NSString *)handlerName Action:(NSString *)actionName handler:(HandlerBlock)handler{
    if (handlerName && actionName && handler) {
        NSMutableDictionary *handlerDic = [self.handlerMap objectForKey:handlerName];
        if (!handlerDic) {
            handlerDic = [[NSMutableDictionary alloc]init];
        }
        [self.handlerMap setObject:handlerDic forKey:handlerName];
        [handlerDic setObject:handler forKey:actionName];
    }
}
​
-(void)removeHandler:(NSString *)handlerName{
    if (handlerName) {
        [self.handlerMap removeObjectForKey:handlerName];
    }
}複製代碼

有了這樣的註冊機制,咱們註冊一個 OC 接受 JS 消息體的處理代碼,業務在任意 webview 類 or vc 類中,能夠調用我提供的註冊 api 來實現業務的 消息體處理代碼

// 這段代碼屬於 業務web 對象
// 業務在任意 webview類 or vc 類中,能夠調用我提供的註冊 api 來實現業務的 消息體處理代碼
[self registerHandler:@"common" Action:@"nativeLog" handler:^(msgObject *msg) {
    NSLog(@"webview log : \n%@",msg)
}];複製代碼

這樣 OC 這邊的代碼都已經準備就緒,只等 JS 通訊到達的時候,進行消息體識別和分發,這是 messageHandler的系統 Api,基本思路就是,從 bridge 對象中的 handlerMap 字典中按着一級 handler 二級 action 二維字典取值去出注冊好的執行代碼block

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSDictionary *msgBody = message.body;
    if (msgBody) {
        msgObject *msg = [[msgObject alloc]initWithDictionary:msgBody];
        NSDictionary *handlerDic = [self.handlerMap objectForKey:msg.handler];
        HandlerBlock handler = [handlerDic objectForKey:msg.action];
        handler(msg);
    }
}複製代碼

通訊解碼

由於 WKWebView 的 messageHandler Api 自動處理的編碼解碼,此處其實直接經過 NSDictionary *msgBody = message.body; 一句話就直接拿到了最終消息體對象,但既然是講編程思想,若是是安卓,若是是 iOS 但使用的不是 messageHandler 這種通訊方式,天然就要在這一行代碼的位置,進行手動解碼,徹底等同於編碼的逆向操做,不作贅述了。

自此,JS CALL OC 無回調,基本流程走通

詳細設計思路 — JS Call OC 回調

JS發送消息 聲明含有回調

含有回調的 JS Call OC 有了回調後整個環節會更加完整,所以回調的設計得從 JS Call OC 的最初階段就進行調整

var msgBody = {};
msgBody.handler = 'common';
msgBody.action = 'nativeLog';
msgBody.params = params; //任意json對象,用於傳參.
//msgBody.callbackId = '';
//msgBody.callbackFunction = '';複製代碼

對JS消息體進行改造,增長用於處理回調相關的數據字段 callbackId 與 callbackFunction

  • callbackId:對每一次消息須要發起回調都會生成一個惟一ID,用來當回調發生時,找到最初的發起調用的 JS Callback

  • callbackFunction:客戶端主動 Call JS 的惟一函數入口,客戶端會用這個字符串來拼接回調注入的 JS 頭,通常設計下,每一個消息這個值都應該不變,不過也能夠靈活處理(原本這個值能夠不須要傳遞,寫死在客戶端,只要前端客戶端約定好,但若是這個值不寫死,而由前端可控操做,那麼靈活性會更大,沒必要擔憂前端大規模修改 Call JS 惟一入口的時候,還得等客戶端發版)

sendMessage: function (data,callback) {
    if (callback && typeof (callback) === 'function') {
        var callbackid = this.getNextCallbackID();
        this.msgCallbackMap[callbackid] = callback;
        params.callbackId = callbackid;
        params.callbackFunction = 'window.callbackDispatcher';
    }
    
    if (this.isIOS) {
        try {
            window.webkit.messageHandlers.WKJSBridge.postMessage(data);
        }
        catch (error) {
            console.log('error native message');
        }
    }
​
    if (this.isAndroid) {
        try {
            prompt(JSON.stringify([data]));
        }
        catch (error) {
            console.log('error native message');
        }
    }
},
    
sendMessage(msgBody,function(result){
    console.log('回調觸發');
});複製代碼

能夠看到咱們着手修改 sendMessage 函數,若是在調用的時候多寫了一個callback函數,那麼就會認爲該次通訊須要回調,所以對 callbackId 與 callbackFunction 進行賦值,callbackId 是一個保證每次通訊都惟一的一個id值 getNextCallbackID ,大概思路能夠是用時間戳+必定程度的隨機小數來進行生成,思路不深刻展開了。 callbackFunction 這裏咱們先寫 window.callbackDispatcher 後面會提到這個入口是怎麼操做的。

這裏有一步最最重要的操做就是,this.msgCallbackMap[callbackid] = callback; 會把 JS 業務的回調函數,保存在一個全局可處理的回調字典之中,而 Key 就是這個惟一ID callbackId,這樣當 OC 發起回調的時候,你才能找到對應的 JS Function

OC接受消息 識別處理回調

OC這邊的消息體也得針對性進行修改,加入了 callbackID , callbackFunction, 加入了OC類的函數回調Api

typedef void (^JSResponseCallback)(NSDictionary* responseData);
​
@interface msgObject : NSObject
​
- (instancetype)initWithDictionary:(NSDictionary *)dict;
​
@property (nonatomic, copy, readonly) NSString * handler;
@property (nonatomic, copy, readonly) NSString * action;
@property (nonatomic, copy, readonly) NSDictionary * parameters;
@property (nonatomic, copy, readonly) NSString * callbackID;
@property (nonatomic, copy, readonly) NSString  *callbackFunction;
​
-(void)setCallback:(JSResponseCallback)callback; //block 做爲屬性,保存在msgObject的.m文件裏
​
-(void)callback:(NSDictionary *)result;//在msgObject的.m文件裏 調用保存在消息體裏的block
​
@end複製代碼

因此咱們繼續修改 OC 這邊收到 JS 消息的函數體,當判斷消息體含有回調信息的時候,就會生成用於回調的 OC Block,當OC業務處理完畢,準備回調回傳數據的時候使用

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSDictionary *msgBody = message.body;
    if (msgBody) {
        msgObject *msg = [[msgObject alloc]initWithDictionary:msgBody];
        NSDictionary *handlerDic = [self.handlerMap objectForKey:msg.handler];
        HandlerBlock handler = [handlerDic objectForKey:msg.action];
        //處理回調
        if (msg.callbackID && msg.callbackID.length > 0) {
            //生成OC的回調block,輸入參數是,任意字典對象的執行結果
            JSResponseCallback callback = ^(id responseData){
                //執行OC 主動 Call JS 的編碼與通訊
                [weakSelf injectMessageFuction:callbackFunction withActionId:callbackId withParams:responseData];
            };
            [msg setCallback:callback];
        }
        if (handler){
            handler(msg);
        }
    }
}複製代碼

那業務在註冊 OC 消息處理函數的時候,就可使用這個block 進行回調

// 這段代碼屬於 業務web 對象
// 業務在任意 webview類 or vc 類中,能夠調用我提供的註冊 api 來實現業務的 消息體處理代碼
[self registerHandler:@"common" Action:@"nativeLog" handler:^(msgObject *msg) {
    NSLog(@"webview log : \n%@",msg)
    NSDictionary *result = @{@"result":"result"};
    //回調一個key value均爲 result 字符串的字典當作數據
    [msg callback:result];
}];複製代碼

通訊編碼

以前說到 WKWebView 的 Api 自動處理的 JS CALL OC 的編碼解碼,可是 OC CALL JS 的編碼解碼並無自動處理,因此咱們得親自作,這就是上面提到的 injectMessageFuction:withActionId:withParams 函數,介紹一下三個輸入參數

  • Fuction:就是前邊JS 傳過來的 window.callbackDispatcher

  • ActionId:就是前邊JS 傳過來的 每一個消息體的惟一ID

  • Params:就是客戶端要回調的數據體,能夠爲空

咱們會按着下面這種方式去拼接 JS 而後用 evaluateJavaScript: 來注入調用JS

[NSString stringWithFormat:@"%@('%@', '%@');", msg,actionId,paramString]

能夠看到咱們這麼拼接出來的 JS 代碼字符串實際上是

window.callbackDispatcher('12345callbackid','{\'result\':\'result\'}');複製代碼

可是編碼過程仍是須要注意的,咱們如何把字典 params 轉化爲 paramString,確實直接用系統API NSJSONSerialization 轉一下就看起來沒問題了,但這裏其實存在必定的隱患。

OC 主動 Call JS 的原理實際上是,在客戶端拼接出一段 JS 代碼,但若是 params 這個數據中存在必定特殊字符好比 \r \n \f 等等,這些特殊字符會破壞 JS 的代碼結構,打破本來的 JS 語法,這塊要很是當心,尤爲是你要傳遞大型嵌套字典數據的時候,簡單的測試數據這個問題是沒法暴露出來的,若是 JS 代碼結構被破壞,那麼全部通訊 JS 的方法就失效了,因此編碼這塊大體代碼思路能夠是這樣

// 字典JSON化
- (NSString *)_serializeMessageData:(id)message{
    if (message) {
        return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
    }
    return nil;
}
// JSON Javascript編碼處理
- (NSString *)_transcodingJavascriptMessage:(NSString *)message
{
    //NSLog(@"dispatchMessage = %@",message);
    message = [message stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    message = [message stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    message = [message stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    message = [message stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    message = [message stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    message = [message stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    message = [message stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    message = [message stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
    
    return message;
}
// 通訊回調
-(void)injectMessageFuction:(NSString *)msg withActionId:(NSString *)actionId withParams:(NSDictionary *)params{
    if (!params) {
        params = @{};
    }
    NSString *paramsString = [self _serializeMessageData:params];
    NSString *paramsJSString = [self _transcodingJavascriptMessage:paramsString];
    NSString* javascriptCommand = [NSString stringWithFormat:@"%@('%@', '%@');", msg,actionId,paramsJSString];
    if ([[NSThread currentThread] isMainThread]) {
        [self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    } else {
        __strong typeof(self)strongSelf = self;
        dispatch_sync(dispatch_get_main_queue(), ^{
            [strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
        });
    }
}複製代碼

JS 接收 OC 回調

上面提到了,客戶端會把 callbackId callbackFuction ResultString 拼接成以下 JS 代碼,注入回 WebView

window.callbackDispatcher('12345callbackid','{\'result\':\'result\'}');複製代碼

那麼前端要作的就是準備好對應的函數,在window的對象上,掛上 callbackDispatcher 這個函數,這就是爲啥我一開始說 callbackFunction 寫死 window.callbackDispatcher 的緣由,客戶端用這個字符串,拼出了 JS 代碼,這個 JS 代碼執行的時候,就恰好window下有這麼一個函數接着

window.callbackDispatcher: function (callbackId, resultjson) {
    var handler = this.msgCallbackMap[callbackId];
    if (handler && typeof (handler) === 'function') {
        // JSON.parse(resultjson)
        console.log(resultjson);
        var resultObj = resultjson ? JSON.parse(resultjson) : {};
        handler(resultObj);
    }
},複製代碼

當OC 已經成功回調到 JS 了,那麼就用 callbackId 在剛纔保存的回調字典裏找到要回調的方法,而後把傳過來的 resultjson 用 JS 的 JSON.parse 反序列化成字典,而後用找到的回調方法把數據傳遞進去

詳細設計思路 — OC 主動 Call JS

完全介紹完了 JS Call OC + 回調了,其實大體的思路已經說個七七八八了,再介紹OC 主動 Call JS 會簡單許多,甚至真的本身沿着相似的設計思路思考捉摸一下,也能自行設計一個比較合理的方案了

JS 監聽來自 OC 的主動消息

既然是容器框架代碼層與業務解耦,提供監聽的 Api 是一種比較好的方式,業務方會把監聽事件用一個字符串來約定,好比鎖屏事件約定爲 applicationEnterBackground ,調用 API 的時候把事件字符串與事件處理函數傳入,在一個全局能夠管理的 eventCallMap 字典中進行存儲,等待事件監聽到達的時候,發起調用

//監聽的API
window.onListenEvent: function (eventId, handler) {
    var handlerArr = this.eventCallMap[eventId];
    if (handlerArr === undefined) {
        handlerArr = [];
        this.eventCallMap[eventId] = handlerArr;
    }
    if (handler !== undefined) {
        handlerArr.push(handler);
    }
},複製代碼

那麼當某個H5頁面打算使用這個監聽API的時候就這麼使用就行了

//業務調用該API
window.onListenEvent('applicationEnterBackground', function () {
   console.log('home press')
});複製代碼

剛纔提到 JS Call OC 回調的時候有一個 callbackDispatcher 函數來承接,那麼這種OC Call JS也得用相似的方式進行承接,因而咱們準備一個 eventDispatcher ,思路是相似的,我很少介紹了

//接收OC事件的API
window.eventDispatcher: function (eventId, resultjson) {
    var handlerArr = this.eventCallMap[eventId];
    for (var key in handlerArr) {
        if (handlerArr.hasOwnProperty(key)) {
            var handler = handlerArr[key];
            if (handler && typeof (handler) === 'function') {
                var resultObj = resultjson ? JSON.parse(resultjson) : {};
                handler(resultObj);
            }
        }
    }
},複製代碼

OC 主動調用 JS

既然是OC主動發起,那麼咱們就拿鎖屏這個事件來舉例

-(void)addLifeCycleListenerCommon{
    // app從後臺進入前臺都會調用這個方法
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
    // 添加檢測app進入後臺的觀察者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
​
}
​
-(void)removeLifeCycleListenerCommon{
    [[NSNotificationCenter defaultCenter]removeObserver:self];
}
​
-(void)applicationEnterForeground{
    //最關鍵的主動通訊JS的函數
    [self sendEventName:@"applicationEnterForeground" withParams:nil];
}
​
-(void)applicationEnterBackground{
    //最關鍵的主動通訊JS的函數
    [self sendEventName:@"applicationEnterBackground" withParams:nil];
}複製代碼

剛剛 JS Call OC 回調的時候使用的是injectMessageFuction:withActionId:withParams:函數,那麼這裏介紹的 sendEventName:withParams:則是相似的用於拼接JS進行回調的函數

-(void)sendEventName:(NSString *)event withParams:(NSDictionary *)params{
    NSString *jsFunction = 'window.eventDispatcher'; 
    //仍是走`injectMessageFuction:withActionId:withParams:` 這個函數,統一進行通訊編碼處理
    [self injectMessageFuction:jsFunction withActionId:event withParams:params];
}複製代碼

TIPS 思路擴展:

此處window.eventDispatcher 是客戶端寫死的,但咱們以前提到過 callbackDispatcher就設計成 JS 經過主動通訊傳給客戶端而不是寫死,這樣便於擴展。那麼eventDispatcher 能不能也這樣呢?其實很簡單,咱們設計一個 JS Call OC 的消息,傳過來讓客戶端保存住就行了嘛。

詳細設計思路 — OC 主動 Call JS 回調

剛纔設計一個sendEventName:withParams:的方法,而這個方法內部調用的是injectMessageFuction: withActionId:event withParams:,來把OC主動發起的消息與數據參數傳給JS,那麼咱們在此基礎上進行擴展,擴展成支持回調回傳數據。 WKWebView的evaluateJavaScript:completionHandler:這個函數自然有一個參數是completionHandler,所以自然支持OC主動調用evaluateJavaScript,而且回傳數據結果,就是經過completionHandler,因此咱們只須要把咱們設計出來的sendEventXxxxxinjectMessageXxxx兩個方法多設計一個block回調輸入參數 

-(void)injectMessageFuction:(NSString *)msg withActionId:(NSString *)actionId withParams:(NSDictionary *)params withCallback:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))handler{
    if (!params) {
        params = @{};
    }
    NSString *paramsString = [self _serializeMessageData:params];
    NSString *paramsJSString = [self _transcodingJavascriptMessage:paramsString];
    NSString* javascriptCommand = [NSString stringWithFormat:@"%@('%@', '%@');", msg,actionId,paramsJSString];
    if ([[NSThread currentThread] isMainThread]) {
        [self.webView evaluateJavaScript:javascriptCommand completionHandler:handler];
    } else {
        __strong typeof(self)strongSelf = self;
        dispatch_sync(dispatch_get_main_queue(), ^{
            [strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:handler];
        });
    }
}


-(void)sendEventName:(NSString *)event withParams:(NSDictionary *)params withCallback:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))handler{
    NSString *jsFunction = @'window.eventDispatcher'; 
    [self injectMessageFuction:jsFunction withActionId:event withParams:params withCallback:handler];
} 複製代碼


那麼JS要作的事情就簡單了,以前JS這邊代碼eventDispatcher在查找到handler以後直接調用就行了,那麼如今呢?多寫一個return

eventDispatcher: function (eventId, resultjson) {
    var handlerArr = this.eventCallMap[eventId];
    var me = this;
    for (var key in handlerArr) {
        if (handlerArr.hasOwnProperty(key)) {
            var handler = handlerArr[key];
            if (handler && typeof (handler) === 'function') {
                var resultObj = resultjson ? JSON.parse(resultjson) : {};
                var returnData = handler(resultObj);
                //多寫一個return
                return returnData; 
            }
        }
    }
},複製代碼


詳細設計思路 — 同步返回 JS Call OC

因爲JS Call OC 同步返回我這裏採用了不一樣於異步messageHandler的通訊方式,所以同步返回要單獨進行設計。

消息體和編碼協議徹底保持不變,從新設計一下發送接口,經過prompt()

JS 發送同步消息給 OC

sendSyncMessage: function (data) {
    if (this.isIOS) {
        try {
            //將消息體直接JSON字符串化,調用Prompt()            
            var resultjson = prompt(JSON.stringify(params));
            //直接用 = 接收 Prompt()的返回數據,JSON反解
            var resultObj = resultjson ? JSON.parse(resultjson) : {};
            return resultObj;
        }
        catch (error) {
            console.log('error native message');
        }
    }
},複製代碼

OC 攔截Prompt() 接收消息

WKWebView有個攔截Prompt()的UIDelegate,咱們在這裏進行彈窗攔截,通過同步運算後將數據經過completionHandler同步返回給JS

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
    if (webView != self.webView) {
        completionHandler(@"");
        return;
    }
    
    NSData *jsonData = [prompt dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
                                                    options:NSJSONReadingMutableContainers
                                                          error:&err];
    //能夠看出來,消息體結構不變,全部邏輯保持和異步一致
    msgObject *msg = [[msgObject alloc]initWithDictionary:msgBody];
    NSDictionary *handlerDic = [self.handlerMap objectForKey:msg.handler];
    HandlerBlock handler = [handlerDic objectForKey:msg.action];
    handler(msg);
    //修改 msg的callback方法,當發現是同步消息的時候,callback在block執行完畢後將數據保存到msg的syncReturn中
    NSString *resultjson = [self _serializeMessageData:msg.syncReturn];
    completionHandler(resultjson);
    
}
複製代碼

這樣就完成了同步返回數據 JS 中,能夠很開心的寫同步代碼了

var params = syncGetPastboard();
var pastboard = params.content;
console.alert(pastboard);複製代碼

業務模塊擴展

上面把基本的 JS Call OC / 回調 / OC Call JS 的基本通訊流程設計思路串了一遍,但咱們提一下代碼模塊設計思路,由於通訊是底層通用邏輯,但在這之上,業務會發展出各類專爲業務服務的消息體,這些消息是堆積在一個代碼裏越積越多,毫無管理,仍是設計成模塊式劃分,橫向靈活擴展可插拔式的代碼結構?這裏只提一些個人我的的想法,代碼的整潔之道有不少,每一個人都有本身的體會,並非說怎樣就是最好的,我這裏也僅僅是很粗略的提一下。

JS 代碼模塊設計

  • jsbridge-core.js

全部底層通訊的相關代碼能力,都會放到core這個js代碼裏,也就是上面咱們介紹的各類核心通訊框架代碼

  • jsbridge-common.js

假若有一些通用的bridge消息需求,好比日誌/獲取設備信息/屏幕鎖屏監聽/屏幕,各類Common相關的需求代碼,都放到這裏

  • jsbridge-haha.js

假若有一些業務獨有的需求,好比加入購物車,好比購買兌換積分等等,能夠統一歸類到 haha 模塊,全部跟 haha 模塊相關的代碼,都放在這裏

var Core = function () {
    this.ua = navigator.userAgent;
    this.isAndroid = (/(Android);?[\s\/]+([\d.]+)?/.test(this.ua));
    this.isIOS = !!this.ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
    this.msgCallbackMap = {};
    this.eventCallMap = {};
    this.sendMessage = function(xxxx){xxxx};
    this.onListenEvent = function(xxxx){xxxx};
    this.eventDispatcher = function(xxxx){xxxx};
    this.callbackDispatcher = function(xxxx){xxxx};
};
window.bridge.Core = Core;複製代碼

那麼咱們就把 JS 的通訊底層 Core 模塊設置好了,剛纔提到的各類代碼都在裏面,這裏這麼寫有點low,只爲展現思路,有了思路,前端怎麼整理代碼優化代碼均可以。另外注意此時咱們已是 window.bridge.core.callbackDispatcher了,因此傳遞 callbackFunction 的時候要注意

var Common = function () {
    this.webviewAppearEvent = 'webviewAppear';
    this.webviewDisappearEvent = 'webviewDisappear';
    this.applicationEnterBackgroundEvent = 'applicationEnterBackground';
    this.applicationEnterForegroundEvent = 'applicationEnterForeground';
};
​
// dataDic爲Object對象
Common.nativeLog = function (dataDic) {
    var params = {};
    params.dataDic = dataDic;
    this.sendCommonMessage('nativeLog', params);
},
​
// traceData爲字符串
Common.crashTrace = function (traceData) {
    var params = {};
    params.data = traceData;
    this.sendCommonMessage('crashTrace', params);
},
// 複製剪切板
Common.copyContent = function (content) {
    var params = {};
    params.str = content;
    this.sendCommonMessage('copyContent', params);
},
//獲取設備一些通用信息
Common.getCommonParams = function (callback) {
    this.sendCommonMessage('commonParams', {}, callback);
},
// common模塊的基礎類,選用一樣的 handler name => Common
Common.sendCommonMessage: function (action, params, callback) {
    var msgBody = {};
    msgBody.handler = 'Common';
    msgBody.action = action;
    msgBody.params = params;
    window.bridge.Core.sendMessage(msgBody, callback);
}
window.bridge.Common = Common;複製代碼

那麼業務頁面中使用就這樣咯

//具體的某個h5頁面
//頁面有複製進入剪切板的需求
window.bridge.Common.copyContent('哈哈哈哈,我複製進剪切板啦')
//頁面有讀取客戶端信息的需求
window.bridge.Common.getCommonParams(function (params) {
    console.log(params);
});
//頁面有監聽鎖屏的需求
window.bridge.Core.onListenEvent(window.bridge.Common.applicationEnterBackgroundEvent, function () {
    console.log('home press')
});複製代碼

全部代碼都有點low,只是介紹思路,咱們業務代碼也不會這樣簡單粗暴的寫,只是爲了簡單說明意圖,而且我也不深刻擴展 haha 模塊了就是舉例。之後若是有新的一類業務需求,能夠擴展新的模塊,若是某個模塊有新消息需求,能夠單獨新增消息

OC 代碼模塊設計

前文:

個人 Hybrid WebView 設計理念是組合,而不是繼承,所以我設計的不是一個 XXWebView / XXWebViewController 基類,使用者不須要在業務代碼中使用 WebView 必須從我這裏繼承。我設計的是一個 NSObject 的 bridge 對象,使用者只須要跟本身業務中用的任意一種 WKWebView 的業務本身的類進行綁定,就能夠擁有 Hybrid 的能力

我也會準備一個類作核心通訊類,好比就叫 XXBridge ,集成自 NSObject,用戶在ViewController裏能夠建立各自業務本身封裝的任意WebView對象,而後執行綁定操做,把 XXBridge 對象與 WebView 綁定起來,相似這樣

//在任意業務VC的viewDidLoad裏
//建立WKWebView
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
config.preferences = [WKPreferences new];
config.preferences.minimumFontSize = 10;
config.preferences.javaScriptEnabled = YES;
config.preferences.javaScriptCanOpenWindowsAutomatically = YES;
WKWebView *webView = [[WKWebView alloc]initWithFrame:CGRectZero configuration:config];
self.webView = webView;
//建立並綁定Bridge
self.jsBridge = [[WKJSBridge alloc]init];
self.jsBridge.delegate = self;
[self.jsBridge bindBridgeWithWebView:webView];複製代碼

所謂的綁定過程大概思路其實只是,把WebView的一些 navigationDelegate/UIDelegate、configuration.userContentController 設置指向 XXBridge 內的處理函數,再把全部 WKWebView 的navigationDelegate UIDelegate經過 XXBridge 的delegate 轉發給原VC,其實就是一層簡單的代理攔截,我就不詳解了,剩下的都是上面的提到過的具體通訊代碼了。

固然你徹底可使用繼承,強調過不少次,只說設計思路,而且並非惟一思路,也不表明是最優思路。

  • XXBridge Class

  • XXBridge+Common Category

  • XXBridge+haha Category

提到模塊化可插拔式擴展,在OC裏面最快想到的固然是Category,既然 JS 代碼都劃分爲 Core/Common/haha,那麼OC也這麼作唄(不用category固然也行,只要把代碼按着模塊簡潔合理的分割開來易於擴展和管理就好)

@implementation XXBridge (Common)
​
-(void)registCommonHandler{
    [self addLifeCycleListenerCommon];
    __weak typeof(self) weakSelf = self;
    
    [self registerHandler:@"Common" Action:@"commonParams" handler:^(WKJSBridgeMessage *msg) {
        NSDictionary *result = [weakSelf getCommonParams];
        [msg callback:result];
    }];
    
    [self registerHandler:@"Common" Action:@"copyContent" handler:^(WKJSBridgeMessage *msg) {
        NSDictionary *params = msg.parameters;
        NSString *content = [params objectForKey:@"str"];
        UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
        pasteboard.string = content;
    }];
    
    [self registerHandler:@"Common" Action:@"nativeLog" handler:^(WKJSBridgeMessage *msg) {
        [weakSelf nativeLog:msg.parameters];
    }];
    [self registerHandler:@"Common" Action:@"crashTrace" handler:^(WKJSBridgeMessage *msg) {
        [weakSelf crashTrace:msg.parameters];
    }];
}
​
-(void)applicationEnterForeground{
    [self sendEventName:WKJSBridgeAppEnterForegroundEvent withParams:nil];
}
​
-(void)applicationEnterBackground{
    [self sendEventName:WKJSBridgeAppEnterBackgroundEvent withParams:nil];
}
​
-(void)addLifeCycleListenerCommon{
    // app從後臺進入前臺都會調用這個方法
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
    // 添加檢測app進入後臺的觀察者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
}
​
-(void)removeLifeCycleListenerCommon{
    [[NSNotificationCenter defaultCenter]removeObserver:self];
}
​
@end複製代碼

UserAgent管理

WebView容器其實有一個很重要的需求,就是修改WebView UA,做爲區別巨有容器能力的WebView識別方式,通常狀況下拿到的UA會長這樣

Mozilla/5.0 (Linux; Android 6.0.1; XT1650-05 Build/MCC24.246-37; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/51.0.2704.81 Mobile Safari/537.36複製代碼

但咱們在APP內不管是建立有 Hybrid 能力的 WebView,仍是常規 WebView ,他們的UA都是系統默認UA,沒法作到從 UA 上區別,客戶端使用的 WebView 容器了,所以其實還有一個很實用的需求就是,針對 Hybrid WebView 擴充 UA

Mozilla/5.0 (Linux; Android 6.0.1; XT1650-05 Build/MCC24.246-37; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/51.0.2704.81 Mobile Safari/537.36 NBBRIDGE_屏幕寬_屏幕高_操做系統版本號_App版本號_設備類型複製代碼

若是我擴充這樣的UA有什麼好處呢?前端能夠很方便的經過對UA進行正則處理,快速便捷的取到

  • 屏幕尺寸

  • 操做系統尺寸

  • APP版本號

  • 設備類型

有人會問,這些數據也能夠經過設計一個Bridge消息,直接從客戶端拿,爲啥非得走UA呢?由於對於網頁來講,分網頁的 Client Side 和網頁的 Server Side ,Client就說明這段 JS 已經運行在客戶端的瀏覽器裏,但 Server Side 會發生在 WebView 向 URL 發起請求,打到服務器端,此時多是 PHP/Node/JAVA/GO等各類語言寫的服務器,但他們的共同特色是在 Server Side 是沒有bridge,是不可能創建通訊的,此時UA就有意義了。

全局UA

iOS 8及 8 如下只能進行全局 UA 修改,能夠經過 NSUserDefaults 的方式修改,一次修改每一個WebView都有效(不管是常規 WebView 仍是被你改造過的 Hybrid WebView)

NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:UAStringXXX, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];複製代碼

獨立UA

iOS 9 有了獨立UA,能夠針對每個 WKWebView 對象實例,設置專屬的UA

if (@available(iOS 9.0, *)) {
    self.webView.customUserAgent = self.fullUserAgent;
}複製代碼

Cookie管理

前邊介紹UA的時候說過,在 Server Side 的時候是沒法創建 Hybrid 通訊的,所以傳遞數據的方式就只有 UA/Cookie/URL Query

  • UA適合傳遞跟設備相關的,描述設備固定不變的信息

  • Cookie適合傳遞任何你想要的數據,但Cookie有失效與域名限制

  • URL Query 適合傳遞任何你想要的數據,不過最好這個數據沒什麼安全敏感,由於GET請求是明文的(POST請求也能夠,類比一下很少說了)

提到 WKWebView 就不得不把Cookie管理單獨說一下。由於,WKWebView在Cookie上有太多的坑了,因此很是有必要把 Cookie 進行專門的手動代碼管理。

傳統的NSHTTPCookieStorage

經過 NSHTTPCookieStorage 設置的 Cookie ,這樣設置的Cookie 不管是 UIWebView 頁面請求仍是 NSURLSession 網絡請求,都會帶上 Cookie,因此十分方便

WKWebView 的 Cookie 大坑

【騰訊Bugly乾貨分享】WKWebView 那些坑

這篇文章裏介紹了不少WKWebView的坑,其中會詳細說好多Cookie的問題,簡單說就是

  • WKWebView 發起的請求並不會帶上 NSHTTPCookieStorage 裏面的 Cookie

而好比用戶登錄狀態token等,最基礎的設計就是把 token 寫到 cookie 裏,若是 WebView 獲取不到 Cookie 的登錄狀態應該怎麼辦

WKWebView ServerSide Cookie 設置

簡單的說就是把 WKWebView 發起的 NSURLRequest 攔截,MutableCopy 一個,而後手動在RequestHeader裏從NSHTTPCookieStorage讀取Cookie進行添加

-(void)syncRequestCookie:(NSMutableURLRequest *)request
{
    if (!request.URL) {
        return;
    }
    
    NSArray *availableCookie = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL];
    NSMutableArray *filterCookie = [[NSMutableArray alloc]init];
 
    if (filterCookie.count > 0) {
        NSDictionary *reqheader = [NSHTTPCookie requestHeaderFieldsWithCookies:filterCookie];
        NSString *cookieStr = [reqheader objectForKey:@"Cookie"];
        [request setValue:cookieStr forHTTPHeaderField:@"Cookie"];
    }
    return;
}複製代碼

TIPS:

當服務器發生重定向的時候,此時第一次在 RequestHeader 中寫入的 Cookie 會丟失,還須要從新對重定向的 NSURLRequest 進行 RequestHeader 的 Cookie 處理 ,簡單的說就是在 webView:decidePolicyForNavigationAction:decisionHandler: 的時候,判斷此時 Request 是否有你要的 Cookie 沒有就Cancel掉,修改Request 從新發起

WKWebView ClientSide Cookie 設置

上面這麼寫完了,當頁面加載的時候,後端不管是啥語言,都能從請求裏看到 Cookie 了,可是後端渲染返回頁面後,在 Client Side 瀏覽器裏運行的時候,JS 在執行的時候用 document.cookie API 是讀取不到的。因此還得針對 Client Side Cookie 進行處理

-(void)syncClientCookieScripts:(NSMutableURLRequest *)request{
    if (!request.URL) {
        return;
    }
    NSArray *availableCookie = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL];
    NSMutableArray *filterCookie = [[NSMutableArray alloc]init];
   
    for (NSHTTPCookie * cookie in availableCookie) {
        if (self.syncCookieMode) {
            //httponly需求不得寫入js cookie
            if (!cookie.HTTPOnly) {
                [filterCookie addObject:cookie];
            }
        }
    }
    
    // 拼接 JS 代碼 對 Client Side 注入Cookie
    NSDictionary *reqheader = [NSHTTPCookie requestHeaderFieldsWithCookies:filterCookie];
    NSString *cookieStr = [reqheader objectForKey:@"Cookie"];
    if (filterCookie.count > 0) {
        for (NSHTTPCookie *cookie in filterCookie) {
            NSTimeInterval expiretime = [cookie.expiresDate timeIntervalSince1970];
            NSString *js = [NSString stringWithFormat:@"document.cookie ='%@=%@;expires=%f';",cookie.name,cookie.value,expiretime];
            WKUserScript *jsscript = [[WKUserScript alloc]initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
            [self.userContentController addUserScript:jsscript];
        }
    }
    return;
}複製代碼

Client Side Cookie 注入依靠的是建立一個 JS 腳本,讓 WebView 去執行,介紹通訊的時候咱們用的是 evaluateScript 去進行主動注入,好處是隨時隨地調用均可以執行,但眼下這個場景用 WKUserScript 更好,而且推薦使用 WKUserScriptInjectionTimeAtDocumentStart 這個時機。

本地腳本管理

UIWebView代碼注入時機與姿式

個人另外一篇文章就提到了一個問題,注入時機。

在頁面加載前主動注入,因爲頁面加載的時候,JS 會啓用全新的 JSContext 所以以前注入全都無效,在頁面加載完畢的時候注入,JS 會注入的比較晚,致使在 JS代碼開始執行 -> 頁面徹底加載完畢 期間,Client Side 是沒有 JS 注入的效果的

  • 個人Link中提到的CSS注入

  • 個人上文提到的Cookie注入

所以 WKWebView 的 WKUserScript 就提供了對於這個問題的解決辦法,他的使用方式不是主動注入,而是提早準備好要注入的 JS 代碼,在 WKWebView 加載頁面的系統處理期間,由系統幫你選擇在固定的時機,注入你準備好的 JS 代碼,目前只有2個時機,DocumentStart/DocumentEnd

  • WKUserScriptInjectionTimeAtDocumentStart

  • WKUserScriptInjectionTimeAtDocumentEnd

所以這種提早預置靜態 JS 注入的需求,也是一個 Hybrid WebView 容器應該考慮到的

WebView容器 - 常規擴展

一個 Hybrid WebView 容器,若是設計好了基礎通訊流程,設計好了模塊擴展模式,其實還能夠作一些比較通用的功能組件,這思路其實也和 RN 很相似的,好的架構設計好了,就應該能夠橫向自由靈活的本身擴展任意業務組件,但 RN 不也內置了不少 FB 提早幫你寫好了的通用組件麼?

  • Common 組件:咱們的示例代碼就是 Common 組件的一些基礎操做

    • 複製剪切板

    • 獲取設備信息

    • 打客戶端Log,上報日誌

    • 打客戶端Crash追蹤Log,隨Crash上報

    • ……打開你的想象力

  • CommonUI 組件:也有一些 Common 而且與 UI 相關的基礎操做

    • showTips 展現客戶端文字浮層

    • showDialog 展現客戶端確認彈框,回調用戶選擇按鈕

    • pullRefresh 採用客戶端的下拉刷新,但配合 H5 進行數據加載

    • Router 跳轉任意 App 內路由頁面

    • NaviBarControl 可讓前端來定製客戶端頂部 NaviBar

      • share Button 增長原生分享按鈕,點擊後出發原生分享

      • Other Button 增長任意原生按鈕,點擊後跳轉任意 App 內路由頁面

    • ……打開你的想象力

  • NetWork 組件:判斷瀏覽器調試環境下走 AJAX 網絡請求,判斷是客戶端就經過客戶端發起原生網絡請求,請求結果回調 JS (爲何作?通常原生會封裝網絡請求,有更精細粒度的cache控制 ,和通用無痕日誌埋點)

    • Get 不解釋了

    • Post 不解釋了

    • ……打開你的想象力

  • Storage 組件:前端的存儲只能使用 LocalStorage 和 Cookie 這兩者都有很大的缺陷

    • Key - Value Plist Storage : 可讓前端把 Key Value 發給客戶端,讓客戶端經過本地Plist 存儲/讀取/刪除

    • File Storage:可讓前端把大段須要存儲的字符串,發給客戶端,讓客戶端在App沙盒內開闢文件路徑,saveToFile存儲成文件,而且提供目錄操做能力,建立目錄/刪除目錄/建立文件/刪除文件/讀取文件

    • ……打開你的想象力

  • Push組件:可讓前端有能力寫本地Push鬧鈴到App 或者上報遠程Push Token

    • 本地 Push 設置

    • 遠程 Push 獲取 Token

其實若是基礎功能擴充的足夠強大,Hybrid WebView 能夠有很強的能力,能夠充分打開你的想象力,Hybrid 的宗旨就是,若是 WebView 本來作不到,或者作起來有很大限制或者性能不佳,那麼就讓原生配合,一塊兒作到

WebView容器 - 腦洞預告

其實這部份內容原本應該是第三篇的內容,但也能夠提早簡單說說吊吊胃口╮(╯_╰)╭

  • 深度調試能力

在客戶端下調試 WebView 只能 safari 調試?能不能更方便一些? 能不能在QA黑盒測試不從新打包運行連電腦的狀況下進行 JS 調試?

  • 動態調用能力

全部的消息都必須提早在 OC 客戶端寫好處理模塊,前端才能調用,能不能不發版就調用新原生邏輯?

能不能直接在JS裏面寫OC?

  • 原生渲染能力

RN被不少人拿來講 RN 作出來的是原生 App ,界面層級都是原生的,WebView就作不到麼?

小程序底層就是 WebView ,但小程序有些組件官方文檔會寫,此組件爲原生,沒法控制與其餘 Dom 的 Z軸層級,他是怎麼作的?(視頻/地圖/Canvas組件)

咱們本身寫的 WebView 容器能不能作到?

  • 異步線程能力

都說 JS 是單線程,並且慢,能不能給 JS 增長多線程能力?(沒錯 WebKit 在2017年有個WebKit新標準提案就是這個,但咱們暫時先不須要瀏覽器內核支持,客戶端Hybrid能不能先簡單支持一下?)

  • 離線秒開能力

小程序會打包,而後下發給微信客戶端,這樣小程序的界面框架加載徹底無網絡請求,極大程度的加快前端散碎靜態資源的加載速度,秒開能力(注意這和騰訊的 VasSonic 的秒開並非一類方案)

相關文章
相關標籤/搜索