目录

WKWebView环境中的交互操作

Web环境中注入JS代码

JS调用原生方法

原生调用JS方法

WKWebView与原生交互实现


之前分析了使用UIWebView与原生交互的实现方式,在iOS8.0之后apple建议开发者使用WKWebView来做web界面的加载展示,尤其是在iOS12.0之后已经开始废弃对UIWebView的更新支持,之所以apple开始推荐使用WKWebView的使用是因为WKWebView使用多进程处理web加载在性能上远远优于UIWebView.

WKWebView环境中的交互操作

在WKWebView中,原生可以通过三种方式完成与JS的交互:即

  1. Web环境中注入JS代码;
  2. JS调用原生方法;
  3. 原生调用JS方法.

Web环境中注入JS代码

WKWebView将JS调用原生的过程进一步的封装

  • 使用WKUserScript封装需要注入的JS方法;
/*! @abstract Returns an initialized user script that can be added to a @link WKUserContentController @/link.
 @param source The script source.
 @param injectionTime When the script should be injected.
 @param forMainFrameOnly Whether the script should be injected into all frames or just the main frame.
 */

- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
  • 使用WKUserContentController添加WKUserScript对象:
- (void)addUserScript:(WKUserScript *)userScript;
  • 将WKUserContentController赋值给WKWebViewConfiguration对象;
/*! @abstract The user content controller to associate with the web view.
*/
@property (nonatomic, strong) WKUserContentController *userContentController;
  • 使用WKWebViewConfiguration初始化WKWebView对象:
/*! @abstract Returns a web view initialized with a specified frame and
 configuration.
 @param frame The frame for the new web view.
 @param configuration The configuration for the new web view.
 @result An initialized web view, or nil if the object could not be
 initialized.
 @discussion This is a designated initializer. You can use
 @link -initWithFrame: @/link to initialize an instance with the default
 configuration. The initializer copies the specified configuration, so
 mutating the configuration after invoking the initializer has no effect
 on the web view.
 */
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;

JS调用原生方法

在WKWebView中可以通过WKUserContentController注册供JS调用的方法:

  • 这里需要注意的是scriptMessageHandler会被WKUserContentController强引用,所以如果scriptMessageHandler本身对WKUserContentController进行了强引用就有可能导致循环引用从而造成内存泄漏(例如控制器强持有WKWebView,而控制器由实现了WKScriptMessageHandler协议成为scriptMessageHandler就会导致内存泄漏,此时可以通过在适当的时机移除scriptMessageHandler来循环引用).所以一般可以通过其他对象来实现WKScriptMessageHandler协议成为scriptMessageHandler.
/*! @abstract Adds a script message handler.
 @param scriptMessageHandler The message handler to add.
 @param name The name of the message handler.
 @discussion Adding a scriptMessageHandler adds a function
 window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all
 frames.
 */
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

然后在scriptMessageHandler中实现WKScriptMessageHandler协议方法,

@required

/*! @abstract Invoked when a script message is received from a webpage.
 @param userContentController The user content controller invoking the
 delegate method.
 @param message The script message received.
 */
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

即可以在JS发起调用时:

window.webkit.messageHandlers.`registerName`.postMessage{`parameters`}

监听到该方法的调用:

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSLog(@"getMessage:%@", message.name);//name为注册的方法名
    NSLog(@"getMessage:%@", message.body);//body为方法调用的参数
}

这样JS调用的方法以及参数就可以被原生监听到,完成JS调用原生方法实现.不过需要注意的是在确定scriptMessageHandler不再使用时,需要通过显式移除.

/*! @abstract Removes a script message handler.
 @param name The name of the message handler to remove.
 */
- (void)removeScriptMessageHandlerForName:(NSString *)name;

原生调用JS方法

在WKWebView的实现中,使用

/* @abstract Evaluates the given JavaScript string.
 @param javaScriptString The JavaScript string to evaluate.
 @param completionHandler A block to invoke when script evaluation completes or fails.
 @discussion The completionHandler is passed the result of the script evaluation or an error.
*/
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

