豆瓣的混合开发框架 -- 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阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容