豆瓣的混合開發框架 -- Rexxar詳解

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拼接出沙盒路徑去尋找資源,存在的話就直接加載,否則從網絡獲取,在此同時混存捕捉器會捕捉下載沒有的資源。講到這里如果你還不太明白就打開源碼,一步一步的去探尋他的奧秘吧。

總結

以上是我對豆瓣框架使用工程中的一些感悟和總結,可能有不對的地方,希望大家能夠指出,更希望給想使用此框架的人們一些啟發,謝謝觀賞!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容