Hybrid之JSBridge的实现原理(WebViewJavascriptBridge源码分析)
前言
小编之前写的 iOS WebView和JS的交互 这篇文章介绍了iOS和js交互的几种方式。其中现在最常用的是JSBridge的方式,我们在上一篇也介绍了具体的使用,本文详细介绍JSBridge(WebViewJavascriptBridge)的实现原理。android版本的JSBridge实现原理由于能力有限,不作介绍。不过可以脑补一下,其实现思路应该和iOS版本的JSBridge是一样的。下边我们正式介绍了
交互详细流程图梳理
jsBridge交互.png
WebViewJavascriptBridge源码解读
目录和相关文件功能介绍
首先先看一下WebViewJavascriptBridge插件的目录
QQ截图20200724150849.png
WebViewJavascriptBridge 在使用的时候,初始化对象做了对UIWebView和WKWebView的兼容。对外暴露外界使用的方法,以及拦截UIWebView的代理方法
WebViewJavascriptBridge_JS 注入js代码,主要是js和native之间相互调用的js代码
WebViewJavascriptBridgeBase 主要是管理js和native交互的数据,以及native和js之间相互调用的工具方法
WKWebViewJavascriptBridge 为WKWebView的定制的对象,主要实现拦截WKWebView的代理方法,以及暴露外界使用的方法
WebViewJavascriptBridge的初始化流程
要想了解源码,必定要先会使用,那么我们先从使用的最开始,初始化一步步了解
首先初始化native端的WebViewJavascriptBridge初始化,会执行WebViewJavascriptBridge对象的bridge方法
WebViewJavascriptBridge的初始化
+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}
#endif
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
[NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
return nil;
}
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
_webView = webView;
_webView.policyDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
这里有两个变量:
supportsWKWebView 判断系统是否支持WKWebView
WVJB_WEBVIEW_TYPE 在ios端是UIWebView
1、如果支持WKWebView,且传入的webView是WKWebView类型,使用WKWebViewJavascriptBridge对象初始化,代码如下所示
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
其实WKWebViewJavascriptBridge和WebViewJavascriptBridge对象做的事情基本一样,只是两者在内部分别处理了UIWebView和WKWebView本身api的调用和实现的代理方法,暴露给外界的方法一模一样。所以这里我们就只介绍WebViewJavascriptBridge这个对象的实现,WKWebViewJavascriptBridge这个对象的实现可以自己看一下。这里就省略了。
2、 不满足1的条件,就使用WebViewJavascriptBridge对象初始化
3、设置UIWebView/WKWebView的代理,在各自对象里边实现UIWebView/WKWebView的代理方法,最后初始化WebViewJavascriptBridgeBase对象。
native端WebViewJavascriptBridgeBase的初始化
这里初始化了messageHandlers,startupMessageQueue,responseCallbacks,_uniqueId四个属性,代码如下
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
messageHandlers:存放native注册的事件交互的字典对象
startupMessageQueue:存放在js注入之前,native端调用js的事件交互的队列。因为js端需要手动触发通知native端注入js代码,在这时候注入完成需要立即执行队列里的任务
responseCallbacks:存在native端调用js传入的回调函数字典对象
_uniqueId:这是一个记录自增数字,主要用来生成不同的`callBackId
这时候,native端的初始化已经完成。但是有的小伙伴存在一个疑问,js全局Bridege初始化呢?js端和native端是怎么相互调用的呢?这一切都是疑问,下边我们一步步解开她的面纱
js端初始化和js代码的注入
js端触发初始化
官方文档提示js端需要执行下边代码。那我们就从这里开始分析js代码是如何注入的。js端执行代码如下:
function iosSetupWebViewJavaScriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
return callback(window.WebViewJavascriptBridge)
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback)
}
window.WVJBCallbacks = [callback]
const WVJBIframe = document.createElement('iframe')
WVJBIframe.style.display = 'none'
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'
document.documentElement.appendChild(WVJBIframe)
setTimeout(function timeCB() {
document.documentElement.removeChild(WVJBIframe)
}, 0)
}
iosSetupWebViewJavaScriptBridge(function(JSBridge){
console.log(JSBridge); // js端获取到的WebViewJavascriptBridge对象
})
我们可以看到这里js先创建了iframe,挂在到document文档。然后设置了src=wvjbscheme://__BRIDGE_LOADED__,最后把iframe移除。
native的webView拦截初始化url
在上边的创建iframe,挂载iframe,设置iframe的src属性的过程结束后,native端webView会发生重新请求,会触发native端WebViewJavascriptBridge对象实现的UIWebView代理方法,在这一步对请求的url做拦截,判断是初始化动作还是执行事件交互的动作。如下代码
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener {
if (webView != _webView) { return; }
NSURL *url = [request URL];
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) { // 初始化会执行
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) { // js端调用native端会执行
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
[listener ignore];
} else if (_webViewDelegate && [_webViewDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:request:frame:decisionListener:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:actionInformation request:request frame:frame decisionListener:listener];
} else {
[listener use];
}
}
因为这里webview获取到的url是iframe的src='wvjbscheme://__BRIDGE_LOADED__',这里native端执行[_base isBridgeLoadedURL:url]返回true,然后会执行WebViewJavascriptBridgeBase对象injectJavascriptFile的实例方法,这个方法其实就是注入js代码的方法,代码如下
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js(); // 获取将要注入的js代码
[self _evaluateJavascript:js]; // webview注入js代码 UIWebView和WKWebView注入的方法不一样
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
这里主要是获取WebViewJavascriptBridge_js文件中的js代码,然后webview注入这一段js代码。那么我们来看一下注入的js代码究竟是什么东西,代码里边我做了一些注释,可以看一下,方法具体的执行内容这里先不讲解,后边会详细介绍讲解
native端WebViewJavascriptBridge_js对象(这个对象维护的是注入的js代码)
;(function() {
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
// 注入的全局对象和方法
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
// iframe
var messagingIframe;
// 存放js调用native的事件交互的队列,因为js端调用native的事件交互是异步的过程,
// js可能会同时调用多个native端注册的事件交互,所以会把js端调用的事件交互放到这里,native端一起执行这些任务
var sendMessageQueue = [];
// 存放js注册的事件交互对象,数据结构:{name:function(){}}
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
// 存放js调用native的回调responseCallback数据,数据结构:{callBackId:responseCallback}
var responseCallbacks = {};
var uniqueId = 1; // 用来生成自增的callBackId
var dispatchMessagesWithTimeoutSafety = true;
// js注册native端调用的事件交互的全局方法
function registerHandler(handlerName, handler) {
....省略
}
// js 调用 native 端事件交互的全局方法
function callHandler(handlerName, data, responseCallback) {
....省略
}
function disableJavscriptAlertBoxSafetyTimeout() {
....省略
}
function _doSend(message, responseCallback) {
....省略
}
// 提供给native端调用,目的是获取js端调用native的所有待执行事件
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
....省略
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
// 创建iframe全局对象,设置src,隐藏iframe
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
// js代码注入后,执行到这里,执行js传过来的callBack,把WebViewJavascriptBridge返回出去,
//这样外界就可以获取到全局的WebViewJavascriptBridge对象
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i本文章由javascript技术分享原创和收集
发表评论 (审核通过后显示评论):