從零收拾一個hybrid框架(一)-- 從選擇JS通訊方案開始

相信不少人都在項目裏熟練使用各類Hybrid技術,不管是使用了知名得 WebViewJavascriptBridge 框架來作本身的Hybrid Web容器,又或是本身從頭着手寫了一個知足本身業務需求的bridge,從而構建起本身的Hybrid Web容器,也有的乾脆直接使用了cordova 這一大型Hybrid容器框架,cordova + ionic 來進行Hybrid的開發javascript

拆解學習框架源碼是一個好事,可是在拆解優秀框架源碼的背後,如何將多個優秀源碼的精華打碎重塑,結合本身的產品業務需求從新組合成爲適合本身的,而且紮實掌握能夠靈活修改自如控制的代碼,這也算是另外一個層面的提高。html

  • 選擇合適的JS通訊方案(第一篇)
  • 實現基本的WebView容器能力(第二篇 待續)
  • 嘗試拓展WebView容器的額外能力(第三篇 待續)

這系列文章我想表達的並非在推廣什麼我本身的新Bridge輪子,也不是針對某個開源Bridge框架進行深度的源碼分析。咱們從看開源框架輪子如何設計,如何使用,源碼如何工做的思惟方式中跳出來前端

換一種模式去從目的出發,從需求出發,思考當你什麼都沒有的時候,你要從零思考構建一個hybrid框架的時候,你都要考慮哪些方面?這些方面採用怎樣的設計思想能作到將來在使用中靈活自如,不至於面臨侷限java

這一篇先重點聊聊 JS與Native通訊的通訊方案android

幾種JS Native相互通訊方式的介紹

你們可能看了不少大框架源碼,不管是cordova仍是WebViewJavascriptBridge他們核心的通訊方式就都是 假跳轉請求攔截git

但其實JS與Native通訊並不止一種方式,還有不少種通訊方式,尤其重要的是,不一樣的通訊方式有着不一樣的特色,有的甚至雖然受限於安卓/蘋果平臺差別不通用,但獨有的優勢倒是 假跳轉請求攔截 沒法比擬的github

JS 調用 Native 的幾種通訊方案

  • 假跳轉的請求攔截
  • 彈窗攔截
    • alert()
    • prompt()
    • confirm()
  • JS上下文注入
    • 蘋果JavaScriptCore注入
    • 安卓addJavascriptInterface注入
    • 蘋果scriptMessageHandler注入

Native 調用 JS 的幾種通訊方案

JS是一個腳本語言,在設計之初就被設計的任什麼時候候均可以執行一段字符串js代碼,換句話說,任何一個js引擎都是能夠在任意時機直接執行任意的JS代碼,咱們能夠把任何Native想要傳遞的消息/數據直接寫進JS代碼裏,這樣就能傳遞給JS了web

  • evaluatingJavaScript 直接注入執行JS代碼

你們在PC上用電腦,用Chrome的時候都知道,能夠直接用'javascript:xxxx'來簡單的執行一些JS代碼,彈個框,這個方法只有安卓能夠用,由於iOS必須先將url字符串生成Request再交給webview去load,這種'javascript:xxxx'生成request會失敗npm

  • loadUrl 瀏覽器用'javascript:'+JS代碼作跳轉地址

WKWebView官方提供了一個Api,可讓WebView在加載頁面的時候,自動執行注入一些預先準備好的JSjson

  • WKUserScript WKWebView的addUserScript方法,在加載時機注入

JS 調用 Native 的幾種通訊方案

假跳轉的請求攔截

何謂 假跳轉的請求攔截 就是由網頁發出一條新的跳轉請求,跳轉的目的地是一個非法的壓根就不存在的地址好比

//常規的Http地址
https://wenku.baidu.com/xxxx?xx=xx
//假的請求通訊地址
wakaka://wahahalalala/action?param=paramobj
複製代碼

看我下面寫的那條假跳轉地址,這麼一條什麼都不是的扯淡地址,直接放到瀏覽器裏,直接扔到webview裏,確定是妥妥的什麼都打不開的,而若是在通過咱們改造過的hybrid webview裏,進行攔截不進行跳轉

url地址分爲這麼幾個部分

  • 協議:也就是http/https/file等,上面用了wakaka
  • 域名:上面的 wenku.baidu.com 和 wahahalalala
  • 路徑:上面的 xxxx?或action?
  • 參數:上面的 xx=xx或param=paramobj

若是咱們構建一條假url

  • 用協議與域名當作通訊識別
  • 用路徑當作指令識別
  • 用參數當作數據傳遞

客戶端會無差異攔截全部請求,真正的url地址應該照常放過,只有協議域名匹配的url地址才應該被客戶端攔截,攔截下來的url不會致使webview繼續跳轉錯誤地址,所以無感知,相反攔截下來的url咱們能夠讀取其中路徑當作指令,讀取其中參數當作數據,從而根據約定調用對應的native原生代碼

以上實際上是一種 協議約定 只要JS側按着這個約定協議生成假url,native按着約定協議攔截/讀取假url,整個流程就能跑通。

