前言:iOS 开发中,h5 和原生实现通信有多种方式, JSBridge 就是最常用的一种,各 JSBridge 类库的实现原理大同小异,这篇文章主要是针对当前使用最为广泛的 WebViewJavascriptBridge(v6.0.2),从功能 API、实现原理到源码解读、最佳实践,做一个简单介绍。
目录
- 一、简介
- 1.设计目的
- 2.特点
- 3.安装、导入
- 4.API
- 二、实现原理
- 1.目录结构
- 2.主要流程
- 2.1 初始化
- 2.2 JS 调用原生
- 2.3 原生调用 JS
- 2.4 小结
- 三、源码解读
- 四、最佳实践
- 1.JS 端的优化
- 2.Objective-C 端的优化
- 五、问题与讨论
- 六、延伸阅读
长文警告:由于文章篇幅较长,如果你不需要了解太多细节的话,可以忽略掉第三部分『源码解读』,通过阅读第二部分『实现原理』(含流程图)就基本可以了解到整个核心流程了(大图加载会比较慢,建议到电脑上阅读)。
一、简介
1. 设计目的
我们平时使用 UIWebView
时,原生和 JavaScript 的交互一般是通过以下两种方式实现的:
- Native to JavaScript:原生通过
-stringByEvaluatingJavaScriptFromString:
方法执行一段 JavaScript - JavaScript to Native:在网页中加载一个 Custom URL Scheme 的链接(直接设置 window.location 或者新建一个 iframe 去加载这个 URL),原生中拦截
UIWebView
的代理方法- webView:shouldStartLoadWithRequest:navigationType:
,然后根据约定好的协议做相应的处理
这两种方式的弊端在于代码过于松散,长而久之,- webView:shouldStartLoadWithRequest:navigationType:
方法变得越来越臃肿杂乱,就像下面这样:
1 |
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { |
WebViewJavascriptBridge
框架提供了一种更优雅的方式,用来在 WKWebView
、UIWebView
(iOS) 以及 WebView
(OSX)中,建立一个 Objective-C 和 JavaScript 之间互相“发送消息”的机制,让我们可以在 JS 中像直接调 JS 方法那样发消息给 Objective-C,同时可以在 Objective-C 中像直接调 Objective-C 方法那样发消息给 JS。
2. 特点
- Objective-C 中发送消息给 web view 中的 JavaScript
- web view 中的 JavaScript 发送消息给 Objective-C
- 不论是原生还是 JavaScript,发送消息的过程就像平时调用同一语言/环境的方法一样简单
- 发送消息时不仅可以带参数,还可以传 callback 用于回调
3. 安装
3.1 使用 pod 安装
直接在 podfile 中加入下面这行代码,并执行 pod install
命令:
1 |
pod 'WebViewJavascriptBridge', '~> 6.0' |
3.2 手动导入
在 WebViewJavascriptBridge 的 GitHub repository 上下载源码后,从下载好的文件中将 WebViewJavascriptBridge
文件夹直接拖入你的工程中。
4. API
4.1 Objective-C API
1 |
// 为指定的 web view (WKWebView/UIWebView/WebView)创建一个 JavaScript Bridge |
1 |
// 注册一个名称为 handlerName 的 handler 给 JavaScript 调用 |
1 |
// 调用 JavaScript 中注册过的 handler |
1 |
// 如果你需要监听 web view 的代理方法的回调,可以通过该方法设置你的 delegate |
4.2 JavaScript API
1 |
registerHandler(handlerName: String, handler: function); |
1 |
// 调用 Objective-C 中注册过的 handler |
5. 基本用法
5.1 导入头文件,声明一个 WebViewJavascriptBridge
属性:
1 |
#import "WebViewJavascriptBridge.h" ... @property WebViewJavascriptBridge* bridge; |
5.2 为你的 WKWebView
、UIWebView
(iOS)或者WebView
(OSX) 创建一个 WebViewJavascriptBridge
对象:
1 |
self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; |
5.3 在 Objective-C 中注册 handler 和调用 JavaScript 中的 handler:
1 |
[self.bridge registerHandler:@"ObjC Echo" handler:^(id data, WVJBResponseCallback responseCallback) { |
5.4 复制下面的 setupWebViewJavascriptBridge
函数到你的 JavaScript 代码中:
1 |
function (callback) { |
5.5 调用 setupWebViewJavascriptBridge
函数,使用 bridge
来注册 handler 和调用 Objective-C 中的 handler:
1 |
setupWebViewJavascriptBridge(function(bridge) { /* 在这里做一些初始化操作 */ bridge.registerHandler('JS Echo', function(data, responseCallback) { |
二、实现原理
1. 目录结构
类名 | 功能 | |
---|---|---|
WebViewJavascriptBridgeBase |
① 用来进行 bridge 初始化和消息处理的核心类; ② 这个类是在支持 WKWebView 后从 WebViewJavascriptBridge 中独立出来的逻辑,专门用来处理 bridge 相关的逻辑,不再与具体的 Web View 相关联了 |
|
WebViewJavascriptBridge |
① 桥接的入口,针对不同类型的 Web View (UIWebView 、WKWebView 、WebView )进行分发;② 针对 UIWebView 和 WebView 做的一层封装,主要用来执行 JS 代码,以及实现 UIWebView 和 WebView 的代理方法,并通过拦截 URL 来通知 WebViewJavascriptBridgeBase 做相应操作 |
|
WKWebViewJavascriptBridge |
针对 WKWebView 做的一层封装,主要用来执行 JS 代码,以及实现 WKWebView 的代理方法,并通过拦截 URL 来通知 WebViewJavascriptBridgeBase 做相应操作 |
|
WebViewJavascriptBridge_JS |
JS 端负责“收发消息”的代码 |
2. 主要流程
说明:
WebViewJavascriptBridge
中虽然对不同类型的 Web View 做了不同的处理,但是核心逻辑还是一样的,为了简单说明,这里只讨论UIWebView
情况下的逻辑。
WebViewJavascriptBridge
参与交互的流程包括三个部分:初始化、JS 调用原生、原生调用 JS。
2.1 初始化
WebViewJavascriptBridge 的初始化分为两部分,一部分是 Objective-C 中的 WebViewJavascriptBridge
对象的初始化,另一部分是 JavaScript 中的 window.WebViewJavascriptBridge
的初始化。
最终的目标是, Objective-C 和 JavaScript 两边各有一个 WebViewJavascriptBridge
对象,有了这两个对象,两边都可以收发“消息”,同时两边还各自维护一个管理响应事件的 messageHandlers 容器、一个管理回调的 callbackId 容器。
所以,我们这里讨论的初始化,不单单是一个对象的初始化,而是一个完整的准备过程,如下图所示。
(1) Objective-C 中的初始化
- 初始化 UIWebView
- 初始化 WebViewJavascriptBridge,设置 web view 代理
- 初始化 WebViewJavascriptBridgeBase,初始化相关的属性
(2) 注册 handler 供 JS 调用——把注册过的 handler 保存起来
1 |
[self.bridge registerHandler:@"share" handler:^(id data, WVJBResponseCallback responseCallback) { |
(3) Objective-C 中通过调用 UIWebView
的 loadRequest:
方法加载 URL
(4) 网页一加载就会执行 web 页中的 bridge 初始化代码,也就是调用上面提到的 setupWebViewJavascriptBridge(bridge)
函数
- 保存要执行的自定义初始化函数,比如注册 JS 中的 handler
- 通过添加一个 iframe 加载初始化链接
https://__bridge_loaded__
(5) 原生 WebViewJavascriptBridge 类中代理方法会拦截 https://__bridge_loaded__
的加载
- 在 web view 中执行本地 WebViewJavascriptBridge_JS.m 文件中的代码,初始化
window.WebViewJavascriptBridge
对象:- 在 JS 中创建一个
WebViewJavascriptBridge
对象,并设置成window
的一个属性 - 定义几个用于管理消息的全局变量
- 给
WebViewJavascriptBridge
对象定义几个处理消息的方法和函数
- 在 JS 中创建一个
- 执行原生端
startupMessageQueue
中保存的消息,也就是本地 JS 文件还未加载时就发送了的消息
(6) 初始化完毕
2.2 JS 调用原生
实际上,相比 原生调用 JS,JS 调用原生的逻辑更婉转,对照上面的示意图,我们可以把JS 调用原生的逻辑简化成以下五个环节:
- JS 中调用
callHandler()
方法,发消息给原生 - 在 JS 中将参数和回调包装成一个 message
- JS 通知原生到它那边去取 message
- 原生处理 message 中的数据
- 原生回调 JS
(1)JS 中调用 callHandler()
方法,发消息给原生
1 |
WebViewJavascriptBridge.callHandler(@"share", |
(2)在 JS 中将参数和回调包装成一个 message
把要调用的 handlerName
、要传给 native 的数据 data 以及原生回调 JS 的 responseCallback
对应的 id 包装成一个 message,然后再保存到一个全局的数组 sendMessageQueue
里面。
值得注意的是,那个用于处理原生回调的 responseCallback
是一个函数,是不能直接传给原生的,所以这里只传了其对应的 id,而 responseCallback
本身会被存到一个全局的 responseCallbacks
对象的属性里面去,属性名就是 responseCallback
对应的 id。原生回调 JS 时,就会根据 id 从 responseCallbacks
对象中去取对应的 callback。
(3)JS 通知原生到它那边去取 message
在 iframe 中加载发送消息的 URL,通知原生“我 JS 发消息给你了,麻烦你到信箱里查收一下”,原生中的 WebViewJavascriptBridge
就会在 webView 代理方法里面拦截到这个事件,然后再调用 JS,将 sendMessageQueue
中的 message 全部取出来,然后转成 JSON string 的形式。
(4) 原生处理 message 中的数据
原生拿到转为 JSON string 的 message 之后,先将其解析成原生的字典,然后再取出 data
、 callbackId
和 handlerName
,最后根据 handlerName
从之前注册过的 messageHandlers
里面取出对应的 handler
(block),再调用这个 handler
,第一个参数就是 data
,第二个参数是根据 callbackId
创建的 responseCallback
(block),然后原生就可以在 handler
(block) 中处理接收到的 data 以及回调 JS。
(5)原生回调 JS
那么这个回调 JS 的 responseCallback
(block) 是怎么处理的呢?当这个 responseCallback
被回调时,在这个 callback
中会创建一个 message
(NSDictionary)对象,其中包含两个字段,一个是 callbackId
,另一个传进 responseCallback
的参数 data
,然后再将这个 message
(NSDictionary)对象转成 JSON String,最后调用 JS 中的 _handleMessageFromObjC(messageJSON)
方法,同时将 JSON String 形式的 message
作为参数传给 JS,接下来 JS 就会通过 message
中的 callbackId
找出之前保存的 responseCallback
,并把 message
中的 data
作为参数,回调这个 responseCallback
。至此,整个 JS 调原生的流程就跑通了。
2.3 原生调用 JS
原生调用 JS 其实本身可以直接通过 web view 来执行 JavaScript 脚本来实现的,但是 WebViewJavascriptBridge
提供了一个更贴近原生的方式。一是调用更规范,二是使用 block 的方式将调用与 JS 回调归并到一起了,代码逻辑更连贯。
与上面的 JS 调用原生恰好相反,原生调用 JS 时调用过程很简单,但是回调过程相对比较复杂。简单来看,如上图所示,原生调用 JS 也可以分成以下几步:
- 原生通过调用
callHandler()
方法,发消息给 JS - 在原生中将参数和回调包装成一个 message
- 原生直接调用 JS 函数将 message 传给 JS
- JS 回调原生
(1)原生通过调用 callHandler()
方法,发消息给 JS
1 |
[self.bridge callHandler:@"share" data:nil responseCallback:^(id responseData) { |
(2)在原生中将参数和回调包装成一个 message
跟 JS 调用原生类似,原生调用 JS 时,也是将要调用的 handlerName
、要传给 JS 的数据 data
以及 JS 回调原生的 responseCallback
对应的 id 包装成一个 message
(NSDictionary),然后将这个 message
对象转成 JSON String。接着再调用 JS 的 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)
方法,把 JSON String 传给 JS。
同样值得注意的是,那个用于处理 JS 回调的 responseCallback
是一个 block,也是不能直接传给 JS 的,所以这里只传了其对应的 id,而 responseCallback
(block) 本身会被存到一个全局的 responseCallbacks
字典里面去,key 值就是 responseCallback
对应的 id。JS 回调原生时,就会根据 id 从 responseCallbacks
字典中去取对应的 block。
(3)原生直接调用 JS 函数将 message 传给 JS
JS 拿到转为 JSON string 形式的 message 之后,先将其解析成 JS 对象,然后再取出 data
、 callbackId
和 handlerName
,最后根据 handlerName
从之前注册过的 messageHandlers
里面取出对应的 handler
函数,再执行这个 handler
函数,第一个参数就是 data
,第二个参数是根据 callbackId
创建的 responseCallback
(function),然后 JS 就可以在 handler
函数中处理接收到的 data
以及回调原生。
(4)JS 回调原生
那么这个回调原生的 responseCallback
(function) 是怎么处理的呢?与前面 JS 调原生时原生回调 JS 的处理不太一样,因为 JS 调原生是不能直接调的,所以当这个 responseCallback
(function) 被回调时,在这个 function 中会用“发消息”的方式,直接走前面所提到的 JS 调原生的流程。
但这其中有两点与 JS 直接调原生不太一样的地方,一是 message 的内容,二是原生对这个 message 的处理:
- JS 在回调原生时,会把
handlerName
,responseId
(也就是原生调 JS 时传过来的callbackId
) 和responseData
包装成一个message
对象。与 JS 直接调原生不同的是,这里的message
对象没有 JS 回调函数的callbackId
,因为这里不需要原生再次回调 JS 了。但是多了一个responseId
,这是因为原生执行 JS 的回调时,会根据这个responseId
从responseCallbacks
(NSDictionary) 中去取对应的block
。 - 当原生收到并解析 JS 回调的消息后,会直接根据 message 中的
responseId
找出之前保存的responseCallback
(block),并把 message 中的responseData
作为参数,然后再回调这个responseCallback
。**与 JS 直接调原生不同的是,这个responseCallback
只有一个参数data
,没有用于再次回调 JS 的 block 了。
至此,整个原生调 JS 的流程就圆满结束了。
2.4 小结
一句话来概括的话,那就是——WebViewJavascriptBridge
以下面两个方法为桥梁(以 UIWebView
为例):
- 原生调 JS:
-stringByEvaluatingJavaScriptFromString:
- JS 调原生:
- webView:shouldStartLoadWithRequest:navigationType:
在 JS 和原生两边封装了一套『方法调用』转『消息发送』的机制,各自维护了一套注册的方法列表、回调函数列表,优雅地解决了回调的问题。(注:不要把这里的消息发送和 Objective-C 运行时的消息发送混淆了)
三、源码解读
这里只针对核心逻辑进行分析,详见 带有注释的源码。
对照上面的流程我们来看看 WebViewJavascriptBridge
的源码具体是如何实现的。
1. 初始化
(1) Objective-C 中的初始化
首先从创建 UIWebView
和 WebViewJavascriptBridge
对象开始:
1 |
@implementation ViewController |
WebViewJavascriptBridge
的 + bridgeForWebView:
方法在内部针对不同的 web view 做了不同的逻辑处理,由于我这里使用的是 UIWebView
,所以这里最终会调用 -_platformSpecificSetup:
方法:
1 |
@implementation WebViewJavascriptBridgeBase |
然后在 -_platformSpecificSetup:
方法中会设置 web view 的代理为 self
,并且创建了一个 WebViewJavascriptBridgeBase
对象,同时设置这个对象的代理为 self
。其目的是为了:
① 监听 UIWebView
的代理方法回调;
② 把桥接的核心逻辑交给 WebViewJavascriptBridgeBase
去处理;
1 |
@implementation WebViewJavascriptBridge |
WebViewJavascriptBridgeBase
在初始化时,会为原生端初始化几个后面用于处理消息的变量:
1 |
@implementation WebViewJavascriptBridgeBase |
(2)通过调用 -registerHandler:handler:
方法,注册 handler 供 JS 调用,这个方法会把要注册的 handler(block)保存到 WebViewJavascriptBridgeBase
对象的 messageHandlers
(NSDictionary)属性中去,当 JS 回调后,就会根据 handlerName 从这个变量中去取对应的 handler:
1 |
@implementation WebViewJavascriptBridge |
(3) Objective-C 中通过调用 UIWebView
的 -loadRequest:
方法加载 URL 后,网页一加载就会执行 web 页中的 bridge 初始化代码,也就是调用 setupWebViewJavascriptBridge(bridge)
函数:
1 |
<script type="text/JavaScript"> |
这里调用 setupWebViewJavascriptBridge()
函数时,传入的参数是一个带有自定义初始化逻辑(比如 JS 中注册 handler)的 function
,相当于原生中的 block。
这个 function
setupWebViewJavascriptBridge()
函数主要做了两件事情:
- 将传进来的参数保存到
window.WVJBCallbacks
中,等到后面 JS 端的 bridge 初始化成功后,再取出来调用 - 通过添加一个 iframe 加载初始化链接
https://__bridge_loaded__
,调起原生,然后再移除这个 iframe
(5) 原生中的 WebViewJavascriptBridge
对象中代理方法会拦截到 https://__bridge_loaded__
的加载:
1 |
@implementation WebViewJavascriptBridge |
在 -webView:shouldStartLoadWithRequest:navigationType:
方法中,首先会根据 URL 的 scheme 来判断这个 URL 是不是跟 bridge 相关的 URL,然后再根据 URL 的 host 来判断是用来初始化 JS 中的 bridge 的(__bridge_loaded__
),还是用来发消息给原生的(__wvjb_queue_message__
):
1 |
... |
此时的 URL 是 https://__bridge_loaded__
,所以肯定是走 [_base injectJavascriptFile];
的逻辑,这个方法主要是加载 WebViewJavascriptBridge_js.m 文件中的 JS:
1 |
/// 注入 JS ,进行一些初始化操作 |
(6)在 web view 中执行本地 WebViewJavascriptBridge_JS.m 文件中的代码后,首先会初始化 window.WebViewJavascriptBridge
对象,并定义几个方法和全局变量:
- 在 JS 中初始化
WebViewJavascriptBridge
对象,并设置成window
的一个属性 - 定义几个全局变量
-
sendMessageQueue
:保存待发送消息的数组 -
messageHandlers
:保存注册过的 handler 的对象 -
responseCallbacks
:保存 callback 的对象 -
uniqueId
:保存 callback 时对应的 id
-
- 定义以下几个方法
-
registerHandler(handlerName, handler)
:注册 hander -
callHandler(handlerName, data, responseCallback)
:调用 handler -
_fetchQueue()
:获取待发送消息 -
disableJavscriptAlertBoxSafetyTimeout()
:让OC可以关闭回调超时 -
_handleMessageFromObjC(messageJSON)
:调用_dispatchMessageFromObjC
处理来自 Objective-C 的消息
-
- 定义两个函数,给上面几个方法调用
-
_doSend(message, responseCallback)
:将要发送的消息保存到 sendMessageQueue 中,同时加载 URL 调起原生 -
_dispatchMessageFromObjC(messageJSON)
:处理 Objective-C 中发来的消息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121WebViewJavascriptBridge_JS.m
...
// 初始化 WebViewJavascriptBridge 对象
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
}; var messagingIframe;
var sendMessageQueue = []; // 保存消息的数组
var messageHandlers = {}; // 保存 handler 的对象 var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; // 发送消息的 URL scheme var responseCallbacks = {}; // 回调函数
var uniqueId = 1; // 保存 callback 的唯一标识
var dispatchMessagesWithTimeoutSafety = true; // 注册 handler 的方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
} // 调用 Native handler 的方法
function callHandler(handlerName, data, responseCallback) {
// 如果只有两个参数,并且第二个参数是 函数
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
} // 发送消息给 Native
_doSend({ handlerName:handlerName, data:data }, responseCallback);
} function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
} // 发送消息给 Native
// 一个消息包含一个 handler 和 data,以及一个 callbackId
// 因为 JavaScript 中的 callback 是函数,不能直接传给 Objective-C,
function _doSend(message, responseCallback) { if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); // callbackId 的格式:cb + 唯一标识 id + 时间戳 responseCallbacks[callbackId] = responseCallback; // 保存 responseCallback 到 responseCallbacks 中去 message['callbackId'] = callbackId;
} sendMessageQueue.push(message); // 将要发送的消息保存到 sendMessageQueue 中 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; // https://__wvjb_queue_message__
} // 从消息队列中拉取消息
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = [];
return messageQueueString;
} // 处理 Objective-C 中发来的消息
function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
} // 处理 Objective-C 中发来的消息
fu 大专栏 [iOS 开发] WebViewJavascriptBridge 从原理到实战 · Shannon's Blognction _doDispatchMessageFromObjC() { var message = JSON.parse(messageJSON); // JSON 解析
var messageHandler;
var responseCallback; if (message.responseId) { // 执行 JavaScript 调用原生时的回调 responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId]; } else { // 原生调用 JavaScript if (message.callbackId) { // JavaScript 回调 Native 的 callback var callbackResponseId = message.callbackId; // 取出原生传过来的 callbackId
responseCallback = function(responseData) {
// 调用 _doSend 方法发送消息给 Native
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
} var handler = messageHandlers[message.handlerName]; // 根据 handlerName 取出 JavaScript 中的 handler
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else { handler(message.data, responseCallback); // 调用 JavaScript 中的 handler
}
}
}
} // 处理 Objective-C 中发来的消息
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
...
-
初始化 window.WebViewJavascriptBridge
对象之后,JS 端还会创建一个 messagingIframe
,用来加载 URL 发送消息给 Native:
1 |
// 创建 iframe,用来加载 URL 发送消息给 Native |
最后,执行 window.WVJBCallbacks
中的回调函数,也就是通过前面的 setupWebViewJavascriptBridge()
函数添加的用来初始化配置的 callback
:
1 |
setTimeout(_callWVJBCallbacks, 0); |
至此,整个初始化的过程就结束了,Objective-C 端和 JS 端各自有一个 bridge 环境,可以收发“消息”,处理回调。
2. JS 调用原生
(1)JS 是以发消息的形式调用原生,发消息的过程包括三步:
- JS 中调用
callHandler()
方法,传入数据和回调函数。 - 紧接着为每个
responseCallback
生成一个对应的callbackId
,然后再将handlerName
、参数data
和callbackId
包装成一个 message 对象,存到全局数组sendMessageQueue
中。同时把responseCallback
也保存到responseCallbacks
对象中去,等原生回调时再取。 - 最后 JS 中加载发送消息的链接
https://__wvjb_queue_message__
,通知原生到它那边去取 message。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function callHandler(handlerName, data, responseCallback) {
// 如果只有两个参数,并且第二个参数是 函数
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
} // 发送消息给 Native
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
// 发送消息给 Native
// 一个消息包含一个 handler 和 data,以及一个 callbackId
// 因为 JavaScript 中的 callback 是函数,不能直接传给 Objective-C,
function _doSend(message, responseCallback) { if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); // callbackId 的格式:cb + 唯一标识 id + 时间戳 responseCallbacks[callbackId] = responseCallback; // 保存 responseCallback 到 responseCallbacks 中去 message['callbackId'] = callbackId;
} sendMessageQueue.push(message); // 将要发送的消息保存到 sendMessageQueue 中 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; // https://__wvjb_queue_message__
}
(2)原生在 -webView: shouldStartLoadWithRequest: navigationType:
方法中拦截到 https://__wvjb_queue_message__
的加载,然后执行 JS 脚本WebViewJavascriptBridge._fetchQueue();
从 JS 拉取 message :
1 |
@ implementation WebViewJavascriptBridge |
1 |
@implementation WebViewJavascriptBridgeBase |
原生拿到转为 JSON string 的 message 之后,先将其解析成原生的字典,然后再取出 data
、 callbackId
和 handlerName
,最后根据 handlerName
从之前注册过的 messageHandlers
里面取出对应的 handler
(block),接着再调用这个 handler
,第一个参数就是 data
,第二个参数是根据 callbackId
创建的 responseCallback
(block),然后原生就可以在 handler
(block) 中处理接收到的 data 以及回调 JS:
1 |
@implementation WebViewJavascriptBridgeBase |
(3)原生回调 JS
当这个 responseCallback
被回调时,在这个 callback
(block) 中会创建一个 message
(NSDictionary)对象,其中包含两个字段,一个是callbackId
,另一个是传进 responseCallback
的参数 data
,然后再将这个 message
(NSDictionary)对象转成 JSON String,最后调用 JS 中的 _handleMessageFromObjC(messageJSON)
方法,同时将 JSON String 形式的 message
作为参数传给 JS:
1 |
@implementation WebViewJavascriptBridgeBase |
接下来 JS 就会根据 message
中的 callbackId
找出之前保存的 responseCallback
,并把 message
中的 data
作为参数,回调这个 responseCallback
:
1 |
WebViewJavascriptBridge_JS.m |
至此,整个 JS 调原生的流程就结束了。
3. 原生调用 JS
(1)JS 包装并发送消息给 JS
JS 中首先调用 callHandler()
方法,传入要传递的数据和回调函数:
1 |
@implementation WebViewJavascriptBridge |
接着跟 JS 调用原生一样,原生调用 JS 时,也是将要调用的 handlerName
、要传给 JS 的数据 data 以及 JS 回调原生的 responseCallback
对应的 id 包装成一个 message
(NSDictionary),然后将这个 message
对象转成 JSON String。接着再调用 JS 的 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)
方法,把 JSON String 传给 JS:
1 |
@implementation WebViewJavascriptBridgeBase |
(2)JS 处理原生发来的消息WebViewJavascriptBridge._handleMessageFromObjC()
方法在内部调用了 _dispatchMessageFromObjC()
方法,先将其解析成 JS 对象,然后再取出 data
、 callbackId
和 handlerName
,最后根据 handlerName
从之前注册过的 messageHandlers
里面取出对应的 handler
函数,再执行这个 handler
函数,第一个参数就是 data
,第二个参数是根据 callbackId
创建的 responseCallback
(function),然后 JS 就可以在 handler
函数中处理接收到的 data
以及回调原生:
1 |
WebViewJavascriptBridge_JS.m |
(3)JS 回调原生
当这个 responseCallback
(function) 被回调时,在这个 function 中会直接走前面所提到的 JS 调原生的流程——调用 _doSend()
函数,传入 handlerName
、callbackResponseId
和要传给原生的数据 responseData
。然后 _doSend()
函数中会把 handlerName
,responseId
(也就是原生调 JS 时传过来的 callbackId
) 和 responseData
包装成一个 meassage
对象,并保存到消息队列中去,接着再在 iframe 中加载链接 https://__wvjb_queue_message__
触发 UIWebView
代理方法的回调。
原生收到代理回调时,就会执行 JS 脚本 WebViewJavascriptBridge._fetchQueue();
,到 JS 环境中去取消息,取到消息(JSON string)后,再将其解析为 Objective-C 对象。
这些 JS 调原生的代码前面已经有了,就不再重复贴出来了。最后,Objective-C 中拿到解析过的 message 对象后,根据 responseId
取出之前存过的对应的 responseCallback
(block)进行回调,参数就是 message 中的 responseData
:
1 |
/// 处理 JavaScript 消息队列中的消息,发送给 Objective-C 方 |
到此为止,原生调用 JS 的逻辑就结束了。
四、最佳实践
在了解了 WebViewJavascriptBridge
的基本使用和原理之后,我们就可以更优雅地更灵活地使用这个工具了。
我们平时在实际开发中,更多的还是 JS 调原生的情况,比如调用原生分享、获取地理位置信息,等等。借助 WebViewJavascriptBridge
,我们可以直接在原生项目中注册 handler,然后在 h5 中 call handler,这样比之前 Custom URL Scheme 的形式更易于使用和维护。
但是,这里仍然有两个问题需要考虑一下:
- JS 调用原生时,每次还是需要写一长串的
WebViewJavascriptBridge.callHandler(handlerName, data, callback);
,我们能不能做到像直接调 JS 方法那样简单直接?比如像这样,share(data, callback);
。 - 在原生 App 中,我们一般会定义一个
WebViewController
专门用来加载 h5,然后我们会在这个类中注册所有的 handler,一开始只有少量的几个 handler,一切 OK,但是,随着时间推移,业务不断发展,handler 会越来越多,而且有些不同的页面需要注册不同的 handler,最终会导致这个WebViewController.m
文件变得越来越庞大——Massive View Controller(MVC)。所以,我们期望的是,要处理的 handler 应该分成两类,一类 handler 是通用的,大部分页面都需要支持,比如,调用原生的分享,而另一部分 handler 就是各个页面自己特有的逻辑,比如调用原生的支付。
1. 先来看看第一个问题,如何实现以下形式的转换:
WebViewJavascriptBridge.callHandler('share', data, callback);
===> share(data, callback);
不论怎样,因为 WebViewJavascriptBridge.callHandler()
方法是必须要调用的,所以我们能想到的是,在别的方法内部调用这个方法。要想通过调用 share
方法来实现这个目标,那就得先定义一个对象来保存这个方法。
因此,我们可以定义一个全局对象 MyApp
,然后给 MyApp
对象定义一个方法 share()
,然后再在其内部调用 WebViewJavascriptBridge.callHandler()
方法。
1 |
var handlerNames = new Array("share", "requestLocation"); for (var i in handlerNames) { |
有了上面的“转换”后,我们在 JS 中就可以以下几种形式调用 handler 了:
1 |
MyApp.functionName(data, callback); // 有参数,有回调 |
比如要调用分享接口,直接这样调用就行了:
1 |
MyApp.share({ |
还有个问题是,这段代码应该在什么时候执行呢?前面我们提到 WebViewJavascriptBridge
提供了 setupWebViewJavascriptBridge()
函数用于初始化,所以,我们可以在这个函数中进行上面的“转换”。
2. 接下来再来看看原生这边的问题,原生如何按照不同页面所需来管理 handler 呢?
我这里采用的是“基础API+特定API”的方式,首先需要定义一个基础的 handler processor,用来管理基础 API 的调用,然后在针对其余一些有特殊逻辑的页面,基于这个 basic handler processor 定义对应的 special handler processor。
另外一个问题是,在打开 WebViewController
时,如何根据不同页面创建对应的 handler processor 呢?一个可行的方式是给每个 有特殊逻辑的页面传入一个 pageId 属性,没有特殊逻辑的页面 pageId 默认为空,WebViewController
维护一张 pageId-handlerProcess 的关系映射表,初始化后再根据这个 pageId 去创建对应的 handler processor 类。
page | handlerProcessor | |
---|---|---|
page_id_1 | handlerProcessor_1(Based on basicHandlerProcessor) | |
page_id_2 | handlerProcessor_2(Based on basicHandlerProcessor) | |
… | … + baseHandlerProcessor |
以下面的 3 个页面为例,这 3 个页面是在同一个 WebViewController
中加载的,其中页面 1 中只有两个基础功能:分享和获取地理位置,页面 2 中相比页面 1 多了一个 拨打电话的功能,页面 3 中相比页面 1 多了一个支付的功能。
首先我们创建一个管理公共 API 的 handler processor SCWebViewMessageHandler
:
1 |
@interface SCWebViewMessageHandler : NSObject @property (weak, nonatomic) SCWebViewController *controller; /// 注册 handler |
这个类中主要干三件事,一是获取所有要注册的 handler name,并注册这些 handler;二是通过在 handler回调时,通过 runtime 调用与 handler 同名的 Objective-C 方法,参数只有一个 args,args 中包括两部分,一部分是 JS 传过来的 data,另一部分是回调 JS 的 responseCallback。
子类可以继承该类,通过重写 -specialHandlerNames
方法添加一些特定的 handler name,另外就是实现 handler 对应的 Objective-C 方法。
因此,第一个页面的 handler 可以交给 SCWebViewMessageHandler
处理,第二个页面和第三个页面就需要分别交给子类 SCWebViewSpecialMessageHandlerA
和 SCWebViewSpecialMessageHandlerB
来处理。
1 |
@interface SCWebViewSpecialMessageHandlerA : SCWebViewMessageHandler @end @implementation SCWebViewSpecialMessageHandlerA - (NSArray *)specialHandlerNames { |
1 |
@interface SCWebViewSpecialMessageHandlerB : SCWebViewMessageHandler |
定义好了这几个处理 handler 的类之后,我们就可以在 WebViewController
中进行相关的配置了:
1 |
- (void)viewDidLoad { |
到此为止,我们就解决了原生中 handler 管理的问题了。
完整示例代码见这里。
五、问题与讨论
已知 bug:在
WKWebView
中使用时,一旦- webView:decidePolicyForNavigationAction:decisionHandler:
方法被调用,就会出现连续回调两次decisionHandler
的问题。
首先,逻辑上讲,跟UIWebView
类似,- webView:decidePolicyForNavigationAction:decisionHandler:
方法中的拦截只应该回调一次decisionHandler
即可。
另外,这个问题还会导致应用在 iOS11 + XCode9 的环境下出现崩溃。解决办法见相关 Pull Request #296,期待 maintainer 能够早点 merge。在加载
WebViewJavascriptBridge_JS
中的 JS 时,就会在创建messagingIframe
的同时,加载https://__wvjb_queue_message__
,
实际上这个时候sendMessageQueue
数组肯定是空的,也就是说完全不需要发消息,那为什么还要这么做呢?
就想问题中所说的,这个时候sendMessageQueue
数组肯定是空的,因为这个文件加载了,h5 中才会有WebViewJavascriptBridge
对象,所以,理论上来讲,根本就不存在在这个文件加载前就调用了WebViewJavascriptBridge.callHandler()
方法的情况。
因此,这里的原因肯定不是并不像有些朋友说的“跟WebViewJavascriptBridgeBase
中的startupMessageQueue
一样,就是在 JavaScript 环境初始化完成以后,把 JavaScript 要发送给 OC 的消息立即发送出去”。
通过查找原来版本的提交记录,终于找到了真正的原因,具体见相关 commit。为什么
WebViewJavascriptBridge
中 JS 调用原生时,把要传给原生的数据放到 messageQueue 中,再让原生调 JS 去取,而不是直接拼在 URL 后面?WebViewJavascriptBridge
中加载 URL 调起原生时,为什么不是用window.location="https://xxx"
这种形式,而是新添加一个 iframe 来加载这个 URL?
因为如果当前页面正在加载时,就有用户操作导致window.location="https://xxx"
被触发,这样会使当前页面中还未加载完成的请求被取消掉。回调的处理
其实在 JS 与 Objective-C 通信时,互相传参数并不难,比较难处理的就是回调的处理,WebViewJavascriptBridge
采用的策略是,call 的时候只传 id,callback 本身不传,它在 JS 和 Objective-C 两边,各自维护一个 callback 表,每个 callback 对应一个 id,回调的时候就根据这个 id 去取对应的 callback。
在这一点上,跟 React Native 的做法是一样的。WebViewJavascriptBridge
中 web view 执行 JS 脚本时,为什么将其限制在主线程上?初始化的 JS 内容(也就是
setupWebViewJavascriptBridge
函数的定义和调用)是放在 APP bundle 中好呢,还是放到服务器上让 h5 自己去加载好呢?-
JS 中的闭包作用域问题
在一开始,为了能实现MyApp.share(data, callback)
的效果,我尝试了下面的这种做法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var handlerNames = new Array("share", "requestLocation"); for (var i in handlerNames) {
var handlerName = handlerNames[i]; MyApp[handlerName] = Myfunction() { if (typeof data == "function") { // 意味着没有参数 data,只有一个参数 callback bridge.callHandler(handlerName, null, data); } else if (callback == null) { // 第二个参数 callback 为 null 或者只有第一个参数 data bridge.callHandler(handlerName, data); } else { // 两个参数都有 bridge.callHandler(handlerName, data, callback);
}
} };
但是,与 Objective-C 中的 block 不同,这里的闭包并没有将外面的 handlerName
copy 进去。
六、延伸阅读
- WebViewJavascriptBridge/README.md
- WebViewJavascriptBridge 原理解析
- WebViewJavascriptBridge 机制解析
- 微信公众号 JS-SDK 文档
欢迎关注公众号:老*专栏