前言: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 為你的 WKWebView
、UIWebView
(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ì) UIWebView 和 WebView 做的一層封裝仔役,主要用來執(zhí)行 JS 代碼掷伙,以及實(shí)現(xiàn) UIWebView 和 WebView 的代理方法,并通過攔截 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)用 UIWebView
的 loadRequest:
方法加載 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ù)
- 在 JS 中創(chuàng)建一個(gè)
- 執(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
亲桦、 callbackId
和 handlerName
,最后根據(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ì)象嘹履,然后再取出 data
、 callbackId
和 handlerName
债热,最后根據(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è)responseId
從responseCallbacks
(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)建 UIWebView
和 WebViewJavascriptBridge
對(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ù)data
和callbackId
包裝成一個(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 之后揽乱,先將其解析成原生的字典名眉,然后再取出 data
、 callbackId
和 handlerName
凰棉,最后根據(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
受裹、 callbackId
和 handlerName
,最后根據(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ì)把 handlerName
,responseId
(也就是原生調(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è)支付的功能。
首先我們創(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è)頁面就需要分別交給子類 SCWebViewSpecialMessageHandlerA
和 SCWebViewSpecialMessageHandlerB
來處理掰派。
@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 管理的問題了。
完整示例代碼見這里俊性。
五略步、問題與討論
已知 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儡毕。在加載
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峰鄙。為什么
WebViewJavascriptBridge
中 JS 調(diào)用原生時(shí),把要傳給原生的數(shù)據(jù)放到 messageQueue 中太雨,再讓原生調(diào) JS 去取吟榴,而不是直接拼在 URL 后面?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)求被取消掉。回調(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 的做法是一樣的襟己。WebViewJavascriptBridge
中 web view 執(zhí)行 JS 腳本時(shí),為什么將其限制在主線程上牍陌?初始化的 JS 內(nèi)容(也就是
setupWebViewJavascriptBridge
函數(shù)的定義和調(diào)用)是放在 APP bundle 中好呢擎浴,還是放到服務(wù)器上讓 h5 自己去加載好呢?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)去。