WKWebView-Bridge 在react-native封装的实现 iOS开发

1071 查看

需求:

1、监控与重写url跳转。

2、自定义Loading样式不够强大。

3、RN与Web通信的Bridge。

4、一些特殊应用:tel://,mailto://,微信的,甚至于一些自定义协议。

5、将来做自己的WebAPI。


实现

一、文件结构


OC文件示意图.png

js文件示意图.png

二、导入说明

参考链接:

UIWebView-bridge:GItHub链接

W KWebView:GitHub链接;

上述两个示例,一个可以实现RN与Web的通信,另一个无法通信却可以实现对加载进度的监控,方便我们实现自己的loading.

导入方式我们选择WKWebView的方式导入(项目中我已经导入完毕);
然后在导入的源代码里加入我们要实现的Bridge功能即可。

三、使用说明

1、监控与重写URL跳转
    /*
     *Allows custom handling of any webview requests by a JS handler. Return true
     *or false from this method to continue loading the request.
     *@platform ios
     */
    onShouldStartLoadWithRequest: PropTypes.func,

这个方法是webView的一个属性,可以绑定一个函数,例如:

<WKWebView  source={{uri:this.state.src}}
                    ref = {'webviewref'}
                    startInLoadingState={true}
                    renderLoading={()=><Text>正在加载页面...</Text>}
                    style={styles.webViewStyle}
                    onLoad = {()=>TestMBProgressManager.textExample("加载中.....")}
                    onShouldStartLoadWithRequest  = {(e)=>{
                      console.log(e);
                      return true;
                    }}
                    injectedJavaScript = {injectScript}
                    onBridgeMessage = { this.onBridgeMessage.bind(this) }
        />

在这个函数里我们就可以对当前加载的URL进行筛选和判断。

输出的日志示例如下:

{ target: 9,
canGoBack: false,
lockIdentifier: 1189458900,
loading: false,
title: '百度一下',
canGoForward: false,
 navigationType: -1,
  url: 'wkwvb://message1473669712719' }

在获取到URL之后,返回false就是不让加载当前URL。

若是想跳转新的页面,只需重新设置soruce并reload即可。

2、自定义Loading样式不够强大

在这里提供了多种方式去自定义loading。

(1)在react里自定义视图,设置样式。

示例如下:

//WebView的属性之一,返回一个视图,在加载的时候呈现。
renderLoading={()=><Text>正在加载页面...</Text>}

(2) 通过native实现。

在这里我创建了一个MBPrograssHUD的管理类,可以实现自定义文字、图片等的 加载图。在开始加载的时候,使他显示,加载完毕后,使其消失。

但不建议使用这种方法去实现。

3、RN与Web通信的Bridge

我们用一个简单的打招呼的过程来理解Bridge的使用(这仅仅是一个示例,更复杂的逻辑,根据需求由我们的工程师自己去实现吧。)
首先在建立WebView的时候,我们为当前页面注入如下js代码,由Web向RN打招呼:(reactive中实现)

const injectScript = `
  (function(){
     if (WebViewBridge) {

        WebViewBridge.onMessage = function(message){
          if (message === "hello from react-native") {
            WebViewBridge.send("got the message inside webview");
          }
        };

        WebViewBridge.send("hello from webview");
        }
  }());

`;

在native的RCTWebView.m中,我实现了以下方法:

