目录
之前分析了使用UIWebView与原生交互的实现方式,在iOS8.0之后apple建议开发者使用WKWebView来做web界面的加载展示,尤其是在iOS12.0之后已经开始废弃对UIWebView的更新支持,之所以apple开始推荐使用WKWebView的使用是因为WKWebView使用多进程处理web加载在性能上远远优于UIWebView.
WKWebView环境中的交互操作
在WKWebView中,原生可以通过三种方式完成与JS的交互:即
- Web环境中注入JS代码;
- JS调用原生方法;
- 原生调用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]; }