Contents
  1. 1. Native开发中为什么需要H5容器
  2. 2. UIWebView的基本用法
    1. 2.1. 加载网页
    2. 2.2. UIWebViewDelegate几个常用的代理方法
    3. 2.3. Native调用JS中的方法
    4. 2.4. JS中调用Naitve的方法
  3. 3. WKWebView的基本用法
    1. 3.1. 加载网页
    2. 3.2. 几个常用的代理方法
    3. 3.3. Native调用JS中的方法
    4. 3.4. JS中调用Naitve的方法
  4. 4. UIWebView和WKWebView的比较和选择

Native开发中为什么需要H5容器

Native开发原生应用是手机操作系统厂商(目前主要是苹果的iOS和google的Android)对外界提供的标准化的开发模式,他们对于native开发提供了一套标准化实现和优化方案。但是他们存在一些硬伤,比如

App的发版周期偏长、有时无法跟上产品的更新节奏;灵活性差,如果有较大的方案变更,需要发版才能解决;如果存在bug,在当前版本修复的难度比较大(iOS的JSPatch方案和Android的Dex修复方案);需要根据不同的平台写不同的代码,iOS主要为object_c和swift,android为Java。

而作为H5为主要开发模式的Web App的灵活性就比较强,他利用操作系统中的h5容器作为一个承载,对外提供一个url链接,而该url链接对应的内容可以实时在服务端进行修改,灵活行很强,避免了Native发版周期带来的时间成本。但是h5虽然灵活,但是他也有自己的硬伤。每次都需要下载完整的UI数据(html,css,js),弱网用户体验较差,流量消耗较大;无法调用系统文件系统,硬件资源等等;

Native App和Web App都有他们的优势和劣势。我们也不能一棍子拍死说谁好谁劣。通常的经验是:对于一些比较稳当的业务,对用户体验要求较高的,我们可以选择Native开发。而对于一些业务变更比较快、处在不断试水的过程,而且不涉及调用文件系统和硬件调用的业务我们可以选择h5开发。所以说,在一款app中我们需要同时支持Native代码和h5代码。这也是我们标题所说的Native开发中需要H5容器的必要性。

iOS存在的h5容器主要包括UIWebViewWKWebView,下面我们就分别来说说他们的用法和优劣。

UIWebView的基本用法

加载网页

1
2
3
4
5
6
7
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.delegate = self;
[self.view addSubview:webView];
//网络地址
NSURL *url = [[NSURL alloc] initWithString:@"http://www.taobao.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];

UIWebViewDelegate几个常用的代理方法

1
2
3
4
5
6
7
8
//进行加载前的预判断,如果返回YES,则会进入后续流程(StartLoad,FinishLoad)。如果返回NO,这不会进入后续流程。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
//开始加载网页
- (void)webViewDidStartLoad:(UIWebView *)webView;
//加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView;
//加载失败
- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;

Native调用JS中的方法

比如我们在加载的HTML文件中有如下js代码:

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
function hello(){
alert("你好!");
}
function helloWithName(name){
alert(name + ",你好!");
}
</script>

我们可以调用- (nullable NSString )stringByEvaluatingJavaScriptFromString:(NSString )script;函数进行js调用。

1
2
[webView stringByEvaluatingJavaScriptFromString:@"hello()"];
[webView stringByEvaluatingJavaScriptFromString:@"helloWithName('jack')"];

js代码不一定要在js文件中预留,也可以在代码中通过字符串的形式进行调用,比如下面:

1
2
3
4
5
6
7
8
9
10
11
12
//自定义js函数
NSString *jsString = @"function sayHello(){ \
alert('jack11') \
} \
sayHello()";
[_webView stringByEvaluatingJavaScriptFromString:jsString];
NSString *jsString = @" var p = document.createElement('p'); \
p.innerText = 'New Line'; \
document.body.appendChild(p); \
";
[_webView stringByEvaluatingJavaScriptFromString:jsString];

JS中调用Naitve的方法

具体让js通知native进行方法调用,我们可以让js产生一个特殊的请求。可以让Native代码可以拦截到,而且不然用户察觉。业界一般的实现方案是在网页中加载一个隐藏的iframe来实现该功能。通过将iframe的src指定为一个特殊的URL,实现在- (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType;方案中进行拦截处理。对应的js调用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 发起请求后这个iFrame就没用了,所以把它从dom上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}

比如我们在js代码中,调用一下两个js方法:

1
2
3
4
5
6
function iOS_alert() {//调用自定义对话框
loadURL("alert://abc");
}
function call() {// js中进行拨打电话处理
loadURL("tel://17715022071");
}

当你触发以上方法的时候,就会进入webview的代理方法中进行拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSURL * url = [request URL];
if ([[url scheme] isEqualToString:@"alert"]) {//拦截请求,弹出自定义对话框
UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"test" message:[url host] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alertView show];
return NO;
}else if([[url scheme] isEqualToString:@"tel"]){//拦截拨打电话请求
BOOL result = [[UIApplication sharedApplication] openURL:url];
if (!result) {
NSLog(@"您的设备不支持打电话");
} else {
NSLog(@"电话打了");
}
return NO;
}
return YES;
}

