目录
WKWebView环境中的交互操作
Web环境中注入JS代码
JS调用原生方法
原生调用JS方法
WKWebView与原生交互实现
之前分析了使用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];
}