1.首先我們要了解豆瓣框架為何而生,作用是什么粱快。
在大型移動應(yīng)用的開發(fā)中秩彤,項目代碼龐雜叔扼,通常還需要 iOS,Android漫雷,移動 Web 和 桌面 Web 全平臺支持瓜富。這種情況下,更高的開發(fā)效率就成了開發(fā)者不得不考慮的議題降盹。這也是為何雖然移動端的 Web 技術(shù)在使用范圍和性能上有諸多劣勢与柑,仍然有很多開發(fā)者付出努力,探索如何在移動開發(fā)中使用 Web 技術(shù)蓄坏,隨之有了混合開發(fā)价捧。混合開發(fā)的直白解釋是 Native 和 Web 技術(shù)都要用涡戳。但形式上结蟋,應(yīng)用仍然和瀏覽器無關(guān),用戶還是需要在 App Store 和 Android Market 下載應(yīng)用渔彰。只是在開發(fā)時椎眯,開發(fā)者以 Native 代碼為主體,在合適的地方部分使用 Web 技術(shù)胳岂。豆瓣的混合開發(fā)框架就是為了解決我們怎么優(yōu)雅的在Native鑲嵌Web從而實現(xiàn)高效率的界面開發(fā)编整,通過Web實現(xiàn)跨平臺及熱更新,從而提升開發(fā)效率及用戶體驗乳丰。
2.為什么選擇豆瓣的混合框架
首先了解其內(nèi)部的實現(xiàn)機制
1.通過url的參數(shù)傳輸信息掌测,兩端進行交互,所有的行為都是Web發(fā)起产园,最后由Native實現(xiàn)汞斧,Web是主導(dǎo),思路清晰什燕,避免了Native在Web不需要的情況下進行傳輸數(shù)據(jù)粘勒,消耗流量,同時也混淆Web端對信息的接收
2.框架的結(jié)構(gòu)清晰屎即,通過制定協(xié)議來規(guī)范各個類庙睡,從而實現(xiàn)不同的功能;這里主要分為兩大類:(1)Widget調(diào)起本地控件(2)ContainerAPI 上傳數(shù)據(jù)使用
3.輕量級技俐,可擴展性強
4.簡單易懂乘陪,便于使用
3.具體的內(nèi)部實現(xiàn)
一言不合就上圖
1.首先大家最關(guān)心的就是它是怎么完成數(shù)據(jù)的接收和回調(diào)的,這是功能的核心雕擂。
(1)在widget(調(diào)起本地控件)中通過web的代理方法啡邑,在代理方法中會捕捉到該網(wǎng)頁傳過來的url從而通過參數(shù)篩選其需要做出的回應(yīng),從而完成功能的實現(xiàn)
(2)在RXRContainerAPI(上傳數(shù)據(jù))中通過RXRContainerInterceptor(RXRNSURLProtocol)捕捉器捕捉web發(fā)送的url井赌,然后篩選并通過捕捉的web的request請求把數(shù)據(jù)回傳給web
可能說到這里大家根本不知道什么是widget谤逼、什么是RXRContainerAPI贵扰,一臉懵逼。接下來就讓我們揭開這面紗流部,從一步一步的實現(xiàn)過程里找到答案戚绕。
a.首先說widget,widget是一套協(xié)議贵涵,規(guī)定了你所創(chuàng)建的widget要實現(xiàn)的方法:
@import Foundation;
@class RXRViewController;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRWidget` 是一個 Widget 協(xié)議。
* 實現(xiàn) RXRWidget 協(xié)議的類將完成一個 Web 對 Native 的功能調(diào)用恰画。
*/
@protocol RXRWidget <NSObject>
/**
* 判斷該 Widget 是否要對該 URL 做出反應(yīng)宾茂。
*
* @param URL 對應(yīng)的 URL。
*/
- (BOOL)canPerformWithURL:(NSURL *)URL;
/**
* 對該 URL拴还,執(zhí)行 Widget 的各項準備工作跨晴。
*
* @param URL 對應(yīng)的 URL。
*/
- (void)prepareWithURL:(NSURL *)URL;
/**
* 執(zhí)行 Widget 的操作片林。
*
* @param controller 執(zhí)行該 Widget 的 Controller端盆。
*/
- (void)performWithController:(RXRViewController *)controller;
@end
你需要根據(jù)你的功能服從協(xié)議,創(chuàng)建自己的widget去實現(xiàn)自己的功能
b.了解RXRViewController费封,這個vc是你呈現(xiàn)web頁面的容器焕妙,你所有和web相關(guān)的操作的頁面,又要繼承與他弓摘,并制定自己的json表焚鹊,創(chuàng)建映射uri,初始化你的web韧献,當(dāng)然widget也會全部集中在這里處理末患。json表就是這里的路由表,你自己要根據(jù)它的格式去配置自己的url锤窑,一個頁面對應(yīng)一個url璧针,通過uri來打開,可以下載官方demo一看便知渊啰。https://github.com/douban/rexxar-ios
@import UIKit;
@protocol RXRWidget;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRViewController` 是一個 Rexxar Container探橱。
* 它提供了一個使用 web 技術(shù) html, css, javascript 開發(fā) UI 界面的容器。
*/
@interface RXRViewController : UIViewController <UIWebViewDelegate>
/**
* 對應(yīng)的 uri绘证。
*/
@property (nonatomic, strong, readonly) NSURL *uri;
/**
* 內(nèi)置的 WebView走搁。
*/
@property (nonatomic, strong, readonly) UIWebView *webView;
/**
* activities 代表該 Rexxar Container 可以響應(yīng)的協(xié)議。
*/
@property (nonatomic, strong) NSArray<id<RXRWidget>> *widgets;
/**
* 初始化一個RXRViewController迈窟。
*
* @param uri 該頁面對應(yīng)的 uri私植。
*
* @discussion 會根據(jù) uri 從 Route Map File 中選擇對應(yīng)本地 html 文件加載。如果無本地 html 文件车酣,則從服務(wù)器加載 html 資源曲稼。
* 在 UIWebView 中索绪,遠程 URL 需要注意跨域問題。
*/
- (instancetype)initWithURI:(NSURL *)uri;
/**
* 初始化一個RXRViewController贫悄。
*
* @param uri 該頁面對應(yīng)的 uri瑞驱。
* @param htmlFileURL 該頁面對應(yīng)的 html file url。
*
* @discussion 會根據(jù) uri 從 Route Map File 中選擇對應(yīng)本地 html 文件加載窄坦。如果無本地 html 文件唤反,則從服務(wù)器加載 html 資源。
* 在 UIWebView 中鸭津,遠程 URL 需要注意跨域問題彤侍。
*/
- (instancetype)initWithURI:(NSURL *)uri htmlFileURL:(NSURL *)htmlFileURL;
/**
* 重新加載 WebView。
*/
- (void)reloadWebView;
/**
* 通知 WebView 頁面顯示逆趋,缺省會在 viewWillAppear 里調(diào)用盏阶。本方法可以由業(yè)務(wù)層自主定制向 WebView 通知 onPageVisible 的時機。
*/
- (void)onPageVisible;
/**
* 通知 WebView 頁面消失闻书,缺省會在 viewDidDisappear 里調(diào)用名斟。本方法可以由業(yè)務(wù)層自主定制向 WebView 通知 onPageInvisible 的時機。
*/
- (void)onPageInvisible;
/**
* 調(diào)用 WebView 的一個 JavaScript 函數(shù)魄眉,并傳入一個 json 串作為參數(shù)砰盐。
*
* @param function 調(diào)用的函數(shù)。
* @param jsonParameter 傳遞的參數(shù)坑律,json 串楞卡。
*/
- (nullable NSString *)callJavaScript:(NSString *)function jsonParameter:(nullable NSString *)jsonParameter;
@end
#pragma mark - Public Route Methods
/**
* 暴露出 Route 相關(guān)的接口。
*/
@interface RXRViewController (Router)
/**
* 更新 Route Files脾歇。
*
* @param completion 更新完成后將執(zhí)行這個 block蒋腮。
*/
+ (void)updateRouteFilesWithCompletion:(nullable void (^)(BOOL success))completion;
/**
* 判斷路由表是否存在對應(yīng)于 uri 的 route 信息。
*
* @param uri 待判斷的 uri藕各。
*/
+ (BOOL)isRouteExistForURI:(NSURL *)uri;
/**
* 判斷本地(緩存池摧,或預(yù)置資源中)是否已經(jīng)下載了存在對應(yīng)于 uri 的 route 信息的資源。
*
* @param uri 待判斷的 uri激况。
*/
+ (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri;
@end
自己實現(xiàn)的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:// 開頭乌逐,則打開網(wǎng)頁
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.接下來就是上傳數(shù)據(jù)
上傳數(shù)據(jù)完全也可以在web的代理里面去集中處理竭讳,但是這樣就會顯得十分臃腫,代碼也會比較繁雜浙踢。這里采用NSURLProtocol捕捉請求绢慢,去篩選需要的url,從而實現(xiàn)數(shù)據(jù)上傳洛波。這里也是采用集中處理胰舆,同樣由代理去規(guī)范類的行為骚露。
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
/**
* `RXRContainerAPI` 是一個請求模擬器協(xié)議乳蛾。請求模擬器代表了一個可用于模擬 http 請求的類的協(xié)議默勾。
* 符合該協(xié)議的類可以用于模擬 Rexxar-Container 內(nèi)發(fā)出的 Http 請求。
*/
@protocol RXRContainerAPI <NSObject>
/**
* 判斷是否應(yīng)該截獲該請求挽荡,對該請求做模擬操作倦零。
*/
- (BOOL)shouldInterceptRequest:(NSURLRequest *)request;
/**
* 模擬請求的返回误续,返回 NSURLResponse 對象。
*/
- (NSURLResponse *)responseWithRequest:(NSURLRequest *)request;
/**
* 模擬請求返回的內(nèi)容扫茅,返回二進制數(shù)據(jù)蹋嵌。
*/
- (nullable NSData *)responseData;
@optional
/**
* 準備對請求的模擬。
*
* @param request 對應(yīng)的請求
*/
- (void)prepareWithRequest:(NSURLRequest *)request;
/**
* 執(zhí)行對請求的模擬诞帐。
*
* @param request 對應(yīng)的請求
*/
- (void)performWithRequest:(NSURLRequest *)request;
@end
實現(xiàn)的每個ContainerAPI類最后由捕捉器去集中處理:
/**
* `RXRContainerInterceptor` 是一個 Rexxar-Container 的請求偵聽器欣尼。
* 這個偵聽器用于模擬網(wǎng)絡(luò)請求爆雹。這些網(wǎng)絡(luò)請求并不會發(fā)送出去停蕉,而是由 Native 處理。
* 比如向 Web 提供當(dāng)前位置信息钙态。
*
*/
@interface RXRContainerInterceptor : RXRNSURLProtocol
/**
* 設(shè)置這個偵聽器所有的請求模仿器數(shù)組慧起,該數(shù)組成員是符合 `RXRContainerAPI` 協(xié)議的對象,即一組請求模仿器册倒。
*
* @param mockers 模仿器數(shù)組
*/
+ (void)setContainerAPIs:(NSArray<id<RXRContainerAPI>> *)containerAPIs;
/**
* 這個偵聽器所有的請求模仿器蚓挤,該數(shù)組成員是符合 `RXRContainerAPI` 協(xié)議的對象,即一組請求模仿器驻子。
*/
+ (nullable NSArray<id<RXRContainerAPI>> *)containerAPIs;
/**
* 注冊一個偵聽器灿意。
*/
+ (BOOL)registerInterceptor;
/**
* 注銷一個偵聽器。
*/
+ (void)unregisterInterceptor;
@end
最后把自己實現(xiàn)的RXRContainerAPI都注冊到捕捉器里面在NSURLProtocol的類方法里面去集中處理自己實現(xiàn)的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),實現(xiàn)過程類似于RXRContainerInterceptor域慷,可以在請求過程中修改一些信息荒辕,根據(jù)自己的需求使用。
除了這些還有一個很重要的捕捉器(RXRCacheFileIntercepter)犹褒,實現(xiàn)過程同樣類似于RXRContainerInterceptor抵窒,用來下載網(wǎng)頁資源。
講到這里基本的數(shù)據(jù)傳輸問題已經(jīng)解決叠骑,估計大家也有了一定的了解李皇。
4.緩存機制
他首先在初始化配置的時候是要給一個服務(wù)端的json表下載地址的,前期為了快捷可以不設(shè)置宙枷,先在本地配置使用疙赠。json表里的內(nèi)容根據(jù)規(guī)則去增加url和uri付材,最后根據(jù)uri去加載url(內(nèi)部有解析json表,通過uri找到對應(yīng)的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) {
}];
在下載方法內(nèi)部會對下載的json表進行拆分富寿,并對每個url對應(yīng)的頁面資源異步下載到本地存放在沙盒里面,每次下載json表都會去遍歷表內(nèi)容對比url(根據(jù)url和固定參數(shù)拼接獲得存放地址)去下載沒有資源锣夹,這些資源是不會根據(jù)url對應(yīng)的頁面變化而產(chǎn)生變化的页徐,這是一個問題,因此每當(dāng)頁面發(fā)生變化是银萍,都要自己去改變json表里的url变勇,從而下載最新的,舊的依然會保存在沙盒里贴唇,里面提供了清空沙盒的方法搀绣,需要自己根據(jù)自己的需求在合適的時機里調(diào)用。由于這個內(nèi)部并沒有想象的那么智能去動態(tài)的替換本地下載的資源戳气,所以想更一步的實現(xiàn)需要自己去摸索链患。
這里為了雙重保險,已經(jīng)在RXRViewController里面注冊了緩存捕捉器
[RXRCacheFileInterceptor registerInterceptor]
根據(jù)相同的規(guī)則形成path存放沙盒里瓶您。
當(dāng)啟用緩存時會先根據(jù)uri去找對應(yīng)的url麻捻,再根據(jù)url拼接出沙盒路徑去尋找資源,存在的話就直接加載呀袱,否則從網(wǎng)絡(luò)獲取贸毕,在此同時混存捕捉器會捕捉下載沒有的資源。講到這里如果你還不太明白就打開源碼夜赵,一步一步的去探尋他的奧秘吧明棍。
總結(jié)
以上是我對豆瓣框架使用工程中的一些感悟和總結(jié),可能有不對的地方油吭,希望大家能夠指出击蹲,更希望給想使用此框架的人們一些啟發(fā),謝謝觀賞婉宰!