前言
隨著移動互聯(lián)網(wǎng)的發(fā)展,APP 開發(fā)模式也在不斷的創(chuàng)新收班,從最初的 Native 開發(fā)到后來的 Hybrid 混合開發(fā)故痊,再到最近比較火爆的 React Native、Weex 等項目看靠,這些都標志著 APP 開發(fā)已經(jīng)不再是純 Native 的工作赶促,還要涉及很多跨平臺的技術。
作為一種混合開發(fā)模式挟炬,Hybrid APP 底層依賴 Native 端的 Web 容器(UIWebview 和 WKWebview)鸥滨,上層使用前端 Html5、CSS谤祖、Javascript 做業(yè)務開發(fā)婿滓,這種開發(fā)模式非常適合業(yè)務快速拓展和迭代,在不發(fā)版本的前提下直接更新線上資源粥喜,受到不少公司的青睞與關注凸主。
對于 Hybrid APP 開發(fā),雖然業(yè)內(nèi)早已出現(xiàn) Cordova(PhoneGap)额湘、jQuery Mobile 等框架卿吐,但是由于性能、維護成本等原因锋华,并沒有在業(yè)內(nèi)非常流行嗡官,有些公司轉(zhuǎn)而選擇自己開發(fā)一套 Hybrid 框架,但是由于沒有豐富的經(jīng)驗和應用場景導致開發(fā)出來的 Hybrid 框架后期維護成本很高毯焕。本文我將對在公司開發(fā)的 Hybrid 解決方案跟大家做一個介紹衍腥,希望對各位的技術選型起到幫助,也歡迎大家積極交流纳猫。
Hybrid APP 特點
Hybrid APP 優(yōu)勢很明顯:
- 跨平臺婆咸,開發(fā)效率高,節(jié)約開發(fā)成本
- 業(yè)務快速拓展和迭代
- 及時修復線上 Bug芜辕,不需發(fā)版
但是 Hybrid 也有自己的劣勢尚骄,比如體驗上肯定比不了 Native,而且對于一個 Native 開發(fā)者而言要理解前后端的技術物遇,對開發(fā)者的要求較高乖仇,但我相信這是好事兒~~
根據(jù)之前的經(jīng)驗,我覺得 Hybrid 需要找到自己的應用場景询兴,比如營銷乃沙、活動等需要快速試錯和占領市場的團隊來說,Hybrid 很適用诗舰,但是對于像 APP 首頁這樣要求體驗高的場景 Hybrid 就不太適用警儒,具體情況可以根據(jù)自己公司的 APP 場景做適當?shù)恼{(diào)整。
Hybrid APP 框架
一個完整的 Hybrid APP 框架主要包括 WebView 容器、Bridge蜀铲、UI边琉、預加載、緩存等模塊兒记劝,當然 Bridge变姨、預加載、緩存等也需要相應前后端的支持厌丑,比如發(fā)布平臺定欧、灰度平臺、增量更新怒竿、CDN 平臺等等砍鸠。
框架結(jié)構(gòu)如下:
在設計這套框架之前,需要弄清楚 Native 與前端的分工耕驰,Native 主要提供一個宿主環(huán)境爷辱,對 WebView 進行封裝,提供 Bridge 方法朦肘,Header 組件設計饭弓,賬號信息設計,底層提供預加載和緩存機制厚骗,框架的業(yè)務方是各個前端團隊示启,所以我們需要站在前端的角度對以上方面進行考慮兢哭。本文主要對 WebView领舰、Bridge、Header 設計進行介紹迟螺,后續(xù)文章會對賬號信息設計冲秽、預加載和緩存進行持續(xù)跟進。
UIWebView 和 WKWebView 兼容
iOS8 以后蘋果推出了一套新的 WKWebView矩父,對于 UIWebView 和 WKWebView 的區(qū)別锉桑,總結(jié)如下:
Feature | UIWebView | WKWebView |
---|---|---|
JS執(zhí)行速度 | 慢 | 快 |
內(nèi)存占用 | 大 | 小 |
進度條 | 無 | 有 |
Cookie | 自動存儲 | 需手動存儲 |
緩存 | 有 | 無 |
NSURLProtocol攔截 | 可以 | 不可以 |
WKWebView 的主要優(yōu)點是 JS 執(zhí)行速度快、內(nèi)存占用小窍株,剛一推出就被開發(fā)者所追捧民轴,但是不知道是不是因為蘋果爸爸太任性,WKWebView 設計上并沒有與 UIWebView 保持一致球订,無法自動存儲 Cookie 和不能通過 NSURLProtocol 自定義請求等坑~導致 WKWebView 并沒有被開發(fā)者大規(guī)模推薦使用后裸。
本套框架的預加載和緩存模塊兒需要借助 NSURLProtocol 實現(xiàn),所以這里還是優(yōu)先使用 UIWebView(想吐個槽冒滩,其實如果預加載和緩存這套系統(tǒng)做好以后微驶,UIWebView 的效果并沒不比 WKWebView 差),這里也不能把 WKWebView 一棒子打死不用,對于那些對無需預加載和緩存的頁面因苹,可以為前端提供參數(shù)(比如 wkwebview=true)讓前端自己的去選擇是否使用 WKWebView苟耻,所以這里需要對 WKWebView 進行兼容。
YZWebView 是對 UIWebView 和 WKWebView 進行封裝的類扶檐,結(jié)構(gòu)設計如下:
YZWebViewDelegate凶杖,UIWebView 和 WKWebView 代理的回調(diào)代理。
@protocol YZWebViewDelegate <NSObject>
@optional
- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(YZWebView *)webView;
- (void)webViewDidFinishLoad:(YZWebView *)webView;
- (void)webView:(YZWebView *)webView didFailLoadWithError:(NSError *)error;
@end
NJKWebViewProgressDelegate款筑,進度條代理方法官卡。
@protocol NJKWebViewProgressDelegate <NSObject>
- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress;
@end
YZWebView 初始化方法,通過參數(shù) usingUIWebView 來決定初始化 WKWebView 或者 UIWebView醋虏,
- (instancetype)initWithFrame:(CGRect)frame usingUIWebView:(BOOL)usingUIWebView {
self = [super initWithFrame:frame];
if (self) {
_usingUIWebView = usingUIWebView;
[self p_initSelf];
}
return self;
}
- (void)p_initSelf {
Class wkWebView = NSClassFromString(@"WKWebView");
if (wkWebView && !self.usingUIWebView) {
[self initWKWebView]; //初始化WKWebView
} else {
[self initUIWebView]; //初始化UIWebView
}
[self addSubview:self.currentWebView];
}
- (void)initWKWebView {
......
WKWebView *webView =
[[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.bounds
configuration:webViewConfig];
webView.UIDelegate = self;
webView.navigationDelegate = self;
[webView setAutoresizesSubviews:YES];
[webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
[webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
webView.backgroundColor = [UIColor clearColor];
[webView.scrollView setShowsHorizontalScrollIndicator:NO];
[webView addObserver:self
forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptionNew
context:nil];
_currentWebView = webView;
}
//NJKWebViewProgress 沒兼容WKWebView寻咒,這里需要通過KVO進行監(jiān)測
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
self.estimatedProgress = [change[NSKeyValueChangeNewKey] doubleValue];
if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
[_progressDelegate webViewProgress:nil updateProgress:_estimatedProgress];
}
}
}
- (void)initUIWebView {
......
UIWebView *uiWebView = [[UIWebView alloc] initWithFrame:self.bounds];
[uiWebView setAutoresizesSubviews:YES];
[uiWebView setScalesPageToFit:YES];
[uiWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
[uiWebView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
uiWebView.keyboardDisplayRequiresUserAction = NO;
uiWebView.backgroundColor = [UIColor clearColor];
[uiWebView.scrollView setShowsHorizontalScrollIndicator:NO];
uiWebView.delegate = self;
self.njkWebViewProgress = [[NJKWebViewProgress alloc] init];
uiWebView.delegate = _njkWebViewProgress;
_njkWebViewProgress.webViewProxyDelegate = self;
_njkWebViewProgress.progressDelegate = self;
_currentWebView = uiWebView;
}
WebView 最關鍵的地方就是能捕獲到前端資源的請求,UIWebView 的捕獲方法是 webView:shouldStartLoadWithRequest:request navigationType:颈嚼,WKWebView 的捕獲方法是 webView:decidePolicyForNavigationAction:decisionHandler:毛秘,同時 WebView 有完整的生命周期回調(diào)(start,finish阻课,fail等)叫挟。
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
self.currentRequest = request;
BOOL result = [self callback_webViewShouldStartLoadWithRequest:request navigationType:navigationType];
return result;
}
......
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
self.currentRequest = navigationAction.request;
BOOL result = [self callback_webViewShouldStartLoadWithRequest:navigationAction.request
navigationType:navigationAction.navigationType];
if (result) {
decisionHandler(WKNavigationActionPolicyAllow);
} else {
decisionHandler(WKNavigationActionPolicyCancel);
}
}
......
#pragma mark - YZWebViewCallback
- (BOOL)callback_webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(NSInteger)navigationType {
BOOL result = YES;
if ([self.delegate respondsToSelector:@selector(webView:
shouldStartLoadWithRequest:
navigationType:)]) {
if (navigationType == -1) {
navigationType = UIWebViewNavigationTypeOther;
}
result = [self.delegate webView:self shouldStartLoadWithRequest:request navigationType:navigationType];
}
return result;
}
......
#pragma mark - NJKWebViewProgressDelegate
- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress {
self.estimatedProgress = progress;
if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
[_progressDelegate webViewProgress:webViewProgress updateProgress:_estimatedProgress];
}
}
需要強調(diào)的一點是:(UIWebView 的捕獲方法是 webView:shouldStartLoadWithRequest:request navigationType:,WKWebView 的捕獲方法是 webView:decidePolicyForNavigationAction:decisionHandler)這兩個方法只能控制一個請求可不可以被 WebView 發(fā)出限煞,比如 Bridge 就可以在這層進行捕獲抹恳,但是并不可以做請求定制的功能。請求的定制需要借助 NSURLProtocol署驻。
Bridge設計
Hybrid APP 的交互無非是 Native 調(diào)用前端頁面的 JS 方法奋献,或者前端頁面通過 JS 調(diào)用 Native 提供的接口,兩者交互的橋梁皆 Webview:
通過調(diào)研旺上,前端可以通過在 DOM 注入 iframe 發(fā)起 Bridge 請求瓶蚂,該請求可以被 webView:shouldStartLoadWithRequest:request navigationType: 方法捕獲,從而執(zhí)行相應的操作宣吱,但是屬于異步操作窃这;還有一種前端可以通過 Ajax 發(fā)起 Bridge 請求,可以有同步異步兩種方式征候,不過在 WebView 這層捕獲不到此請求杭攻,只能通過 NSURLProtocol 攔截,所以這也是 WKWebView 的一個限制疤坝。
WebViewJavascriptBridge是一個不錯的JavaScript與Native之間雙向通信的庫憾股,多個廠家包括Facebook在使用匪凉,并且新的版本開始支持WKWebView爬范,對了解Native與JS的交互非常有幫助行疏。
Bridge 設計至關重要,設計的好壞對后續(xù)開發(fā)、前端框架維護會造成深遠的影響衣撬,并且這種影響往往是不可逆的乖订,所以這里需要前端與 Native 好好配合,提供通用的接口具练。對于一個公司來說乍构,往往一套底層框架需要服務于多條業(yè)務線、多個 APP扛点,這就需要在設計的時候考慮好哪些橋接可以在框架層實現(xiàn)(比如跳轉(zhuǎn) Web 頁面哥遮,設置數(shù)據(jù),獲取數(shù)據(jù)陵究,Back 事件眠饮,Close 事件,Alert 彈框铜邮,獲取定位等等)仪召,而與業(yè)務相關的橋接需要框架提供接口讓業(yè)務方去注冊(比如跳轉(zhuǎn) Native 頁面,授權跳轉(zhuǎn)等等)松蒜。
首先設計數(shù)據(jù)格式扔茅,根據(jù) URL 格式:
與前端約定請求的格式是:
hybrid_scheme://hybrid_api?hybrid_params={params need encode}&callback=callback_ID
客戶端需要根據(jù)約定,在 Bridge 處理結(jié)束后通過 WebView window 對象中的 callback_ID 調(diào)用回調(diào)秸苗,數(shù)據(jù)返回的格式約定為:
{
data : {},
err : 0, //非0提示msg
msg : "success or fail message"
}
Native 解析 Bridge 代碼邏輯:
#pragma mark - YZWebViewDelegate
- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
......
if ([_jsBridge webViewShouldStartLoadWithRequest:request navigationType:navigationType]) {
//符合橋接規(guī)則
return NO;
}
return YES;
}
- (BOOL)webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
if ([self isWebViewJavascriptBridgeURL:url]) {
if ([self isWebViewJavascriptBridgeHost:url.host]) {
YZURLParseModel* model = [self p_parseWebViewRequestWithURL:url];
if ([[_messageHandlers allKeys] containsObject:model.method]) {
YZJSBridgeHandler messageHandler = _messageHandlers[model.method];
if (messageHandler) {
messageHandler(model.params);
}
.....
} else {
......
}
}
return YES;
} else {
return NO;
}
}
- (void)registerHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
if (![_hostsArray containsObject:handlerName]) {
[_hostsArray addObject:handlerName];
}
_messageHandlers[handlerName] = [handler copy];
}
//業(yè)務注冊橋接接口
static NSMutableDictionary* vocationalAsyncJSBridge = nil;
+ (void)setVocationalJSBridgeWithHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
if (!vocationalAsyncJSBridge) {
vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
}
vocationalAsyncJSBridge[handlerName] = handler;
}
+ (void)setVocationalJSBridge:(NSMutableDictionary*)dic {
if (!vocationalAsyncJSBridge) {
vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
}
[vocationalAsyncJSBridge addEntriesFromDictionary:dic];
}
公共 Bridge 設計
- 跳轉(zhuǎn)
跳轉(zhuǎn)包括三類:
① 頁面內(nèi)跳轉(zhuǎn)召娜,無需走 hybrid。
② H5 跳轉(zhuǎn)新開 WebView 頁面惊楼。
③ H5 跳轉(zhuǎn) Native 頁面玖瘸。
H5 跳轉(zhuǎn)新開 WebView 頁面:
協(xié)議標準 hybrid_scheme://gotoWebview?params={url:~}
__weak typeof(self) weakSelf = self;
//gotoWebview橋接
[self.jsBridge registerHandler:@"gotoWebview" handler:^(id data) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.jsBridge.delegate &&
[strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoWebPageWithDatas:)]) {
if ([strongSelf.jsBridge.delegate gotoWebPageWithDatas:data]) //業(yè)務子類進行拓展
return;
}
if ([data isKindOfClass:[NSDictionary class]]) {
NSString* url = [data objectForKey:@"url"];
if (url) {
YZWebViewContainerViewControllerBase* vc = [[strongSelf getCurrentViewController] routeWithParams:@{@"url" : [NSURL URLWithString:url]}];
[strongSelf.navigationController pushViewController:vc animated:YES];
}
}
}];
H5 跳轉(zhuǎn) Native 頁面:
協(xié)議標準 hybrid_scheme://gotoNative?params={page:~}
__weak typeof(self) weakSelf = self;
//gotoNative橋接
[self.jsBridge registerHandler:@"gotoNative" handler:^(id data) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.jsBridge.delegate &&
[strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoNativeWithDatas:)]) {
if ([strongSelf.jsBridge.delegate gotoNativeWithDatas:data]) //業(yè)務子類進行拓展
return;
}
}];
- 功能 API
例:Back 事件、Reload 事件胁后、Share 方法等店读。
協(xié)議標準 hybrid_scheme://doAction?params={action:back}
協(xié)議標準 hybrid_scheme://doAction?params={action:reload}
協(xié)議標準 hybrid_scheme://doAction?params={action:share, title:, subtitle:, context:, imgUrl:}
//doAction橋接
[self.jsBridge registerHandler:@"doAction" handler:^(id data) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.jsBridge.delegate &&
[strongSelf.jsBridge.delegate respondsToSelector:@selector(doActionWithDatas:)]) {
if ([strongSelf.jsBridge.delegate doActionWithDatas:data]) //業(yè)務子類進行拓展
return;
}
if ([data isKindOfClass:[NSDictionary class]]) {
NSString* action = [data objectForKey:@"action"];
if (action) {
if ([action isEqualToString:@"back"]) {
[strongSelf p_back];
} else if ([action isEqualToString:@"page_reload"]) {
[strongSelf p_reload];
} else if ([action isEqualToString:@"share"]) {
[strongSelf p_shareWithParams:data];
}
}
}
}];
- Header 組件設計
對于 Header 組件,需要完成以下功能:
① Header 的左側(cè)具有返回鍵和關閉鍵(類似微信等 APP)攀芯,右側(cè)可配置文字和圖標,并且可以控制回調(diào)文虏。
② Title 通常在 WebView 加載完成后去獲取 document.title 來顯示侣诺,這里可以做到可配置。
③ Title 可以設置一些特別的 TitleView氧秘,比如 SegmentView年鸳、ListView 等等。
以設置右側(cè)按鈕為例:
協(xié)議標準 hybrid_scheme://configNative?params={configs:[{type:nav_item_right, title:, icon_url:, action:, action_parameters: }]}
//configNative橋接
[self.jsBridge registerHandler:@"configNative" handler:^(id data) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.jsBridge.delegate &&
[strongSelf.jsBridge.delegate respondsToSelector:@selector(configNativeWithDatas:)]) {
if ([strongSelf.jsBridge.delegate configNativeWithDatas:data]) //業(yè)務子類進行拓展
return;
}
if ([data isKindOfClass:[NSDictionary class]]) {
NSString* strOfConfigs = data[@"configs"];
if (strOfConfigs == nil || [strOfConfigs isEqualToString:@""])
return;
NSError *error = nil;
NSArray *arrConfigs = [NSJSONSerialization JSONObjectWithData:[strOfConfigs dataUsingEncoding:NSUTF8StringEncoding]
options:NSJSONReadingAllowFragments error:&error];
if (error || !arrConfigs)
return;
for (NSDictionary* config in arrConfigs) {
NSString *strType = config[@"type"];
if (!strType || [strType isEqualToString:@""])
continue;
if ([strType isEqualToString:@"nav_item_right"]) {
YZNavigationItemConfigModel *model = [[YZNavigationItemConfigModel alloc] initWithDictionary:config error:&error];
if (error) {
error = nil;
continue;
}
[strongSelf.rightMenuBarButtonItems addObject:[strongSelf p_configRightBarButtonItemsWithModel:model]];
if (strongSelf.rightMenuBarButtonItems) {
[strongSelf.navigationItem setRightBarButtonItems:strongSelf.rightMenuBarButtonItems];
}
}
}
}
}];
總結(jié)
Hybrid 框架依靠快速迭代丸相,快速試錯在業(yè)務開發(fā)中使用非常廣泛搔确。本文初衷是想為那些準備使用Hybrid框架的人提供設計上的思路,并通過實際的事例去展示結(jié)果,希望對 Hybrid 感興趣的朋友一起來把 Hybrid 一整套解決方案落地并且能夠提供開源膳算。