一、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容器主要包括UIWebView和WKWebView,下面我們就分別來說說他們的用法和優劣。
二、UIWebView的基本用法
2.1、加載網頁
|
1
2
3
4
5
|
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];
|
2.2、UIWebViewDelegate幾個常用的代理方法
|
1
|
- (
BOOL
)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
//進行加載前的預判斷,如果返回YES,則會進入后續流程(StartLoad,FinishLoad)。如果返回NO,這不會進入后續流程。- (void)webViewDidStartLoad:(UIWebView *)webView;//開始加載網頁- (void)webViewDidFinishLoad:(UIWebView *)webView;//加載完成- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;//加載失敗
|
2.3、Native調用JS中的方法
比如我們在加載的HTML文件中有如下js代碼:
|
1
2
3
4
5
6
|
<script type=
"text javascript"
=
""
>function hello(){
alert(
"你好!"
);
}function helloWithName(name){
alert(name +
",你好!"
);
}
</script type="text>
|
我們可以調用- (nullable NSString )stringByEvaluatingJavaScriptFromString:(NSString )script;函數進行js調用。
|
1
|
[webView stringByEvaluatingJavaScriptFromString:@
"hello()"
];[webView stringByEvaluatingJavaScriptFromString:@
"helloWithName('jack')"
];
|
js代碼不一定要在js文件中預留,也可以在代碼中通過字符串的形式進行調用,比如下面:
|
1
2
3
4
|
//自定義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];
|
2.4、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
|
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
|
function iOS_alert() {
//調用自定義對話框
} function call() {
// js中進行撥打電話處理
}
|
當你觸發以上方法的時候,就會進入webview的代理方法中進行攔截。
|
1
2
3
4
5
6
7
8
9
|
- (
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的基本用法
3.1、加載網頁
|
1
2
3
|
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];
|
3.2、幾個常用的代理方法
|
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
|
/**
* 根據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顆粒度更細的方法。讓開發者可以進行更加細致的配置和處理。
3.3 、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
|
<script type=
"text javascript"
=
""
>function hello(){
alert(
"你好!"
);
}function helloWithName(name){
alert(name +
",你好!"
);
}
</script type="text>
|
我們可以調用如下代碼進行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) {
}];
|
3.4、JS中調用Naitve的方法
除了和UIWebView加載一個隱藏的ifame之外,WKWebView自身還提供了一套js調用native的規范。
我們可以在初始化WKWebView的時候,給他設置一個config參數。
|
1
2
3
4
5
6
7
8
9
10
11
|
//高端配置
//創建配置
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
|
<script type=
"text javascript"
=
""
> function invokeNativeMethod(){ window.webkit.messageHandlers.NativeMethod.postMessage(
"我要調用native的方法"
);
}
</script type="text>
|
觸發以上方法的時候,會在native以下方法中進行攔截處理。
|
1
2
3
4
|
#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。
五、聯系方式
一、前言
NSURLProtocol是iOS中URL Loading System的一部分。如果開發者自定義的一個NSURLProtocol並且注冊到app中,那么在這個自定義的NSURLProtocol中我們可以攔截UIWebView,基於系統的NSURLConnection或者NSURLSession進行封裝的網絡請求,然后做到自定義的response返回。非常強大。
二、NSURLProtocol的使用流程
2.1、在AppDelegate中注冊自定義的NSURLProtocol。
比如我這邊自定義的NSURLProtocol叫做YXNSURLProtocol。
|
1
2
|
@interface YXNSURLProtocol : NSURLProtocol
@end
|
在系統加載的時候,把自定義的YXNSURLProtocol注冊到URL加載系統中,這樣 所有的URL請求都有機會進入我們自定義的YXNSURLProtocol進行攔截處理。
|
1
2
3
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[YXNSURLProtocol class]];
}
|
加載完成后,當產生URL請求的同時,會依次進入NSURLProtocol的以下相關方法進行處理,下面我們依次來講一下每一個方法的作用。
2.2、NSURLProtocol中的幾個方法
2.2.1、是否進入自定義的NSURLProtocol加載器
|
1
2
3
4
5
6
7
8
|
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
BOOL intercept = YES;
NSLog(@"YXNSURLProtocol==%@",request.URL.absoluteString);
if (intercept) {
}
return intercept;
}
|
如果返回YES則進入該自定義加載器進行處理,如果返回NO則不進入該自定義選擇器,使用系統默認行為進行處理。
如果這一步驟返回YES。則會進入2.3的方法中。
2.2.2、重新設置NSURLRequest的信息
|
1
2
3
|
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
|
在這個方法中,我們可以重新設置或者修改request的信息。比如請求重定向或者添加頭部信息等等。如果沒有特殊需求,直接返回request就可以了。但是因為這個方法在會在一次請求中被調用多次(暫時我也不知道什么原因為什么需要回調多洗),所以request重定向和添加頭部信息也可以在開始加載中startLoading方法中重新設置。
2.2.3、這個方法主要是用來判斷兩個request是否相同,如果相同的話可以使用緩存數據,通常調用父類的實現即可
|
1
2
3
|
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b{
return [super requestIsCacheEquivalent:a toRequest:b];
}
|
這個方法基本不常用。
2.2.4、被攔截的請求開始執行的地方
|
1
2
|
- (void)startLoading{
}
|
這個函數使我們重點使用的函數。
2.2.5、結束加載URL請求
|
1
2
|
- (void)stopLoading{
}
|
2.3、NSURLProtocolClient中的幾個方法
上面的NSURLProtocol定義了一系列加載的流程。而在每一個流程中,我們作為使用者該如何使用URL加載系統,則是NSURLProtocolClient中幾個方法該做的事情。
|
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
|
@protocol NSURLProtocolClient
//請求重定向
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
// 響應緩存是否合法
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
//剛接收到Response信息
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
//數據加載成功
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
//數據完成加載
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
//數據加載失敗
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
//為指定的請求啟動驗證
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//為指定的請求取消驗證
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
@end
|
三、實現一個地址重定向的Demo
這個Demo實現的功能是在UIWebView中所有跳轉到sina首頁的請求,都重定位到sohu首頁。
3.1、第一步,新建一個UIWebView,加載sina首頁
|
1
2
3
4
5
6
|
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
_webView.delegate = self;
[self.view addSubview:_webView];
NSURL *url = [[NSURL alloc] initWithString:@"https://sina.cn"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[_webView loadRequest:request];
|
3.2、自定義一個NSURLProtocol
|
1
2
3
|
@interface YXNSURLProtocolTwo : NSURLProtocol
@end
|
3.3、在AppDelegate中,進行注冊
|
1
2
3
4
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[YXNSURLProtocolTwo class]];
return YES;
}
|
3.4、在canInitWithRequest方法中攔截https://sina.cn/
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
NSLog(@"canInitWithRequest url-->%@",request.URL.absoluteString);
//看看是否已經處理過了,防止無限循環
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
NSString *urlString = request.URL.absoluteString;
if([urlString isEqualToString:@"https://sina.cn/"]){
return YES;
}
return NO;
}
|
3.5、在startLoading中進行方法重定向
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
- (void)startLoading{
NSMutableURLRequest * request = [self.request mutableCopy];
// 標記當前傳入的Request已經被攔截處理過,
//防止在最開始又繼續攔截處理
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:request];
self.connection = [NSURLConnection connectionWithRequest:[self changeSinaToSohu:request] delegate:self];
}
//把所用url中包括sina的url重定向到sohu
- (NSMutableURLRequest *)changeSinaToSohu:(NSMutableURLRequest *)request{
NSString *urlString = request.URL.absoluteString;
if ([urlString isEqualToString:@"https://sina.cn/"]) {
urlString = @"http://m.sohu.com/";
request.URL = [NSURL URLWithString:urlString];
}
return request;
}
|
你也可以選擇在+ (NSURLRequest )canonicalRequestForRequest:(NSURLRequest )request替換request。效果都是一樣的。
3.6、因為新建了一個NSURLConnection *connection,所以要實現他的代理方法,如下
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
|
通過以上幾步,我們就可以實現最簡單的url重定向,WebView加載新浪首頁,卻跳轉到了搜狐首頁。
四、小結
通過自定義的NSURLProtocol,我們拿到用戶請求的request之后,我們可以做很多事情。比如:
1、自定義請求和響應
2、網絡的緩存處理(H5離線包 和 網絡圖片緩存)
3、重定向網絡請求
4、為測試提供數據Mocking功能,在沒有網絡的情況下使用本地數據返回。
5、過濾掉一些非法請求
6、快速進行測試環境的切換
8、可以攔截UIWebView,基於系統的NSURLConnection或者NSURLSession進行封裝的網絡請求。目前WKWebView無法被NSURLProtocol攔截。
9、當有多個自定義NSURLProtocol注冊到系統中的話,會按照他們注冊的反向順序依次調用URL加載流程。當其中有一個NSURLProtocol攔截到請求的話,后續的NSURLProtocol就無法攔截到該請求。