来完成原生调用JS的方法实现,如果方法有返回值可以在completionHandler获取到方法返回值以及方法调用实现出现异常.

WKWebView与原生交互实现

以之前UIWebView中JS与原生的交互演示作为基础完成WKWebView中JS与原生进行交互展示.

  • 创建需要注入的js文件:主要用于完成JS调用原生方法时存储回调函数,用于接收原生反馈;
;(function(w, doc){
  //防止重复添加
  if(w.Bridge && w.uuid){
  return;
  }

  var responseCallbacks = {}; //回调方法map
  //产生函数唯一标识
   var uuid=(function(){
      return function(){
        var timestamp = new Date().getTime()
        return  timestamp;
      }
  })();

  //JS调用原生方法
  function callNative(data, responseCallback) {
    data = data || {}
    try{
        var cid = 'cid' + uuid();
        if(responseCallback) {
            responseCallbacks[cid] = responseCallback;//保存回调
            data.callbackID = cid; //回调时使用callbackID取出回调函数
        }
        w.webkit.messageHandlers.callObjc.postMessage(data);
        }catch(e){
            if(typeof console !== 'undefined') {
            console.error('[JSBridge] EXCEPTION: ', e);
        }
    }
  }
  //原生调用JS方法返回消息给JS
  function invokeJSCallback (cid, removeAfterExecute, config) {
    if (!cid) {
      return;
    }
    var cb = responseCallbacks[cid];
    if (!cb) {
      return;
    }

    if (removeAfterExecute) {
      delete (responseCallbacks[cid]);
    }
    var data = config;
    if (data.callbackID) {
      delete data.callbackID;
    }
    cb.call(null, data);
  }
  //将对象绑定在window上
  w.Bridge = {
    callNative:callNative.bind(this),
    invokeJSCallback: invokeJSCallback.bind(this),
  };
})(window, document);
  • 创建用于承载WKWebView的控制器,并创建WKWebView实例:
- (WKWebView *)webview {
    if (!_webview) {
        WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
        WKUserContentController *userContentController = [[WKUserContentController alloc] init];
        //加载需要注入的js
        NSError *error = nil;
        NSString *path = [[NSBundle mainBundle] pathForResource:@"bridge" ofType:@"js"];
        NSString *js = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
        NSAssert(!error, @"加载JS出现异常");
        
        WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:(WKUserScriptInjectionTimeAtDocumentEnd) forMainFrameOnly:true];
        [userContentController addUserScript:script];


        //注册JS方法:如果控制器本身强持有了WKWebView,而控制器本身实现了WKScriptMessageHandler协议成为scriptMessageHandler,则需要在适当的时候移除scriptMessageHandler否则会造成内存泄漏
        [userContentController addScriptMessageHandler:self name:method_function_name];

        configuration.userContentController = userContentController;
        _webview = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:(configuration)];
        
    }
    return _webview;
}
  • 服从WKScriptMessageHandler协议并实现方法处理:
@interface WKViewController ()<WKScriptMessageHandler>
@end



static NSString * const method_function_name = @"callObjc";
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString: method_function_name]) {
        NSDictionary *params = (NSDictionary *)message.body;
        NSString *api = params[@"api"];
        if ([api isEqualToString:@"show.alert"]) {
            NSString *callbackID = params[@"callbackID"];
            NSDictionary *data = params[@"data"];
            NSArray<NSString *> *buttons = data[@"buttons"];
            NSString *title = data[@"title"];
            NSString *msg = data[@"msg"];
            showAlertController(title, msg, buttons, ^(NSDictionary *params){
                NSString *js = [NSString stringWithFormat:@"Bridge.invokeJSCallback(\"%@\", 'true', %@)", callbackID, params.json];
                [self.webview evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
                    NSLog(@"response=%@, error:%@", response, error);
                }];
            });
        } else {
            //other type actions
        }
    }    
}

 

  • 在适当的时候移除scriptMessageHandler防止内存泄漏.
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.webview.configuration.userContentController removeScriptMessageHandlerForName:method_function_name];
}