WebViewJsBridge-iOS 和 WebViewJsBridge-Android 是我写的 Js-Bridge 桥接库,简单易用,供大家参考。
框架简介
marcuswestin/WebViewJavascriptBridge 是用于在 WKWebViews, UIWebViews & WebViews 中,JS 与 Obj-C 相互发送消息的桥接库。
通常实现 iOS 与 JS 桥接的方式有两种:
- 通过 Webview 拦截请求的方式。
- 通过 iOS7 之后的 JavaScriptCore 框架。
marcuswestin/WebViewJavascriptBridge 使用的正是第一种方式。
该桥接库在 Github 上拥有 1.2 w+ 的 star 数,可见该库受欢迎的程度。
功能解析
我们从该框架的几个重要功能点入手,来逐步了解其对应的实现细节。
主要功能分为 6 点:
- Obj-C Bridge 对象的初始化。
- JS Bridge 对象的初始化。
- Obj-C 注册函数。
- JS 调用 Obj-C 注册的函数,并支持回调 JS。
- JS 注册函数。
- Obj-C 调用 JS 的函数,并支持回调 Obj-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
| _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
function setupWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } window.WVJBCallbacks = [callback]; var WVJBIframe = dObj-Cument.createElement('iframe'); WVJBIframe.style.display = 'none'; WVJBIframe.src = 'https://__bridge_loaded__'; dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe); setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0) }
setTimeout(_callWVJBCallbacks, 0); function _callWVJBCallbacks() { var callbacks = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); } }
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { responseCallback(@"Response from testObjcCallback"); }];
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('JS got response', response) })
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) { var responseData = { 'Javascript Says':'Right back atcha!' } responseCallback(responseData) })
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) { NSLog(@"responseCallback called: %@", responseData); }];
|
源码解析
源码解析从作者 ExampleApp-iOS 这个 demo 入手,我们看下 ExampleUIWebViewController.m
中的 demo 代码。
Obj-C 初始化 Bridge
1 2 3 4
| _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
|
bridgeForWebView:
方法中创建 WebViewJavascriptBridge
对象 bridge,并设置 webView 的代理为 bridge,这里主要是需要代理方法 shouldStartLoadWithRequest:
与 JS 通信。创建 WebViewJavascriptBridgeBase
对象 _base,并设置 _base 代理为 bridge,这里是需要 WebView 对象提供 evaluateJavaScript:
方法与 JS 通信。
而 WebViewJavascriptBridgeBase
的初始化方法中,会创建 messageHandlers
,startupMessageQueue
,responseCallbacks
对象,messageHandlers
用于存储 Obj-C 注册的函数,startupMessageQueue
存储 webview
载入页面前就调用的 JS 函数,responseCallbacks
存储 Obj-C 调用 JS 函数后要回调 Obj-C 的回调方法。
这几个 Array 和 Dictionary 如何作用,后面会有介绍。
JS 初始化 bridge
因为 JS 与 Obj-C 之间会相互调用,不容易理清两者的调用关系,所以我画了一个泳道图辅助理解。