徹底能夠不用按着我寫的這種方式約定協議,能夠任意另行約定協議好比,協議當作通訊識別,域名當作模塊識別,路徑當作指令識別,參數當作數據傳遞等等,協議協議,任何一種合理的約定均可以,均可以正常的讓JS與Native進行通訊

假跳轉的請求攔截-JS發起調用

JS其實有不少種方式發起假請求,跟發起一個新請求沒啥兩樣,只要按着 協議約定 生成假請求地址,正常的發起跳轉便可,任何一種方式均可以讓客戶端攔截住

  • A標籤跳轉
//在HTML中寫上A標籤直接填寫假請求地址
<a href="wakaka://wahahalalala/action?param=paramobj">A標籤A標籤A標籤A標籤</a>
複製代碼
  • 原地跳轉
//在JS中用location.href跳轉
location.href = 'wakaka://wahahalalala/action?param=paramobj'
複製代碼
  • iframe跳轉
//在JS中建立一個iframe,而後插入dom之中進行跳轉
$('body').append('<iframe src="' + 'wakaka://wahahalalala/action?param=paramobj' + '" style="display:none"></iframe>');
複製代碼

假跳轉的請求攔截-客戶端攔截

  • 安卓的攔截方式 shouldOverrideUrlLoading
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    //1 根據url,判斷是不是所須要的攔截的調用 判斷協議/域名
    if (是){
      //2 取出路徑,確認要發起的native調用的指令是什麼
      //3 取出參數,拿到JS傳過來的數據
      //4 根據指令調用對應的native方法,傳遞數據
      return true;
    }
    return super.shouldOverrideUrlLoading(view, url);
}

複製代碼
  • iOS的UIWebView的攔截方式 webView:shouldStartLoadWithRequest:navigationType:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    //1 根據url,判斷是不是所須要的攔截的調用 判斷協議/域名
    if (是){
      //2 取出路徑,確認要發起的native調用的指令是什麼
      //3 取出參數,拿到JS傳過來的數據
      //4 根據指令調用對應的native方法,傳遞數據
      return NO;
      //確認攔截,拒絕WebView繼續發起請求
    }    
    return YES;
}
複製代碼
  • iOS的WKWebView的攔截方式 webView:decidePolicyForNavigationAction:decisionHandler:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    //1 根據url,判斷是不是所須要的攔截的調用 判斷協議/域名
    if (是){
      //2 取出路徑,確認要發起的native調用的指令是什麼
      //3 取出參數,拿到JS傳過來的數據
      //4 根據指令調用對應的native方法,傳遞數據

      //確認攔截,拒絕WebView繼續發起請求
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    return YES;
}
複製代碼

彈窗攔截

前端能夠發起不少種彈窗包含

  • alert() 彈出個提示框,只能點確認無回調
  • confirm() 彈出個確認框(確認,取消),能夠回調
  • prompt() 彈出個輸入框,讓用戶輸入東西,能夠回調

每種彈框均可以由JS發出一串字符串,用於展現在彈框之上,而此字符串恰巧就是能夠用來傳遞數據,咱們把全部要傳遞通信的信息,都封裝進入一個js對象,而後生成字典,最後序列化成json轉成字符串

經過任意一種彈框將字符串傳遞出來,交給客戶端就能夠進行攔截,從而實現通訊

彈窗攔截 - JS發起調用

其實alert/confirm/prompt三種彈框使用上沒任何區別和差別,這裏只取其中一種舉例,能夠選一個不經常使用的當作管道進行JS通訊,這裏用prompt舉例

var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
var jsonData = JSON.stringify([data]);
//發起彈框
prompt(jsonData);
複製代碼

彈窗攔截 - 客戶端攔截

  • 安卓的攔截 onJsPrompt(其餘的兩個彈框也有)
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    //1 根據傳來的字符串反解出數據,判斷是不是所須要的攔截而很是規H5彈框
    if (是){
      //2 取出指令參數,確認要發起的native調用的指令是什麼
      //3 取出數據參數,拿到JS傳過來的數據
      //4 根據指令調用對應的native方法,傳遞數據
      return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}
複製代碼
  • iOS的WKWebView webView:runJavaScriptTextInputPanelWithPrompt:balbala
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
    //1 根據傳來的字符串反解出數據,判斷是不是所須要的攔截而很是規H5彈框
    if (是){
        //2 取出指令參數,確認要發起的native調用的指令是什麼
        //3 取出數據參數,拿到JS傳過來的數據
        //4 根據指令調用對應的native方法,傳遞數據
        //直接返回JS空字符串
        completionHandler(@"");
    }else{
        //直接返回JS空字符串
        completionHandler(@"");
    }
}
複製代碼
  • iOS的UIWebView

UIWebView不支持截獲任何一種彈框,所以這條路走不通

通過好心人提醒,UIWebView也存在一種利用Undocumented API(只是未公開API,可是否處於被禁止的私有API不必定)的方式來攔截彈框。

原理是能夠自行建立一個categroy,在裏面實現一個未出如今任何UIWebView頭文件裏的delegate,就能攔截彈框了(這個Undocumented的delegate長得和WKWebView的攔截delegate一個樣子)

