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
/**
* 判断该 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
/**
* 对应的 uri。
*/
@property (nonatomic, strong, readonly) NSURL *uri;
/**
* 内置的 WebView。
*/
@property (nonatomic, strong, readonly) UIWebView *webView;
/**
* activities 代表该 Rexxar Container 可以响应的协议。
*/
@property (nonatomic, strong) NSArray> *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 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
/**
* 判断是否应该截获该请求,对该请求做模拟操作。
*/
- (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> *)containerAPIs;
/**
* 这个侦听器所有的请求模仿器,该数组成员是符合 `RXRContainerAPI` 协议的对象,即一组请求模仿器。
*/
+ (nullable NSArray> *)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 containerAPI in sContainerAPIs) {
if ([containerAPI shouldInterceptRequest:request]) {
return YES;
}
}
return NO;
}
- (void)startLoading
{
for (id 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拼接出沙盒路径去寻找资源,存在的话就直接加载,否则从网络获取,在此同时混存捕捉器会捕捉下载没有的资源。讲到这里如果你还不太明白就打开源码,一步一步的去探寻他的奥秘吧。
总结
以上是我对豆瓣框架使用工程中的一些感悟和总结,可能有不对的地方,希望大家能够指出,更希望给想使用此框架的人们一些启发,谢谢观赏!