移动开发中经常会用到网页,例如活动信息展示并响应网页中的Click事件。这个交互的过程就涉及到js与OC或Swift的相互调用,这里OC或Swift就统称为Native,即原生应用。本文总结了三种最常见的js-Native交互实现方案并附上代码。
场景:点击网页按钮,跳转到Native支付页面,在网页中显示支付结果。
一.拦截URL
1.Native->js
Native调用js主要是通过stringByEvaluatingJavaScriptFromString
方法。
这是本地pay.html
网页代码:
1 2 3 4 5 6 7 8 9
| <p id="result">pay result: </p> <script type="text/javascript"> function payResult(result){ document.getElementById("result").innerHTML = 'pay result: ' + result; } </script>
|
Native中加载HTML并调用其定义的js方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @interface DKUIWebViewController ()<UIWebViewDelegate> @property (weak, nonatomic) IBOutlet UIWebView *mUIWebview; @end
@implementation DKUIWebViewController
- (void)viewDidLoad { [super viewDidLoad]; NSString *path = [[NSBundle mainBundle] pathForResource:@"pay" ofType:@"html"]; NSURLRequest *request = [NSURLRequest requestWithURL: [NSURL fileURLWithPath:path]]; [_mUIWebview loadRequest:request]; }
#pragma -mark UIWebviewDelegate -(void)webViewDidFinishLoad:(UIWebView *)webView{ [_mUIWebview stringByEvaluatingJavaScriptFromString:@"payResult('succeed')"]; } @end
|
2.js->Native
在shouldStartLoadWithRequest:
回调中监听a
标签跳转事件。
修改本地pay.html
代码,加入a
标签跳转:
1 2 3 4 5 6 7 8 9 10 11 12
| <a href="router://pay">pay</a> <br/>
<p id="result">pay result: </p> <script type="text/javascript"> function payResult(result){ document.getElementById("result").innerHTML = 'pay result: ' + result; } </script>
|
Native网页中监听a
标签事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #pragma -mark UIWebviewDelegate - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if (UIWebViewNavigationTypeLinkClicked == navigationType) { if ([request.URL.absoluteString rangeOfString:@"router://pay"].location != NSNotFound) {
DKPayController *controller = [[DKPayController alloc] init]; [self.navigationController pushViewController:controller animated:YES];
return NO; } } return YES; }
|
点击网页中a
标签,Native中监听到router://pay
跳转请求后,本地跳转到支付页面。
二.JavaScriptCore
往网页中传入一个javaScriptContext
对象,利用它作为bridge
中介实现js与Native的交互。注意:这只适用于UIWebView
。
1.JSExport
JSExport
是JavaScriptCore
库提供的一个协议,此协议中可定义一些接口,供js调用。
2.js->Native
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #import <JavaScriptCore/JavaScriptCore.h>
@protocol JSOCExportProtocol <JSExport>
@property (nonatomic, copy) NSString *name;
- (void)pay; @end
@interface JSOCHelper : NSObject<JSOCExportProtocol> - (instancetype)initWithSource:(UIViewController*)viewController; @end
#import "JSOCHelper.h" @interface JSOCHelper() @property (nonatomic, weak) UIViewController *viewController; @end
@implementation JSOCHelper
- (instancetype)initWithSource:(UIViewController *)viewController { self = [super init]; if (self) { self.viewController = viewController; } return self; }
#pragma mark -JSExport Delegate methods - (void)pay{ NSLog(@"++++JS call OC: Go to Pay !"); } @end
|
上面是自定义的JSOCHelper类,其中的JSOCExportProtocol
协议实现了JSExport
协议,在这里定义了一些供js调用的接口并提供对应的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #import "JSOCHelper.h"
@interface DKUIWebViewController ()<UIWebViewDelegate> @property (weak, nonatomic) IBOutlet UIWebView *mUIWebview; @property (nonatomic, strong) JSContext *jsContext; @end
@implementation DKUIWebViewController
- (void)viewDidLoad { [super viewDidLoad]; NSString *path = [[NSBundle mainBundle] pathForResource:@"pay" ofType:@"html"]; NSURLRequest *request = [NSURLRequest requestWithURL: [NSURL fileURLWithPath:path]]; [_mUIWebview loadRequest:request]; }
#pragma -mark UIWebviewDelegate -(void)webViewDidFinishLoad:(UIWebView *)webView { _jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; _jsContext[@"jsbridge"] = [[JSOCHelper alloc] initWithSource:self]; _jsContext[@"login"] = ^(id data,NSString *error){ NSLog(@"++++++js call jsContext block, data:%@,error:%@",data,error); }; } @end
|
上面是Native部分的代码,主要是在合适的时机往web中注入一个jsbridge
对象,后面点击网页中的按钮时,js可通过这个对象直接调用Native函数。
你也可以往这个jsbridge
中绑定新的js方法
和与其对应的Native回调
,后面Native调用此js方法时会自动触发与其绑定的回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <input type="button" name="" value="pay" onclick="pay()"> <br/>
<p id="result">pay result: </p> <script type="text/javascript"> function pay(){ jsbridge.pay(); } function payResult(result){ document.getElementById("result").innerHTML = 'pay result: ' + result; } </script>
|
这是新的pay.html
网页,增加了一个按钮,用来调用Native的方法。
点击网页中的pay
按钮,js即会通过我们在webViewDidFinishLoad
中早先传入的jsbridge对象,直接调用Native的pay
方法,即JSOCHelper中定义好的协议方法。
3.Native->js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @interface DKUIWebViewController ()<UIWebViewDelegate> @property (weak, nonatomic) IBOutlet UIWebView *mUIWebview; @property (nonatomic, strong) JSContext *jsContext; @end
@implementation DKUIWebViewController
...
- (void)nativeCallJS{ JSValue *jsFunc = _jsContext[@"login"]; [jsFunc callWithArguments:@[@"data",@"call by JSValue Error info"]]; [_jsContext evaluateScript:@"login('data','call by [jsContext evaluateScript:] Error info')"]; NSString *jsFuncScript = @"login('data','Error info~')"; [_mUIWebview stringByEvaluatingJavaScriptFromString:jsFuncScript]; } @end
|
前两个是JavaScriptCore
提供的接口,方法3
是我们最熟悉的webview调用js的方法。
三.WKWebView
1.WKScriptMessageHandler
这是WKWebView
中用来进行js与Native交互的主要媒介,我们需要往web中注入一个或多个MessageHandler
对象处理不同的来自js对Native的调用。
它是一个协议并且只有一个代理方法。我们需要定义一个类实现此协议及其代理函数,通过区分每个handler
的名字来调用与之对应的Native函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #import <WebKit/WebKit.h>
UIKIT_EXTERN NSString * const WK_MethodName; @interface DKWKScriptMessageHandler : NSObject<WKScriptMessageHandler> @end
#import "DKWKScriptMessageHandler.h" NSString *const WK_MethodName = @"pay";
@implementation DKWKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:WK_MethodName]) { NSLog(@"++++WK Script message:%@",message.body); } } @end
|
2.js->Native
先看网页端的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <input type="button" name="" value="pay" onclick="pay()"> <br/>
<p id="result">pay result: </p> <script type="text/javascript"> function pay(){ window.webkit.messageHandlers.pay.postMessage('¥100'); } function payResult(result){ document.getElementById("result").innerHTML = 'pay result: ' + result; } </script>
|
第10行,js通过window.webkit.messageHandlers
获取处理交互的媒介:messageHandlers
。从其采用复数形式可知,Native在与js交互时可能会注册多个messageHandler
;
.pay
是指定具体的messageHandler
:
postMessage('¥100')
是向handler
发送消息和传递参数;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
再来看Native端的代码,往web中注入多个MessageHandler
对象来处理不同任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #import <WebKit/WebKit.h> #import "DKWKScriptMessageHandler.h"
@interface DKWKWebViewController ()<WKNavigationDelegate,WKUIDelegate>
@property (weak, nonatomic) IBOutlet WKWebView *mWKWebview; @property (nonatomic, strong) DKWKScriptMessageHandler *mScriptMessHandler; @end
@implementation DKWKWebViewController
- (void)viewDidLoad { [super viewDidLoad]; _mWKWebview.navigationDelegate = self; _mWKWebview.UIDelegate = self; _mScriptMessHandler = [DKWKScriptMessageHandler new]; [_mWKWebview.configuration.userContentController addScriptMessageHandler:_mScriptMessHandler name:WK_MethodName]; NSString *path = [[NSBundle mainBundle] pathForResource:@"jsbridge_pay" ofType:@"html"]; NSURLRequest *request = [NSURLRequest requestWithURL: [NSURL fileURLWithPath:path]]; [_mWKWebview loadRequest:request]; }
-(void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; [self removeMessageHandler]; }
-(void)dealloc{ NSLog(@"++DKWKWebViewController dealloced~"); [_mWKWebview.configuration.userContentController removeScriptMessageHandlerForName:WK_MethodName]; }
#pragma -mark BUSINESS - (void)removeMessageHandler { _mScriptMessHandler = nil; [_mWKWebview.configuration.userContentController removeScriptMessageHandlerForName:WK_MethodName]; } @end
|
此时点击网页上的pay
按钮,即会触发js方法pay()
并调用其中的window.webkit.messageHandlers.pay.postMessage('¥100');
,接着DKWKScriptMessageHandler
的代理函数被触发,通过区分不同handler
的名字来调用不同的Native函数处理本地业务逻辑。
3.Native->js
WK中Native调js很简单,与UIWebview类似,只不过多了回调block:
1 2 3 4 5 6 7 8 9 10 11
| @implementation DKWKWebViewController ...
- (void)nativeCallJs{ NSString *jsFuncScript = @"payResult('Succeed')"; [_mWKWebview evaluateJavaScript:jsFuncScript completionHandler:^(id _Nullable data, NSError * _Nullable error) { NSLog(@"+++++WKWebview执行js:%@",jsFuncScript); }]; } @end
|
四.结尾
以上三者是直接利用系统提供的API来实现js与Native的交互,每种方法都有自己的局限:
拦截URL的做法最笨,只适合少量a
标签或重定向跳转场景;
JavaScriptCore只适用于UIWebView,而WKScriptMessageHandler又只适用于WKWebView;
所以看起来急需一种整合方案,Github上有类似 WebViewJavascriptBridge 这种开源库,待我后续研究研究~