js-Native交互

移动开发中经常会用到网页,例如活动信息展示并响应网页中的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
<!--显示native调用js后的结果-->
<p id="result">pay result: </p>

<script type="text/javascript">
//js方法,供native调 native->h5
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{
//native调用js 显示支付结果
[_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标签,跳转特殊url供native监听 -->
<a href="router://pay">pay</a>
<br/>
<!--显示native调用js后的结果-->
<p id="result">pay result: </p>

<script type="text/javascript">
//js方法,供native调 native->h5
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) {
//native处理逻辑

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

JSExportJavaScriptCore库提供的一个协议,此协议中可定义一些接口,供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>

//声明一个协议,继承并扩展JSExport协议
@protocol JSOCExportProtocol <JSExport>
//声明属性
@property (nonatomic, copy) NSString *name;
//声明供js回调的OC方法
- (void)pay;
@end

//实现JSExport协议,这就是注册JSContext时传递的对象
@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{
//调用self.viewController的协议方法
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
{
//获取JavaScriptContext对象
_jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//为JavaScriptContext注入Bridge对象
_jsContext[@"jsbridge"] = [[JSOCHelper alloc] initWithSource:self];

//往js中注入新的login函数,block为其对应的OC回调
_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/>
<!--显示native调用js后的结果-->
<p id="result">pay result: </p>

<script type="text/javascript">
//调用Native的方法 js->native
function pay(){
jsbridge.pay();
}
//js方法,供native调 native->js
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{
//native调用js
//方法1
JSValue *jsFunc = _jsContext[@"login"];//调用上面注册的block
[jsFunc callWithArguments:@[@"data",@"call by JSValue Error info"]];
//方法2
[_jsContext evaluateScript:@"login('data','call by [jsContext evaluateScript:] Error info')"];//native调用JS中定义的登录方法
//方法3
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
//.h文件
#import <WebKit/WebKit.h>

//宏定义WKScriptMessage对象的名字
UIKIT_EXTERN NSString * const WK_MethodName;
@interface DKWKScriptMessageHandler : NSObject<WKScriptMessageHandler>
@end

//.m文件
#import "DKWKScriptMessageHandler.h"
NSString *const WK_MethodName = @"pay";

@implementation DKWKScriptMessageHandler

//实现代理,js调用native时会回调到这里
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {

//可以定义多个message handler处理来自js的不同任务,这里需要区分其名字
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/>
<!--显示native调用js后的结果-->
<p id="result">pay result: </p>

<script type="text/javascript">
//调用Native的方法 js->native
function pay(){
window.webkit.messageHandlers.pay.postMessage('¥100');
}
//js方法,供native调 native->js
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;

//向webview注入MessHandler(可以注册多个MessHandler)
_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{
//native调用js中定义的函数
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 这种开源库,待我后续研究研究~


js-Native交互
https://davidlii.cn/2017/11/23/js-native.html
作者
Davidli
发布于
2017年11月23日
许可协议