[iOS 開發(fā)] WebViewJavascriptBridge 從原理到實(shí)戰(zhàn)

前言:iOS 開發(fā)中粉私,h5 和原生實(shí)現(xiàn)通信有多種方式, JSBridge 就是最常用的一種近零,各 JSBridge 類庫的實(shí)現(xiàn)原理大同小異诺核,這篇文章主要是針對(duì)當(dāng)前使用最為廣泛的 WebViewJavascriptBridge(v6.0.2),從功能 API秒赤、實(shí)現(xiàn)原理到源碼解讀猪瞬、最佳實(shí)踐憎瘸,做一個(gè)簡單介紹入篮。

目錄

  • 一、簡介
    • 1.設(shè)計(jì)目的
    • 2.特點(diǎn)
    • 3.安裝幌甘、導(dǎo)入
    • 4.API
  • 二潮售、實(shí)現(xiàn)原理
    • 1.目錄結(jié)構(gòu)
    • 2.主要流程
      • 2.1 初始化
      • 2.2 JS 調(diào)用原生
      • 2.3 原生調(diào)用 JS
      • 2.4 小結(jié)
  • 三、源碼解讀
  • 四锅风、最佳實(shí)踐
    • 1.JS 端的優(yōu)化
    • 2.Objective-C 端的優(yōu)化
  • 五酥诽、問題與討論
  • 六、延伸閱讀

長文警告:由于文章篇幅較長皱埠,如果你不需要了解太多細(xì)節(jié)的話肮帐,可以忽略掉第三部分『源碼解讀』,通過閱讀第二部分『實(shí)現(xiàn)原理』(含流程圖)就基本可以了解到整個(gè)核心流程了(大圖加載會(huì)比較慢,建議到電腦上閱讀)训枢。

一托修、簡介

1. 設(shè)計(jì)目的

我們平時(shí)使用 UIWebView 時(shí),原生和 JavaScript 的交互一般是通過以下兩種方式實(shí)現(xiàn)的:

  • Native to JavaScript:原生通過 -stringByEvaluatingJavaScriptFromString: 方法執(zhí)行一段 JavaScript
  • JavaScript to Native:在網(wǎng)頁中加載一個(gè) Custom URL Scheme 的鏈接(直接設(shè)置 window.location 或者新建一個(gè) iframe 去加載這個(gè) URL)恒界,原生中攔截 UIWebView 的代理方法 - webView:shouldStartLoadWithRequest:navigationType:睦刃,然后根據(jù)約定好的協(xié)議做相應(yīng)的處理

這兩種方式的弊端在于代碼過于松散,長而久之十酣,- webView:shouldStartLoadWithRequest:navigationType: 方法變得越來越臃腫雜亂涩拙,就像下面這樣:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *urlString = request.URL.absoluteString;
    NSDictionary *params = [[NSDictionary alloc] init];
    
    if (request.URL.query.length > 0) {
        params = [request.URL.query sc_URLParamKeyValues];
    }

    if ([urlString rangeOfString:@"app://share"].location != NSNotFound) {
           
          // 處理分享邏輯的代碼
          return NO;
    } else if ([urlString rangeOfString:@"app://getLocation"].location != NSNotFound) {
           // 獲取地理位置的代碼...
           // 回調(diào) JS
           [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:"setLocation('%@')", locationString]];
          return NO;

    }   else if  ...  
       // 幾十個(gè) else if
      else {
          return YES;
    }
          return YES;
}


WebViewJavascriptBridge 框架提供了一種更優(yōu)雅的方式,用來在 WKWebView耸采、UIWebView(iOS) 以及 WebView(OSX)中兴泥,建立一個(gè) Objective-C 和 JavaScript 之間互相“發(fā)送消息”的機(jī)制,讓我們可以在 JS 中像直接調(diào) JS 方法那樣發(fā)消息給 Objective-C虾宇,同時(shí)可以在 Objective-C 中像直接調(diào) Objective-C 方法那樣發(fā)消息給 JS郁轻。

2. 特點(diǎn)

  • Objective-C 中發(fā)送消息給 web view 中的 JavaScript
  • web view 中的 JavaScript 發(fā)送消息給 Objective-C
  • 不論是原生還是 JavaScript,發(fā)送消息的過程就像平時(shí)調(diào)用同一語言/環(huán)境的方法一樣簡單
  • 發(fā)送消息時(shí)不僅可以帶參數(shù)文留,還可以傳 callback 用于回調(diào)

3. 安裝

3.1 使用 pod 安裝
直接在 podfile 中加入下面這行代碼好唯,并執(zhí)行 pod install 命令:

pod 'WebViewJavascriptBridge', '~> 6.0'

3.2 手動(dòng)導(dǎo)入
在 WebViewJavascriptBridge 的 GitHub repository 上下載源碼后,從下載好的文件中將 WebViewJavascriptBridge 文件夾直接拖入你的工程中燥翅。

4. API

4.1 Objective-C API

// 為指定的 web view (WKWebView/UIWebView/WebView)創(chuàng)建一個(gè) JavaScript Bridge 
+ (instancetype)bridgeForWebView:(id)webView;
// 注冊(cè)一個(gè)名稱為 handlerName 的 handler 給 JavaScript 調(diào)用
// 當(dāng)在 JavaScript  中調(diào)用 WebViewJavascriptBridge.callHandler("handlerName")  時(shí)骑篙,該方法的 WVJBHandler 參數(shù)會(huì)收到回調(diào)
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
// 調(diào)用 JavaScript 中注冊(cè)過的 handler
// data 參數(shù)為調(diào)用 handler 時(shí)要傳遞給 JavaScript 的參數(shù),responseCallback 傳給 JavaScript 用來回調(diào)
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
// 如果你需要監(jiān)聽 web view 的代理方法的回調(diào)森书,可以通過該方法設(shè)置你的 delegate
- (void)setWebViewDelegate:(id)webViewDelegate;

4.2 JavaScript API

// 注冊(cè)一個(gè)  handler 給 Objective-C 調(diào)用
registerHandler(handlerName: String, handler: function);
// 調(diào)用 Objective-C 中注冊(cè)過的 handler
callHandler(handlerName: String);
callHandler(handlerName: String, data: undefined);
callHandler(handlerName: String, data: undefined, responseCallback: function);

5. 基本用法

5.1 導(dǎo)入頭文件靶端,聲明一個(gè) WebViewJavascriptBridge 屬性:

#import "WebViewJavascriptBridge.h"

...

@property WebViewJavascriptBridge* bridge;

5.2 為你的 WKWebViewUIWebView (iOS)或者WebView (OSX) 創(chuàng)建一個(gè) WebViewJavascriptBridge 對(duì)象:

self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView];

5.3 在 Objective-C 中注冊(cè) handler 和調(diào)用 JavaScript 中的 handler:

[self.bridge registerHandler:@"ObjC Echo" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"ObjC Echo called with: %@", data);
    responseCallback(data);
}];
[self.bridge callHandler:@"JS Echo" data:nil responseCallback:^(id responseData) {
    NSLog(@"ObjC received response: %@", responseData);
}];

5.4 復(fù)制下面的 setupWebViewJavascriptBridge 函數(shù)到你的 JavaScript 代碼中:

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

5.5 調(diào)用 setupWebViewJavascriptBridge 函數(shù)凛膏,使用 bridge 來注冊(cè) handler 和調(diào)用 Objective-C 中的 handler:

setupWebViewJavascriptBridge(function(bridge) {
    
    /* 在這里做一些初始化操作 */

    bridge.registerHandler('JS Echo', function(data, responseCallback) {
        console.log("JS Echo called with:", data)
        responseCallback(data)
    })
    bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
        console.log("JS received response:", responseData)
    })
})

二杨名、實(shí)現(xiàn)原理

1. 目錄結(jié)構(gòu)