iOS--UIWebView 屏蔽 alert警告框

JS上下文注入

說道JS上下文注入,作iOS的都會了解到iOS7新增的一整個JavaScriptCore這個framework,這個framework被普遍使用在了JSPatch,RN等上面,但這個東西通常用法都是徹底脫離於WebView,只有一個JS上下文,這個JS上下文裏,沒有window對象,沒有dom,嚴格意義上講這個和咱們所關注的依賴WebView的Hybrid框架是有很大差別的,就不在這篇文章裏多說了

  • 蘋果UIWebview JavaScriptCore注入
  • 安卓addJavascriptInterface注入
  • 蘋果WKWebView scriptMessageHandler注入

雖然某種意義上講上面三種方式,他們均可以被稱做JS注入,他們都有一個共同的特色就是,不經過任何攔截的辦法,而是直接將一個native對象(or函數)注入到JS裏面,能夠由web的js代碼直接調用,直接操做

但這三種注入方式都操做差別仍是很大,而且各自的侷限性各不相同,咱們下面一一說明

蘋果UIWebview JavaScriptCore注入

UIWebView能夠經過KVC的方法,直接拿到整個WebView當前所擁有的JS上下文

documentView.webView.mainFrame.javaScriptContext

拿到了JSContext,一切的使用方式就和直接操做JavaScriptCore沒啥區別了,咱們能夠把任何遵循JSExport協議的對象直接注入JS,讓JS可以直接控制和操做

因此在介紹如何JS與Native操做的時候換個順序,先介紹客戶端如何把bridge函數注入到JS,在介紹JS如何使用

蘋果UIWebview JavaScriptCore注入 - 客戶端注入

//拿到當前WebView的JS上下文
JSContext *context = [webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//給這個上下文注入callNativeFunction函數當作JS對象
context[@"callNativeFunction"] = ^( JSValue * data )
{
    //1 解讀JS傳過來的JSValue  data數據
    //2 取出指令參數,確認要發起的native調用的指令是什麼
    //3 取出數據參數,拿到JS傳過來的數據
    //4 根據指令調用對應的native方法,傳遞數據
    //5 此時還能夠將客戶端的數據同步返回!
}
複製代碼

經過上面的方法能夠拿到當前WebView的JS上下文JSContext,而後就要準備往這個JSContext裏面注入準備好的block,而這個準備好的block,負責解讀JS傳過來的數據,從而分發調用各類native函數指令

TIPS: 這種注入不止能夠把block注入,在JS裏成爲一個JS函數,還能夠把字符/數字/字典等數據直接注入到JS全局對象之中,可讓JS訪問到Native才能獲取的全局對象,甚至還能夠注入任何NSObject對象,只要這個NSObject對象遵循JSExportOC的協議,至關於JS能夠直接調用訪問OC的內存對象

蘋果UIWebview JavaScriptCore注入 - JS調用

//準備要傳給native的數據,包括指令,數據,回調等
var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
//直接使用這個客戶端注入的函數
callNativeFunction(data);
複製代碼

在沒通過客戶端注入的時候,直接使用調用callNativeFunction()會報 callNativeFunction is not defined這個錯誤,說明此時JS上下全文全局,是沒有這個函數的,調用無效

當執行完客戶端注入的時候,此時JS上下文全局global下面,就擁有了這個callNativeFunction的函數對象,就能夠正常調用,從而傳遞數據到Native

安卓addJavascriptInterface注入

安卓的WebView有一個接口addJavascriptInterface,能夠在loadUrl以前提早準備一個對象,經過這個接口注入給JS上下文,從而讓JS可以操做,這個操做方式很相似蘋果UIWebview JavaScriptCore注入,整個機制也差異不離,但有個很重大的區別,後面在詳述優缺點對比的時候,會重點描述

安卓addJavascriptInterface注入 - 客戶端注入

使用安卓官方的API接口便可,而且能夠在loadUrl以前WebView建立以後,便可配置相關注入功能,這個和UIWebView-JSContext的使用差別很是之大,後面會說

// 經過addJavascriptInterface()將Java對象映射到JS對象
//參數1:Javascript對象名
//參數2:Java對象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "nativeObject");
複製代碼

其中AndroidtoJs這個是一個自定義的安卓對象,他們裏面有個函數callFunction,AndroidtoJs這個對象的其餘函數方法JS均可以調用

安卓addJavascriptInterface注入 - JS調用

剛纔注入的js對象叫nativeObject,因此JS中能夠在全局任意使用

nativeObject.callFunction("js調用了android中的hello方法");
複製代碼

我不是很熟悉android,以上不少安卓代碼都取自 Android:你要的WebView與 JS 交互方式 都在這裏了,後面也會歸入參考文獻之中

蘋果WKWebView scriptMessageHandler注入

蘋果在開放WKWebView這個性能全方位碾壓UIWebView的web組件後,也大幅更改了JS與Native交互的方式,提供了專有的交互APIscriptMessageHandler

由於這是蘋果的API,使用方式搜一下一搜一大堆,我並不詳細解釋了,直接展現一下代碼

蘋果WKWebView scriptMessageHandler注入 - 客戶端注入

//配置對象注入
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeObject"];
//移除對象注入
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeObject"];
複製代碼

須要說明一下,addScriptMessageHandler就像安卓的addJavascriptInterface同樣,能夠在WKWebView loadUrl以前便可進行相關配置

但不同的是,若是當前WebView沒用了,須要銷燬,須要先移除這個對象注入,不然會形成內存泄漏,WebView和所在VC循環引用,沒法銷燬。

蘋果WKWebView scriptMessageHandler注入 - JS調用

剛纔注入的js對象叫nativeObject,但不像前邊兩個注入同樣,直接注入到JS上下文全局Global對象裏,addScriptMessageHandler方法注入的對象被放到了,全局對象下一個Webkit對象下面,想要拿到這個對象須要這樣拿

window.webkit.messageHandlers.nativeObject
複製代碼

而且和以前的兩種注入也不一樣,前兩種注入均可以讓js任意操做所注入自定義對象的全部方法,而addScriptMessageHandler注入其實只給注入對象起了一個名字nativeObject,但這個對象的能力是不能任意指定的,只有一個函數postMessage,所以JS的調用方式也只能是

//準備要傳給native的數據,包括指令,數據,回調等
var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
//傳遞給客戶端
window.webkit.messageHandlers.nativeObject.postMessage(data);
複製代碼

蘋果WKWebView scriptMessageHandler注入 - 客戶端接收調用

前兩種注入方式,都是在注入的時候,就指定了對應的接收JS調用的Native函數,可是此次不是,在蘋果的API設計裏,當JS開始調用後,會調用到指定的iOS的delegate裏

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //1 解讀JS傳過來的JSValue data數據
    NSDictionary *msgBody = message.body;
    //2 取出指令參數,確認要發起的native調用的指令是什麼
    //3 取出數據參數,拿到JS傳過來的數據
    //4 根據指令調用對應的native方法,傳遞數據
}
複製代碼