这样我们就可以让js进行native的调用。

WKWebView的基本用法

加载网页

WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
NSURL *url = [NSURL URLWithString:@"http://www.taobao.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
[self.view addSubview:webView];

几个常用的代理方法

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* 根据webView、navigationAction相关信息决定这次跳转是否可以继续进行,这些信息包含HTTP发送请求,如头部包含User-Agent,Accept,refer
* 在发送请求之前,决定是否跳转的代理
* @param webView
* @param navigationAction
* @param decisionHandler
*/
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
decisionHandler(WKNavigationActionPolicyAllow);
}
/**
* 这个代理方法表示当客户端收到服务器的响应头,根据response相关信息,可以决定这次跳转是否可以继续进行。
* 在收到响应后,决定是否跳转的代理
* @param webView
* @param navigationResponse
* @param decisionHandler
*/
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
decisionHandler(WKNavigationResponsePolicyAllow);
}
/**
* 准备加载页面。等同于UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
*
* @param webView
* @param navigation
*/
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
}
/**
* 这个代理是服务器redirect时调用
* 接收到服务器跳转请求的代理
* @param webView
* @param navigation
*/
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error{
}
/**
* 内容开始加载. 等同于UIWebViewDelegate: - webViewDidStartLoad:
*
* @param webView
* @param navigation
*/
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation{
}
/**
* 页面加载完成。 等同于UIWebViewDelegate: - webViewDidFinishLoad:
*
* @param webView
* @param navigation
*/
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
}
/**
* 页面加载失败。 等同于UIWebViewDelegate: - webView:didFailLoadWithError:
*
* @param webView
* @param navigation
* @param error
*/
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error{
}
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){
}
/*
我们看看WKUIDelegate的几个代理方法,虽然不是必须实现的,但是如果我们的页面中有调用了js的alert、confirm、prompt方法,我们应该实现下面这几个代理方法,然后在原来这里调用native的弹出窗,因为使用WKWebView后,HTML中的alert、confirm、prompt方法调用是不会再弹出窗口了,只是转化成ios的native回调代理方法
*/
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"h5Container" message:message preferredStyle:UIAlertControllerStyleAlert];
// [alertView addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
// textField.textColor = [UIColor redColor];
// }];
[alertView addAction:[UIAlertAction actionWithTitle:@"我很确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alertView animated:YES completion:nil];
}

显然WKWebView的代理方法提供了比UIWebView颗粒度更细的方法。让开发者可以进行更加细致的配置和处理。

Native调用JS中的方法

WKWebView提供的调用js代码的函数是:

1
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

比如我们在加载的HTML文件中有如下js代码:

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
function hello(){
alert("你好!");
}
function helloWithName(name){
alert(name + ",你好!");
}
</script>

我们可以调用如下代码进行js的调用:

1
2
3
4
5
6
7
[_wkView evaluateJavaScript:@"hello()" completionHandler:^(id item, NSError * error) {
}];
[_wkView evaluateJavaScript:@"helloWithName('jack')" completionHandler:^(id item, NSError *error) {
}];

同UIWebView一样,我们也可以通过字符串的形式进行js调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSString *jsString = @"function sayHello(){ \
alert('jack11') \
} \
sayHello()";
[_wkView evaluateJavaScript:jsString completionHandler:^(id item, NSError *error) {
}];
jsString = @" var p = document.createElement('p'); \
p.innerText = 'New Line'; \
document.body.appendChild(p); \
";
[_wkView evaluateJavaScript:jsString completionHandler:^(id item, NSError *error) {
}];

JS中调用Naitve的方法

除了和UIWebView加载一个隐藏的ifame之外,WKWebView自身还提供了一套js调用native的规范。

我们可以在初始化WKWebView的时候,给他设置一个config参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//高端配置
//创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
//创建UserContentController(提供javaScript向webView发送消息的方法)
WKUserContentController *userContent = [[WKUserContentController alloc] init];
//添加消息处理,注意:self指代的是需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
//将UserContentController设置到配置文件中
config.userContentController = userContent;
//高端的自定义配置创建WKWebView
_wkView = [[YXWKView alloc] initWithFrame:self.view.bounds configuration:config];
NSURL *url = [NSURL URLWithString:@"http://localhost:8080/myDiary/index.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[_wkView loadRequest:request];
_wkView.UIDelegate = self;
_wkView.navigationDelegate = self;
[self.view addSubview:_wkView];

我们在js可以通过NativeMethod这个Handler让js代码调用native。

比如在js代码中,我新增了一个方法

1
2
3
4
5
<script type="text/javascript">
function invokeNativeMethod(){
window.webkit.messageHandlers.NativeMethod.postMessage("我要调用native的方法");
}
</script>

触发以上方法的时候,会在native以下方法中进行拦截处理。

1
2
3
4
5
6
7
8
9
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//这里就是使用高端配置,js调用native的处理地方。我们可以根据name和body,进行桥协议的处理。
NSString *messageName = message.name;
if ([@"NativeMethod" isEqualToString:messageName]) {
id messageBody = message.body;
NSLog(@"%@",messageBody);
}
}

UIWebView和WKWebView的比较和选择

WKWebView是苹果在WWDC2014发布会中发布IOS8的时候公布WebKit时候使用的新型的H5容器。它与UIWebView相比较,拥有更快的加载速度和性能,更低的内存占用。将UIWebViewDelegate和UIWebView重构成了14个类,3个协议,可以让开发者进行更加细致的配置。

但是他有一个最致命的缺陷,就是WKWebView的请求不能被NSURLProtocol截获。而我们团队开发的app中对于H5容器最佳的优化点主要就在于使用NSURLProtocol技术对于H5进行离线包的处理和H5的图片和Native的图片公用一套缓存的技术。因为该问题的存在,目前我们团队还没有使用WKWebView代替UIWebVIew。


原文链接

Contents
  1. 1. Native开发中为什么需要H5容器
  2. 2. UIWebView的基本用法
    1. 2.1. 加载网页
    2. 2.2. UIWebViewDelegate几个常用的代理方法
    3. 2.3. Native调用JS中的方法
    4. 2.4. JS中调用Naitve的方法
  3. 3. WKWebView的基本用法
    1. 3.1. 加载网页
    2. 3.2. 几个常用的代理方法
    3. 3.3. Native调用JS中的方法
    4. 3.4. JS中调用Naitve的方法
  4. 4. UIWebView和WKWebView的比较和选择