類名 功能
WebViewJavascriptBridgeBase ① 用來進(jìn)行 bridge 初始化和消息處理的核心類;
② 這個(gè)類是在支持 WKWebView 后從 WebViewJavascriptBridge 中獨(dú)立出來的邏輯猖毫,專門用來處理 bridge 相關(guān)的邏輯台谍,不再與具體的 Web View 相關(guān)聯(lián)了
WebViewJavascriptBridge ① 橋接的入口,針對(duì)不同類型的 Web View (UIWebView吁断、WKWebView趁蕊、WebView)進(jìn)行分發(fā);
② 針對(duì) UIWebViewWebView 做的一層封裝仔役,主要用來執(zhí)行 JS 代碼掷伙,以及實(shí)現(xiàn) UIWebViewWebView的代理方法,并通過攔截 URL 來通知 WebViewJavascriptBridgeBase 做相應(yīng)操作
WKWebViewJavascriptBridge 針對(duì) WKWebView 做的一層封裝又兵,主要用來執(zhí)行 JS 代碼任柜,以及實(shí)現(xiàn) WKWebView 的代理方法,并通過攔截 URL 來通知 WebViewJavascriptBridgeBase 做相應(yīng)操作
WebViewJavascriptBridge_JS JS 端負(fù)責(zé)“收發(fā)消息”的代碼

2. 主要流程

說明WebViewJavascriptBridge 中雖然對(duì)不同類型的 Web View 做了不同的處理,但是核心邏輯還是一樣的宙地,為了簡單說明升熊,這里只討論 UIWebView 情況下的邏輯。

WebViewJavascriptBridge 參與交互的流程包括三個(gè)部分:初始化绸栅、JS 調(diào)用原生级野、原生調(diào)用 JS。

2.1 初始化

WebViewJavascriptBridge 的初始化分為兩部分粹胯,一部分是 Objective-C 中的 WebViewJavascriptBridge 對(duì)象的初始化蓖柔,另一部分是 JavaScript 中的 window.WebViewJavascriptBridge 的初始化。
最終的目標(biāo)是风纠, Objective-C 和 JavaScript 兩邊各有一個(gè) WebViewJavascriptBridge 對(duì)象况鸣,有了這兩個(gè)對(duì)象,兩邊都可以收發(fā)“消息”竹观,同時(shí)兩邊還各自維護(hù)一個(gè)管理響應(yīng)事件的 messageHandlers 容器镐捧、一個(gè)管理回調(diào)的 callbackId 容器。
所以臭增,我們這里討論的初始化懂酱,不單單是一個(gè)對(duì)象的初始化,而是一個(gè)完整的準(zhǔn)備過程誊抛,如下圖所示列牺。

[圖片上傳失敗...(image-72e695-1543845916599)]

[圖片上傳失敗...(image-c51257-1543845916599)]
(1) Objective-C 中的初始化

  • 初始化 UIWebView
  • 初始化 WebViewJavascriptBridge,設(shè)置 web view 代理
  • 初始化 WebViewJavascriptBridgeBase拗窃,初始化相關(guān)的屬性

(2) 注冊(cè) handler 供 JS 調(diào)用——把注冊(cè)過的 handler 保存起來

[self.bridge registerHandler:@"share" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSString *shareContent = [NSString stringWithFormat:@"標(biāo)題:%@\n 內(nèi)容:%@ \n url:%@",
                                  data[@"title"],
                                  data[@"content"],
                                  data[@"url"]];
         responseCallback(@"分享成功");
    }];

(3) Objective-C 中通過調(diào)用 UIWebViewloadRequest: 方法加載 URL

(4) 網(wǎng)頁一加載就會(huì)執(zhí)行 web 頁中的 bridge 初始化代碼瞎领,也就是調(diào)用上面提到的 setupWebViewJavascriptBridge(bridge)函數(shù)

  • 保存要執(zhí)行的自定義初始化函數(shù),比如注冊(cè) JS 中的 handler
  • 通過添加一個(gè) iframe 加載初始化鏈接 https://__bridge_loaded__

(5) 原生 WebViewJavascriptBridge 類中代理方法會(huì)攔截 https://__bridge_loaded__ 的加載

  • 在 web view 中執(zhí)行本地 WebViewJavascriptBridge_JS.m 文件中的代碼随夸,初始化 window.WebViewJavascriptBridge 對(duì)象:
    • 在 JS 中創(chuàng)建一個(gè) WebViewJavascriptBridge 對(duì)象九默,并設(shè)置成 window 的一個(gè)屬性
    • 定義幾個(gè)用于管理消息的全局變量
    • WebViewJavascriptBridge 對(duì)象定義幾個(gè)處理消息的方法和函數(shù)
  • 執(zhí)行原生端 startupMessageQueue 中保存的消息,也就是本地 JS 文件還未加載時(shí)就發(fā)送了的消息

(6) 初始化完畢

2.2 JS 調(diào)用原生

[圖片上傳失敗...(image-7f48b6-1543845916599)]

實(shí)際上宾毒,相比 原生調(diào)用 JS驼修,JS 調(diào)用原生的邏輯更婉轉(zhuǎn)瘪弓,對(duì)照上面的示意圖,我們可以把JS 調(diào)用原生的邏輯簡化成以下五個(gè)環(huán)節(jié):

  • JS 中調(diào)用 callHandler() 方法装悲,發(fā)消息給原生
  • 在 JS 中將參數(shù)和回調(diào)包裝成一個(gè) message
  • JS 通知原生到它那邊去取 message
  • 原生處理 message 中的數(shù)據(jù)
  • 原生回調(diào) JS

(1)JS 中調(diào)用 callHandler() 方法傻寂,發(fā)消息給原生

WebViewJavascriptBridge.callHandler(@"share",
                                    {title: "標(biāo)題"},
                                    function (response) {
                                       console.log(response);
                                    });

(2)在 JS 中將參數(shù)和回調(diào)包裝成一個(gè) message
把要調(diào)用的 handlerName、要傳給 native 的數(shù)據(jù) data 以及原生回調(diào) JS 的 responseCallback 對(duì)應(yīng)的 id 包裝成一個(gè) message术徊,然后再保存到一個(gè)全局的數(shù)組 sendMessageQueue 里面。

值得注意的是,那個(gè)用于處理原生回調(diào)的 responseCallback 是一個(gè)函數(shù)妨退,是不能直接傳給原生的,所以這里只傳了其對(duì)應(yīng)的 id,而 responseCallback 本身會(huì)被存到一個(gè)全局的 responseCallbacks 對(duì)象的屬性里面去咬荷,屬性名就是 responseCallback 對(duì)應(yīng)的 id冠句。原生回調(diào) JS 時(shí),就會(huì)根據(jù) id 從 responseCallbacks 對(duì)象中去取對(duì)應(yīng)的 callback幸乒。

(3)JS 通知原生到它那邊去取 message
在 iframe 中加載發(fā)送消息的 URL懦底,通知原生“我 JS 發(fā)消息給你了,麻煩你到信箱里查收一下”罕扎,原生中的 WebViewJavascriptBridge 就會(huì)在 webView 代理方法里面攔截到這個(gè)事件聚唐,然后再調(diào)用 JS,將 sendMessageQueue 中的 message 全部取出來腔召,然后轉(zhuǎn)成 JSON string 的形式杆查。

(4) 原生處理 message 中的數(shù)據(jù)
原生拿到轉(zhuǎn)為 JSON string 的 message 之后,先將其解析成原生的字典臀蛛,然后再取出 data亲桦、 callbackIdhandlerName,最后根據(jù) handlerName 從之前注冊(cè)過的 messageHandlers 里面取出對(duì)應(yīng)的 handler(block)浊仆,再調(diào)用這個(gè) handler客峭,第一個(gè)參數(shù)就是 data,第二個(gè)參數(shù)是根據(jù) callbackId 創(chuàng)建的 responseCallback(block)抡柿,然后原生就可以在 handler(block) 中處理接收到的 data 以及回調(diào) JS桃笙。

(5)原生回調(diào) JS
那么這個(gè)回調(diào) JS 的 responseCallback(block) 是怎么處理的呢?當(dāng)這個(gè) responseCallback 被回調(diào)時(shí)沙绝,在這個(gè) callback 中會(huì)創(chuàng)建一個(gè) message (NSDictionary)對(duì)象搏明,其中包含兩個(gè)字段,一個(gè)是 callbackId闪檬,另一個(gè)傳進(jìn) responseCallback 的參數(shù) data星著,然后再將這個(gè) message (NSDictionary)對(duì)象轉(zhuǎn)成 JSON String,最后調(diào)用 JS 中的 _handleMessageFromObjC(messageJSON)方法粗悯,同時(shí)將 JSON String 形式的 message 作為參數(shù)傳給 JS虚循,接下來 JS 就會(huì)通過 message 中的 callbackId 找出之前保存的 responseCallback,并把 message 中的 data 作為參數(shù)样傍,回調(diào)這個(gè) responseCallback横缔。至此,整個(gè) JS 調(diào)原生的流程就跑通了衫哥。