Native 調用 JS 的幾種通訊方案

說完了JS調用Native,咱們再聊聊Native發起調用JS

evaluatingJavaScript 執行JS代碼

上面也簡單說了一下,JS是一個腳本語言,能夠在無需編譯的狀況下,直接輸入字符串JS代碼,直接運行執行看結果,這也是爲何在Chrome裏,在網頁運行的時候打開控制檯,能夠輸入各類JS指令的看結果的。

也就是說當Native想要調用JS的時候,能夠由Native把須要數據與調用的JS函數,經過字符串拼接成JS代碼,交給WebView進行執行

說明一下,Android/iOS-UIWebView/iOS-WKWebView,都支持這種方法,這是目前最普遍運用的方法,甚至能夠說,Chrome的DevTools控制檯也是用的一樣的方式。

假如JS網頁裏已經有了這麼一個函數

function calljs(data){
    console.log(JSON.parse(data)) 
    //1 識別客戶端傳來的數據
    //2 對數據進行分析,從而調用或執行其餘邏輯 
}
複製代碼

那麼客戶端此時要調用他須要在客戶端用OC拼接字符串,拼出一個js代碼,傳遞的數據用json

//不展開了,data是一個字典,把字典序列化
NSString *paramsString = [self _serializeMessageData:data];
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//要求必須在主線程執行JS
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只是一行js代碼,固然不管多長多複雜的js代碼均可以用這個方式讓webview執行

calljs('{data:xxx,data2:xxx}');
複製代碼

TIPS:安卓4.4以上纔可使用evaluatingJavaScript這個API

loadUrl 執行JS代碼

安卓在4.4之前是不能用evaluatingJavaScript這個方法的,所以以前安卓都用的是webview直接loadUrl,可是傳入的url並非一個連接,而是以"javascript:"開頭的js代碼,從而達到讓webview執行js代碼的做用

其實這個過程和evaluatingJavaScript沒啥差別

還按着剛纔舉例,假如JS網頁裏已經有了這麼一個函數

function calljs(data){
    console.log(JSON.parse(data)) 
    //1 識別客戶端傳來的數據
    //2 對數據進行分析,從而調用或執行其餘邏輯 
}
複製代碼

我不太熟悉安卓,就不寫安卓的字典數據json序列化的邏輯了

mWebView.loadUrl("javascript:callJS(\'{data:xxx,data2:xxx}\')");
複製代碼

最終實際上至關於執行了一條js代碼

calljs('{data:xxx,data2:xxx}');
複製代碼

WKUserScript 執行JS代碼

對於iOS的WKWebView,除了evaluatingJavaScript,還有WKUserScript這個方式能夠執行JS代碼,他們之間是有區別的

  • evaluatingJavaScript 是在客戶端執行這條代碼的時候馬上去執行當條JS代碼

  • WKUserScript 是預先準備好JS代碼,當WKWebView加載Dom的時候,執行當條JS代碼

很明顯這個雖然是一種通訊方式,但並不能隨時隨地進行通訊,並不適合選則做爲設計bridge的核心方案。但這裏也簡單介紹一下

