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技术分享原创和收集

发表评论 (审核通过后显示评论):