2.3 原生調(diào)用 JS

[圖片上傳失敗...(image-6df196-1543845916599)]

原生調(diào)用 JS 其實(shí)本身可以直接通過 web view 來執(zhí)行 JavaScript 腳本來實(shí)現(xiàn)的茎刚,但是 WebViewJavascriptBridge 提供了一個(gè)更貼近原生的方式。一是調(diào)用更規(guī)范撤逢,二是使用 block 的方式將調(diào)用與 JS 回調(diào)歸并到一起了膛锭,代碼邏輯更連貫粮坞。

與上面的 JS 調(diào)用原生恰好相反,原生調(diào)用 JS 時(shí)調(diào)用過程很簡單初狰,但是回調(diào)過程相對(duì)比較復(fù)雜莫杈。簡單來看,如上圖所示奢入,原生調(diào)用 JS 也可以分成以下幾步:

  • 原生通過調(diào)用 callHandler() 方法筝闹,發(fā)消息給 JS
  • 在原生中將參數(shù)和回調(diào)包裝成一個(gè) message
  • 原生直接調(diào)用 JS 函數(shù)將 message 傳給 JS
  • JS 回調(diào)原生

(1)原生通過調(diào)用 callHandler() 方法,發(fā)消息給 JS

[self.bridge callHandler:@"share" data:nil responseCallback:^(id responseData) {
        NSLog("收到來自 JS 的回調(diào)");
    }];

(2)在原生中將參數(shù)和回調(diào)包裝成一個(gè) message
跟 JS 調(diào)用原生類似腥光,原生調(diào)用 JS 時(shí)关顷,也是將要調(diào)用的 handlerName、要傳給 JS 的數(shù)據(jù) data 以及 JS 回調(diào)原生的 responseCallback 對(duì)應(yīng)的 id 包裝成一個(gè) message(NSDictionary)柴我,然后將這個(gè) message 對(duì)象轉(zhuǎn)成 JSON String解寝。接著再調(diào)用 JS 的 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON) 方法,把 JSON String 傳給 JS艘儒。

同樣值得注意的是聋伦,那個(gè)用于處理 JS 回調(diào)的 responseCallback 是一個(gè) block,也是不能直接傳給 JS 的界睁,所以這里只傳了其對(duì)應(yīng)的 id觉增,而 responseCallback(block) 本身會(huì)被存到一個(gè)全局的 responseCallbacks 字典里面去,key 值就是 responseCallback 對(duì)應(yīng)的 id翻斟。JS 回調(diào)原生時(shí)逾礁,就會(huì)根據(jù) id 從 responseCallbacks 字典中去取對(duì)應(yīng)的 block。

(3)原生直接調(diào)用 JS 函數(shù)將 message 傳給 JS
JS 拿到轉(zhuǎn)為 JSON string 形式的 message 之后访惜,先將其解析成 JS 對(duì)象嘹履,然后再取出 datacallbackIdhandlerName债热,最后根據(jù) handlerName 從之前注冊(cè)過的 messageHandlers 里面取出對(duì)應(yīng)的 handler 函數(shù)砾嫉,再執(zhí)行這個(gè) handler 函數(shù),第一個(gè)參數(shù)就是 data窒篱,第二個(gè)參數(shù)是根據(jù) callbackId 創(chuàng)建的 responseCallback(function)焕刮,然后 JS 就可以在 handler 函數(shù)中處理接收到的 data 以及回調(diào)原生。

(4)JS 回調(diào)原生
那么這個(gè)回調(diào)原生的 responseCallback(function) 是怎么處理的呢墙杯?與前面 JS 調(diào)原生時(shí)原生回調(diào) JS 的處理不太一樣配并,因?yàn)?JS 調(diào)原生是不能直接調(diào)的,所以當(dāng)這個(gè) responseCallback(function) 被回調(diào)時(shí)高镐,在這個(gè) function 中會(huì)用“發(fā)消息”的方式溉旋,直接走前面所提到的 JS 調(diào)原生的流程。
但這其中有兩點(diǎn)與 JS 直接調(diào)原生不太一樣的地方避消,一是 message 的內(nèi)容低滩,二是原生對(duì)這個(gè) message 的處理:

  • JS 在回調(diào)原生時(shí)召夹,會(huì)把 handlerName岩喷,responseId(也就是原生調(diào) JS 時(shí)傳過來的 callbackId) 和 responseData 包裝成一個(gè) message 對(duì)象恕沫。與 JS 直接調(diào)原生不同的是,這里的 message 對(duì)象沒有 JS 回調(diào)函數(shù)的 callbackId纱意,因?yàn)檫@里不需要原生再次回調(diào) JS 了婶溯。但是多了一個(gè) responseId,這是因?yàn)樵鷪?zhí)行 JS 的回調(diào)時(shí)偷霉,會(huì)根據(jù)這個(gè) responseIdresponseCallbacks(NSDictionary) 中去取對(duì)應(yīng)的 block迄委。
  • 當(dāng)原生收到并解析 JS 回調(diào)的消息后,會(huì)直接根據(jù) message 中的 responseId 找出之前保存的 responseCallback(block)类少,并把 message 中的 responseData 作為參數(shù)叙身,然后再回調(diào)這個(gè) responseCallback。**與 JS 直接調(diào)原生不同的是硫狞,這個(gè) responseCallback 只有一個(gè)參數(shù) data信轿,沒有用于再次回調(diào) JS 的 block 了。
    至此残吩,整個(gè)原生調(diào) JS 的流程就圓滿結(jié)束了财忽。

2.4 小結(jié)

一句話來概括的話,那就是——WebViewJavascriptBridge 以下面兩個(gè)方法為橋梁(以 UIWebView 為例):

  • 原生調(diào) JS:-stringByEvaluatingJavaScriptFromString:
  • JS 調(diào)原生:- webView:shouldStartLoadWithRequest:navigationType:

在 JS 和原生兩邊封裝了一套『方法調(diào)用』轉(zhuǎn)『消息發(fā)送』的機(jī)制泣侮,各自維護(hù)了一套注冊(cè)的方法列表即彪、回調(diào)函數(shù)列表,優(yōu)雅地解決了回調(diào)的問題活尊。(注:不要把這里的消息發(fā)送和 Objective-C 運(yùn)行時(shí)的消息發(fā)送混淆了)

三隶校、源碼解讀

這里只針對(duì)核心邏輯進(jìn)行分析,詳見 帶有注釋的源碼蛹锰。

對(duì)照上面的流程我們來看看 WebViewJavascriptBridge 的源碼具體是如何實(shí)現(xiàn)的深胳。

1. 初始化

(1) Objective-C 中的初始化
首先從創(chuàng)建 UIWebViewWebViewJavascriptBridge 對(duì)象開始:

@implementation ViewController
// 創(chuàng)建 web view
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:webView];


// 創(chuàng)建 WebViewJavascriptBridge 對(duì)象
self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
...
@end

WebViewJavascriptBridge+ bridgeForWebView: 方法在內(nèi)部針對(duì)不同的 web view 做了不同的邏輯處理,由于我這里使用的是 UIWebView宁仔,所以這里最終會(huì)調(diào)用 -_platformSpecificSetup: 方法:

@implementation WebViewJavascriptBridgeBase
...
// 創(chuàng)建 Bridge
+ (instancetype)bridgeForWebView:(id)webView {
    return [self bridge:webView];
}

+ (instancetype)bridge:(id)webView {
    
    // 為 WKWebView 初始化
#if defined supportsWKWebView
    if ([webView isKindOfClass:[WKWebView class]]) {
        return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
    }
#endif
    // 為  UIWebView(iOS) 或者 WebView(OSX) 初始化
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        WebViewJavascriptBridge* bridge = [[self alloc] init]; // 注意:這里用的不是 WebViewJavascriptBridge 而是 self
        [bridge _platformSpecificSetup:webView];  // 針對(duì)不同平臺(tái)進(jìn)行初始設(shè)置:保存 webView稠屠,并設(shè)置 webView 的 delegate,創(chuàng)建 WebViewJavascriptBridgeBase 對(duì)象翎苫,并設(shè)置 WebViewJavascriptBridgeBase 的 delegate
        return bridge;
    }
    [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
    return nil;
}
...
@end

