豆瓣的混合開發(fā)框架 -- Rexxar詳解

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ā),謝謝觀賞婉宰!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末歌豺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子心包,更是在濱河造成了極大的恐慌类咧,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痕惋,居然都是意外死亡区宇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門值戳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來议谷,“玉大人,你說我怎么就攤上這事堕虹∥韵” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵赴捞,是天一觀的道長逼裆。 經(jīng)常有香客問我,道長赦政,這世上最難降的妖魔是什么胜宇? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮恢着,結(jié)果婚禮上桐愉,老公的妹妹穿的比我還像新娘。我一直安慰自己然评,他們只是感情好仅财,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布狈究。 她就那樣靜靜地躺著碗淌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抖锥。 梳的紋絲不亂的頭發(fā)上亿眠,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音磅废,去河邊找鬼纳像。 笑死,一個胖子當(dāng)著我的面吹牛拯勉,可吹牛的內(nèi)容都是我干的竟趾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼宫峦,長吁一口氣:“原來是場噩夢啊……” “哼岔帽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起导绷,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤犀勒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贾费,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡钦购,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了褂萧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片押桃。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖导犹,靈堂內(nèi)的尸體忽然破棺而出怨规,到底是詐尸還是另有隱情,我是刑警寧澤锡足,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布波丰,位于F島的核電站,受9級特大地震影響舶得,放射性物質(zhì)發(fā)生泄漏掰烟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一沐批、第九天 我趴在偏房一處隱蔽的房頂上張望纫骑。 院中可真熱鬧,春花似錦九孩、人聲如沸先馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煤墙。三九已至,卻和暖如春宪拥,著一層夾襖步出監(jiān)牢的瞬間仿野,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工她君, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留脚作,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓缔刹,卻偏偏與公主長得像球涛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子校镐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理亿扁,服務(wù)發(fā)現(xiàn),斷路器灭翔,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 混合開發(fā)的直白解釋是 Native 和 Web 技術(shù)都要用魏烫。但形式上辣苏,應(yīng)用仍然和瀏覽器無關(guān),用戶還是需要在 App...
    迪亞波羅閱讀 1,563評論 0 13
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,506評論 25 707
  • 防疫的成功與否關(guān)鍵在于:豬群的健康和穩(wěn)定哄褒、飼料的營養(yǎng)均衡稀蟋、流行性疾病的預(yù)防三個方面。 飼料主要有兩關(guān):原料質(zhì)量關(guān)和...
    一心養(yǎng)豬閱讀 155評論 0 0
  • 愛你,所以才會吃醋链嘀。如果沒有愛萌狂,那么無論你做什么我也無所謂了! 親人怀泊,是上天注定茫藏;戀人,是前世安排霹琼;愛人务傲,是緣分融...
    一生一世不離不棄wzq閱讀 226評論 0 0