JS 初始化 bridge 的过程要比 Obj-C 复杂。
首先,WebView 要载入 html 页面,里面有初始化相关的 JS 代码。
1 2 3 4 5 6
| - (void)loadExamplePage:(UIWebView*)webView { NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"ExampleApp" ofType:@"html"]; NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil]; NSURL *baseURL = [NSURL fileURLWithPath:htmlPath]; [webView loadHTMLString:appHtml baseURL:baseURL]; }
|
载入 html 页面会加载 JS 代码,JS 相关代码如下:
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
| function setupWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } window.WVJBCallbacks = [callback]; var WVJBIframe = dObj-Cument.createElement('iframe'); WVJBIframe.style.display = 'none'; WVJBIframe.src = 'https://__bridge_loaded__'; dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe); setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0) }
setupWebViewJavascriptBridge(function(bridge) { var uniqueId = 1 bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) { var responseData = { 'Javascript Says':'Right back atcha!' } responseCallback(responseData) })
dObj-Cument.body.appendChild(dObj-Cument.createElement('br'))
var callbackButton = dObj-Cument.getElementById('buttons').appendChild(dObj-Cument.createElement('button')) callbackButton.innerHTML = 'Fire testObjcCallback' callbackButton.onclick = function(e) { bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('JS got response', response) }) } })
|
iframe 加载 url https://__bridge_loaded__
后,会调用 webview 的回调方法 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
,
然后判断 url 的 host 是 bridge_loaded 后调用如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if ([_base isBridgeLoadedURL:url]) { [_base injectJavascriptFile]; }
- (void)injectJavascriptFile { NSString *JS = WebViewJavascriptBridge_JS(); [self _evaluateJavascript:JS]; if (self.startupMessageQueue) { NSArray* queue = self.startupMessageQueue; self.startupMessageQueue = nil; for (id queuedMessage in queue) { [self _dispatchMessage:queuedMessage]; } } }
|
WebViewJavascriptBridge_JS
中是一段 JS 代码,需要注入到 JS 环境中,这段代码就是初始化 JS bridge 的代码。
精简后的代码如下:
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
| NSString * WebViewJavascriptBridge_JS() { #define __wvjb_JS_func__(x) #x static NSString * preprObj-CessorJSCode = @__wvjb_JS_func__( ;(function() { if (window.WebViewJavascriptBridge) { return; } window.WebViewJavascriptBridge = { registerHandler: registerHandler, callHandler: callHandler, disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, _fetchQueue: _fetchQueue, _handleMessageFromObjC: _handleMessageFromObjC };
var sendMessageQueue = []; var messageHandlers = {}; var CUSTOM_PROTObj-COL_SCHEME = 'https'; var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__'; messagingIframe = dObj-Cument.createElement('iframe'); messagingIframe.style.display = 'none'; messagingIframe.src = CUSTOM_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE; dObj-Cument.dObj-CumentElement.appendChild(messagingIframe); setTimeout(_callWVJBCallbacks, 0); function _callWVJBCallbacks() { var callbacks = window.WVJBCallbacks; delete window.WVJBCallbacks; for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); } } })(); );
#undef __wvjb_JS_func__ return preprObj-CessorJSCode; };
|
调用 _callWVJBCallbacks
函数,将 window.WVJBCallbacks
数组中的所有 callback 调用一遍,并传入初始化好的 bridge 对象。
此处正好和前面的 setupWebViewJavascriptBridge
的代码呼应上。到此 JS bridge 的初始化结束。
Obj-C 注册函数
1 2 3 4 5 6 7 8
| [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"testObjcCallback called: %@", data); responseCallback(@"Response from testObjcCallback"); }];
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { _base.messageHandlers[handlerName] = [handler copy]; }
|
handlerName 作为 Key,handlerBlock作为 Value 存入 _base 的 messageHandlers 字典中。
JS 调用 Obj-C 函数
管道图辅助理解 JS 与 Obj-C 两者调用关系。

JS 调用 Obj-C 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('JS got response', response) })
function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') { responseCallback = data; data = null; } _doSend({ d:handlerName, data:data }, responseCallback); } function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message['callbackId'] = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
|
将 JS 调用 Obj-C 函数的一些信息封装成 message 放入 sendMessageQueue
队列中,并通知 Obj-C 去获取 JS 的队列消息,并逐一调用 Obj-C 的注册函数。
1 2 3
| message['handlerName'] = handlerName; message['data'] = data; message['callbackId'] = callbackId;
|
webview 代理方法 shouldStartLoadWithRequest:
,接受到 iframe 的 url 请求,判断是 QUEUE_HAS_MESSAGE
后,
1 2 3 4
| if ([_base isQueueMessageURL:url]) { NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; [_base flushMessageQueue:messageQueueString]; }
|
获取 JS bridge 的消息队列,调用 flushMessageQueue
方法来消费这些消息。
代码如下:
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
| - (void)flushMessageQueue:(NSString *)messageQueueString{ id messages = [self _deserializeMessageJSON:messageQueueString]; for (WVJBMessage* message in messages) { NSString* responseId = message[@"responseId"]; if (responseId) { WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; responseCallback(message[@"responseData"]); [self.responseCallbacks removeObjectForKey:responseId]; } else { WVJBResponseCallback responseCallback = NULL; NSString* callbackId = message[@"callbackId"]; if (callbackId) { responseCallback = ^(id responseData) { if (responseData == nil) { responseData = [NSNull null]; } WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData }; [self _queueMessage:msg]; }; } else { responseCallback = ^(id ignoreResponseData) { }; } WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; if (!handler) { NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); continue; } handler(message[@"data"], responseCallback); } } }
|
这里在说明一下,如何在 JS 调用 Obj-C 函数时,再回调 JS。
1 2 3
| bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) { log('JS got response', response) })
|
function(response) {}
这个函数就是 JS 需要的回调函数,那具体是怎么回调的呢?
回过头看下 Obj-C bridge 的 - (void)flushMessageQueue:(NSString *)messageQueueString
函数。
1 2 3 4 5 6 7 8 9 10 11
| if (callbackId) { responseCallback = ^(id responseData) { if (responseData == nil) { responseData = [NSNull null]; } WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData }; [self _queueMessage:msg]; }; } WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; handler(message[@"data"], responseCallback);
|
判断有 callbackId 时说明,JS 需要回调,然后构建一个回调的 message,responseId 就是传来的 callbackId,responseData 是要回传的数据,之所以用 responseId 这个 key 是因为做消息的类型区分。[self _queueMessage:msg]
就是将消息发回给 JS bridge。JS bridge 通过 responseId 找到之前保存的函数,调用即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function _doDispatchMessageFromObjC() { var message = JSON.parse(messageJSON); var messageHandler; var responseCallback; if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { } }
|
JS 注册函数
1 2 3 4 5 6 7 8 9
| bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) { var responseData = { 'Javascript Says':'Right back atcha!' } responseCallback(responseData) })
function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; }
|
Obj-C 调用 JS 函数
管道图辅助理解两者调用关系。