然后在 -_platformSpecificSetup: 方法中會(huì)設(shè)置 web view 的代理為 self权埠,并且創(chuàng)建了一個(gè) WebViewJavascriptBridgeBase 對(duì)象,同時(shí)設(shè)置這個(gè)對(duì)象的代理為 self煎谍。其目的是為了:
① 監(jiān)聽 UIWebView 的代理方法回調(diào)攘蔽;
② 把橋接的核心邏輯交給 WebViewJavascriptBridgeBase 去處理;

@implementation WebViewJavascriptBridge
...
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
...
@end

WebViewJavascriptBridgeBase 在初始化時(shí)呐粘,會(huì)為原生端初始化幾個(gè)后面用于處理消息的變量:

@implementation WebViewJavascriptBridgeBase
...
- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];    // 用來保存 handler (一個(gè) `WVJBHandler` 類型的 block)的字典
        self.startupMessageQueue = [NSMutableArray array];          // 用來保存初始消息的數(shù)組
        self.responseCallbacks = [NSMutableDictionary dictionary];  // 用來保存回調(diào) JS 的 block 的字典(原生調(diào) JS 后满俗,JS 再回調(diào)原生時(shí)會(huì)用到)
        _uniqueId = 0;
    }
    return self;
}
...
@end

(2)通過調(diào)用 -registerHandler:handler: 方法转捕,注冊(cè) handler 供 JS 調(diào)用,這個(gè)方法會(huì)把要注冊(cè)的 handler(block)保存到 WebViewJavascriptBridgeBase對(duì)象的 messageHandlers(NSDictionary)屬性中去唆垃,當(dāng) JS 回調(diào)后五芝,就會(huì)根據(jù) handlerName 從這個(gè)變量中去取對(duì)應(yīng)的 handler:

@implementation WebViewJavascriptBridge
...
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy]; // 保存 handler
}
...
@end

(3) Objective-C 中通過調(diào)用 UIWebView-loadRequest: 方法加載 URL 后,網(wǎng)頁一加載就會(huì)執(zhí)行 web 頁中的 bridge 初始化代碼辕万,也就是調(diào)用 setupWebViewJavascriptBridge(bridge) 函數(shù):

<script type="text/JavaScript">
// 初始化 WebViewJavascriptBridge
function setupWebViewJavascriptBridge(callback) {
                
    // 只在第一次調(diào)用時(shí)不執(zhí)行枢步,為了防止重復(fù)加載 WebViewJavascriptBridge_JS.m
    if (window.WebViewJavascriptBridge) {
        return callback(WebViewJavascriptBridge);
    }
                
    // 保存 callback
    if (window.WVJBCallbacks) {
        return window.WVJBCallbacks.push(callback);
    }
    window.WVJBCallbacks = [callback];
                
    // 開啟一個(gè) iframe,加載這段 URL 'wvjbscheme://__BRIDGE_LOADED__'
    // 其目的是為了觸發(fā) WebViewJavascriptBridge_JS.m 文件內(nèi)容的加載
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(
        function() {
             document.documentElement.removeChild(WVJBIframe);
        }, 0);
}
            
    // 顯式調(diào)用 setupWebViewJavascriptBridge 方法渐尿,觸發(fā) WebViewJavascriptBridge 的初始化
    setupWebViewJavascriptBridge(
            
    function(bridge) {
     /* Initialize your app here */
                                           
        bridge.registerHandler("share", function(data, responseCallback) {
                                        var params = {'title':'測(cè)試分享的標(biāo)題','content':'測(cè)試分享的內(nèi)容','url':'http://www.baidu.com'};
                                                                responseCallback(params);
                                                                });
                                         }
            
            );
...
</script>

這里調(diào)用 setupWebViewJavascriptBridge() 函數(shù)時(shí)醉途,傳入的參數(shù)是一個(gè)帶有自定義初始化邏輯(比如 JS 中注冊(cè) handler)的 function,相當(dāng)于原生中的 block砖茸。
這個(gè) function setupWebViewJavascriptBridge() 函數(shù)主要做了兩件事情:

  • 將傳進(jìn)來的參數(shù)保存到 window.WVJBCallbacks 中隘擎,等到后面 JS 端的 bridge 初始化成功后,再取出來調(diào)用
  • 通過添加一個(gè) iframe 加載初始化鏈接 https://__bridge_loaded__凉夯,調(diào)起原生货葬,然后再移除這個(gè) iframe

(5) 原生中的 WebViewJavascriptBridge 對(duì)象中代理方法會(huì)攔截到 https://__bridge_loaded__ 的加載:

@implementation WebViewJavascriptBridge
...
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    
    if ([_base isWebViewJavascriptBridgeURL:url]) { // 是不是 Bridge 的 URL
        
        if ([_base isBridgeLoadedURL:url]) {  // 是不是第一次加載時(shí)的 URL
            
            [_base injectJavascriptFile];    // 注入 WebViewJavascriptBridge_JS 文件中的 JavaScript
            
        } else if ([_base isQueueMessageURL:url]) {  // 是不是發(fā)送消息給 Native 的 URL
            
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        } else {
            [_base logUnkownMessage:url];
        }
        return NO;
    } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}
...
@end

-webView:shouldStartLoadWithRequest:navigationType:方法中,首先會(huì)根據(jù) URL 的 scheme 來判斷這個(gè) URL 是不是跟 bridge 相關(guān)的 URL恍涂,然后再根據(jù) URL 的 host 來判斷是用來初始化 JS 中的 bridge 的(__bridge_loaded__)宝惰,還是用來發(fā)消息給原生的(__wvjb_queue_message__):


...
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__"

@implementation WebViewJavascriptBridgeBase
...

/// 判斷 URL 是不是跟 bridge 相關(guān)的
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
    if (![self isSchemeMatch:url]) {
        return NO;
    }
    return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}

/// scheme 是不是 wvjbscheme 或者 https
- (BOOL)isSchemeMatch:(NSURL*)url {
    NSString* scheme = url.scheme.lowercaseString;
    
    return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}

/// host 是不是 __wvjb_queue_message__,并且 scheme 也匹配
/// 用來判斷是不是發(fā)送消息給 Native 的 URL
- (BOOL)isQueueMessageURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}

/// host 是不是 __bridge_loaded__再沧,并且 scheme 也匹配
/// 用來判斷是不是第一次加載時(shí)的 URL
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
...
@end

此時(shí)的 URL 是 https://__bridge_loaded__尼夺,所以肯定是走 [_base injectJavascriptFile]; 的邏輯,這個(gè)方法主要是加載 WebViewJavascriptBridge_js.m 文件中的 JS:

/// 注入 JS 炒瘸,進(jìn)行一些初始化操作
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js(); // MARK: 為什么用這種方式加載淤堵,而不是寫成一個(gè) .js 文件再讀取呢?
    [self _evaluateJavascript:js];  // 執(zhí)行 WebViewJavascriptBridge_JS 文件中的 JavaScript
    
    // 如果隊(duì)列中有未發(fā)送的消息顷扩,馬上發(fā)給 JS
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

(6)在 web view 中執(zhí)行本地 WebViewJavascriptBridge_JS.m 文件中的代碼后拐邪,首先會(huì)初始化 window.WebViewJavascriptBridge 對(duì)象,并定義幾個(gè)方法和全局變量:

  • 在 JS 中初始化 WebViewJavascriptBridge 對(duì)象隘截,并設(shè)置成 window 的一個(gè)屬性
  • 定義幾個(gè)全局變量
    • sendMessageQueue:保存待發(fā)送消息的數(shù)組
    • messageHandlers:保存注冊(cè)過的 handler 的對(duì)象
    • responseCallbacks:保存 callback 的對(duì)象
    • uniqueId:保存 callback 時(shí)對(duì)應(yīng)的 id
  • 定義以下幾個(gè)方法
    • registerHandler(handlerName, handler):注冊(cè) hander
    • callHandler(handlerName, data, responseCallback):調(diào)用 handler
    • _fetchQueue():獲取待發(fā)送消息
    • disableJavscriptAlertBoxSafetyTimeout():讓OC可以關(guān)閉回調(diào)超時(shí)
    • _handleMessageFromObjC(messageJSON):調(diào)用 _dispatchMessageFromObjC處理來自 Objective-C 的消息
  • 定義兩個(gè)函數(shù)扎阶,給上面幾個(gè)方法調(diào)用
    • _doSend(message, responseCallback):將要發(fā)送的消息保存到 sendMessageQueue 中,同時(shí)加載 URL 調(diào)起原生
    • _dispatchMessageFromObjC(messageJSON):處理 Objective-C 中發(fā)來的消息