//在loadurl以前使用
//time是一個時機參數,可選dom開始加載/dom加載完畢,2個時機進行執行JS
//構建userscript
WKUserScript *script = [[WKUserScript alloc]initWithSource:source injectionTime:time forMainFrameOnly:mainOnly];
WKUserContentController *userController = webView.userContentController;
//配置userscript
[userController addUserScript:script]
複製代碼

幾種通訊方式的優缺點對比

說完了JS主動調用Native,也說完了Native主動調用JS,有不少不少的方案咱們來聊聊這麼些個方案都有哪些侷限性,是否值得咱們選擇

假請求的通訊攔截的問題 -- 當下最不應選擇的通訊方式

假通訊攔截請求這種方式看起來是使用最普遍的,知名的WebViewJavascriptBridgecordova

爲何這些知名框架選用假請求通訊攔截其實有不少緣由,但我想說的是,基於眼下設計本身的Hybrid框架,最不該該選擇的通訊方式就是假請求通訊攔截

先說說他爲數很少的優勢:

  • 版本兼容性好:iOS6及之前只有這惟一的一種方式

cordova的前身是phonegap,隨手搜一下大概能知道這個框架有多老,也能夠看下WebViewJavascriptBridge,最先第一次提交是在5年前,在沒有iOS7的時候,有切只有這惟一的一種通訊方式,所以他們都選用了他,但看一眼如今已經iOS11了,再看看iOS6及如下的佔有度,呵呵,一到iOS7就有更好的全方位碾壓的bridge方式了

  • webview支持度好:簡單地說框架的開發者容易偷懶

這是全部JS call Native的通訊方式裏,惟一同時支持安卓webview/蘋果UIWebView/蘋果WKWebView的一種通訊方式,這也就是爲何WebViewJavascriptBridge在即使蘋果已經推出了更好的WKWebView而且準備了專屬的通訊APImessageHandler的時候,還依然選擇繼續沿用假請求通訊攔截的緣由,代碼不用重寫了,適合寫那種兼容iOS7如下的UIWebView,在iOS8以上換WKWebView的代碼,但看一眼如今的版本佔有度?沒有任何意義

多說兩句:

即使是老項目還在使用UIWebView,要計劃升級到WKWebView的時候,既然是升級就應該全面升級到新的WK式通訊,作什麼妥協和折中方案?

並且最重要的一點,想要作到同時支持多個WebView兼容支持並不須要選擇妥協方案,在開發框架的時候徹底能夠在框架側解決。想要屏蔽這種webview通訊差別,經過在Hybrid框架層設計,抽象統一的調用入口出口,把通訊差別在內部消化,這樣依然能作到統一對外業務代碼流程和清晰的代碼邏輯,想要作到代碼統一不該該以功能上犧牲和妥協的方面去考慮。

要知道cordova都專門爲WKWebView開發了獨有的cordova-plugin-wkwebview插件來專門適配WKWebView的更優的官方通訊API,而不是像WebViewJavascriptBridge進行妥協,UI與WK都採起同一種有功能性問題的通訊方案

再說說他最嚴重的缺點:

  • 丟消息! 一個通訊方案,結果他最大的問題是丟失通訊消息!
location.href = 'wakaka://wahahalalala/callNativeNslog?param=1111'

location.href = 'wakaka://wahahalalala/callNativeNslog?param=2222'
複製代碼

上面是一段JS調用Native的代碼,能夠靠字面意思猜一下,JS此時的訴求是在同一個運行邏輯內,快速的連續發送出2個通訊請求,用客戶端自己IDE的log,按順序打印111,222,那麼實際結果是222的通訊消息根本收不到,直接會被系統拋棄丟掉。

緣由:由於假跳轉的請求歸根結底是一種模擬跳轉,跳轉這件事情上webview會有限制,當JS連續發送多條跳轉的時候,webview會直接過濾掉後發的跳轉請求,所以第二個消息根本收不到,想要收到怎麼辦?JS裏將第二條消息延時一下

//發第一條消息
location.href = 'wakaka://wahahalalala/callNativeNslog?param=1111'

//延時發送第二條消息
setTimeout(500,function(){
    location.href = 'wakaka://wahahalalala/callNativeNslog?param=2222'
})
複製代碼

這根本治標不治本好麼,這種框架設計下決定了JS在任何通訊邏輯都得考慮是否這個時間有其餘的JS通訊代碼剛交互過,致使消息丟失?是否頁面加載完畢的時候不能同時發送頁面加載完畢其餘具體業務須要的Native消息,是否任何一個AJax網絡請求回來後馬上發起的Native消息,都要謹慎考慮與此同時是否有別的SetTimeout也在發Native消息致使衝突?這TM根本是一個天坑,這麼設計絕對是客戶端開發舒坦省事寫bridge框架代碼,坑死每天作活動上線的前端同窗的。