//since there is no easy way to load the static lib resource in ios,
//we are loading the script from this method.
- (NSString*)webViewBridgeScript{

  return NSStringMultiline((function (window) {
    'use strict';

    //Make sure that if WebViewBridge already in scope we don't override it.
    if (window.WebViewBridge) {
      return;
    }

    var RNWBSchema = 'wkwvb';
    var sendQueue = [];
    var receiveQueue = [];
    var doc = window.document;
    var customEvent = doc.createEvent('Event');

    function callFunc(func, message) {
      if ('function' === typeof func) {
        func(message);
      }
    }

    function signalNative() {
      window.location = RNWBSchema + '://message' + new Date().getTime();
    }

    //I made the private function ugly signiture so user doesn't called them accidently.
    //if you do, then I have nothing to say. :(
    var WebViewBridge = {
      //this function will be called by native side to push a new message
      //to webview.
    __push__: function (message) {
      receiveQueue.push(message);
      //reason I need this setTmeout is to return this function as fast as
      //possible to release the native side thread.
      setTimeout(function () {
        var message = receiveQueue.pop();
        callFunc(WebViewBridge.onMessage, message);
      }, 15); //this magic number is just a random small value. I don't like 0.
    },
    __fetch__: function () {
      //since our sendQueue array only contains string, and our connection to native
      //can only accept string, we need to convert array of strings into single string.
      var messages = JSON.stringify(sendQueue);

      //we make sure that sendQueue is resets
      sendQueue = [];

      //return the messages back to native side.
      return messages;
    },
      //make sure message is string. because only string can be sent to native,
      //if you don't pass it as string, onError function will be called.
    send: function (message) {
      alert(message);
      if ('string' !== typeof message) {
        callFunc(WebViewBridge.onError, "message is type '" + typeof message + "', and it needs to be string");
        return;
      }

      //we queue the messages to make sure that native can collects all of them in one shot.
      sendQueue.push(message);
      //signal the objective-c that there is a message in the queue
      signalNative();
    },
    onMessage: null,
    onError: null
    };

    window.WebViewBridge = WebViewBridge;

    //dispatch event
    customEvent.initEvent('WebViewBridge', true, true);
    doc.dispatchEvent(customEvent);
  }(window));
  );
}

这是一段js代码,在每次加载完毕后,都为当前应用注入,旨在建立web与RN的通信通道。

在RN中,我们监听Web消息的方法如下:

onBridgeMessage(message) {

      // var webview = this.refs.webview.getDOMNode();
      // const { webviewref } = this.refs;
      const webview = this.refs['webviewref']
      ;
      switch (message) {
        case "hello from webview":
        webview.sendToBridge("hello from react-native");
        console.log('我们打招呼给WebView');
        break;
        case "got the message inside webview":
        console.log("we have got a message from webview!yeah!");
        break;
      }
  }
//绑定到WebView的属性上面
onBridgeMessage = { this.onBridgeMessage.bind(this) }

每当web有消息发送过来的时候,这个方法都会被触发,然后我们就可以在RN里面根据我们自己的需求去处理相应的消息。

这里通过sendToBridge的方法,由RN向web发送消息。

然后又由注入的js代码中的WebViewBridge.onMessage这个函数接受消息,并做处理。(即我们最开始注入的js代码)。

这样,就实现了Bridge的通信。

4、一些特殊应用:tel://,mailto://,微信的,甚至于一些自定义协议。

在native代码中,有这样一个方法:

- (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler

在这个方法中:

 NSURLRequest *request = navigationAction.request;
  NSURL* url = request.URL;
  NSString* scheme = url.scheme;

这样就可以根据不同的scheme对不同的协议进行处理了。包括上述bridge,在这里也运用了scheme去实现的,部分代码:

if (isJSNavigation) {
    decisionHandler(WKNavigationActionPolicyCancel);
  }
  else if (navigationAction.targetFrame && ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) {
    decisionHandler(WKNavigationActionPolicyAllow);
  }
  else {
    if([scheme isEqualToString:@"wkwvb"]){
      decisionHandler(WKNavigationActionPolicyCancel);
      return;
    }

    if (![scheme isEqualToString:@"about"]) {
      [[UIApplication sharedApplication] openURL:url];
    }
    decisionHandler(WKNavigationActionPolicyAllow);
  }

在这里wkwvb就是RN与web互相通信时,我自定义的协议,如果有其他的协议,都可以在这个方法中进行处理。具体的逻辑,就根据需求由工程师去实现了。

5、将来做自己的WebAPI

具体逻辑,在将来,根据需求由工程师去实现。

如何使用这个控件,请详细阅读wkwebView.ios.js文件,每个属性的用法和功能都有详细的注释。