1
| [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
|
最终调用的是 WebViewJavascriptBridgeBase
中的 - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName
方法,根据参数拼装 message 字典。
1 2 3
| message[@"data"] = data; message[@"callbackId"] = callbackId; message[@"handlerName"] = handlerName;
|
然后调用方法 - (void)_queueMessage:(WVJBMessage*)message
将拼装好的 message 传入,我们看具体的 _queueMessage 方法实现。
1 2 3 4 5 6 7
| - (void)_queueMessage:(WVJBMessage*)message { if (self.startupMessageQueue) { [self.startupMessageQueue addObject:message]; } else { [self _dispatchMessage:message]; } }
|
如果有 self.startupMessageQueue
对象,将消息放入其中,否则发送消息给 JS,之所以要存入 startupMessageQueue
中,是因为在 viewWillAppear
中的 callHandler 不能立马调用到 JS 中,因为 html 没有载入,JS bridge 的环境也没有准备完成。先存入 startupMessageQueue
中,待 JS bridge 准备完成后,在统一调用 startupMessageQueue
中的消息到 JS,并将 startupMessageQueue
置为空。
接下来我们看如何调用 JS 的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (void)_dispatchMessage:(WVJBMessage*)message { NSString *messageJSON = [self _serializeMessage:message pretty:NO]; NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON]; if ([[NSThread currentThread] isMainThread]) { [self _evaluateJavascript:javascriptCommand];
} else { dispatch_sync(dispatch_get_main_queue(), ^{ [self _evaluateJavascript:javascriptCommand]; }); } }
|
将之前拼装好的 message 传给 JS,用 JS bridge 的 _handleMessageFromObjC
函数处理 Obj-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
| function _handleMessageFromObjC(messageJSON) { _dispatchMessageFromObjC(messageJSON); }
function _dispatchMessageFromObjC(messageJSON) { if (dispatchMessagesWithTimeoutSafety) { setTimeout(_doDispatchMessageFromObjC); } else { _doDispatchMessageFromObjC(); } function _doDispatchMessageFromObjC() { var message = JSON.parse(messageJSON); var messageHandler; var responseCallback; if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function(responseData) { _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData }); }; } var handler = messageHandlers[message.handlerName]; if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); } else { handler(message.data, responseCallback); } } } }
|
JS 通过传来的 message,获取到 handlerName,从注册 JS 函数时,保存 handler 处理函数的 messageHandlers 对象中取出要调用的 handler 函数,并调用。
如何处理 Obj-C 的回调函数。
在Obj-C 调用 JS 的函数时,有时候 Obj-C 需要 JS 调用 Obj-C 的回调函数返回一些数据,如:
1 2 3
| [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) { NSLog(@"responseCallback called: %@", responseData); }];
|
这个 responseCallback
就是 JS 要回调 Obj-C 的回调函数。实现方式是在 JS 处理 Obj-C 消息的 _doDispatchMessageFromObjC
方法中,如果 message 有 callbackId 意味着 Obj-C 需要这个回调,然后构建一个 JS 函数 responseCallback
,传给 JS 的 handler 函数,供注册的 JS 函数回调时调用,调用 responseCallback
之后,responseCallback
会创建一个回调的 message 信息 responseId 就是 callbackId,调用 _doSend
函数发送给 Obj-C。
_doSend
中主要是将消息添加到 sendMessageQueue
队列中,供 Obj-C 获取后,通过 responseId 找到 Obj-C 保存的回调函数并调用,实现请看 - (void)flushMessageQueue:(NSString *)messageQueueString
方法中 if (responseId) { ... }
这一段。
1 2 3 4 5 6
| // - (void)flushMessageQueue:(NSString *)messageQueueString 中 if (responseId) { WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; responseCallback(message[@"responseData"]); [self.responseCallbacks removeObjectForKey:responseId]; }
|
最后
WebViewJavascriptBridge 工具库,接口简单,易于使用,但它也有一些缺点需要完善,如对提供给 JavaScript 的接口没有管理机制,不支持 Android 版本,虽然 JavaScript 中不需要引入额外的 Js 文件,但是需要在 bridge 对象初始化完成后的回调中注册函数,调用原生方法。