若是想繼續使用假跳轉請求,又不想換方案怎麼辦?前端同窗在JS框架層包一層隊列,全部JS代碼調用消息都先進入隊列並不馬上發送,而後前端會週期性好比500毫秒,清空flush一次隊列,保證在很快的時間內絕對不會連續發2次假請求通訊,這種通訊隊列的設計不光運用解決丟消息的問題,就連RN根本沒丟消息問題的JSCore式的通訊,也採用了這種方式,歸根結底他能減小通訊開銷,可是!可是!給假通訊請求作隊列你將面臨第二個根本無法解決的問題

  • URL長度限制

假跳轉請求歸根結底他仍是一個跳轉,拋給客戶端被攔截的時候都已經被封裝成一個request了,那麼若是url超長了呢?那麼這個request裏的url的內容仍是你想要傳遞的原內容麼?不會丟內容麼?尤爲是當你採用了隊列控制,一次性發送的是多條消息組成的數組數據的時候。

假跳轉是如今這個時候最不應使用的通訊方式!!!

假跳轉是如今這個時候最不應使用的通訊方式!!!

假跳轉是如今這個時候最不應使用的通訊方式!!!

重要的事情說三遍

彈窗攔截

這個方式其實沒啥很差的,並且confirm還能夠用更簡單的方式處理callback回調,由於confirm自然是須要返回JS內容的,但callback其實也能夠用其餘的方式實現,也許更好,所以這裏按住不表,第二篇文章會總體聊聊,基於這麼多種通訊手段,如何設計一個本身的Hybrid框架

  • UIWebView不支持,但沒事UIWebView有更好的JS上下文注入的方式,JSContext不只支持直接傳遞對象無需json序列化,還支持傳遞function函數給客戶端呢(藉助隱藏的API也能夠支持)
  • 安卓一切正常,不會出現丟消息的狀況
  • WKWebView一切正常,也不會出現丟消息的狀況,但其實WKWebView蘋果給了更好的API,何不用那個,至少用這個是能夠直接傳遞對象無需進行json序列化的

惟一須要注意的一點,若是你的業務開發中常常但願在前端代碼裏使用系統alert()/confirm()/prompt()那麼,你仍是挑一個不經常使用的進行hook,以避免干擾常規業務

修訂補充優勢!

彈窗攔截也能夠支持同步返回!

prompt( ) 攔截在客戶端須要執行confirm(data)從而用同步的方式給客戶端返回數據到JS

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

JS上下文注入

JS上下文注入其實一共3種狀況,這3種狀況每一個狀況都不一樣,我會一一進行優缺點說明

UIWebView的JSContext注入

說實話這是我以爲最完美的一種交互方式了,蘋果在iOS7開放了JavaScriptCore這個框架,支撐起了RN,Weex這麼牛逼的擺脫了WebView的深度混合框架,他的能力是最完美的。

牛逼的優勢:

  • 支持JS同步返回!

要知道咱們看到的全部JS通訊框架設計的都是異步返回,包括RN(這有設計緣由,但不表明JSC不支持同步返回),都是設計了一套callback機制,一條通訊消息到達Native後,若是須要返回數據,須要調用這個callback接口由Native反向通知JS,他們在JS側寫代碼但是差別很是很是很是之大的!

//同步JS調用Native JS這邊能夠直接寫= !!!
var nativeNetStatus = nativeObject.getNetStatus();

//異步JS調用Native JS只能這麼寫
nativeObject.getNetSatus(callback(net){
    console.log(net)
})
複製代碼
  • 支持直接傳遞對象,無需經過字符串序列化

一個JS對象在JS代碼中若是想經過假跳轉/彈窗攔截等方式,那麼必須把JS對象搞成json,而後才能傳遞給端,端拿到後還要反解成字典對象,而後才能識別,可是JS上下文注入不須要(其實他本質上是框架層幫你作了這件事情,就是JSValue這個iOS類的能力)

  • 支持傳遞JS函數,客戶端可以直接快速調用callback

在JS裏若是是一個function,能夠直接當作參數發送給客戶端,在客戶端獲得一個JSValue,能夠經過JSValue的callWithParmas的方式直接當作函數去調用

  • 支持直接注入任意客戶端類,客戶端對象,JS能夠直接向調用客戶端

JavaScriptCore有一種使用方法,是可讓任意iOS對象,遵循<JSExport>協議,就能夠直接把一整個Native對象直接注入,讓JS能夠直接操做這個對象,讀取這個對象的屬性,調用這個對象的方法

有點尷尬的缺點:

  • only UIWebView

這一點簡直是最大的遺憾,只有UIWebView能夠用KVC取到JSContext,取到了JSContext才能發揮JavaScriptCore的牛逼能力,可是若是爲了更好的性能升級到了WKWebView,那就得忍痛,我依稀記得曾幾什麼時候我在哪看到過經過私有API,讓WKWebView也能獲取JSContext,但我找不到了,但願知道的同窗能給我點指引。但我有一個見解 爲了WKWebView的性能提高,捨棄JSContext的優勢,值得!

  • JSContext獲取時機

UIWebView的JSContext是經過iOS的kvc方法拿到,而非UIWebView的直接接口API,所以UIWebView-JSContext注入使用上要很是注意注入時機

  • UIWebView-JSContext 在loadUrl以前注入無效
  • UIWebView-JSContext 在FinishLoad以後注入有效但有延遲