WebViewJavascriptBridge_JS.m
...
// 初始化 WebViewJavascriptBridge 對(duì)象
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

    var messagingIframe;
    var sendMessageQueue = [];  // 保存消息的數(shù)組
    var messageHandlers = {};   // 保存 handler 的對(duì)象
    
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';  // 發(fā)送消息的 URL scheme
    
    var responseCallbacks = {};  // 回調(diào)函數(shù)
    var uniqueId = 1;            // 保存 callback 的唯一標(biāo)識(shí)
    var dispatchMessagesWithTimeoutSafety = true;

    // 注冊(cè) handler 的方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    
    // 調(diào)用 Native handler 的方法
    function callHandler(handlerName, data, responseCallback) {
        // 如果只有兩個(gè)參數(shù)婶芭,并且第二個(gè)參數(shù)是 函數(shù)
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        
        // 發(fā)送消息給 Native
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
        
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
    
    // 發(fā)送消息給 Native
    // 一個(gè)消息包含一個(gè) handler 和 data东臀,以及一個(gè) callbackId
    // 因?yàn)?JavaScript 中的 callback 是函數(shù),不能直接傳給 Objective-C犀农,
    function _doSend(message, responseCallback) {
        
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();  // callbackId 的格式:cb + 唯一標(biāo)識(shí) id + 時(shí)間戳
            
            responseCallbacks[callbackId] = responseCallback;  // 保存 responseCallback 到 responseCallbacks 中去
            
            message['callbackId'] = callbackId;
        }
        
        sendMessageQueue.push(message); // 將要發(fā)送的消息保存到 sendMessageQueue 中
        
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;  // https://__wvjb_queue_message__
    }

    // 從消息隊(duì)列中拉取消息
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
    
        sendMessageQueue = [];
        return messageQueueString;
    }

    // 處理 Objective-C 中發(fā)來的消息
    function _dispatchMessageFromObjC(messageJSON) {
        
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        // 處理 Objective-C 中發(fā)來的消息
        function _doDispatchMessageFromObjC() {
            
            var message = JSON.parse(messageJSON);  // JSON 解析
            var messageHandler;
            var responseCallback;

            if (message.responseId) {  // 執(zhí)行 JavaScript 調(diào)用原生時(shí)的回調(diào)
                
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
                
            } else {  // 原生調(diào)用 JavaScript
                
                if (message.callbackId) {  // JavaScript 回調(diào) Native 的 callback
                    
                    var callbackResponseId = message.callbackId; // 取出原生傳過來的 callbackId
                    responseCallback = function(responseData) {
                        // 調(diào)用 _doSend 方法發(fā)送消息給 Native
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                var handler = messageHandlers[message.handlerName]; // 根據(jù) handlerName 取出 JavaScript 中的 handler
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    
                    handler(message.data, responseCallback);  // 調(diào)用 JavaScript 中的 handler
                }
            }
        }
    }
    
    // 處理 Objective-C 中發(fā)來的消息
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }
...

初始化 window.WebViewJavascriptBridge 對(duì)象之后惰赋,JS 端還會(huì)創(chuàng)建一個(gè) messagingIframe,用來加載 URL 發(fā)送消息給 Native:

// 創(chuàng)建 iframe呵哨,用來加載 URL 發(fā)送消息給 Native
    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;  // https://__wvjb_queue_message__
    document.documentElement.appendChild(messagingIframe);

最后赁濒,執(zhí)行 window.WVJBCallbacks 中的回調(diào)函數(shù)轨奄,也就是通過前面的 setupWebViewJavascriptBridge() 函數(shù)添加的用來初始化配置的 callback

    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

至此,整個(gè)初始化的過程就結(jié)束了拒炎,Objective-C 端和 JS 端各自有一個(gè) bridge 環(huán)境挪拟,可以收發(fā)“消息”,處理回調(diào)枝冀。

2. JS 調(diào)用原生

(1)JS 是以發(fā)消息的形式調(diào)用原生舞丛,發(fā)消息的過程包括三步:

  • JS 中調(diào)用 callHandler()方法耘子,傳入數(shù)據(jù)和回調(diào)函數(shù)果漾。
  • 緊接著為每個(gè) responseCallback 生成一個(gè)對(duì)應(yīng)的 callbackId,然后再將 handlerName谷誓、參數(shù) datacallbackId 包裝成一個(gè) message 對(duì)象绒障,存到全局?jǐn)?shù)組 sendMessageQueue 中。同時(shí)把 responseCallback 也保存到 responseCallbacks 對(duì)象中去捍歪,等原生回調(diào)時(shí)再取户辱。
  • 最后 JS 中加載發(fā)送消息的鏈接 https://__wvjb_queue_message__,通知原生到它那邊去取 message糙臼。
function callHandler(handlerName, data, responseCallback) {
        // 如果只有兩個(gè)參數(shù)庐镐,并且第二個(gè)參數(shù)是 函數(shù)
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        
        // 發(fā)送消息給 Native
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
// 發(fā)送消息給 Native
    // 一個(gè)消息包含一個(gè) handler 和 data,以及一個(gè) callbackId
    // 因?yàn)?JavaScript 中的 callback 是函數(shù)变逃,不能直接傳給 Objective-C必逆,
    function _doSend(message, responseCallback) {
        
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();  // callbackId 的格式:cb + 唯一標(biāo)識(shí) id + 時(shí)間戳
            
            responseCallbacks[callbackId] = responseCallback;  // 保存 responseCallback 到 responseCallbacks 中去
            
            message['callbackId'] = callbackId;
        }
        
        sendMessageQueue.push(message); // 將要發(fā)送的消息保存到 sendMessageQueue 中
        
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;  // https://__wvjb_queue_message__
    }

(2)原生在 -webView: shouldStartLoadWithRequest: navigationType: 方法中攔截到 https://__wvjb_queue_message__ 的加載,然后執(zhí)行 JS 腳本WebViewJavascriptBridge._fetchQueue(); 從 JS 拉取 message :

@ implementation WebViewJavascriptBridge
...
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
...
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
...

- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}
...
}
...
@end
@implementation WebViewJavascriptBridgeBase
...
/// 從消息隊(duì)列中拉取消息
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
...
@end

原生拿到轉(zhuǎn)為 JSON string 的 message 之后揽乱,先將其解析成原生的字典名眉,然后再取出 datacallbackIdhandlerName凰棉,最后根據(jù) handlerName 從之前注冊(cè)過的 messageHandlers 里面取出對(duì)應(yīng)的 handler(block)损拢,接著再調(diào)用這個(gè) handler,第一個(gè)參數(shù)就是 data撒犀,第二個(gè)參數(shù)是根據(jù) callbackId 創(chuàng)建的 responseCallback(block)福压,然后原生就可以在 handler(block) 中處理接收到的 data 以及回調(diào) JS:

@implementation WebViewJavascriptBridgeBase
...
- (void)flushMessageQueue:(NSString *)messageQueueString{

    // 解析消息隊(duì)列中的消息
    id messages = [self _deserializeMessageJSON:messageQueueString];
    
    for (WVJBMessage* message in messages) {
        if (![message isKindOfClass:[WVJBMessage class]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];
        
        NSString* responseId = message[@"responseId"];
        if (responseId) {   // JS 回調(diào)原生的處理
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
            
        } else {  // JS 直接調(diào)原生的處理
            
            // 1. JavaScript 中 callback 的轉(zhuǎn)換
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];  // 取出 JavaScript 中傳過來的 callbackId
            
            if (callbackId) {  // 有 JavaScript 回調(diào),將 callback 轉(zhuǎn)換為 block
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    // 回調(diào) JavaScript
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            // 2. 根據(jù) handlerName 取出對(duì)應(yīng)的 handler
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            // 3. 執(zhí)行 handler
            handler(message[@"data"], responseCallback);
        }
    }
}
...
@end

(3)原生回調(diào) JS
當(dāng)這個(gè) responseCallback 被回調(diào)時(shí)或舞,在這個(gè) callback(block) 中會(huì)創(chuàng)建一個(gè) message (NSDictionary)對(duì)象荆姆,其中包含兩個(gè)字段,一個(gè)是callbackId嚷那,另一個(gè)是傳進(jìn) responseCallback 的參數(shù) data胞枕,然后再將這個(gè) message (NSDictionary)對(duì)象轉(zhuǎn)成 JSON String,最后調(diào)用 JS 中的 _handleMessageFromObjC(messageJSON) 方法魏宽,同時(shí)將 JSON String 形式的 message 作為參數(shù)傳給 JS:

@implementation WebViewJavascriptBridgeBase
...
- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

// Objective-C 發(fā)消息給 JavaScript
- (void)_dispatchMessage:(WVJBMessage*)message {
    
    NSString *messageJSON = [self _serializeMessage:message pretty:NO]; // 將消息轉(zhuǎn)成 JSON string
    ...
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    [self _evaluateJavascript:javascriptCommand];
}

...
@end

接下來 JS 就會(huì)根據(jù) message 中的 callbackId 找出之前保存的 responseCallback腐泻,并把 message 中的 data 作為參數(shù)决乎,回調(diào)這個(gè) responseCallback

WebViewJavascriptBridge_JS.m
...
    // 處理 Objective-C 中發(fā)來的消息
      function _handleMessageFromObjC(messageJSON) {
          _dispatchMessageFromObjC(messageJSON);
    }
      function _doDispatchMessageFromObjC() {
               ...
               var message = JSON.parse(messageJSON);  // JSON 解析
               var messageHandler;
               var responseCallback;
               if (message.callbackId) {  // JavaScript 回調(diào) Native 的 callback
                    
                    var callbackResponseId = message.callbackId; // 取出原生傳過來的 callbackId
                    responseCallback = function(responseData) {
                        // 調(diào)用 _doSend 方法發(fā)送消息給 Native
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                var handler = messageHandlers[message.handlerName]; // 根據(jù) handlerName 取出 JavaScript 中的 handler
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    
                    handler(message.data, responseCallback);  // 調(diào)用 JavaScript 中的 handler
                }
              ...
        }

至此,整個(gè) JS 調(diào)原生的流程就結(jié)束了派桩。

3. 原生調(diào)用 JS

(1)JS 包裝并發(fā)送消息給 JS
JS 中首先調(diào)用 callHandler() 方法构诚,傳入要傳遞的數(shù)據(jù)和回調(diào)函數(shù):

@implementation WebViewJavascriptBridge
...
- (void)send:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:nil];
}

- (void)callHandler:(NSString *)handlerName {
    [self callHandler:handlerName data:nil responseCallback:nil];
}

- (void)callHandler:(NSString *)handlerName data:(id)data {
    [self callHandler:handlerName data:data responseCallback:nil];
}

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
...
@end

接著跟 JS 調(diào)用原生一樣,原生調(diào)用 JS 時(shí)铆惑,也是將要調(diào)用的 handlerName范嘱、要傳給 JS 的數(shù)據(jù) data 以及 JS 回調(diào)原生的 responseCallback 對(duì)應(yīng)的 id 包裝成一個(gè) message(NSDictionary),然后將這個(gè) message 對(duì)象轉(zhuǎn)成 JSON String员魏。接著再調(diào)用 JS 的 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON) 方法丑蛤,把 JSON String 傳給 JS:

@implementation WebViewJavascriptBridgeBase
...
// 原生發(fā)送消息給 JavaScript
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    
    if (data) {
        message[@"data"] = data;
    }
    
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];  // 保存  responseCallback
        message[@"callbackId"] = callbackId;  // 將 callbackId 傳給 JavaScript
    }
    
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    
    [self _queueMessage:message];
}

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

// 把消息傳給 JS
- (void)_dispatchMessage:(WVJBMessage*)message {
    
    NSString *messageJSON = [self _serializeMessage:message pretty:NO]; // 將消息轉(zhuǎn)成 JSON string
   ...
    
...
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[self _evaluateJavascript:javascriptCommand];
...
}
...
@end

(2)JS 處理原生發(fā)來的消息
WebViewJavascriptBridge._handleMessageFromObjC() 方法在內(nèi)部調(diào)用了 _dispatchMessageFromObjC() 方法,先將其解析成 JS 對(duì)象撕阎,然后再取出 data受裹、 callbackIdhandlerName,最后根據(jù) handlerName 從之前注冊(cè)過的 messageHandlers 里面取出對(duì)應(yīng)的 handler 函數(shù)虏束,再執(zhí)行這個(gè) handler 函數(shù)棉饶,第一個(gè)參數(shù)就是 data,第二個(gè)參數(shù)是根據(jù) callbackId 創(chuàng)建的 responseCallback(function)镇匀,然后 JS 就可以在 handler 函數(shù)中處理接收到的 data 以及回調(diào)原生:

WebViewJavascriptBridge_JS.m
// 處理 Objective-C 中發(fā)來的消息
    function _dispatchMessageFromObjC(messageJSON) {
        ...
        // 處理 Objective-C 中發(fā)來的消息
        function _doDispatchMessageFromObjC() {
            
            var message = JSON.parse(messageJSON);  // JSON 解析
            var messageHandler;
            var responseCallback;

            if (message.responseId) {  // 執(zhí)行 JavaScript 調(diào)用原生時(shí)的回調(diào)
                ...
            } else {  // 原生調(diào)用 JavaScript
                
                if (message.callbackId) {  // JavaScript 回調(diào) Native 的 callback
                    
                    var callbackResponseId = message.callbackId; // 取出原生傳過來的 callbackId
                    responseCallback = function(responseData) {
                        // 調(diào)用 _doSend 方法發(fā)送消息給 Native
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                var handler = messageHandlers[message.handlerName]; // 根據(jù) handlerName 取出 JavaScript 中的 handler
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    
                    handler(message.data, responseCallback);  // 調(diào)用 JavaScript 中的 handler
                }
            }
        }
    }
...
@end

(3)JS 回調(diào)原生
當(dāng)這個(gè) responseCallback(function) 被回調(diào)時(shí)照藻,在這個(gè) function 中會(huì)直接走前面所提到的 JS 調(diào)原生的流程——調(diào)用 _doSend() 函數(shù),傳入 handlerName汗侵、callbackResponseId和要傳給原生的數(shù)據(jù) responseData幸缕。然后 _doSend() 函數(shù)中會(huì)把 handlerNameresponseId(也就是原生調(diào) JS 時(shí)傳過來的 callbackId) 和 responseData 包裝成一個(gè) meassage 對(duì)象晃择,并保存到消息隊(duì)列中去冀值,接著再在 iframe 中加載鏈接 https://__wvjb_queue_message__ 觸發(fā) UIWebView 代理方法的回調(diào)。

原生收到代理回調(diào)時(shí)宫屠,就會(huì)執(zhí)行 JS 腳本 WebViewJavascriptBridge._fetchQueue(); 列疗,到 JS 環(huán)境中去取消息,取到消息(JSON string)后浪蹂,再將其解析為 Objective-C 對(duì)象抵栈。

這些 JS 調(diào)原生的代碼前面已經(jīng)有了,就不再重復(fù)貼出來了坤次。最后古劲,Objective-C 中拿到解析過的 message 對(duì)象后,根據(jù) responseId 取出之前存過的對(duì)應(yīng)的 responseCallback(block)進(jìn)行回調(diào)缰猴,參數(shù)就是 message 中的 responseData

/// 處理 JavaScript 消息隊(duì)列中的消息产艾,發(fā)送給 Objective-C 方
- (void)flushMessageQueue:(NSString *)messageQueueString{
    ...
    
    // 解析消息隊(duì)列中的消息
    id messages = [self _deserializeMessageJSON:messageQueueString];
    
    for (WVJBMessage* message in messages) {
        
        NSString* responseId = message[@"responseId"];
        if (responseId) {   // JS 回調(diào)原生的處理
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
            
        } else {
            ...
        }
    }
}

到此為止,原生調(diào)用 JS 的邏輯就結(jié)束了。

四闷堡、最佳實(shí)踐

在了解了 WebViewJavascriptBridge 的基本使用和原理之后隘膘,我們就可以更優(yōu)雅地更靈活地使用這個(gè)工具了。

我們平時(shí)在實(shí)際開發(fā)中杠览,更多的還是 JS 調(diào)原生的情況弯菊,比如調(diào)用原生分享、獲取地理位置信息踱阿,等等管钳。借助 WebViewJavascriptBridge,我們可以直接在原生項(xiàng)目中注冊(cè) handler软舌,然后在 h5 中 call handler才漆,這樣比之前 Custom URL Scheme 的形式更易于使用和維護(hù)。

但是葫隙,這里仍然有兩個(gè)問題需要考慮一下:

  • JS 調(diào)用原生時(shí)栽烂,每次還是需要寫一長串的 WebViewJavascriptBridge.callHandler(handlerName, data, callback);,我們能不能做到像直接調(diào) JS 方法那樣簡單直接恋脚?比如像這樣,share(data, callback);焰手。
  • 在原生 App 中糟描,我們一般會(huì)定義一個(gè) WebViewController專門用來加載 h5,然后我們會(huì)在這個(gè)類中注冊(cè)所有的 handler书妻,一開始只有少量的幾個(gè) handler船响,一切 OK,但是躲履,隨著時(shí)間推移见间,業(yè)務(wù)不斷發(fā)展,handler 會(huì)越來越多工猜,而且有些不同的頁面需要注冊(cè)不同的 handler米诉,最終會(huì)導(dǎo)致這個(gè) WebViewController.m 文件變得越來越龐大——Massive View Controller(MVC)。所以篷帅,我們期望的是,要處理的 handler 應(yīng)該分成兩類,一類 handler 是通用的省骂,大部分頁面都需要支持止吐,比如,調(diào)用原生的分享箭昵,而另一部分 handler 就是各個(gè)頁面自己特有的邏輯税朴,比如調(diào)用原生的支付。

1. 先來看看第一個(gè)問題,如何實(shí)現(xiàn)以下形式的轉(zhuǎn)換:

WebViewJavascriptBridge.callHandler('share', data, callback);
===> share(data, callback);

不論怎樣正林,因?yàn)?WebViewJavascriptBridge.callHandler() 方法是必須要調(diào)用的茧跋,所以我們能想到的是,在別的方法內(nèi)部調(diào)用這個(gè)方法卓囚。要想通過調(diào)用 share 方法來實(shí)現(xiàn)這個(gè)目標(biāo)瘾杭,那就得先定義一個(gè)對(duì)象來保存這個(gè)方法。
因此哪亿,我們可以定義一個(gè)全局對(duì)象 MyApp粥烁,然后給 MyApp 對(duì)象定義一個(gè)方法 share(),然后再在其內(nèi)部調(diào)用 WebViewJavascriptBridge.callHandler() 方法蝇棉。

