相关示例源码在这里
由于原生开发的时间周期相对表较长,难免带来一些更新不灵活的问题,有时候为了业务的需要不得不在原生APP中嵌入一些以Web实现的内容,大部分都是静态页面,一般也不会进行二次跳转,而这些页面有时候也会做一些与原生APP进行交互的功能,这些功能通常需要事先制定相应的协议,这些协议中的方法一般会当做通用的API,方便三端在新的规则出现时不需要反复制定规则,只需要在方法中更改相应的参数即可实现新规则下的交互。
这里分别介绍使用WKWebView 与 UIWebView 实现 OC与JS 交互的方式,以及一些在业务上的个人建议。在代码上这些都是很容易实现的功能,关键点是如何使用这项功能。
-
准备工作
-
1 引入 WebKit 框架
#import <WebKit/WebKit.h> @interface WKWebViewController () < WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler> // JS 调用原生需要实现的相关协议 @property(nonatomic, strong) WKWebView *webView; @end
-
2 创建 WKWebView 添加供JS调用的方法名
#pragma mark - Getter - (WKWebView *)webView { if (_webView == nil) { WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.preferences = [[WKPreferences alloc] init]; config.preferences.minimumFontSize = 10; config.preferences.javaScriptEnabled = YES; config.preferences.javaScriptCanOpenWindowsAutomatically = NO; config.userContentController = [[WKUserContentController alloc] init]; config.processPool = [[WKProcessPool alloc] init]; _webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config]; _webView.UIDelegate = self; _webView.navigationDelegate = self; _webView.frame = self.view.bounds; [config.userContentController addScriptMessageHandler:self name:@"sendMsgToApp"]; } return _webView; }
-
3 这里需要介绍一下 WKWebViewConfiguration,不做深入了解的话可以跳过直接看代码
WKWebView 初始化时,有一个参数叫configuration,它是WKWebViewConfiguration类型的参数,而WKWebViewConfiguration有一个属性叫userContentController,它又是WKUserContentController类型的参数。WKUserContentController对象有一个方法**- addScriptMessageHandler:name:,我把这个功能简称为MessageHandler**。添加MessageHandler其实就是添加供WKWebView 中 JS 调用的对象(heandle)和方法名(name)。
-
-
交互逻辑之 JS 调用 OC
当我们注册了userContentController之后,JS 调用iOS原生就会走这个代理,并且会返回WKScriptMessage对象message,其中WKScriptMessage对象的两个属性是我们所需要的,message.name 是我们给JS添加的方法名,message.body 则是JS给我们发送的参数值,通常我们只需要注册一个方法名,业务逻辑的区分放到body里面来处理,这样可以方便前端与Native制定新的交互规则的时候,不需要维护新的公共API。
# pragma mark - WKScriptMessageHandler /** 此处接收并处理来自 JS 的调用 可以根据message.name 来区分调用的是哪个方法 , 建议只设计一个通用方法来供JS调用 然后根据参数值 message.body 来区分如何处理调用逻辑 也可以根据 参数值来区分如何处理调用事件 @param userContentController 控制器 @param message : message.name (方法名) message.body (参数) */ - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"sendMsgToApp"]) { [self sendMsgToApp:message]; } } # pragma mark - WKNavigationDelegate // webview跳转新页面的时候回调这个方法 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { } # pragma mark WKUIDelegate // webview如果需要调用系统的控件(alert)则会调用这个方法, 如果设置了UIDelegate,没有实现这个方法,WebView的alert就不会弹出来 - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"OC 调用 JS" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *action = [UIAlertAction actionWithTitle:@"知道了小肥仔" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { completionHandler(); }]; [alert addAction:action]; [self presentViewController:alert animated:YES completion:nil]; }
-
交互逻辑之 OC 调用 JS
这里的代码非常简单,相关的调用只有一行代码: - evaluateJavaScript:jsStr completionHandler: 其中sendMessageToWebView()是WebView公开给Native调用的公共接口,相关的参数转成字符串放到括号内即可,同样的,前端只需要公开一个API,相关的业务逻辑放到参数里面处理就可以了,也是为了减少维护公共API的成本。
// Native 调用 JS - (void)sendMessageToWebView:(UIBarButtonItem *)sender { NSString *msg = [NSString stringWithFormat:@"我只是一只小兔几,我什么都不知道。"]; NSString * result = [self noWhiteSpaceString:msg]; // sendMessageToWebView NSString * jsStr = [NSString stringWithFormat:@"sendMessageToWebView(\"%@\")",result]; [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) { NSLog(@"交互错误value :%@ error: %@",result,error); }]; }
-
准备工作
自然是创建UIWebView并设置代理了
self.webView = [[UIWebView alloc]initWithFrame:CGRectMake(0, 20, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)]; self.webView.delegate = self; NSString* path = [[NSBundle mainBundle] pathForResource:@"source" ofType:@"html"]; NSURL* url = [NSURL fileURLWithPath:path]; NSURLRequest* request = [NSURLRequest requestWithURL:url] ; [self.webView loadRequest:request]; [self.view addSubview:self.webView];
-
遵守协议
在该协议中,定义供JS调用的方法,建议设置为一个通用接口,方便JS调用
# import <JavaScriptCore/JavaScriptCore.h> @protocol JSObjcDelegate <JSExport> /** 前端调用Native 此API用来供前端(H5)调用, 为了方便制定调用的协议, 此API应该设计成通用API, 其中所涉及的场景应该由参数来决定, 不应设计过多的API 同理前端也应该只需设计一个API供Native调用 @param param 调用参数 */ - (void)sendMsgToApp:(NSString *)param; @end @interface UIWebViewController : UIViewController<UIWebViewDelegate,JSObjcDelegate> @property (nonatomic, strong) JSContext *jsContext; @property (strong, nonatomic) UIWebView *webView; @end
-
实现协议方法,并向JSContext注册对象
所谓注册对象,就是告诉JS该调用谁的什么方法,总体来说也就是以下三行代码,只不过根据每个公司前端所写的业务不同,注入时机可能会有所区别,正常来说都是WebView通知Native在合适的时机注入即可,其中的区别我写在这里了。
-
# pragma mark - UIWebViewDelegate - (void)webViewDidStartLoad:(UIWebView *)webView { self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 将JS中的iOS_NativeModel对象(JS中定义为什么名称就是什么名称)设置为当前控制器,JS才可以调用当前控制器所遵守协议中的方法 self.jsContext[@"iOS_NativeModel"] = self; self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"异常信息:%@", exceptionValue); }; }
-
JS 调用 Native
这里就是在WebView所在的控制器实现之前的协议中的方法即可,JS调用协议中的方法就会来到方法的具体实现,PS:JS所传值为字符串,需要根据相关业务参数看是否需要转化为JSON或其他对象
-
# pragma mark - JSObjcDelegate // 为保证交互结果的安全可控 // 在native中执行的相关代码务必放在主线程中执行 - (void)sendMsgToApp:(NSString *)param { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"param:%@",param); }); }
-
OC 调用 JS
这里比WKWebView稍微复杂一些,但是基本原理是一样的,UIWebView这里使用JSValue对象来实现,设置所调用的JS函数与参数与WKWebView是一样的。
-
# pragma mark - Public // 通过 JSValue 对象发送消息给 WEB 页面 - (void)sendMessageToWebView:(UIBarButtonItem *)sender { JSValue *jsObject = self.jsContext[@"receiveMsgFromApp"]; NSString *param = [NSString stringWithFormat:@"%@\n%@\n%@\n详细信息:%@",@"商品获取成功",@"商品名称:哈哈",@"商品ID:123456",@"这是商品信息"]; NSString *callBackStr = [NSString stringWithFormat:@"receiveMsgFromApp(%@)", param ]; [jsObject callWithArguments:@[callBackStr]]; }