1.首先我們要了解豆瓣框架為何而生,作用是什么。
在大型移動應用的開發中,項目代碼龐雜,通常還需要 iOS,Android,移動 Web 和 桌面 Web 全平臺支持。這種情況下,更高的開發效率就成了開發者不得不考慮的議題。這也是為何雖然移動端的 Web 技術在使用范圍和性能上有諸多劣勢,仍然有很多開發者付出努力,探索如何在移動開發中使用 Web 技術,隨之有了混合開發。混合開發的直白解釋是 Native 和 Web 技術都要用。但形式上,應用仍然和瀏覽器無關,用戶還是需要在 App Store 和 Android Market 下載應用。只是在開發時,開發者以 Native 代碼為主體,在合適的地方部分使用 Web 技術。豆瓣的混合開發框架就是為了解決我們怎么優雅的在Native鑲嵌Web從而實現高效率的界面開發,通過Web實現跨平臺及熱更新,從而提升開發效率及用戶體驗。
2.為什么選擇豆瓣的混合框架
首先了解其內部的實現機制
1.通過url的參數傳輸信息,兩端進行交互,所有的行為都是Web發起,最后由Native實現,Web是主導,思路清晰,避免了Native在Web不需要的情況下進行傳輸數據,消耗流量,同時也混淆Web端對信息的接收
2.框架的結構清晰,通過制定協議來規范各個類,從而實現不同的功能;這里主要分為兩大類:(1)Widget調起本地控件(2)ContainerAPI 上傳數據使用
3.輕量級,可擴展性強
4.簡單易懂,便于使用
3.具體的內部實現
一言不合就上圖
1.首先大家最關心的就是它是怎么完成數據的接收和回調的,這是功能的核心。
(1)在widget(調起本地控件)中通過web的代理方法,在代理方法中會捕捉到該網頁傳過來的url從而通過參數篩選其需要做出的回應,從而完成功能的實現
(2)在RXRContainerAPI(上傳數據)中通過RXRContainerInterceptor(RXRNSURLProtocol)捕捉器捕捉web發送的url,然后篩選并通過捕捉的web的request請求把數據回傳給web
可能說到這里大家根本不知道什么是widget、什么是RXRContainerAPI,一臉懵逼。接下來就讓我們揭開這面紗,從一步一步的實現過程里找到答案。
a.首先說widget,widget是一套協議,規定了你所創建的widget要實現的方法:
@import Foundation;
@class RXRViewController;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRWidget` 是一個 Widget 協議。
* 實現 RXRWidget 協議的類將完成一個 Web 對 Native 的功能調用。
*/
@protocol RXRWidget <NSObject>
/**
* 判斷該 Widget 是否要對該 URL 做出反應。
*
* @param URL 對應的 URL。
*/
- (BOOL)canPerformWithURL:(NSURL *)URL;
/**
* 對該 URL,執行 Widget 的各項準備工作。
*
* @param URL 對應的 URL。
*/
- (void)prepareWithURL:(NSURL *)URL;
/**
* 執行 Widget 的操作。
*
* @param controller 執行該 Widget 的 Controller。
*/
- (void)performWithController:(RXRViewController *)controller;
@end
你需要根據你的功能服從協議,創建自己的widget去實現自己的功能
b.了解RXRViewController,這個vc是你呈現web頁面的容器,你所有和web相關的操作的頁面,又要繼承與他,并制定自己的json表,創建映射uri,初始化你的web,當然widget也會全部集中在這里處理。json表就是這里的路由表,你自己要根據它的格式去配置自己的url,一個頁面對應一個url,通過uri來打開,可以下載官方demo一看便知。https://github.com/douban/rexxar-ios
@import UIKit;
@protocol RXRWidget;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRViewController` 是一個 Rexxar Container。
* 它提供了一個使用 web 技術 html, css, javascript 開發 UI 界面的容器。
*/
@interface RXRViewController : UIViewController <UIWebViewDelegate>
/**
* 對應的 uri。
*/
@property (nonatomic, strong, readonly) NSURL *uri;
/**
* 內置的 WebView。
*/
@property (nonatomic, strong, readonly) UIWebView *webView;
/**
* activities 代表該 Rexxar Container 可以響應的協議。
*/
@property (nonatomic, strong) NSArray<id<RXRWidget>> *widgets;
/**
* 初始化一個RXRViewController。
*
* @param uri 該頁面對應的 uri。
*
* @discussion 會根據 uri 從 Route Map File 中選擇對應本地 html 文件加載。如果無本地 html 文件,則從服務器加載 html 資源。
* 在 UIWebView 中,遠程 URL 需要注意跨域問題。
*/
- (instancetype)initWithURI:(NSURL *)uri;
/**
* 初始化一個RXRViewController。
*
* @param uri 該頁面對應的 uri。
* @param htmlFileURL 該頁面對應的 html file url。
*
* @discussion 會根據 uri 從 Route Map File 中選擇對應本地 html 文件加載。如果無本地 html 文件,則從服務器加載 html 資源。
* 在 UIWebView 中,遠程 URL 需要注意跨域問題。
*/
- (instancetype)initWithURI:(NSURL *)uri htmlFileURL:(NSURL *)htmlFileURL;
/**
* 重新加載 WebView。
*/
- (void)reloadWebView;
/**
* 通知 WebView 頁面顯示,缺省會在 viewWillAppear 里調用。本方法可以由業務層自主定制向 WebView 通知 onPageVisible 的時機。
*/
- (void)onPageVisible;
/**
* 通知 WebView 頁面消失,缺省會在 viewDidDisappear 里調用。本方法可以由業務層自主定制向 WebView 通知 onPageInvisible 的時機。
*/
- (void)onPageInvisible;
/**
* 調用 WebView 的一個 JavaScript 函數,并傳入一個 json 串作為參數。
*
* @param function 調用的函數。
* @param jsonParameter 傳遞的參數,json 串。
*/
- (nullable NSString *)callJavaScript:(NSString *)function jsonParameter:(nullable NSString *)jsonParameter;
@end
#pragma mark - Public Route Methods
/**
* 暴露出 Route 相關的接口。
*/
@interface RXRViewController (Router)
/**
* 更新 Route Files。
*
* @param completion 更新完成后將執行這個 block。
*/
+ (void)updateRouteFilesWithCompletion:(nullable void (^)(BOOL success))completion;
/**
* 判斷路由表是否存在對應于 uri 的 route 信息。
*
* @param uri 待判斷的 uri。
*/
+ (BOOL)isRouteExistForURI:(NSURL *)uri;
/**
* 判斷本地(緩存,或預置資源中)是否已經下載了存在對應于 uri 的 route 信息的資源。
*
* @param uri 待判斷的 uri。
*/
+ (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri;
@end
自己實現的widget最終都存儲在widgets這個屬性里,最終在web的代理里面去集中處理。
#pragma mark - UIWebViewDelegate's method
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *reqURL = request.URL;
if ([reqURL isEqual:self.requestURL]) {
return YES;
}
// http:// or https:// 開頭,則打開網頁
if ([reqURL rxr_isHttpOrHttps] && navigationType == UIWebViewNavigationTypeLinkClicked) {
return ![self _rxr_openWebPage:reqURL];
}
NSString *scheme = [RXRConfig rxrProtocolScheme];
NSString *host = [RXRConfig rxrProtocolHost];
if ([request.URL.scheme isEqualToString:scheme]
&& [request.URL.host isEqualToString:host] ) {
NSURL *URL = request.URL;
for (id<RXRWidget> widget in self.widgets) {
if ([widget canPerformWithURL:URL]) {
[widget prepareWithURL:URL];
[widget performWithController:self];
RXRDebugLog(@"Rexxar callback handle: %@", URL);
return NO;
}
}
RXRDebugLog(@"Rexxar callback can not handle: %@", URL);
}
return YES;
}
2.接下來就是上傳數據
上傳數據完全也可以在web的代理里面去集中處理,但是這樣就會顯得十分臃腫,代碼也會比較繁雜。這里采用NSURLProtocol捕捉請求,去篩選需要的url,從而實現數據上傳。這里也是采用集中處理,同樣由代理去規范類的行為。
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRContainerAPI` 是一個請求模擬器協議。請求模擬器代表了一個可用于模擬 http 請求的類的協議。
* 符合該協議的類可以用于模擬 Rexxar-Container 內發出的 Http 請求。
*/
@protocol RXRContainerAPI <NSObject>
/**
* 判斷是否應該截獲該請求,對該請求做模擬操作。
*/
- (BOOL)shouldInterceptRequest:(NSURLRequest *)request;
/**
* 模擬請求的返回,返回 NSURLResponse 對象。
*/
- (NSURLResponse *)responseWithRequest:(NSURLRequest *)request;
/**
* 模擬請求返回的內容,返回二進制數據。
*/
- (nullable NSData *)responseData;
@optional
/**
* 準備對請求的模擬。
*
* @param request 對應的請求
*/
- (void)prepareWithRequest:(NSURLRequest *)request;
/**
* 執行對請求的模擬。
*
* @param request 對應的請求
*/
- (void)performWithRequest:(NSURLRequest *)request;
@end
實現的每個ContainerAPI類最后由捕捉器去集中處理:
/**
* `RXRContainerInterceptor` 是一個 Rexxar-Container 的請求偵聽器。
* 這個偵聽器用于模擬網絡請求。這些網絡請求并不會發送出去,而是由 Native 處理。
* 比如向 Web 提供當前位置信息。
*
*/
@interface RXRContainerInterceptor : RXRNSURLProtocol
/**
* 設置這個偵聽器所有的請求模仿器數組,該數組成員是符合 `RXRContainerAPI` 協議的對象,即一組請求模仿器。
*
* @param mockers 模仿器數組
*/
+ (void)setContainerAPIs:(NSArray<id<RXRContainerAPI>> *)containerAPIs;
/**
* 這個偵聽器所有的請求模仿器,該數組成員是符合 `RXRContainerAPI` 協議的對象,即一組請求模仿器。
*/
+ (nullable NSArray<id<RXRContainerAPI>> *)containerAPIs;
/**
* 注冊一個偵聽器。
*/
+ (BOOL)registerInterceptor;
/**
* 注銷一個偵聽器。
*/
+ (void)unregisterInterceptor;
@end
最后把自己實現的RXRContainerAPI都注冊到捕捉器里面在NSURLProtocol的類方法里面去集中處理自己實現的RXRContainerAPI
#pragma mark - Implement NSURLProtocol methods
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
// 請求不是來自瀏覽器,不處理
if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) {
return NO;
}
for (id<RXRContainerAPI> containerAPI in sContainerAPIs) {
if ([containerAPI shouldInterceptRequest:request]) {
return YES;
}
}
return NO;
}
- (void)startLoading
{
for (id<RXRContainerAPI> containerAPI in sContainerAPIs) {
if ([containerAPI shouldInterceptRequest:self.request]) {
if ([containerAPI respondsToSelector:@selector(prepareWithRequest:)]) {
[containerAPI prepareWithRequest:self.request];
}
if ([containerAPI respondsToSelector:@selector(performWithRequest:)]) {
[containerAPI performWithRequest:self.request];
}
NSData *data = [containerAPI responseData];
NSURLResponse *response = [containerAPI responseWithRequest:self.request];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
break;
}
}
}
整個傳輸過程基本講解完成。
此外里面還有一個更改請求的捕捉器(RXRRequestInterceptor),實現過程類似于RXRContainerInterceptor,可以在請求過程中修改一些信息,根據自己的需求使用。
除了這些還有一個很重要的捕捉器(RXRCacheFileIntercepter),實現過程同樣類似于RXRContainerInterceptor,用來下載網頁資源。
講到這里基本的數據傳輸問題已經解決,估計大家也有了一定的了解。
4.緩存機制
他首先在初始化配置的時候是要給一個服務端的json表下載地址的,前期為了快捷可以不設置,先在本地配置使用。json表里的內容根據規則去增加url和uri,最后根據uri去加載url(內部有解析json表,通過uri找到對應的url,web再去加載),所有的web頁面都要通過uri去加載出來。所以說json表是項目里面web頁面的集中源。
也因此在此處去異步下載資源再好不過了:
NSString *routesMapURL = @"http://chf.x x x x.com/credoohybridroutes.json";
[RXRConfig setRoutesMapURL:[NSURL URLWithString:routesMapURL]];
[RXRConfig setRoutesCachePath:@"cn.com.credoo.enterprise.credit"];
[RXRConfig setRoutesResourcePath:@"hybrid"];
//下載json表
[RXRViewController updateRouteFilesWithCompletion:^(BOOL success) {
}];
在下載方法內部會對下載的json表進行拆分,并對每個url對應的頁面資源異步下載到本地存放在沙盒里面,每次下載json表都會去遍歷表內容對比url(根據url和固定參數拼接獲得存放地址)去下載沒有資源,這些資源是不會根據url對應的頁面變化而產生變化的,這是一個問題,因此每當頁面發生變化是,都要自己去改變json表里的url,從而下載最新的,舊的依然會保存在沙盒里,里面提供了清空沙盒的方法,需要自己根據自己的需求在合適的時機里調用。由于這個內部并沒有想象的那么智能去動態的替換本地下載的資源,所以想更一步的實現需要自己去摸索。
這里為了雙重保險,已經在RXRViewController里面注冊了緩存捕捉器
[RXRCacheFileInterceptor registerInterceptor]
根據相同的規則形成path存放沙盒里。
當啟用緩存時會先根據uri去找對應的url,再根據url拼接出沙盒路徑去尋找資源,存在的話就直接加載,否則從網絡獲取,在此同時混存捕捉器會捕捉下載沒有的資源。講到這里如果你還不太明白就打開源碼,一步一步的去探尋他的奧秘吧。
總結
以上是我對豆瓣框架使用工程中的一些感悟和總結,可能有不對的地方,希望大家能夠指出,更希望給想使用此框架的人們一些啟發,謝謝觀賞!