var handlerNames = new Array("share", "requestLocation");

for (var i in handlerNames) {
    var handlerName = handlerNames[i];

    MyApp[handlerName] = function(tempHandlerName) {
        return function(data, callback) {

            if (typeof data == "function") { // 意味著沒有參數(shù) data讨阻,只有一個(gè)參數(shù) callback

                bridge.callHandler(tempHandlerName, null, data);

            } else if (callback == null) { // 第二個(gè)參數(shù) callback 為 null 或者只有第一個(gè)參數(shù) data

                bridge.callHandler(tempHandlerName, data);

            } else { // 兩個(gè)參數(shù)都有

                bridge.callHandler(tempHandlerName, data, callback);
            }
        }
    }(handlerName);

};

有了上面的“轉(zhuǎn)換”后,我們?cè)?JS 中就可以以下幾種形式調(diào)用 handler 了:

MyApp.functionName(data, callback);     // 有參數(shù)篡殷,有回調(diào)
MyApp.functionName(data);               // 有參數(shù)钝吮,沒有回調(diào)
MyApp.functionName(callback);           // 沒有參數(shù),有回調(diào)
MyApp.functionName();                   // 沒有參數(shù)板辽,也沒有回調(diào)

比如要調(diào)用分享接口奇瘦,直接這樣調(diào)用就行了:

MyApp.share({
  title: '標(biāo)題',
  subtitle: '副標(biāo)題',
  image: 'http://yhouse_logo.png',
  content: '內(nèi)容'
}, 
function (responseData) {
  var status = response.statusCode;  // 0-失敗,1-成功劲弦,2-取消
});

還有個(gè)問題是耳标,這段代碼應(yīng)該在什么時(shí)候執(zhí)行呢?前面我們提到 WebViewJavascriptBridge 提供了 setupWebViewJavascriptBridge() 函數(shù)用于初始化邑跪,所以次坡,我們可以在這個(gè)函數(shù)中進(jìn)行上面的“轉(zhuǎn)換”。

2. 接下來再來看看原生這邊的問題画畅,原生如何按照不同頁面所需來管理 handler 呢砸琅?

我這里采用的是“基礎(chǔ)API+特定API”的方式,首先需要定義一個(gè)基礎(chǔ)的 handler processor轴踱,用來管理基礎(chǔ) API 的調(diào)用症脂,然后在針對(duì)其余一些有特殊邏輯的頁面,基于這個(gè) basic handler processor 定義對(duì)應(yīng)的 special handler processor寇僧。

另外一個(gè)問題是摊腋,在打開 WebViewController 時(shí),如何根據(jù)不同頁面創(chuàng)建對(duì)應(yīng)的 handler processor 呢嘁傀?一個(gè)可行的方式是給每個(gè) 有特殊邏輯的頁面?zhèn)魅胍粋€(gè) pageId 屬性兴蒸,沒有特殊邏輯的頁面 pageId 默認(rèn)為空,WebViewController 維護(hù)一張 pageId-handlerProcess 的關(guān)系映射表细办,初始化后再根據(jù)這個(gè) pageId 去創(chuàng)建對(duì)應(yīng)的 handler processor 類橙凳。

page handlerProcessor
page_id_1 handlerProcessor_1(Based on basicHandlerProcessor)
page_id_2 handlerProcessor_2(Based on basicHandlerProcessor)
... ... + baseHandlerProcessor

以下面的 3 個(gè)頁面為例蕾殴,這 3 個(gè)頁面是在同一個(gè) WebViewController 中加載的,其中頁面 1 中只有兩個(gè)基礎(chǔ)功能:分享和獲取地理位置岛啸,頁面 2 中相比頁面 1 多了一個(gè) 撥打電話的功能钓觉,頁面 3 中相比頁面 1 多了一個(gè)支付的功能。

Example_01

Example_02

Example_03

首先我們創(chuàng)建一個(gè)管理公共 API 的 handler processor SCWebViewMessageHandler

@interface SCWebViewMessageHandler : NSObject


@property (weak, nonatomic) SCWebViewController *controller;

/// 注冊(cè) handler
- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge;


/// 要注冊(cè)的特定 handler name坚踩,子類重寫
- (NSArray *)specialHandlerNames;


@end

@implementation SCWebViewMessageHandler

- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge {
    
    NSMutableArray *handlerNames = @[@"requestLocation", @"share"].mutableCopy;

    [handlerNames addObjectsFromArray:[self specialHandlerNames]];
    
    for (NSString *aHandlerName in handlerNames) {
        [bridge registerHandler:aHandlerName handler:^(id data, WVJBResponseCallback responseCallback) {
            
            NSMutableDictionary *args = [NSMutableDictionary dictionary];
            
            if ([data isKindOfClass:[NSDictionary class]]) {
                [args addEntriesFromDictionary:data];
            }
            
            if (responseCallback) {
                [args setObject:responseCallback forKey:@"responseCallback"];
            }
            
            
            NSString *ObjCMethodName = [aHandlerName stringByAppendingString:@":"];
            
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:NSSelectorFromString(ObjCMethodName) withObject:args];
#pragma clang diagnostic pop
            
        }];
    }
}

- (NSArray *)specialHandlerNames {
    return @[];
}


#pragma mark - Handler Methods

// 獲取地理位置信息
- (void)requestLocation:(NSDictionary *)args {
    WVJBResponseCallback responseCallback = args[@"responseCallback"];
    
    if (responseCallback) {
        
        responseCallback(@"上海市浦東新區(qū)張江高科");
    }
}

// 分享
- (void)share:(NSDictionary *)args {
    
    NSString *shareContent = [NSString stringWithFormat:@"標(biāo)題:%@\n 內(nèi)容:%@ \n url:%@",
                              args[@"title"],
                              args[@"content"],
                              args[@"url"]];
    [self.controller showAlertViewWithTitle:@"調(diào)用原生分享菜單" message:shareContent];
}

@end

這個(gè)類中主要干三件事荡灾,一是獲取所有要注冊(cè)的 handler name,并注冊(cè)這些 handler瞬铸;二是通過在 handler回調(diào)時(shí)批幌,通過 runtime 調(diào)用與 handler 同名的 Objective-C 方法,參數(shù)只有一個(gè) args嗓节,args 中包括兩部分荧缘,一部分是 JS 傳過來的 data,另一部分是回調(diào) JS 的 responseCallback拦宣。