由於WebView每次加載一個新地址都會啓用一個新的JSContext,在loadUrl以前注入,會由於舊的JSContext已被捨棄致使注入無效,若在WebView觸發FinishLoad事件的時候注入,又會致使在FinishLoad以前執行的JS代碼,是沒法調用native通訊的

曾經寫過一篇文章UIWebView代碼注入時機與姿式,能夠參考看看,有私有API解決辦法,不在這裏多言

若是你還在使用UIWebView,真的應該完全丟棄什麼假跳轉,直接使用這個方案(iOS7.0如今已經不是門檻了吧),而且深度開發JavaScriptCore這麼多牛逼優點所帶來的一些黑科技(我感受會在第三篇文章裏提這個)

若是你還在使用UIWebView,就用JSContext吧!不要猶豫!

若是你還在使用UIWebView,就用JSContext吧!不要猶豫!

若是你還在使用UIWebView,就用JSContext吧!不要猶豫!

安卓的addJavascriptInterface注入

我不太瞭解安卓,所以這粗略寫一寫,此處若是有錯誤很是但願你們幫我指出

安卓的addJavascriptInterface注入,其實原理機制幾乎和UIWebView的JSContext注入同樣,因此UIWebView的JSContext注入的有點他其實都有

  • 能夠同步返回
  • 無需json化透傳數據
  • 能夠傳遞函數(不肯定)
  • 能夠注入Native對象

可是安卓的addJavascriptInterface沒有注入時機這個缺點(類比-UIWebView的JSContext獲取時機),緣由是UIWebView缺失一個時機由內核通知外圍,當前JSContext剛剛建立完畢,還未開始執行相關JS,致使在iOS下沒法在這個最應該進行注入的時機進行注入,除非經過私有API,但安卓沒事,安卓系統提供了個API來讓外圍得到這個最佳時機 onResourceloaded,詳細說明見 UIWebView代碼注入時機與姿式

WKWebView的scriptMessageHandler注入

蘋果iOS8以後官方抓們推出的新一代webview,號稱全面優化,性能大幅度提高,是和safari同樣的web內核引擎,帶着光環出生,而scriptMessageHandler正是這個新WKWebView欽點的交互API

優勢:

  • 無需json化傳遞數據

是的,webkit.messageHandlers.xxx.postMessage()是支持直接傳遞json數據,無需前端客戶端字符串處理的

  • 不會丟消息

咱們團隊的之前老代碼在丟消息上吃了無數的大虧,致使我對這個事情耿耿於懷,怨念極深!真是坑了好幾代前端開發,叫苦不堪

缺點:

  • 版本要求iOS8

咱們捨棄了,不是問題

  • 不支持JSContext那樣的同步返回

喪失了不少黑科技黑玩法的想象力!但我以爲仍是有可能有辦法哪怕用私有API的方式想辦法找回來的,但願知道的朋友提供更多信息

若是你已經上了WKWebView,就用它,不須要考慮

若是你已經上了WKWebView,就用它,不須要考慮

若是你已經上了WKWebView,就用它,不須要考慮

evaluatingJavaScript 直接執行JS代碼

說完了JS主動調用Native,咱們再說說Native主動調用JS,evaluatingJavaScript是一個很是很是通用廣泛的方式了,緣由也在介紹裏解釋過,js的腳本引擎自然支持,直接扔字符串進去,當作js代碼開始執行

也沒啥優缺點能夠說的,除了有個特性須要在介紹WKUserScript的時候在多解釋一下

安卓/UIWebView/WKWebView都支持

loadUrl 跳轉javascript地址執行JS代碼

具體的使用方式不詳細介紹了,直說一個優勢

  • 版本支持

在安卓4.4之前是沒有evaluatingJavaScript API的,所以經過他來執行JS代碼,但本質上和evaluatingJavaScript區別不大

WKUserScript 執行JS代碼

這裏要特別說明一下WKUserScript並不適合當作Hybrid Bridge的通訊手段,緣由是這種Native主動調用JS,只能在WebView加載時期發起,並不能在任意時刻發起通訊

WKUserScript不能採用在Hybrid設計裏當作通訊手段

WKUserScript不能採用在Hybrid設計裏當作通訊手段

WKUserScript不能採用在Hybrid設計裏當作通訊手段

但WKUserScript卻有一點值得說一下,上文也提到的UIWebView的注入時機,若是你想在恰當時機讓JS上下文執行一段JS代碼,在UIWebView你是找不到一個合適的加載時機的,除非你動用私有API,但WKWebView解決了這個問題,在構造WKUserScript的時候能夠選擇dom load start的時候執行JS,也能夠選擇在dom load end的時候執行JS。但這個有點其實與設計Hybrid框架的核心通訊方案,關係不大,但預加載JS預加載CSS也是一個Hybrid框架的擴展功能,後面第二篇會介紹的。

橫向對比

若是咱們要自主設計一個Hybrid框架,通訊方案到底該如何取捨?

JS主動調用Native的方案

