WebViewJavascriptBridge源码探究--看OC和JS交互过程

BusterMcCra 8年前
   <p>今天把实现OC代码和JS代码交互的第三方库 WebViewJavascriptBridge源码看了下,oc调用js方法我们是知道的,系统提供了 stringByEvaluatingJavaScriptFromString函数</p>    <p>。现在主要是了解js是如何调用oc方法的,分享下探究过程。</p>    <p>源码不多,就一个头文件WebViewJavascriptBridge.h和实现文件WebViewJavascriptBridge.m, 和一个js文件,实现在js那边可以调用oc方法,也可以在oc里面调用js方法。</p>    <p>先上图,实现简单的oc和js互相调用的demo, 另外附加一个模拟项目中用到的oc和js互相调用场景:</p>    <p><img src="https://simg.open-open.com/show/93a87838ef9ef5a5e7f907ff9b0ca976.gif"></p>    <p>一、然后说说js调用oc方法的原理,它们是如何实现的?库文件三个</p>    <p><img src="https://simg.open-open.com/show/04b0985bd86da4f01face10d47f62513.png"></p>    <p>我们跟踪下oc控制器加载UIWebView的过程和js调用oc方法过程</p>    <p>1、程序启动,在自定义控制器里,创建一个 WebViewJavascriptBridge对象时,会加载 WebViewJavascriptBridge.js.txt文件,里面是初始js代码</p>    <p>在这个js里面,创建了一个 WebViewJavascriptBridge脚本对象,另外创建一个隐藏的iframe标签:每次js调用oc方法,都是修改iframe标签的src来触发UIWebView的代理监听方法</p>    <pre>  <code class="language-javascript">  ;(function() {      if (window.WebViewJavascriptBridge) { return }      var messagingIframe      var sendMessageQueue = []      var receiveMessageQueue = []      var messageHandlers = {}            var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'      var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'            var responseCallbacks = {}      var uniqueId = 1            function _createQueueReadyIframe(doc) {          messagingIframe = doc.createElement('iframe')          messagingIframe.style.display = 'none'          messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE          doc.documentElement.appendChild(messagingIframe)      }        function init(messageHandler) {          if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }          WebViewJavascriptBridge._messageHandler = messageHandler          var receivedMessages = receiveMessageQueue          receiveMessageQueue = null          for (var i=0; i<receivedMessages.length; i++) {              _dispatchMessageFromObjC(receivedMessages[i])          }      }        function send(data, responseCallback) {          _doSend({ data:data }, responseCallback)      }            function registerHandler(handlerName, handler) {          messageHandlers[handlerName] = handler      }            function callHandler(handlerName, data, responseCallback) {          _doSend({ handlerName: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); //将字典放入数组          //修改iframe标签的src属性,UIWebView监听执行代理方法          messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;      }        function _fetchQueue() {  //json数组转成json字符串          var messageQueueString = JSON.stringify(sendMessageQueue)          sendMessageQueue = []          return messageQueueString      }        function _dispatchMessageFromObjC(messageJSON) {          setTimeout(function _timeoutDispatchMessageFromObjC() {              var message = JSON.parse(messageJSON)              var messageHandler                            if (message.responseId) {                  var responseCallback = responseCallbacks[message.responseId]                  if (!responseCallback) { return; }                  responseCallback(message.responseData)                  delete responseCallbacks[message.responseId]              } else {                  var responseCallback                  if (message.callbackId) {                      var callbackResponseId = message.callbackId                      responseCallback = function(responseData) {                          _doSend({ responseId:callbackResponseId, responseData:responseData })                      }                  }                                    var handler = WebViewJavascriptBridge._messageHandler                  if (message.handlerName) {                      handler = messageHandlers[message.handlerName]                  }                                    try {                      handler(message.data, responseCallback)                  } catch(exception) {                      if (typeof console != 'undefined') {                          console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)                      }                  }              }          })      }            function _handleMessageFromObjC(messageJSON) {          if (receiveMessageQueue) {              receiveMessageQueue.push(messageJSON)          } else {              _dispatchMessageFromObjC(messageJSON)          }      }        window.WebViewJavascriptBridge = {          init: init,          send: send,          registerHandler: registerHandler,          callHandler: callHandler,          _fetchQueue: _fetchQueue,          _handleMessageFromObjC: _handleMessageFromObjC      }        var doc = document      _createQueueReadyIframe(doc)      var readyEvent = doc.createEvent('Events')      readyEvent.initEvent('WebViewJavascriptBridgeReady')      readyEvent.bridge = WebViewJavascriptBridge      doc.dispatchEvent(readyEvent)  })();    View Code</code></pre>    <p><img src="https://simg.open-open.com/show/8b700c9b6d210ab0cb93ebfcca2316d8.png"></p>    <p><img src="https://simg.open-open.com/show/5c86a50969983085b3292d9cbb0d0464.png"> <img src="https://simg.open-open.com/show/f5e50af1b436de0ed1424cbecd87d141.png"></p>    <p>2、UIWebView加载我们自定义的html页面TestJSBridge.html, 里面有脚本注册js调用oc方法标识,和oc调用js标识</p>    <pre>  <code class="language-javascript">  <html>      <head>          <meta charset="utf-8"/>          <style type="text/css">              html { font-family:Helvetica; color:#222; background:#D5FFFD; border: 5px dashed blue;}              .rowH3{margin: 0px; text-align: center;}              .jsBtn{font-size: 18px;}          </style>      </head>      <body>          <h3 class="rowH3">测试OC和JS互相调用</h3>          <button class="jsBtn" id="jsBtn">JS调用OC方法</button>          <div id="logDiv"><!-- 打印日志 --></div>      </body>  </html>  <script type="text/javascript">      window.onerror = function(err) {          printLog(err);      }            function connectWebViewJavascriptBridge(callback) {          if (window.WebViewJavascriptBridge) {              callback(WebViewJavascriptBridge);          } else {              document.addEventListener('WebViewJavascriptBridgeReady', function() {                  callback(WebViewJavascriptBridge);              }, false);          }      }            var uniqueId = 1;      //日志打印方法      function printLog(data) {          var logObj = document.getElementById('logDiv');          var el = document.createElement('div');          el.className = 'logLine';          el.innerHTML = uniqueId++ + ': ' + JSON.stringify(data); //json转字符串          if (logObj.children.length) { logObj.insertBefore(el, logObj.children[0]) }          else { logObj.appendChild(el) }      }            //初始化调用函数connectWebViewJavascriptBridge      connectWebViewJavascriptBridge(function(bridge) {                    bridge.init(function(message, responseCallback) {});                                                 //注册js响应方法,响应OC调用,标识objc_Call_JS_Func          bridge.registerHandler('objc_Call_JS_Func', function(data, responseCallback) {              printLog(data);  //打印oc传过来的参数          });            //给标签按钮设置点击事件          var callbackButton = document.getElementById('jsBtn');          callbackButton.onclick = function(e) {              e.preventDefault();              //注册标识js_Call_Objc_Func,便于js给IOS发送消息              bridge.callHandler('js_Call_Objc_Func', {id: 1, info: 'hello, iOS, 我从js那边过来!'},  function(response) { });          }      });        </script>    View Code</code></pre>    <p>3、点击html标签按钮,触发js事件</p>    <pre>  <code class="language-javascript">//给标签按钮设置点击事件    var callbackButton = document.getElementById('jsBtn');    callbackButton.onclick = function(e) {     e.preventDefault();              //注册标识js_Call_Objc_Func,便于js给IOS发送消息     bridge.callHandler('js_Call_Objc_Func', {id: 1, info: 'hello, iOS, 我从js那边过来!'},  function(response) { });    }</code></pre>    <p>我们跟踪bridge.callHandler方法,进入WebViewJavascriptBridge.js</p>    <p>var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'</p>    <p>var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'</p>    <p><img src="https://simg.open-open.com/show/e47d0ec8d4cf79b5c87b426d177266fc.png"></p>    <p>messagingIframe是个iframe标签,点击我们自定义html按钮标签,触发js事件,最后进入callHandler -->  _doSend ,</p>    <p>当messagingIframe标签src重新赋值时,会触发UIWebView的代理方法(src的值一直是:wvjbscheme:// __WVJB_QUEUE_MESSAGE__ ,也可自定义,这个在进入oc UIWebView代理方法时会用来作为判断标识)。</p>    <p>跟踪后面执行的过程:</p>    <p><img src="https://simg.open-open.com/show/c8d967d4046365421fc2e5b1aaaa216e.png"></p>    <p><img src="https://simg.open-open.com/show/b75995f6e05da96e7e3af4725d3f3a67.png"></p>    <p><img src="https://simg.open-open.com/show/66bebad33ddb0652bfa0a921a6e905a9.png"></p>    <p><img src="https://simg.open-open.com/show/6feb1b88a52099638e0fe71b1f99e4e7.png"></p>    <p>至此,js调用oc成功</p>    <p>总结js调用oc过程:</p>    <p>-->   触发js事件</p>    <p>-->   把要传入参数和自定义注册标识“ js_Call_Objc_Func” 存入js数组 sendMessageQueue</p>    <p>-->  重新赋值iframe标签的src属性,触发UIWebView代理方法, 根据src的值进入相应处理方法中</p>    <p>-->    在oc方法里面调用js方法 _fetchQueue, 获取js数组里面所有的参数  </p>    <p>-->   根据传入的自定义注册标识 js_Call_Objc_Func  从oc字典 _messageHandlers找出匹配block, 最后执行block,里面有我们自定义处理的后续代码</p>    <p>二、oc调用js过程</p>    <p>从oc内部发起</p>    <p>-- > 调用bridge的 callHandler方法,传入需要的参数和自定义注册标识</p>    <p>--> 最后使用UIWebView系统方法 stringByEvaluatingJavaScriptFromString调用js脚本 WebViewJavascriptBridge._handleMessageFromObjC 完成参数的传递</p>    <p>---------------------------------------------  end --------------------------------------</p>    <p>DEMO下载</p>    <p>github地址: <a href="/misc/goto?guid=4959675646233227865" rel="nofollow,noindex">https://github.com/xiaotanit/Tan_WebViewJavaScriptBridge</a></p>    <p>另外记录一个UIWebView不能加载带中文参数的url问题:</p>    <p>假设加载url为: http://baidu.com/?search= 博客园</p>    <p>这样UIWebView加载这个带中文参数的url, 是不能显示的,需要把中文进行转义,才能显示。</p>    <p>使用字符串方法 stringByAddingPercentEncodingWithAllowedCharacters对中文进行转义</p>    <pre>  <code class="language-javascript">NSString *str = @"http://baidu.com/?search=博客园";    //  str = [str stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];    str = [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];         NSURL *url = [NSURL URLWithString:str];    NSURLRequest *request = [NSURLRequest requestWithURL:url];  </code></pre>    <p> </p>    <p>来自:http://www.cnblogs.com/tandaxia/p/5699886.html</p>    <p> </p>