子類可以繼承該類截粗,通過重寫 -specialHandlerNames 方法添加一些特定的 handler name,另外就是實(shí)現(xiàn) handler 對(duì)應(yīng)的 Objective-C 方法鸵隧。

因此绸罗,第一個(gè)頁面的 handler 可以交給 SCWebViewMessageHandler 處理,第二個(gè)頁面和第三個(gè)頁面就需要分別交給子類 SCWebViewSpecialMessageHandlerASCWebViewSpecialMessageHandlerB 來處理掰派。

@interface SCWebViewSpecialMessageHandlerA : SCWebViewMessageHandler

@end

@implementation SCWebViewSpecialMessageHandlerA

- (NSArray *)specialHandlerNames {
    return @[
             @"makeACall"
             ];
}

- (void)makeACall:(NSDictionary *)args {
    [self.controller showAlertViewWithTitle:@"撥打電話" message:args[@"number"]];
}

@end

@interface SCWebViewSpecialMessageHandlerB : SCWebViewMessageHandler
@end

@implementation SCWebViewSpecialMessageHandlerB

- (NSArray *)specialHandlerNames {
    return @[@"pay"];
}

- (void)pay:(NSDictionary *)args {
    NSString *paymentInfo = [NSString stringWithFormat:@"支付方式:%@\n價(jià)格:%@", args[@"type"], args[@"price"]];
    [self.controller showAlertViewWithTitle:@"去支付" message:paymentInfo];
}
@end

定義好了這幾個(gè)處理 handler 的類之后从诲,我們就可以在 WebViewController 中進(jìn)行相關(guān)的配置了:

- (void)viewDidLoad {
...
// 根據(jù) pageId,獲取對(duì)應(yīng)的 MessageHandler

// 注冊(cè) handler
    SCWebViewMessageHandler *handler = [[self.messageHandlerClass alloc] init];
    handler.controller = self;
    [handler registerHandlersForJSBridge:self.bridge];
...
}

到此為止靡羡,我們就解決了原生中 handler 管理的問題了。
完整示例代碼見這里俊性。

五略步、問題與討論

  1. 已知 bug:在 WKWebView 中使用時(shí),一旦 - webView:decidePolicyForNavigationAction:decisionHandler: 方法被調(diào)用定页,就會(huì)出現(xiàn)連續(xù)回調(diào)兩次 decisionHandler 的問題趟薄。
    首先,邏輯上講典徊,跟 UIWebView 類似杭煎,- webView:decidePolicyForNavigationAction:decisionHandler: 方法中的攔截只應(yīng)該回調(diào)一次 decisionHandler 即可。
    另外卒落,這個(gè)問題還會(huì)導(dǎo)致應(yīng)用在 iOS11 + XCode9 的環(huán)境下出現(xiàn)崩潰羡铲。解決辦法見相關(guān) Pull Request #296,期待 maintainer 能夠早點(diǎn) merge儡毕。

  2. 在加載 WebViewJavascriptBridge_JS 中的 JS 時(shí)也切,就會(huì)在創(chuàng)建 messagingIframe 的同時(shí),加載 https://__wvjb_queue_message__
    實(shí)際上這個(gè)時(shí)候 sendMessageQueue 數(shù)組肯定是空的雷恃,也就是說完全不需要發(fā)消息疆股,那為什么還要這么做呢?
    就想問題中所說的倒槐,這個(gè)時(shí)候 sendMessageQueue 數(shù)組肯定是空的旬痹,因?yàn)檫@個(gè)文件加載了,h5 中才會(huì)有 WebViewJavascriptBridge 對(duì)象讨越,所以两残,理論上來講,根本就不存在在這個(gè)文件加載前就調(diào)用了 WebViewJavascriptBridge.callHandler() 方法的情況谎痢。
    因此磕昼,這里的原因肯定不是并不像有些朋友說的“跟 WebViewJavascriptBridgeBase 中的 startupMessageQueue一樣,就是在 JavaScript 環(huán)境初始化完成以后节猿,把 JavaScript 要發(fā)送給 OC 的消息立即發(fā)送出去”票从。
    通過查找原來版本的提交記錄,終于找到了真正的原因滨嘱,具體見相關(guān) commit峰鄙。

  3. 為什么 WebViewJavascriptBridge 中 JS 調(diào)用原生時(shí),把要傳給原生的數(shù)據(jù)放到 messageQueue 中太雨,再讓原生調(diào) JS 去取吟榴,而不是直接拼在 URL 后面?

  4. WebViewJavascriptBridge 中加載 URL 調(diào)起原生時(shí)囊扳,為什么不是用 window.location="https://xxx" 這種形式吩翻,而是新添加一個(gè) iframe 來加載這個(gè) URL?
    因?yàn)槿绻?dāng)前頁面正在加載時(shí)锥咸,就有用戶操作導(dǎo)致 window.location="https://xxx" 被觸發(fā)狭瞎,這樣會(huì)使當(dāng)前頁面中還未加載完成的請(qǐng)求被取消掉。

  5. 回調(diào)的處理
    其實(shí)在 JS 與 Objective-C 通信時(shí)搏予,互相傳參數(shù)并不難熊锭,比較難處理的就是回調(diào)的處理,WebViewJavascriptBridge 采用的策略是雪侥,call 的時(shí)候只傳 id碗殷,callback 本身不傳,它在 JS 和 Objective-C 兩邊速缨,各自維護(hù)一個(gè) callback 表锌妻,每個(gè) callback 對(duì)應(yīng)一個(gè) id,回調(diào)的時(shí)候就根據(jù)這個(gè) id 去取對(duì)應(yīng)的 callback鸟廓。
    在這一點(diǎn)上从祝,跟 React Native 的做法是一樣的襟己。

  6. WebViewJavascriptBridge 中 web view 執(zhí)行 JS 腳本時(shí),為什么將其限制在主線程上牍陌?

  7. 初始化的 JS 內(nèi)容(也就是 setupWebViewJavascriptBridge函數(shù)的定義和調(diào)用)是放在 APP bundle 中好呢擎浴,還是放到服務(wù)器上讓 h5 自己去加載好呢?

  8. JS 中的閉包作用域問題
    在一開始毒涧,為了能實(shí)現(xiàn) MyApp.share(data, callback) 的效果贮预,我嘗試了下面的這種做法:

var handlerNames = new Array("share", "requestLocation");

for (var i in handlerNames) {
    var handlerName = handlerNames[i];

    MyApp[handlerName] = Myfunction() {

            if (typeof data == "function") { // 意味著沒有參數(shù) data,只有一個(gè)參數(shù) callback

                bridge.callHandler(handlerName, null, data);

            } else if (callback == null) { // 第二個(gè)參數(shù) callback 為 null 或者只有第一個(gè)參數(shù) data

                bridge.callHandler(handlerName, data);

            } else { // 兩個(gè)參數(shù)都有

                bridge.callHandler(handlerName, data, callback);
            }
          }

};

但是契讲,與 Objective-C 中的 block 不同仿吞,這里的閉包并沒有將外面的 handlerName copy 進(jìn)去。

六捡偏、延伸閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末唤冈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子银伟,更是在濱河造成了極大的恐慌你虹,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彤避,死亡現(xiàn)場離奇詭異傅物,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)琉预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門董饰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圆米,你說我怎么就攤上這事卒暂。” “怎么了娄帖?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵介却,是天一觀的道長。 經(jīng)常有香客問我块茁,道長,這世上最難降的妖魔是什么桂肌? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任数焊,我火速辦了婚禮,結(jié)果婚禮上崎场,老公的妹妹穿的比我還像新娘佩耳。我一直安慰自己,他們只是感情好谭跨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布干厚。 她就那樣靜靜地躺著李滴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛮瞄。 梳的紋絲不亂的頭發(fā)上所坯,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音挂捅,去河邊找鬼芹助。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闲先,可吹牛的內(nèi)容都是我干的状土。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼伺糠,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蒙谓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起训桶,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤累驮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后渊迁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慰照,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年琉朽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了毒租。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡箱叁,死狀恐怖墅垮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耕漱,我是刑警寧澤算色,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站螟够,受9級(jí)特大地震影響灾梦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妓笙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一若河、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寞宫,春花似錦萧福、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膏燕。三九已至,卻和暖如春悟民,著一層夾襖步出監(jiān)牢的瞬間坝辫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工逾雄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阀溶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓鸦泳,卻偏偏與公主長得像银锻,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子做鹰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容