通訊方案 版本支持 丟消息 支持同步返回 傳遞對象 注入原生對象 數據長度限制
假跳轉 全版本全平臺 會丟失 不支持 不支持 不支持 有限制
彈窗攔截 UIWebView不支持 不丟失 支持 不支持 不支持 無限制
JSContext注入 只有UIWebView支持 不丟失 支持 支持 支持 無限制
安卓interface注入 安卓全版本 不丟失 支持 支持 支持 無限制
MessageHandler注入 只有WKWebView支持 不丟失 不支持 不支持 不支持 無限制

Native主動調用JS的方案

  • iOS: evaluatingJavaScript
  • 安卓: 其實2個區別不大,使用方法差別也不大
    • 4.4以上 evaluatingJavaScript
  • 4.4如下 loadUrl

這樣對比優缺點,再根據本身項目須要支持的版本號,能夠比較方便的選擇合適的通訊方案,進一步親自設計一個Hybrid框架

一點我的見解

即使是老項目還在使用UIWebView,要計劃升級到WKWebView的時候,既然是升級就應該全面升級到新的WK式通訊,作什麼妥協和折中方案?

並且最重要的一點,想要作到同時支持多個WebView兼容支持並不須要選擇妥協方案,在開發框架的時候徹底能夠在框架側解決。想要屏蔽這種webview通訊差別,經過在Hybrid框架層設計,抽象統一的調用入口出口,把通訊差別在內部消化,這樣依然能作到統一對外業務代碼流程和清晰的代碼邏輯,想要作到代碼統一不該該以功能上犧牲和妥協的方面去考慮。

前面其實提到過這個見解不過說的還不完全,可能有些人會以爲假跳轉這個方案最大的好處是全平臺全版本的適配與統一,甚至還能夠統一安卓平臺,能夠保證代碼一致性,但我認爲這絕對不能創建在有嚴重功能短板致使開發中帶來很嚴重問題的基礎之上的,爲了代碼一致性,而妥協了框架的功能與能力

可能由於不一樣的平臺/不一樣的版本/不一樣的WebView的使用與兼容,致使了咱們須要在開發Hybrid框架的時候須要適配,但這一切都是能夠經過設計良好的框架對外輸入輸出,把全部區別適配內部消化,從而作到在框架外層的業務代碼依然保持代碼一致性,保持乾淨整潔的。這裏所說的框架毫不僅僅包括客戶端這一側,JS側也同理,誰說區分安卓和IOS平臺來進行不一樣的通訊方式代碼就不整潔了,那是你框架層設計的不夠優秀,合理框架層代碼應該能夠作到當新的系統組件出現,新的更優秀的通訊方案出現的時候,可以馬上的支持和擴充,得到最新的能力和性能,但又在業務上層作到無感知,保持框架外圍使用的一致性,這纔是良好的設計。

因此我以前微博曾經說過一小段話:

就爲了兼容從而選擇放棄更合理的WKWebview 官方注入interface方式,爲了湊和UIWebView依然採用不管是iframe仍是location.href的糊弄方式,這種我實在不以爲是美學,只是一種偷懶而已,抱着UIWebview時代的包袱不想丟還讓WKWebview去遷就

沒錯,說的就是WebViewJavascriptBridge

若是是你,你會怎麼設計Hybrid框架

聊了這麼多這個好好,若是換作咱們項目,我會選擇啥?

  • iOS:MessageHandler注入/Prompt彈框攔截(JSToNative) + evaluatingJavaScript (NativeToJS)

通過修正,異步返回採用MessageHandler 同步返回採用Prompt彈框攔截(JSToNative)

其實同步/異步在iOS上均可以採用 Prompt彈框攔截(JSToNative) 但畢竟MessageHandler是系統欽定API,而且擁有直接傳遞JSON對象,無需手動序列化這一優點,因此咱們依然選擇2個方案都用,一個用來異步,一個用來同步

但其實,你也能夠同步/異步都使用 Prompt彈框攔截

  • 安卓: 攔截彈窗(JSToNative)+loadUrl(NativeToJS)

咱們安卓還須要支持更低的版本╮(╯_╰)╭

安卓就直接把 Prompt彈框攔截 當作同步/異步都選擇的通訊方式

以上在設計Hybrid框架API的時候,都考慮了2種sendMessage模式的,一種異步,一種同步

通過各類優缺點對比,咱們確認了最核心的JS與Native通訊方案,下一步就是親自設計一個Hybrid框架了,這篇也太長了,挖個坑後面在寫吧

本篇參考文獻

因爲我不是很懂安卓,本篇不少安卓的信息來自我和同事之間的探討以及這篇文章

Android:你要的WebView與 JS 交互方式 都在這裏了

另外聊到UIWebView的JSContext ,扯了好多JS上下文時機的事情,詳細介紹在我本身的另外一篇文章裏

UIWebView代碼注入時機與姿式

系列相關文章

從零收拾一個hybrid框架(一)-- 從選擇JS通訊方案開始

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

從零收拾一個Hybrid框架(三)-- WebView 容器的一些腦洞方案思路探討 (挖坑ing)

相關文章
相關標籤/搜索