├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_EN.md ├── Rexxar.podspec ├── Rexxar.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Rexxar.xcscheme │ └── RexxarTests.xcscheme ├── Rexxar ├── ContainerAPI │ ├── RXRContainerAPI.h │ ├── RXRContainerInterceptor.h │ └── RXRContainerInterceptor.m ├── Core │ ├── Extension │ │ ├── NSData+RXRDigest.h │ │ ├── NSData+RXRDigest.m │ │ ├── NSDictionary+RXRMultipleItems.h │ │ ├── NSDictionary+RXRMultipleItems.m │ │ ├── NSHTTPURLResponse+Rexxar.h │ │ ├── NSHTTPURLResponse+Rexxar.m │ │ ├── NSMutableDictionary+RXRMultipleItems.h │ │ ├── NSMutableDictionary+RXRMultipleItems.m │ │ ├── NSString+RXRURLEscape.h │ │ ├── NSString+RXRURLEscape.m │ │ ├── NSURL+Rexxar.h │ │ ├── NSURL+Rexxar.m │ │ ├── RXRURLRequestSerialization.h │ │ ├── RXRURLRequestSerialization.m │ │ ├── UIColor+Rexxar.h │ │ └── UIColor+Rexxar.m │ ├── RXRCacheFileInterceptor.h │ ├── RXRCacheFileInterceptor.m │ ├── RXRConfig+Rexxar.h │ ├── RXRConfig+Rexxar.m │ ├── RXRConfig.h │ ├── RXRConfig.m │ ├── RXRCustomSchemeHandler.h │ ├── RXRCustomSchemeHandler.m │ ├── RXRDataValidator.h │ ├── RXRErrorHandler.h │ ├── RXRErrorHandler.m │ ├── RXRLogger.h │ ├── RXRLogger.m │ ├── RXRNSURLProtocol.h │ ├── RXRNSURLProtocol.m │ ├── RXRRoute.h │ ├── RXRRoute.m │ ├── RXRRouteFileCache.h │ ├── RXRRouteFileCache.m │ ├── RXRRouteManager.h │ ├── RXRRouteManager.m │ ├── RXRURLSessionDemux.h │ ├── RXRURLSessionDemux.m │ ├── RXRViewController+Router.m │ ├── RXRViewController.h │ ├── RXRViewController.m │ ├── RXRWebViewController.h │ ├── RXRWebViewController.m │ └── RXRWidget.h ├── Decorator │ ├── RXRDecorator.h │ ├── RXRRequestDecorator.h │ ├── RXRRequestDecorator.m │ ├── RXRRequestInterceptor.h │ └── RXRRequestInterceptor.m ├── Info.plist ├── Proxy │ └── RXRProxy.h ├── Rexxar.h └── Widget │ ├── Model │ ├── RXRAlertDialogData.h │ ├── RXRAlertDialogData.m │ ├── RXRModel.h │ └── RXRModel.m │ ├── RXRAlertDialogWidget.h │ ├── RXRAlertDialogWidget.m │ ├── RXRNavTitleWidget.h │ ├── RXRNavTitleWidget.m │ ├── RXRPullRefreshWidget.h │ └── RXRPullRefreshWidget.m ├── RexxarDemo ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Bridge-Header.h ├── ContainerAPI │ ├── RXRGeoContainerAPI.h │ ├── RXRGeoContainerAPI.m │ ├── RXRLogContainerAPI.h │ └── RXRLogContainerAPI.m ├── FullRXRViewController.swift ├── Info.plist ├── Library │ └── FRDToast │ │ ├── FRDToast.swift │ │ ├── LoadingView.swift │ │ ├── ToastView.swift │ │ └── UIColor+helper.swift ├── PartialRXRViewController.swift ├── Resource │ └── hybrid │ │ ├── common │ │ └── vendor-b5a42da096.js │ │ ├── rexxar │ │ └── demo-c775298867.html │ │ └── routes.json ├── RoutesViewController.swift └── Widget │ ├── Model │ └── Menu │ │ ├── RXRMenuItem.h │ │ └── RXRMenuItem.m │ ├── RXRNavMenuWidget.h │ ├── RXRNavMenuWidget.m │ ├── RXRToastWidget.h │ └── RXRToastWidget.m ├── RexxarTests ├── Info.plist ├── RXRRouteFileCacheTests.m ├── RXRRouteManagerTests.m ├── RexxarTests.m └── www │ └── routes.json └── docs └── images └── Rexxar.png /.gitignore: -------------------------------------------------------------------------------- 1 | #global 2 | *.orig 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | 19 | #ios 20 | .DS_Store 21 | *~ 22 | project.xcworkspace/ 23 | *.xccheckout 24 | xcuserdata/ 25 | .svn 26 | .idea 27 | .ropeproject 28 | .venv 29 | 30 | # CocoaPods 31 | Pods 32 | 33 | #CrashMonkey 34 | instrumentscli*.trace/ 35 | crash_monkey_result/ 36 | 37 | # Docs 38 | docs/_build 39 | reports 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11.4 2 | language: objective-c 3 | script: 4 | - | 5 | xcodebuild test \ 6 | -project Rexxar.xcodeproj \ 7 | -scheme RexxarTests \ 8 | -sdk iphonesimulator \ 9 | -destination 'platform=iOS Simulator,name=iPhone 8,OS=13.3' \ 10 | ONLY_ACTIVE_ARCH=NO 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Douban Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rexxar iOS 2 | 3 | [![Test Status](https://travis-ci.org/douban/rexxar-ios.svg?branch=master)](https://travis-ci.org/douban/rexxar-ios) 4 | [![IDE](https://img.shields.io/badge/XCode-8-blue.svg)]() 5 | [![iOS](https://img.shields.io/badge/iOS-7.0-green.svg)]() 6 | [![Language](https://img.shields.io/badge/language-ObjC-blue.svg)](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html) 7 | 8 | **[README in English](https://github.com/douban/rexxar-ios/blob/master/README_EN.md)** 9 | 10 | **Rexxar** 是一个针对移动端的混合开发框架。现在支持 Android 和 iOS 平台。`Rexxar-iOS` 是 Rexxar 在 iOS 系统上的客户端实现。 11 | 12 | 通过 Rexxar,你可以使用包括 javascript,css,html 在内的传统前端技术开发移动应用。Rexxar 的客户端实现 Rexxar Container 对于 Web 端使用何种技术并无要求。我们现在的 Rexxar 的前端实现 Rexxar Web,以及 Rexxar Container 在两个平台的实现 Rexxar-iOS 和 Rexxar-Android 项目中所带的 Demo 都使用了 [React](https://facebook.github.io/react/)。但你完全可以选择自己的前端框架在 Rexxar Container 中进行开发。 13 | 14 | Rexxar-iOS 现在支持 iOS 7 及以上版本。 15 | 16 | 17 | ## Rexxar 简介 18 | 19 | 关于 Rexxar 的整体介绍,可以看看这篇博客:[豆瓣的混合开发框架 -- Rexxar](http://lincode.github.io/Rexxar-OpenSource)。 20 | 21 | 关于 Rexxar 的设计思想,可以看看这篇问答:[Infoq 采访:豆瓣混合开发框架 Rexxar](http://lincode.github.io/Rexxar-Interview)。 22 | 23 | Rexxar 包含三个库: 24 | 25 | - Rexxar Web :[https://github.com/douban/rexxar-web](https://github.com/douban/rexxar-web)。 26 | - Rexxar Android:[https://github.com/douban/rexxar-android](https://github.com/douban/rexxar-android)。 27 | - Rexxar iOS:[https://github.com/douban/rexxar-ios](https://github.com/douban/rexxar-ios)。 28 | 29 | ## 安装 30 | 31 | ### 安装 Cocoapods 32 | 33 | [CocoaPods](http://cocoapods.org) 是一个 Objective-c 和 Swift 的依赖管理工具。你可以通过以下命令安装 CocoaPods: 34 | 35 | ```bash 36 | $ gem install cocoapods 37 | ``` 38 | 39 | ### Podfile 40 | 41 | ```ruby 42 | target 'TargetName' do 43 | pod 'Rexxar', :git => 'https://github.com/douban/rexxar-ios.git', :commit => '0.2.1' 44 | end 45 | ``` 46 | 47 | 然后,运行以下命令: 48 | 49 | ```bash 50 | $ pod install 51 | ``` 52 | 53 | 54 | ## 使用 55 | 56 | 你可以查看 Demo 中的例子。了解如何使用 Rexxar。Demo 给出了完善的示例。 57 | 58 | Demo 中使用 github 的 raw 文件服务提供一个简单的路由表文件 routes.json,demo.html 以及相关 javascript 资源的访问服务。在你的线上服务中,当然会需要一个真正的生产环境,以应付更大规模的路由表文件,以及 javascript,css,html 资源文件的访问。你可以使用任何服务端框架。Rexxar 对服务端框架并无要求。`RXRConfig` 提供了对路由表文件地址的配置接口。下一节描述了配置方法。 59 | 60 | ### 配置 RXRConfig 61 | 62 | #### 设置路由表文件 api: 63 | 64 | ```Swift 65 | RXRConfig.setRoutesMapURL(NSURL(string:"https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/routes.json")!) 66 | ``` 67 | 68 | Rexxar 使用 url 来标识页面。提供一个正确的 url 就可以创建对应的 RXRViewController。路由表提供了每个 url 对应的 html 资源的下载地址。Demo 中的路由表如下: 69 | 70 | ```json 71 | { 72 | "items": [{ 73 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html", 74 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 75 | "uri": "douban://douban.com/rexxar_demo[/]?.*" 76 | }], 77 | "partial_items": [{ 78 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html", 79 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 80 | "uri": "douban://partial.douban.com/rexxar_demo/_.*" 81 | }], 82 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 83 | } 84 | ``` 85 | 86 | #### 设置预置资源文件路径 87 | 88 | ```Swift 89 | RXRConfig.setRoutesResourcePath("rexxar") 90 | ``` 91 | 92 | 使用 Rexxar 一般会预置一份路由表,以及资源文件在应用包中。这样就可以减少用户的下载,加快第一次打开页面的速度。在没有网络的情况下,如果没有数据请求的话,页面也可访问。这都有利于用户体验。 93 | 94 | 注意,如果设置了预置资源文件路径,即意味着在应用包内预置一份资源文件。这个文件夹需要是 folder references 类型,即在 Xcode 中呈现为蓝色文件夹图标。创建方法是将文件夹拖入 Xcode 项目,选择 Create folder references 选项。 95 | 96 | #### 设置缓存路径 97 | 98 | ```Swift 99 | RXRConfig.setRoutesCachePath("com.douban.RexxarDemo.rexxar") 100 | ``` 101 | 102 | 缓存文件夹存在的目的也是减少资源文件的下载次数,加快打开页面的速度。使得用户可以得到近似原生页面的页面加载体验。 103 | 104 | 缓存资源文件一般会出现在 Rexxar 部署了一次路由表的更新之后。这也是 Rexxar 支持`热部署`的方法:路由表控制资源文件的更新。一般可以让应用定期访问路由表。比如,在开启应用时,或者关闭应用时更新路由表。更新路由表的方法如下: 105 | 106 | ```Swift 107 | RXRViewController.updateRouteFiles(completion: nil) 108 | ``` 109 | 110 | 如果,新的路由表中出现了 html 文件的更新,或者出现了新的 url。也就是说这些文件并不存在于预置资源文件夹中,Rexxar Container 就会在下载完路由表之后,主动下载新资源,并将新资源储存在缓存文件夹中。 111 | 112 | #### 预置资源文件和缓存文件关系 113 | 114 | 正常程序逻辑下,预置资源文件夹存在的资源,就不会再去服务器下载,也就不会有缓存的资源文件。 115 | 116 | 在进入一个 RXRViewController 时,会读取资源文件。在读取时,Rexxar Container 先读取缓存文件,如果存在就使用缓存文件。如果缓存文件不存在,就读取预置资源文件。如果,预置资源文件也不存在。RXRViewController 会尝试更新一次路由表,下载路由表中新出现的资源,并再次尝试读取缓存资源文件。如果仍然不存在,就会出现页面错误。 117 | 118 | 读取顺序如下: 119 | 120 | 1. 缓存文件夹中读取 html 文件; 121 | 2. 预置资源文件夹中读取 html 文件; 122 | 3. 重新下载路由表 Routes.json,遍历路由表将新的 html 文件下载到缓存文件夹。再次尝试从缓存文件夹读取 html 文件; 123 | 124 | 以上三步中,任何一步读取成功就停止,并返回读取的结果。如果,三步都完成了仍没有找到文件,就会出现页面错误。 125 | 126 | 有了预置资源文件和缓存文件的双重保证,一般用户打开 Rexxar 页面时都不会临时向服务器请求资源文件。这大大提升了用户打开页面的体验。 127 | 128 | ### 使用 RXRViewController 129 | 130 | 你可以直接使用 `RXRViewController` 作为你的混合开发客户端容器。或者你也可以继承 `RXRViewController`,在 `RXRViewController` 基础上实现你自己的客户端容器。在 Demo 中,创建了 `FullRXRViewController`,它继承于 `RXRViewController`。 131 | 132 | 为了初始化 RXRViewController,你只需要一个 url。在路由表文件 api 提供的路由表中可以找到这个 url。这个 url 标识了该页面所需使用的资源文件的位置。Rexxar Container 会通过 url 在路由表中寻找对应的 javascript,css,html 资源文件。 133 | 134 | ```Swift 135 | let controller = RXRViewController(URI: uri) 136 | let titleWidget = RXRNavTitleWidget() 137 | let alertDialogWidget = RXRAlertDialogWidget() 138 | controller.activities = [titleWidget, alertDialogWidget] 139 | navigationController?.pushViewController(controller, animated: true) 140 | ``` 141 | 142 | 143 | ## 定制你自己的 Rexxar Container 144 | 145 | 首先,可以继承 `RXRViewController`,在 `RXRViewController` 基础上以实现你自己客户端容器。 146 | 147 | 然后,可以使用 Rexxar 提供的三个接口。下面会介绍如何使用这三个接口,更方便地扩展属于自己的特定功能。 148 | 149 | ### 定制 RXRWidget 150 | 151 | Rexxar Container 提供了一些原生 UI 组件,供 Rexxar Web 使用。RXRWidget 是一个 Objective-C 协议(Protocol)。该协议是对这类原生 UI 组件的抽象。如果,你需要实现某些原生 UI 组件,例如,弹出一个 Toast,或者添加原生效果的下拉刷新,你就可以实现一个符合 RXRWidget 协议的类,并实现以下三个方法:`canPerformWithURL:`,`prepareWithURL:`,`performWithController:`。 152 | 153 | 在 Demo 中可以找到一个例子:`RXRNavTitleWidget` ,通过它可以设置导航栏的标题文字。 154 | 155 | ```Objective-C 156 | @interface RXRNavTitleWidget () 157 | 158 | @property (nonatomic, copy) NSString *title; 159 | 160 | @end 161 | 162 | 163 | @implementation RXRNavTitleWidget 164 | 165 | - (BOOL)canPerformWithURL:(NSURL *)URL 166 | { 167 | NSString *path = URL.path; 168 | if (path && [path isEqualToString:@"/widget/nav_title"]) { 169 | return true; 170 | } 171 | return false; 172 | } 173 | 174 | - (void)prepareWithURL:(NSURL *)URL 175 | { 176 | self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; 177 | } 178 | 179 | - (void)performWithController:(RXRViewController *)controller 180 | { 181 | if (controller) { 182 | controller.title = self.title; 183 | } 184 | } 185 | 186 | @end 187 | ``` 188 | 189 | ### 定制 RXRContainerAPI 190 | 191 | 我们常常需要在 Rexxar Container 和 Rexxar Web 之间做数据交互。比如 Rexxar Container 可以为 Rexxar Web 提供一些计算结果。如果你需要提供一些由原生代码计算的数据给 Rexxar Web 使用,你就可以选择实现 RXRContainerAPI 协议(Protocol),并实现以下三个方法:`shouldInterceptRequest:`, `responseWithRequest:`, `responseData`。 192 | 193 | 在 Demo 中可以找到一个例子:`RXRGeoContainerAPI`。这个例子中,`RXRGeoContainerAPI` 返回了设备所在城市,以及经纬度信息。当然,这个 ContainerAPI 仅仅是一个示例,它提供的是一个假数据,数据永远不会变化。你当然可以遵守 `RXRContainerAPI` 协议,实现一个类似的但是数据是真实的功能。 194 | 195 | ### 定制 RXRDecorator 196 | 197 | 如果你需要修改运行在 Rexxar Container 中的 Rexxar Web 所发出的请求。例如,在 http 头中添加登录信息,你可以实现 `RXRDecorator` 协议(Protocol),并实现这两个方法:`shouldInterceptRequest:`, `prepareWithRequest:`。 198 | 199 | 在 Demo 的 `FullRXRViewController` 类中,可以找到一个使用 `RXRRequestDecorator` 添加登录信息,和 URL 参数的例子。这个例子为 Rexxar Web 发出的请求添加了登录信息,并在 URL 参数中增加了 apikey 信息。 200 | 201 | ### 使用 RXRContainerAPI 和 RXRDecorator 202 | 203 | RXRContainerAPI 和 RXRDecorator 生效期应该和 RXRViewController 的生命周期一致。 204 | 205 | 这就是说可以在 RXRViewController 的 `viewDidLoad:` 方法中注册 RXRContainerAPI 和 RXRDecorator: 206 | 207 | ```Swift 208 | override func viewDidLoad() { 209 | super.viewDidLoad() 210 | 211 | ... 212 | 213 | URLProtocol.registerClass(RXRContainerInterceptor.self) 214 | 215 | ... 216 | 217 | URLProtocol.registerClass(RXRRequestInterceptor.self) 218 | } 219 | ``` 220 | 221 | 可以在 Objective-C 的 `dealloc` 或者 Swift 的 `deinit` 方法中取消 RXRContainerAPI 和 RXRDecorator 的注册: 222 | 223 | ```Swift 224 | deinit { 225 | URLProtocol.unregisterClass(RXRContainerInterceptor.self) 226 | URLProtocol.unregisterClass(RXRRequestInterceptor.self) 227 | } 228 | ``` 229 | 230 | 在 Demo 中的 `FullRXRViewController` 可以看到如何注册和取消注册 RXRContainerAPI 和 RXRDecorator。 231 | 232 | ### 定制 RXRProxy 233 | 234 | RXRProxy 是本地请求代理服务,当一个请求允许被本地服务代理时,可以直接返回本地内容(类似 ContainerAPI),当请求不能被本地服务代理时,继续原来的请求(类似 RequestInterceptor)。 235 | 236 | ## Partial RXRViewController 237 | 238 | 如果,你发现一个页面无法全部使用 Rexxar 实现。你可以在一个原生页面内内嵌一个 RXRViewController,部分功能使用原生实现,另一部分功能使用 Rexxar 实现。 239 | 240 | Demo 中的 PartialRexxarViewController 给出了一个示例。 241 | 242 | 243 | ## Rexxar 的公开接口 244 | 245 | * Rexxar Container 246 | - `RXRConfig` 247 | - `RXRViewController` 248 | 249 | * Widget 250 | - `RXRWidget` 251 | - `RXRNavTitleWidget` 252 | - `RXRAlertDialogWidget` 253 | - `RXRPullRefreshWidget` 254 | 255 | * ContainerAPI 256 | - `RXRNSURLProtocol` 257 | - `RXRContainerInterceptor` 258 | - `RXRContainerAPI` 259 | 260 | * Decorator 261 | - `RXRRequestInterceptor` 262 | - `RXRDecorator` 263 | - `RXRRequestDecorator` 264 | 265 | * Util 266 | - `NSURL+Rexxar` 267 | - `NSDictionary+RXRMultipleItem` 268 | 269 | 270 | ## Changelog 271 | - 0.3.1 iOS 13.4及以上的系统,可以设置RXRConfig.useCustomScheme = true来拦截rexxar-http和rexxar-https请求,避免直接拦截http和https请求。 272 | - 0.3.0 使用 WKWebView 替换 UIWebView, 由于 WKWebView 对 NSURLProtocol 支持不够友好,你需要特别关心一下 NSURLProtocol 截获 Post 请求时 Body 被清空的问题。 273 | - master 2018-12-28 配置文件 `RXRConfig` 删除 `+ (void)setExternalUserAgent:(NSString *)userAgent;` 和 `+ (NSString *)externalUserAgent;`新增 `+ (**void**)setRxrUserAgent:(NSString *)userAgent;` 和 `+ (NSString *)rxrUserAgent;` 274 | 275 | ## Unit Test 276 | 277 | 在项目的 RexxarTests 文件夹下可以找到一系列单元测试。这些单元测试可以通过命令 `cmd+u` 在 Xcode 中运行。单元测试除了可以验证代码的正确性之外,还提供了如何使用这些代码的示例。可以查看这些单元测试,以了解如何使用 Rexxar。 278 | 279 | 280 | ## License 281 | 282 | The MIT license. 283 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Rexxar iOS 2 | 3 | [![Test Status](https://travis-ci.org/douban/rexxar-ios.svg?branch=master)](https://travis-ci.org/douban/rexxar-ios) 4 | [![IDE](https://img.shields.io/badge/XCode-8-blue.svg)]() 5 | [![iOS](https://img.shields.io/badge/iOS-7.0-green.svg)]() 6 | [![Language](https://img.shields.io/badge/language-ObjC-blue.svg)](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html) 7 | 8 | **Rexxar** is a cross-platform library for hybrid mobile application. It supports both iOS and Android. `Rexxar iOS` is Rexxar's Container in iOS. 9 | 10 | Rexxar brings the ease of web technologies, including HTML, JavaScript and CSS, into mobile application development. We have used [React](https://facebook.github.io/react/) for the web front demo, but a Rexxar Container does not confine your choice of web front end framework. You can use your own web front side framework to develop the application in Rexxar Container. 11 | 12 | Rexxar iOS supports iOS 7.0 and above. 13 | 14 | 15 | ## Rexxar 16 | 17 | About Rexxar's integral introduction, please check this article: [豆瓣的混合开发框架 -- Rexxar](http://lincode.github.io/Rexxar-OpenSource). In order to bring its full power, Rexxar iOS or Android need a Web implementation to offer the routes map api and the Web resources including HTML, JavaScript, CSS. 18 | 19 | Rexxar includes threes libraries: 20 | 21 | - Rexxar Web:[https://github.com/douban/rexxar-web](https://github.com/douban/rexxar-web)。 22 | - Rexxar Android:[https://github.com/douban/rexxar-android](https://github.com/douban/rexxar-android)。 23 | - Rexxar iOS:[https://github.com/douban/rexxar-ios](https://github.com/douban/rexxar-ios)。 24 | 25 | 26 | ## Installation 27 | 28 | ### Install Cocoapods 29 | 30 | [CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C and Swift. You can install it with the following command: 31 | 32 | ```bash 33 | $ gem install cocoapods 34 | ``` 35 | 36 | ### Podfile 37 | 38 | ```ruby 39 | target 'TargetName' do 40 | pod 'Rexxar', :git => 'https://github.com/douban/rexxar-ios.git', :commit => '0.2.1' 41 | end 42 | ``` 43 | 44 | Then, run the following command: 45 | 46 | ```bash 47 | $ pod install 48 | ``` 49 | 50 | 51 | ## Usage 52 | 53 | Please check out RexxarDemo for demo usage. We have used Github raw file as the routes map api. You would want to dynamically generate the routes map via an api endpoint, and need a real server to serve HTML, Javascript, and CSS in production. 54 | 55 | It's possible to change this endpoint with RXRConfig, see below for details. 56 | 57 | ### Configure routes map api address 58 | 59 | ```Swift 60 | RXRConfig.setRoutesMapURL(NSURL(string:"https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/routes.json)!) 61 | ``` 62 | 63 | Rexxar use url to identify the page in mobile application. With a valid url, we can create a RXRViewController. There is the map from url to HTML resources in the routes map api file. In the Demo, you can see a routes map api file like this: 64 | 65 | ```json 66 | { 67 | "items": [{ 68 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html", 69 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 70 | "uri": "douban://douban.com/rexxar_demo[/]?.*" 71 | }], 72 | "partial_items": [{ 73 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-252452ae58.html", 74 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 75 | "uri": "douban://partial.douban.com/rexxar_demo/_.*" 76 | }], 77 | "deploy_time": "Sun, 09 Oct 2016 07:43:47 GMT", 78 | } 79 | ``` 80 | 81 | ### Configure the resource path 82 | 83 | ```Swift 84 | RXRConfig.setRoutesResourcePath("rexxar") 85 | ``` 86 | 87 | We usually ship a copy of HTML resource files with the application's release package, in order to boost the initial load of the hybrid page. You can set the local path to the folder that contains the resource files with this API. Please be reminded that the folder type in Xcode must be set as `folder reference`, with a blue folder icon; instead of the `group` type with a yellow icon. 88 | 89 | ### Configure the cache path 90 | 91 | ```Swift 92 | RXRConfig.setRoutesCachePath("com.douban.RexxarDemo.rexxar") 93 | ``` 94 | 95 | Cache is also used to improve performance over routing. Rexxar can be set to check the routes map periodically and save the latest version in the cache. By deploying different path with the routes, it's possible to do a `hot deployment`, with which you can update the page just by replacing the resource file, and no need to go through the tedious process of asking a new release via App Store. 96 | 97 | This is the way to call to update routes map file: 98 | 99 | ```Swift 100 | RXRViewController.updateRouteFiles(completion: nil) 101 | ``` 102 | 103 | ### Use `RXRViewController` 104 | 105 | You can use `RXRViewController` directly as your hybrid container. Or you can inherit `RXRViewController` to implement your own Rexxar Container. In RexxarDemo, we implement `FullRXRViewController` inheriting from `RXRViewController`. 106 | 107 | To initialize a RXRViewController, you just need a valid url. This url should exist in the routes map file. Every url represents a page in app. Rexxar Container search the page's resource files via the url in the routes map file. 108 | 109 | ```Swift 110 | let controller = RXRViewController(URI: uri) 111 | let titleWidget = RXRNavTitleWidget() 112 | let alertDialogWidget = RXRAlertDialogWidget() 113 | controller.activities = [titleWidget, alertDialogWidget] 114 | navigationController?.pushViewController(controller, animated: true) 115 | ``` 116 | 117 | 118 | ## Customize Rexxar Container 119 | 120 | First, you inherit `RXRViewController` to implement your own Rexxar Container. 121 | 122 | Then, you use the three interfaces provided by Rexxar to make your customization easier. 123 | 124 | ### Create your own RXRWidget 125 | 126 | The `RXRWidget` protocol provides threes three methods: `canPerformWithURL:`, `prepareWithURL:`, `performWithContoller:`. Override these three methods to conform the `RXRWidget` protocol to implement a native UI, for example displaying a toast or adding pull to refresh UI widget etc. 127 | 128 | You can find an example `RXRNavTitleWidget` in Rexxar. 129 | 130 | ```Objective-C 131 | @interface RXRNavTitleWidget () 132 | 133 | @property (nonatomic, copy) NSString *title; 134 | 135 | @end 136 | 137 | 138 | @implementation RXRNavTitleWidget 139 | 140 | - (BOOL)canPerformWithURL:(NSURL *)URL 141 | { 142 | NSString *path = URL.path; 143 | if (path && [path isEqualToString:@"/widget/nav_title"]) { 144 | return true; 145 | } 146 | return false; 147 | } 148 | 149 | - (void)prepareWithURL:(NSURL *)URL 150 | { 151 | self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; 152 | } 153 | 154 | - (void)performWithController:(RXRViewController *)controller 155 | { 156 | if (controller) { 157 | controller.title = self.title; 158 | } 159 | } 160 | 161 | @end 162 | ``` 163 | 164 | ### Create your own RXRContainerAPI 165 | 166 | In order to offer information computed by native but consumed by web, for example, getting the device's GPS location information, you can create a class conforming to `RXRContainerAPI` protocol and implement the three methods: `shouldInterceptRequest:`, `responseWithRequest:`, `responseData`. 167 | 168 | You can find an example `RXRLocContainerAPI` in RexxarDemo. In this example, `RXRLocContainerAPI` returns the city information. To reduce Demo's complexity, we create it as a mock with false and always the same city information. You can implement your own loc information service on the base of this example. 169 | 170 | ### Create your own RXRDecorator 171 | 172 | If the modification of request from Rexxar Container is needed, for example, adding the authentication information in http header, you can inherit `RXRDecorator` and implementing two methods `shouldInterceptRequest:`, `prepareWithRequest:`. 173 | 174 | In RexxarDemo, you will find an example of usage of `RXRRequestDecorator` in `FullRXRViewController` to add the authentication information in http request. 175 | 176 | 177 | ## Partial RXRViewcontroller 178 | 179 | If a page cannot be fully implemented in HTML, you still have a choice to render the page partially with Rexxar. A partial RXRViewController allows you to write part of a page in HTML, and the reset of it in native code. 180 | 181 | Check out class `PartialRexxarViewController` in RexxarDemo for example. 182 | 183 | 184 | ## Rexxar's public interfaces 185 | 186 | * Rexxar Container 187 | - `RXRConfig` 188 | - `RXRViewController` 189 | 190 | * Widget 191 | - `RXRWidget` 192 | - `RXRNavTitleWidget` 193 | - `RXRAlertDialogWidget` 194 | - `RXRPullRefreshWidget` 195 | 196 | * ContainerAPI 197 | - `RXRNSURLProtocol` 198 | - `RXRContainerInterceptor` 199 | - `RXRContainerAPI` 200 | 201 | * Decorator 202 | - `RXRRequestInterceptor` 203 | - `RXRDecorator` 204 | - `RXRRequestDecorator` 205 | 206 | * Util 207 | - `NSURL+Rexxar` 208 | - `NSDictionary+RXRMultipleItem` 209 | 210 | 211 | ## Changelog 212 | 213 | - 0.3.0 Replace UIWebView with WKWebView. NSURLProtocol is not supported in WKWebView. The body of the POST request will be clean when intercepted by NSURLProtocol. So you need to take care of that. 214 | 215 | ## Unit Test 216 | 217 | Rexxar iOS includes a suite of unit tests within the RexxarTests subdirectory. 218 | 219 | 220 | ## License 221 | 222 | The MIT License. 223 | -------------------------------------------------------------------------------- /Rexxar.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "Rexxar" 4 | s.version = "0.3.0" 5 | s.license = { :type => 'MIT', :text => 'LICENSE' } 6 | 7 | s.summary = "Rexxar Hybrid Framework" 8 | s.description = "Rexxar is Douban Hybrid Framework. By Rexxar, You can develop UI interface with Web tech." 9 | s.homepage = "https://www.github.com/douban/rexxar-ios" 10 | s.author = { "iOS Dev" => "ios-dev@douban.com" } 11 | s.platform = :ios, "7.0" 12 | s.source = { :git => "https://github.com/douban/rexxar-ios.git", 13 | :tag => "#{s.version}" } 14 | s.requires_arc = true 15 | s.source_files = "Rexxar/**/*.{h,m}" 16 | s.public_header_files = 'Rexxar/Rexxar.h' 17 | 18 | s.framework = "UIKit" 19 | 20 | s.subspec 'Core' do |core| 21 | core.source_files = 'Rexxar/Core/**/*.{h,m}', 'Rexxar/ContainerAPI/**/*.{h,m}', 'Rexxar/Decorator/**/*.{h,m}', 'Rexxar/Proxy/**/*.{h,m}' 22 | core.frameworks = 'UIKit' 23 | core.requires_arc = true 24 | end 25 | 26 | s.subspec 'Widget' do |widget| 27 | widget.source_files = 'Rexxar/Widget/**/*.{h,m}' 28 | widget.requires_arc = true 29 | widget.xcconfig = {"GCC_PREPROCESSOR_DEFINITIONS" => 'DSK_WIDGET=1'} 30 | widget.dependency 'Rexxar/Core' 31 | end 32 | 33 | s.default_subspec = 'Widget' 34 | 35 | end 36 | -------------------------------------------------------------------------------- /Rexxar.xcodeproj/xcshareddata/xcschemes/Rexxar.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Rexxar.xcodeproj/xcshareddata/xcschemes/RexxarTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Rexxar/ContainerAPI/RXRContainerAPI.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRContainerAPI.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * `RXRContainerAPI` 是一个请求模拟器协议。请求模拟器代表了一个可用于模拟 http 请求的类的协议。 15 | * 符合该协议的类可以用于模拟 Rexxar-Container 内发出的 Http 请求。 16 | */ 17 | @protocol RXRContainerAPI 18 | 19 | /** 20 | * 判断是否应该截获该请求,对该请求做模拟操作。 21 | */ 22 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request; 23 | 24 | /** 25 | * 模拟请求的返回,返回 NSURLResponse 对象。 26 | */ 27 | - (NSURLResponse *)responseWithRequest:(NSURLRequest *)request; 28 | 29 | /** 30 | * 模拟请求返回的内容,返回二进制数据。 31 | */ 32 | - (nullable NSData *)responseData; 33 | 34 | @optional 35 | 36 | /** 37 | * 准备对请求的模拟。 38 | * 39 | * @param request 对应的请求 40 | */ 41 | - (void)prepareWithRequest:(NSURLRequest *)request; 42 | 43 | /** 44 | * 执行异步请求 45 | * 46 | * @param request 对应的请求 47 | * @param completion 完成回调 48 | */ 49 | - (void)performWithRequest:(NSURLRequest *)request completion:(void (^)(void))completion; 50 | 51 | @end 52 | 53 | NS_ASSUME_NONNULL_END 54 | 55 | -------------------------------------------------------------------------------- /Rexxar/ContainerAPI/RXRContainerInterceptor.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRContainerInterceptor.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | #import "RXRNSURLProtocol.h" 12 | 13 | @protocol RXRContainerAPI; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | /** 18 | * `RXRContainerInterceptor` 是一个 Rexxar-Container 的请求侦听器。 19 | * 这个侦听器用于模拟网络请求。这些网络请求并不会发送出去,而是由 Native 处理。 20 | * 比如向 Web 提供当前位置信息。 21 | * 22 | */ 23 | @interface RXRContainerInterceptor : RXRNSURLProtocol 24 | 25 | /** 26 | 这个侦听器所有的请求模仿器数组,该数组成员是符合 `RXRContainerAPI` 协议的对象,即一组请求模仿器。 27 | */ 28 | @property (class, nonatomic, copy, nullable) NSArray> *containerAPIs; 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /Rexxar/ContainerAPI/RXRContainerInterceptor.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRContainerInterceptor.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRContainerInterceptor.h" 10 | #import "NSHTTPURLResponse+Rexxar.h" 11 | #import "RXRContainerAPI.h" 12 | 13 | static NSArray> *_containerAPIs; 14 | 15 | @implementation RXRContainerInterceptor 16 | 17 | + (void)setContainerAPIs:(NSArray> *)containerAPIs 18 | { 19 | _containerAPIs = [containerAPIs copy]; 20 | } 21 | 22 | + (NSArray> *)containerAPIs 23 | { 24 | return _containerAPIs; 25 | } 26 | 27 | #pragma mark - Implement NSURLProtocol methods 28 | 29 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 30 | { 31 | // 请求不是来自浏览器,不处理 32 | if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { 33 | return NO; 34 | } 35 | 36 | for (id containerAPI in _containerAPIs) { 37 | if ([containerAPI shouldInterceptRequest:request]) { 38 | return YES; 39 | } 40 | } 41 | 42 | return NO; 43 | } 44 | 45 | - (void)startLoading 46 | { 47 | [self beforeStartLoadingRequest]; 48 | 49 | for (id containerAPI in _containerAPIs) { 50 | if ([containerAPI shouldInterceptRequest:self.request]) { 51 | 52 | if ([containerAPI respondsToSelector:@selector(prepareWithRequest:)]) { 53 | [containerAPI prepareWithRequest:self.request]; 54 | } 55 | 56 | __weak __typeof(self) weakSelf = self; 57 | void (^completion)(void) = ^() { 58 | NSData *data = [containerAPI responseData]; 59 | NSURLResponse *response = [containerAPI responseWithRequest:weakSelf.request]; 60 | [weakSelf.client URLProtocol:weakSelf didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 61 | [weakSelf.client URLProtocol:weakSelf didLoadData:data]; 62 | [weakSelf.client URLProtocolDidFinishLoading:weakSelf]; 63 | }; 64 | 65 | if ([containerAPI respondsToSelector:@selector(performWithRequest:completion:)]) { 66 | [containerAPI performWithRequest:self.request completion:completion]; 67 | } else { 68 | completion(); 69 | } 70 | break; 71 | } 72 | } 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSData+RXRDigest.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+RXRDigest.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 11/10/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSData (RXRDigest) 12 | 13 | - (NSString *)md5; 14 | - (NSString *)sha1; 15 | - (NSString *)sha256; 16 | - (NSString *)sha512; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSData+RXRDigest.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSData+DOUDigest.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 10/04/13. 6 | // Copyright (c) 2013 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "NSData+RXRDigest.h" 10 | #include 11 | 12 | #define DOU_DIGEST_PERFORM(_LENGTH, _FUNCTION) \ 13 | NSMutableString *result; \ 14 | do { \ 15 | size_t i; \ 16 | unsigned char md[(_LENGTH)]; \ 17 | \ 18 | bzero(md, sizeof(md)); \ 19 | (_FUNCTION)([self bytes], (CC_LONG)[self length], md); \ 20 | \ 21 | result = [NSMutableString stringWithCapacity:(_LENGTH) * 2]; \ 22 | for (i = 0; i < (_LENGTH); ++i) { \ 23 | [result appendFormat:@"%02x", md[i]]; \ 24 | } \ 25 | } while (0); \ 26 | return [result copy] 27 | 28 | @implementation NSData (RXRDigest) 29 | 30 | - (NSString *)md5 31 | { 32 | DOU_DIGEST_PERFORM(CC_MD5_DIGEST_LENGTH, CC_MD5); 33 | } 34 | 35 | - (NSString *)sha1 36 | { 37 | DOU_DIGEST_PERFORM(CC_SHA1_DIGEST_LENGTH, CC_SHA1); 38 | } 39 | 40 | - (NSString *)sha256 41 | { 42 | DOU_DIGEST_PERFORM(CC_SHA256_DIGEST_LENGTH, CC_SHA256); 43 | } 44 | 45 | - (NSString *)sha512 46 | { 47 | DOU_DIGEST_PERFORM(CC_SHA512_DIGEST_LENGTH, CC_SHA512); 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+RXRMultipleItems.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSDictionary (RXRMultipleItems) 12 | 13 | /** 14 | * 字典对应关键字的元素,该元素如果是数组,返回数组的首个元素。 15 | * 16 | * Return the first item of array for the specificed key. 17 | * -[NSDictionary objectForKey:] will return an object or an array depending on how the NSDictionary is created. 18 | * 19 | * @param key 关键字 20 | */ 21 | - (id)rxr_itemForKey:(id)key; 22 | 23 | /** 24 | * 字典对应该关键字的元素,该元素如果是数组,返回该数组。 25 | * 26 | * Return a NSArray object which contains all the items for specificed key. 27 | * -[NSDictionary objectForKey:] will return an object or an array depending on how the NSDictionary is created. 28 | * 29 | * @param key 关键字 30 | */ 31 | - (NSArray *)rxr_allItemsForKey:(id)key; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSDictionary+RXRMultipleItems.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+RXRMultipleItems.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "NSDictionary+RXRMultipleItems.h" 10 | 11 | @implementation NSDictionary (RXRMultipleItems) 12 | 13 | - (id)rxr_itemForKey:(id)key { 14 | id obj = [self objectForKey:key]; 15 | if ([obj isKindOfClass:[NSArray class]]) { 16 | return [obj count] > 0 ? [obj objectAtIndex:0] : nil; 17 | } else { 18 | return obj; 19 | } 20 | } 21 | 22 | - (NSArray *)rxr_allItemsForKey:(id)key { 23 | id obj = [self objectForKey:key]; 24 | return [obj isKindOfClass:[NSArray class]] ? obj : (obj ? [NSArray arrayWithObject:obj] : nil); 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSHTTPURLResponse+Rexxar.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponse+Rexxar.h 3 | // Rexxar 4 | // 5 | // Created by XueMing on 03/03/2017. 6 | // Copyright © 2017 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface NSHTTPURLResponse (Rexxar) 14 | 15 | /** 16 | Returns a new http response. If `noAccessControl` = YES, set CORS disabled. 17 | */ 18 | + (nullable instancetype)rxr_responseWithURL:(NSURL *)url 19 | statusCode:(NSInteger)statusCode 20 | headerFields:(nullable NSDictionary *)headerFields 21 | noAccessControl:(BOOL)noAccessControl; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSHTTPURLResponse+Rexxar.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponse+Rexxar.m 3 | // Rexxar 4 | // 5 | // Created by XueMing on 03/03/2017. 6 | // Copyright © 2017 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "NSHTTPURLResponse+Rexxar.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @implementation NSHTTPURLResponse (Rexxar) 14 | 15 | + (nullable instancetype)rxr_responseWithURL:(NSURL *)url 16 | statusCode:(NSInteger)statusCode 17 | headerFields:(nullable NSDictionary *)headerFields 18 | noAccessControl:(BOOL)noAccessControl 19 | { 20 | if (!noAccessControl) { 21 | return [[NSHTTPURLResponse alloc] initWithURL:url statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; 22 | } 23 | 24 | NSMutableDictionary *mutableHeaderFields = [NSMutableDictionary dictionary]; 25 | [mutableHeaderFields setValue:@"*" forKey:@"Access-Control-Allow-Origin"]; 26 | [mutableHeaderFields setValue:@"Origin, X-Requested-With, Content-Type" forKey:@"Access-Control-Allow-Headers"]; 27 | 28 | if (headerFields != nil && [headerFields count] > 0) { 29 | [mutableHeaderFields addEntriesFromDictionary:headerFields]; 30 | } 31 | 32 | return [[NSHTTPURLResponse alloc] initWithURL:url statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:mutableHeaderFields]; 33 | } 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | 39 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableDictionary+RXRMultipleItems.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSMutableDictionary (RXRMultipleItems) 12 | 13 | /** 14 | * 在字典以关键字添加一个元素。 15 | * 16 | * @param item 待添加的元素 17 | * @param key 关键字 18 | */ 19 | - (void)rxr_addItem:(id)item forKey:(id)key; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSMutableDictionary+RXRMultipleItems.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableDictionary+RXRMultipleItems.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "NSMutableDictionary+RXRMultipleItems.h" 10 | 11 | @implementation NSMutableDictionary (RXRMultipleItems) 12 | 13 | - (void)rxr_addItem:(id)item forKey:(id)key { 14 | if (item == nil) { 15 | return; 16 | } 17 | id obj = [self objectForKey:key]; 18 | NSMutableArray *array = nil; 19 | if ([obj isKindOfClass:[NSArray class]]) { 20 | array = [NSMutableArray arrayWithArray:obj]; 21 | } else { 22 | array = obj ? [NSMutableArray arrayWithObject:obj] : [NSMutableArray array]; 23 | } 24 | [array addObject:item]; 25 | [self setObject:[array copy] forKey:key]; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSString+RXRURLEscape.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RXRURLEscape.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSString (RXRURLEscape) 12 | 13 | /** 14 | * url 字符串编码 15 | */ 16 | - (NSString *)rxr_encodingStringUsingURLEscape; 17 | 18 | /** 19 | * url 字符串解码 20 | */ 21 | - (NSString *)rxr_decodingStringUsingURLEscape; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSString+RXRURLEscape.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RXRURLEscape.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "NSString+RXRURLEscape.h" 10 | 11 | @implementation NSString (RXRURLEscape) 12 | 13 | - (NSString *)rxr_encodingStringUsingURLEscape 14 | { 15 | CFStringRef originStringRef = (__bridge_retained CFStringRef)self; 16 | CFStringRef escapedStringRef = CFURLCreateStringByAddingPercentEscapes(NULL, 17 | originStringRef, 18 | NULL, 19 | (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", 20 | kCFStringEncodingUTF8); 21 | NSString *escapedString = (__bridge_transfer NSString *)escapedStringRef; 22 | CFRelease(originStringRef); 23 | return escapedString; 24 | } 25 | 26 | - (NSString *)rxr_decodingStringUsingURLEscape 27 | { 28 | CFStringRef originStringRef = (__bridge_retained CFStringRef)self; 29 | CFStringRef escapedStringRef = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, 30 | originStringRef, 31 | CFSTR(""), 32 | kCFStringEncodingUTF8); 33 | NSString *escapedString = (__bridge_transfer NSString *)escapedStringRef; 34 | CFRelease(originStringRef); 35 | return escapedString; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSURL+Rexxar.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+Rexxar.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 1/18/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @interface NSURL (Rexxar) 12 | 13 | /** 14 | * 将一个字典内容转换成 url 的 query 的形式。 15 | * 16 | * @param dict 需要转换成的 query 的 dictionary。 17 | */ 18 | + (NSString *)rxr_queryFromDictionary:(NSDictionary *)dict; 19 | 20 | /** 21 | * 该 url 的 scheme 是否是 http 或 https? 22 | */ 23 | - (BOOL)rxr_isHttpOrHttps; 24 | 25 | /** 26 | * 将该 url 的 query 以字典形式返回。 27 | */ 28 | - (NSDictionary *)rxr_queryDictionary; 29 | 30 | /** 31 | * 该 url 的 scheme 是否是 rexxar-http 或 rexxar-https 32 | */ 33 | - (BOOL)rxr_isRexxarHttpScheme; 34 | 35 | /** 36 | * 将该 url 的 scheme 从 rexxar-http, rexxar-https 替换为 http, https。 37 | */ 38 | - (NSURL *)rxr_urlByReplacingRexxarSchemeWithHttp; 39 | 40 | /** 41 | * 将该 url 的 scheme 从 http, https 替换为 rexxar-http, rexxar-https。 42 | */ 43 | - (NSURL *)rxr_urlByReplacingHttpWithRexxarScheme; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/NSURL+Rexxar.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURL+Rexxar.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 1/18/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "NSURL+Rexxar.h" 10 | #import "NSString+RXRURLEscape.h" 11 | #import "NSMutableDictionary+RXRMultipleItems.h" 12 | 13 | @implementation NSURL (Rexxar) 14 | 15 | + (NSString *)rxr_queryFromDictionary:(NSDictionary *)dict 16 | { 17 | NSMutableArray *pairs = [NSMutableArray array]; 18 | [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { 19 | [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, obj]]; 20 | }]; 21 | 22 | NSString *query = nil; 23 | if (pairs.count > 0) { 24 | query = [pairs componentsJoinedByString:@"&"]; 25 | } 26 | return query; 27 | } 28 | 29 | - (BOOL)rxr_isHttpOrHttps 30 | { 31 | if ([self.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || 32 | [self.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { 33 | return YES; 34 | } 35 | return NO; 36 | } 37 | 38 | - (NSDictionary *)rxr_queryDictionary { 39 | NSArray *queryItems = [NSURLComponents componentsWithURL:self resolvingAgainstBaseURL:YES].queryItems; 40 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 41 | for (NSURLQueryItem *item in queryItems) { 42 | if (item.name && item.value) { 43 | [dict rxr_addItem:item.value forKey:item.name]; 44 | } 45 | } 46 | 47 | return dict; 48 | } 49 | 50 | - (BOOL)rxr_isRexxarHttpScheme 51 | { 52 | if ([self.scheme caseInsensitiveCompare:@"rexxar-http"] == NSOrderedSame || 53 | [self.scheme caseInsensitiveCompare:@"rexxar-https"] == NSOrderedSame) { 54 | return YES; 55 | } 56 | return NO; 57 | } 58 | 59 | - (NSURL *)rxr_urlByReplacingRexxarSchemeWithHttp 60 | { 61 | if ([self rxr_isRexxarHttpScheme]) { 62 | NSURLComponents *comp = [NSURLComponents componentsWithURL:self resolvingAgainstBaseURL:YES]; 63 | comp.scheme = [comp.scheme stringByReplacingOccurrencesOfString:@"rexxar-" withString:@""]; 64 | return comp.URL; 65 | } 66 | return self; 67 | } 68 | 69 | - (NSURL *)rxr_urlByReplacingHttpWithRexxarScheme 70 | { 71 | if ([self rxr_isHttpOrHttps]) { 72 | NSURLComponents *comp = [NSURLComponents componentsWithURL:self resolvingAgainstBaseURL:YES]; 73 | if ([comp.scheme isEqualToString:@"http"]) { 74 | comp.scheme = @"rexxar-http"; 75 | } else if ([comp.scheme isEqualToString:@"https"]) { 76 | comp.scheme = @"rexxar-https"; 77 | } 78 | return comp.URL; 79 | } 80 | return self; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/UIColor+Rexxar.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Rexxar.h 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 12/9/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | @interface UIColor (Rexxar) 12 | 13 | /** 14 | * 字符串形式创建的 UIColor。 15 | * 16 | * @param colorComponents 颜色的字符串,颜色格式:rgba(0,0,0,0)。 17 | */ 18 | + (instancetype)rxr_colorWithComponent:(NSString *)colorComponents; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Rexxar/Core/Extension/UIColor+Rexxar.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Rexxar.m 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 12/9/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "UIColor+Rexxar.h" 10 | 11 | @implementation UIColor (Rexxar) 12 | 13 | + (instancetype)rxr_colorWithComponent:(NSString *)colorComponents 14 | { 15 | UIColor *color = nil; 16 | 17 | NSScanner *scanner = [NSScanner scannerWithString:colorComponents]; 18 | scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet]; 19 | 20 | NSString *colorType = nil; 21 | if ([scanner scanUpToString:@"(" intoString:&colorType] && colorType // 解析颜色值类型 22 | && scanner.scanLocation < (scanner.string.length - 1) && ++scanner.scanLocation && !scanner.atEnd // 跳过类型后的 `(` 23 | ) { 24 | NSUInteger length = colorType.length; 25 | if (length <= 4) { 26 | // RGB / HSL 三部分 + alpha 27 | NSInteger components[4] = {-1, -1, -1, 255}; 28 | for (NSUInteger index = 0; index < length; ++index) { 29 | if (index > 0) { 30 | [scanner scanString:@"," intoString:nil]; 31 | } 32 | [scanner scanInteger:&components[index]]; 33 | } 34 | 35 | if (components[0] >= 0 && components[1] >= 0 && components[2] >= 0 && components[3] >= 0 36 | && [colorType hasPrefix:@"rgb"]) { 37 | color = [UIColor colorWithRed:(components[0] / 255.f) 38 | green:(components[1] / 255.f) 39 | blue:(components[2] / 255.f) 40 | alpha:(components[3] / 255.f)]; 41 | } 42 | } 43 | } 44 | 45 | if (color == nil) { 46 | NSLog(@"Unknown color: %@", colorComponents); 47 | } 48 | 49 | return color; 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRCacheFileInterceptor.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRCacheFileInterceptor.h 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/4/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRNSURLProtocol.h" 10 | 11 | /** 12 | * `RXRCacheFileIntercepter` 用于拦截进入 Rexxar Container 的请求,并可对请求做所需的变化。 13 | * 目前完成: 1 本地文件映射,如请求服务器上的 html, css, js 资源,先检查本地,如存在则使用本地 css, js 文件(包括本地缓存,和应用内置资源)显示。 14 | */ 15 | @interface RXRCacheFileInterceptor : RXRNSURLProtocol 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRCacheFileInterceptor.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRCacheFileInterceptor.m 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/4/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRCacheFileInterceptor.h" 10 | #import "NSHTTPURLResponse+Rexxar.h" 11 | #import "RXRURLSessionDemux.h" 12 | #import "RXRRouteFileCache.h" 13 | #import "RXRLogger.h" 14 | #import "NSURL+Rexxar.h" 15 | #import "RXRConfig.h" 16 | 17 | @interface RXRCacheFileInterceptor () 18 | 19 | @property (nonatomic, strong) NSFileHandle *fileHandle; 20 | @property (nonatomic, copy) NSString *responseDataFilePath; 21 | 22 | @end 23 | 24 | 25 | @implementation RXRCacheFileInterceptor 26 | 27 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 28 | { 29 | // 不是 HTTP 请求,不处理 30 | if (![request.URL rxr_isHttpOrHttps]) { 31 | return NO; 32 | } 33 | // 请求被忽略(被标记为忽略或者已经请求过),不处理 34 | if ([self isRequestIgnored:request]) { 35 | return NO; 36 | } 37 | // 请求不是来自浏览器,不处理 38 | if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { 39 | return NO; 40 | } 41 | 42 | // 如果请求不需要被拦截,不处理 43 | if (![self shouldInterceptRequest:request] || ![RXRConfig isCacheEnable]) { 44 | return NO; 45 | } 46 | 47 | return YES; 48 | } 49 | 50 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 51 | { 52 | return request; 53 | } 54 | 55 | - (void)startLoading 56 | { 57 | NSParameterAssert(self.dataTask == nil); 58 | RXRDebugLog(@"Intercept <%@> within <%@>", self.request.URL, self.request.mainDocumentURL); 59 | 60 | [self beforeStartLoadingRequest]; 61 | 62 | NSURL *localURL = [[self class] _rxr_localFileURL:self.request.URL]; 63 | if (localURL) { 64 | NSData *data = [NSData dataWithContentsOfURL:localURL]; 65 | if ([data length] > 0) { 66 | NSHTTPURLResponse *response = [NSHTTPURLResponse rxr_responseWithURL:self.request.URL 67 | statusCode:200 68 | headerFields:nil 69 | noAccessControl:YES]; 70 | [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 71 | [self.client URLProtocol:self didLoadData:data]; 72 | [self.client URLProtocolDidFinishLoading:self]; 73 | 74 | return; 75 | } 76 | } 77 | 78 | NSMutableURLRequest *newRequest = nil; 79 | if ([self.request isKindOfClass:[NSMutableURLRequest class]]) { 80 | newRequest = (NSMutableURLRequest *)self.request; 81 | } else { 82 | newRequest = [self.request mutableCopy]; 83 | } 84 | 85 | [[self class] markRequestAsIgnored:newRequest]; 86 | 87 | NSMutableArray *modes = [NSMutableArray array]; 88 | [modes addObject:NSDefaultRunLoopMode]; 89 | 90 | NSString *currentMode = [[NSRunLoop currentRunLoop] currentMode]; 91 | if (currentMode != nil && ![currentMode isEqualToString:NSDefaultRunLoopMode]) { 92 | [modes addObject:currentMode]; 93 | } 94 | [self setModes:modes]; 95 | 96 | NSURLSessionTask *dataTask = [[[self class] sharedDemux] dataTaskWithRequest:newRequest delegate:self modes:self.modes]; 97 | [dataTask resume]; 98 | [self setDataTask:dataTask]; 99 | } 100 | 101 | #pragma mark - NSURLSessionDataDelegate 102 | 103 | - (void)URLSession:(NSURLSession *)session 104 | task:(NSURLSessionTask *)task 105 | willPerformHTTPRedirection:(nonnull NSHTTPURLResponse *)response 106 | newRequest:(nonnull NSURLRequest *)request 107 | completionHandler:(nonnull void (^)(NSURLRequest * _Nullable))completionHandler 108 | { 109 | if (self.client != nil && self.dataTask == task) { 110 | NSMutableURLRequest *mutableRequest = [request mutableCopy]; 111 | [[self class] unmarkRequestAsIgnored:mutableRequest]; 112 | [self.client URLProtocol:self wasRedirectedToRequest:mutableRequest redirectResponse:response]; 113 | 114 | NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil]; 115 | [self.dataTask cancel]; 116 | [self.client URLProtocol:self didFailWithError:error]; 117 | } 118 | } 119 | 120 | - (void)URLSession:(NSURLSession *)session 121 | dataTask:(NSURLSessionDataTask *)dataTask 122 | didReceiveResponse:(NSURLResponse *)response 123 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler 124 | { 125 | NSURLRequest *request = dataTask.currentRequest; 126 | 127 | if (![request.URL isFileURL] && 128 | [[self class] shouldInterceptRequest:request] && 129 | [[self class] _rxr_isCacheableResponse:response]) { 130 | self.responseDataFilePath = [self _rxr_temporaryFilePath]; 131 | [[NSFileManager defaultManager] createFileAtPath:self.responseDataFilePath contents:nil attributes:nil]; 132 | self.fileHandle = nil; 133 | self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.responseDataFilePath]; 134 | } 135 | 136 | NSHTTPURLResponse *URLResponse = nil; 137 | if ([response isKindOfClass:[NSHTTPURLResponse class]]) { 138 | URLResponse = (NSHTTPURLResponse *)response; 139 | URLResponse = [NSHTTPURLResponse rxr_responseWithURL:URLResponse.URL 140 | statusCode:URLResponse.statusCode 141 | headerFields:URLResponse.allHeaderFields 142 | noAccessControl:YES]; 143 | } 144 | [self.client URLProtocol:self 145 | didReceiveResponse:URLResponse ?: response 146 | cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 147 | completionHandler(NSURLSessionResponseAllow); 148 | } 149 | 150 | - (void)URLSession:(NSURLSession *)session 151 | dataTask:(NSURLSessionDataTask *)dataTask 152 | didReceiveData:(NSData *)data 153 | { 154 | if ([[self class] shouldInterceptRequest:dataTask.currentRequest] && self.fileHandle) { 155 | [self.fileHandle writeData:data]; 156 | } 157 | [self.client URLProtocol:self didLoadData:data]; 158 | } 159 | 160 | - (void)URLSession:(NSURLSession *)session 161 | task:(NSURLSessionTask *)task 162 | didCompleteWithError:(nullable NSError *)error 163 | { 164 | if (self.client != nil && (self.dataTask == nil || self.dataTask == task)) { 165 | if (error == nil) { 166 | if ([[self class] shouldInterceptRequest:task.currentRequest] && self.fileHandle) { 167 | [self.fileHandle closeFile]; 168 | self.fileHandle = nil; 169 | 170 | NSInteger statusCode = 200; 171 | if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) { 172 | statusCode = ((NSHTTPURLResponse *)task.response).statusCode; 173 | } 174 | if (statusCode >= 400) { 175 | [[NSFileManager defaultManager] removeItemAtPath:self.responseDataFilePath error:nil]; 176 | } else { 177 | NSData *data = [NSData dataWithContentsOfFile:self.responseDataFilePath]; 178 | NSURL *cacheURL = [[self class] _rxr_cacheURL:task.currentRequest.URL]; 179 | [[RXRRouteFileCache sharedInstance] saveRouteFileData:data withRemoteURL:cacheURL]; 180 | RXRDebugLog(@"Download resource %@", cacheURL); 181 | } 182 | } 183 | [self.client URLProtocolDidFinishLoading:self]; 184 | } else { 185 | if ([[self class] shouldInterceptRequest:task.currentRequest] && self.fileHandle) { 186 | [self.fileHandle closeFile]; 187 | self.fileHandle = nil; 188 | [[NSFileManager defaultManager] removeItemAtPath:self.responseDataFilePath error:nil]; 189 | } 190 | 191 | if ([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { 192 | // Do nothing. 193 | } else { 194 | [self.client URLProtocol:self didFailWithError:error]; 195 | } 196 | } 197 | 198 | [self afterStopLoadingRequest]; 199 | } 200 | } 201 | 202 | #pragma mark - Public methods 203 | 204 | + (BOOL)shouldInterceptRequest:(NSURLRequest *)request 205 | { 206 | NSString *extension = request.URL.pathExtension; 207 | if ([extension isEqualToString:@"js"] || 208 | [extension isEqualToString:@"css"] || 209 | [extension isEqualToString:@"html"]) { 210 | return YES; 211 | } 212 | return NO; 213 | } 214 | 215 | #pragma mark - Private methods 216 | 217 | + (NSURL *)_rxr_cacheURL:(NSURL *)remoteURL 218 | { 219 | return [[NSURL alloc] initWithScheme:[remoteURL scheme] host:[remoteURL host] path:[remoteURL path]]; 220 | } 221 | 222 | + (NSURL *)_rxr_localFileURL:(NSURL *)remoteURL 223 | { 224 | NSURL *cacheURL = [self _rxr_cacheURL:remoteURL]; 225 | NSURL *localURL = [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:cacheURL]; 226 | return localURL; 227 | } 228 | 229 | + (BOOL)_rxr_isCacheableResponse:(NSURLResponse *)response 230 | { 231 | NSSet *cacheableTypes = [NSSet setWithObjects:@"application/javascript", 232 | @"application/x-javascript", 233 | @"text/javascript", 234 | @"text/css", 235 | @"text/html", nil]; 236 | return [cacheableTypes containsObject:response.MIMEType]; 237 | } 238 | 239 | - (NSString *)_rxr_temporaryFilePath 240 | { 241 | NSString *fileName = [[NSUUID UUID] UUIDString]; 242 | return [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; 243 | } 244 | 245 | @end 246 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRConfig+Rexxar.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRConfig+Rexxar.h 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 21/12/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRConfig.h" 10 | #import "RXRLogger.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface RXRConfig (Rexxar) 15 | 16 | + (BOOL)rxr_canLog; 17 | + (void)rxr_logWithLogObject:(nullable RXRLogObject *)object; 18 | + (void)rxr_logWithType:(RXRLogType)type 19 | error:(nullable NSError *)error 20 | requestURL:(nullable NSURL *)url 21 | localFilePath:(nullable NSString *)localFilePath 22 | userInfo:(nullable NSDictionary *)userInfo; 23 | 24 | + (BOOL)rxr_canHandleError; 25 | + (void)rxr_handleError:(nullable NSError *)error fromReporter:(nullable id)reporter; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRConfig+Rexxar.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRConfig+Rexxar.m 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 21/12/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRConfig+Rexxar.h" 10 | #import "RXRLogger.h" 11 | #import "RXRErrorHandler.h" 12 | #import "RXRRouteManager.h" 13 | 14 | @implementation RXRConfig (Rexxar) 15 | 16 | + (BOOL)rxr_canLog 17 | { 18 | return self.logger && [self.logger respondsToSelector:@selector(rexxarDidLogWithLogObject:)]; 19 | } 20 | 21 | + (void)rxr_logWithLogObject:(RXRLogObject *)object 22 | { 23 | if ([self rxr_canLog] && object) { 24 | [self.logger rexxarDidLogWithLogObject:object]; 25 | } 26 | } 27 | 28 | + (void)rxr_logWithType:(RXRLogType)type 29 | error:(NSError *)error 30 | requestURL:(NSURL *)url 31 | localFilePath:(NSString *)localFilePath 32 | userInfo:(nullable NSDictionary *)userInfo 33 | { 34 | if (![self rxr_canLog]) { 35 | return; 36 | } 37 | 38 | NSMutableDictionary *info = [NSMutableDictionary dictionary]; 39 | [info setValue:[RXRRouteManager sharedInstance].routesVersion forKey:logOtherInfoRoutesVersionKey]; 40 | if (userInfo != nil) { 41 | [info addEntriesFromDictionary:userInfo]; 42 | } 43 | RXRLogObject *obj = [[RXRLogObject alloc] initWithLogType:type error:error requestURL:url localFilePath:localFilePath otherInformation:info]; 44 | [RXRConfig rxr_logWithLogObject:obj]; 45 | } 46 | 47 | + (BOOL)rxr_canHandleError 48 | { 49 | return self.errorHandler && [self.errorHandler respondsToSelector:@selector(handleError:fromReporter:)]; 50 | } 51 | 52 | + (void)rxr_handleError:(NSError *)error fromReporter:(id)reporter 53 | { 54 | if ([self rxr_canHandleError] && error) { 55 | [self.errorHandler handleError:error fromReporter:reporter]; 56 | } 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRConfig.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/30/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @protocol RXRDataValidator; 12 | @protocol RXRLogger; 13 | @protocol RXRErrorHandler; 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | typedef void(^RXRDidCompleteRequestBlock)(NSURL *_Nonnull url, NSURLResponse *_Nullable response, NSError *_Nullable error, NSTimeInterval timeElapsed); 18 | 19 | /** 20 | * 设置静态文件缓存Key 21 | * 当 URL 包含与缓存无关的参数时,可以清理掉,保证缓存结果能够通用。 22 | */ 23 | @protocol RXRURLCacheConverter 24 | 25 | - (NSString *)cacheKeyForURL:(NSURL *)URL; 26 | 27 | @end 28 | 29 | 30 | /** 31 | * `RXRConfig` 提供对 Rexxar 的全局配置接口。 32 | */ 33 | @interface RXRConfig : NSObject 34 | 35 | /** 36 | 设置 `RXRLogger`,调用者需要实现 `rexxarDidLogWithLogObject:` 方法。 37 | */ 38 | @property (nullable, class, nonatomic, weak) id logger; 39 | 40 | @property (nullable, class, nonatomic, weak) id URLCacheConverter; 41 | 42 | /** 43 | 设置 `RXRErrorHandler`,调用者需要实现 `reporter:didReceiveError:` 方法。 44 | */ 45 | @property (nullable, class, nonatomic, weak) id errorHandler; 46 | 47 | /** 48 | 设置当遇到远程 html 文件找不到(http:// 地址对应的文件) 时重新加载 webview 的次数,默认为2次。 49 | 50 | - Note: 每一次 reload 会调用 `updateRoutesWithCompletion:` 方法更新路由及本地文件。 51 | */ 52 | @property (class, nonatomic) NSInteger reloadLimitWhen404; 53 | 54 | /** 55 | RXRRequestInterceptor处理请求完成时的回调 56 | */ 57 | @property (class, nonatomic, copy, nullable) RXRDidCompleteRequestBlock didCompleteRequestBlock; 58 | 59 | /** 60 | 使用WKURLSchemeHandler拦截网络请求,默认NO,iOS 13.4以上版本可以设置为YES 61 | */ 62 | @property (class, nonatomic) BOOL useCustomScheme; 63 | 64 | /** 65 | 自定义的webView Class类型,需为WKWebView的子类 66 | */ 67 | @property (nullable, class, nonatomic) Class customWebViewClass; 68 | 69 | /** 70 | * 设置 rxrProtocolScheme。 71 | * 72 | * @discussion Rexxar-Container 实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 73 | * `rxrProtocolHost` 是对这些特定请求的 scheme 的商定。如不设置,缺省为 douban。 74 | */ 75 | + (void)setRXRProtocolScheme:(NSString *)scheme; 76 | 77 | /** 78 | * 读取 rxrProtocolScheme。 79 | * 80 | * @discussion Rexxar-Container 实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 81 | * `rxrProtocolHost` 是对这些特定请求的 scheme 的商定。如不设置,缺省为 douban。 82 | */ 83 | + (NSString *)rxrProtocolScheme; 84 | 85 | /** 86 | * 设置 rxrProtocolHost。 87 | * 88 | * @discussion Rexxar-Container 实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 89 | * `rxrProtocolHost` 是对这些特定请求的 host 的商定。如不设置,缺省为 rexxar-container。 90 | */ 91 | + (void)setRXRProtocolHost:(NSString *)host; 92 | 93 | /** 94 | * 读取 rxrProtocolHost。 95 | * 96 | * @discussion Rexxar-Container 实现了一些供 Web 调用的功能。Web 调用这些功能的方式是发出一个特定的请求。 97 | * `rxrProtocolHost` 是对这些特定请求的 host 的商定。如不设置,缺省为 rexxar-container。 98 | */ 99 | + (NSString *)rxrProtocolHost; 100 | 101 | /** 102 | * 设置 Routes Map URL。 103 | */ 104 | + (void)setRoutesMapURL:(NSURL *)routesMapURL; 105 | 106 | /** 107 | * 读取 Routes Map URL。 108 | */ 109 | + (nullable NSURL *)routesMapURL; 110 | 111 | /** 112 | * 设置 Route Files 的 Cache URL。 113 | */ 114 | + (void)setRoutesCachePath:(nullable NSString *)routesCachePath; 115 | 116 | /** 117 | * 读取 Route Files 的 Cache URL。 118 | */ 119 | + (nullable NSString *)routesCachePath; 120 | 121 | /** 122 | * 设置 Route Files 的 Resource Path。 123 | */ 124 | + (void)setRoutesResourcePath:(nullable NSString *)routesResourcePath; 125 | 126 | /** 127 | * 读取 Route Files 的 Resource Path。 128 | */ 129 | + (nullable NSString *)routesResourcePath; 130 | 131 | /** 132 | * 设置 Rexxar 接收的外部 User-Agent。Rexxar 会将这个 UserAgent 加到其所发出的所有的请求的 Headers 中。 133 | */ 134 | + (void)setUserAgent:(NSString *)userAgent; 135 | 136 | /** 137 | * 读取 Rexxar 接收的外部 User-Agent。 138 | */ 139 | + (NSString *)userAgent; 140 | 141 | /** 142 | * 设置请求参数列表,Rexxar 会将这些参数加到其发出的所有请求的 url query 中。 143 | */ 144 | + (void)setExtraRequestParams:(NSArray *)params; 145 | 146 | /** 147 | * 读取请求参数列表。 148 | */ 149 | + (NSArray *)extraRequestParams; 150 | 151 | /** 152 | * 更新全局配置。 153 | */ 154 | + (void)updateConfig; 155 | 156 | /** 157 | * 全局设置 Rexxar Container 是否使用路由文件的本地 Cache。 158 | * 如果使用,优先读取本地缓存的 html 文件;如果不使用,则每次都读取服务器的 html 文件。 159 | */ 160 | + (void)setCacheEnable:(BOOL)isCacheEnable; 161 | 162 | /** 163 | * 读取 Rexxar Container 是否使用缓存的全局配置。该缺省是打开的。Rexxar Container 会使用缓存保存 html 文件。 164 | */ 165 | + (BOOL)isCacheEnable; 166 | 167 | /** 168 | * 设置 RXRRouteManager 是否忽略 routes 版本。缺省 NO。 169 | * 缺省设置下,RXRRouteManager 会根据拉取的 routes 版本跟本地已拉取的 routes 版本比较,只有版本更大的情况下才会更新 routes 文件。 170 | * 如果设置为 YES,RXRRouteManager 检查 routes 文件更新时,将不比较 version 版本大小,立刻更新到新获取到的 routes。 171 | * 建议在本地开发中设置为 YES,线上版本为缺省设置 NO。 172 | */ 173 | + (void)setNeedsIgnoreRoutesVersion:(BOOL)needsIgnoreRoutesVersion; 174 | 175 | + (BOOL)needsIgnoreRoutesVersion; 176 | 177 | /** 178 | 设置 `RXRDataValidator`。设置后,将会在下载 HTML file 时验证文件合法性(可用来做完整性检验)。 179 | */ 180 | + (void)setHTMLFileDataValidator:(id)dataValidator; 181 | 182 | /** 183 | 设置 Rexxar 所有请求的 URLSessionConfiguration 184 | */ 185 | + (void)setRequestsURLSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration; 186 | 187 | /** 188 | 获取 Rexxar 所有请求的 URLSessionConfiguration 189 | */ 190 | + (NSURLSessionConfiguration *)requestsURLSessionConfiguration; 191 | 192 | @end 193 | 194 | NS_ASSUME_NONNULL_END 195 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRConfig.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRConfig.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/30/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | #import "RXRConfig.h" 12 | #import "RXRRouteManager.h" 13 | 14 | @implementation RXRConfig 15 | 16 | static NSString *sRXRProtocolScheme; 17 | static NSString *sRXRProtocolHost; 18 | static NSString *sRXRUserAgent; 19 | static NSArray *sRXRExtraRequestParams; 20 | static NSURL *sRoutesMapURL; 21 | static NSString *sRoutesCachePath; 22 | static NSString *sRoutesResourcePath; 23 | static BOOL sIsCacheEnable = YES; 24 | static BOOL sNeedsIgnoreRoutesVersion = NO; 25 | static id sLogger; 26 | static id sURLCacheConverter; 27 | static id sErrorHandler; 28 | static NSInteger sReloadLimitWhen404 = 2; 29 | static NSURLSessionConfiguration *sURLSessionConfiguration; 30 | static RXRDidCompleteRequestBlock _didCompleteRequestBlock; 31 | static BOOL sUseCustomScheme = NO; 32 | static Class sCustomWebViewClass; 33 | 34 | static NSString * const DefaultRXRScheme = @"douban"; 35 | static NSString * const DefaultRXRHost = @"rexxar-container"; 36 | 37 | + (id)logger 38 | { 39 | return sLogger; 40 | } 41 | 42 | + (void)setLogger:(id)logger 43 | { 44 | sLogger = logger; 45 | } 46 | 47 | + (id)URLCacheConverter 48 | { 49 | return sURLCacheConverter; 50 | } 51 | 52 | + (void)setURLCacheConverter:(id)URLCacheConverter 53 | { 54 | sURLCacheConverter = URLCacheConverter; 55 | } 56 | 57 | + (id)errorHandler 58 | { 59 | return sErrorHandler; 60 | } 61 | 62 | + (void)setErrorHandler:(id)errorHandler 63 | { 64 | sErrorHandler = errorHandler; 65 | } 66 | 67 | + (NSInteger)reloadLimitWhen404 68 | { 69 | return sReloadLimitWhen404; 70 | } 71 | 72 | + (void)setReloadLimitWhen404:(NSInteger)reloadLimitWhen404 73 | { 74 | sReloadLimitWhen404 = reloadLimitWhen404; 75 | } 76 | 77 | + (void)setRXRProtocolScheme:(NSString *)scheme 78 | { 79 | @synchronized (self) { 80 | sRXRProtocolScheme = scheme; 81 | } 82 | } 83 | 84 | + (NSString *)rxrProtocolScheme 85 | { 86 | if (sRXRProtocolScheme) { 87 | return sRXRProtocolScheme; 88 | } 89 | return DefaultRXRScheme; 90 | } 91 | 92 | + (void)setRXRProtocolHost:(NSString *)host 93 | { 94 | @synchronized (self) { 95 | sRXRProtocolHost = host; 96 | } 97 | } 98 | 99 | + (NSString *)rxrProtocolHost 100 | { 101 | if (sRXRProtocolHost) { 102 | return sRXRProtocolHost; 103 | } 104 | return DefaultRXRHost; 105 | } 106 | 107 | + (void)setRoutesMapURL:(NSURL *)routesMapURL 108 | { 109 | @synchronized (self) { 110 | sRoutesMapURL = routesMapURL; 111 | } 112 | } 113 | 114 | + (NSURL *)routesMapURL 115 | { 116 | return sRoutesMapURL; 117 | } 118 | 119 | + (void)setRoutesCachePath:(NSString *)routesCachePath 120 | { 121 | @synchronized (self) { 122 | sRoutesCachePath = routesCachePath; 123 | } 124 | } 125 | 126 | + (NSString *)routesCachePath 127 | { 128 | return sRoutesCachePath; 129 | } 130 | 131 | + (void)setRoutesResourcePath:(NSString *)routesResourcePath 132 | { 133 | @synchronized (self) { 134 | sRoutesResourcePath = routesResourcePath; 135 | } 136 | } 137 | 138 | + (NSString *)routesResourcePath 139 | { 140 | return sRoutesResourcePath; 141 | } 142 | 143 | + (void)setUserAgent:(NSString *)userAgent 144 | { 145 | if ([sRXRUserAgent isEqualToString:userAgent]) { 146 | return; 147 | } 148 | sRXRUserAgent = userAgent; 149 | } 150 | 151 | + (NSString *)userAgent 152 | { 153 | return sRXRUserAgent; 154 | } 155 | 156 | + (void)setExtraRequestParams:(NSArray *)params 157 | { 158 | sRXRExtraRequestParams = params; 159 | } 160 | 161 | + (NSArray *)extraRequestParams 162 | { 163 | return sRXRExtraRequestParams; 164 | } 165 | 166 | + (void)updateConfig 167 | { 168 | RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; 169 | routeManager.routesMapURL = sRoutesMapURL; 170 | [routeManager setCachePath:sRoutesCachePath]; 171 | [routeManager setResoucePath:sRoutesResourcePath]; 172 | } 173 | 174 | + (void)setCacheEnable:(BOOL)isCacheEnable 175 | { 176 | @synchronized (self) { 177 | sIsCacheEnable = isCacheEnable; 178 | } 179 | } 180 | 181 | + (BOOL)isCacheEnable 182 | { 183 | return sIsCacheEnable; 184 | } 185 | 186 | + (void)setNeedsIgnoreRoutesVersion:(BOOL)needsIgnoreRoutesVersion 187 | { 188 | sNeedsIgnoreRoutesVersion = needsIgnoreRoutesVersion; 189 | } 190 | 191 | + (BOOL)needsIgnoreRoutesVersion 192 | { 193 | return sNeedsIgnoreRoutesVersion; 194 | } 195 | 196 | + (void)setHTMLFileDataValidator:(id)dataValidator 197 | { 198 | [RXRRouteManager sharedInstance].dataValidator = dataValidator; 199 | } 200 | 201 | + (NSURLSessionConfiguration *)requestsURLSessionConfiguration 202 | { 203 | if (!sURLSessionConfiguration) { 204 | sURLSessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; 205 | } 206 | return sURLSessionConfiguration; 207 | } 208 | 209 | + (void)setRequestsURLSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration 210 | { 211 | sURLSessionConfiguration = sessionConfiguration; 212 | } 213 | 214 | + (RXRDidCompleteRequestBlock)didCompleteRequestBlock 215 | { 216 | return _didCompleteRequestBlock; 217 | } 218 | 219 | + (void)setDidCompleteRequestBlock:(RXRDidCompleteRequestBlock)didCompleteRequestBlock 220 | { 221 | _didCompleteRequestBlock = [didCompleteRequestBlock copy]; 222 | } 223 | 224 | + (BOOL)useCustomScheme 225 | { 226 | return sUseCustomScheme; 227 | } 228 | 229 | +(void)setUseCustomScheme:(BOOL)useCustomScheme 230 | { 231 | sUseCustomScheme = useCustomScheme; 232 | } 233 | 234 | + (Class)customWebViewClass 235 | { 236 | return sCustomWebViewClass; 237 | } 238 | 239 | +(void)setCustomWebViewClass:(Class)customWebViewClass 240 | { 241 | sCustomWebViewClass = customWebViewClass; 242 | } 243 | 244 | @end 245 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRCustomSchemeHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRCustomSchemeHandler.h 3 | // Rexxar 4 | // 5 | // Created by hao on 2020/5/21. 6 | // 7 | 8 | @import Foundation; 9 | @import WebKit; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface RXRCustomSchemeHandler: NSObject 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRCustomSchemeHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRCustomSchemeHandler.m 3 | // Rexxar 4 | // 5 | // Created by hao on 2020/5/21. 6 | // 7 | 8 | #import "RXRCustomSchemeHandler.h" 9 | #import "RXRURLSessionDemux.h" 10 | #import "RXRConfig+Rexxar.h" 11 | #import "NSHTTPURLResponse+Rexxar.h" 12 | #import "NSURL+Rexxar.h" 13 | 14 | API_AVAILABLE(ios(11.0)) 15 | @protocol RXRCustomSchemeRunnerDelegate 16 | 17 | @optional 18 | - (void)schemeTask:(id )task didCompleteWithError:(nullable NSError *)error; 19 | 20 | @end 21 | 22 | 23 | API_AVAILABLE(ios(11.0)) 24 | @interface RXRCustomSchemeDataTaskRunner: NSObject 25 | 26 | @property (nonatomic, strong) id schemeTask; 27 | @property (nonatomic, strong) NSURLSessionDataTask *dataTask; 28 | @property(nonatomic, strong) RXRURLSessionDemux *sessionDemux; 29 | 30 | @property (nonatomic, weak) id delegate; 31 | 32 | @property (nonatomic, assign) BOOL hasReceiveResponse; 33 | 34 | @end 35 | 36 | @implementation RXRCustomSchemeDataTaskRunner 37 | 38 | - (instancetype)initWithSchemeTask:(id )schemeTask sessionDemux:(RXRURLSessionDemux *)sessionDemux 39 | { 40 | self = [super init]; 41 | if (self) { 42 | _sessionDemux = sessionDemux; 43 | _schemeTask = schemeTask; 44 | 45 | NSMutableURLRequest *request = [schemeTask.request mutableCopy]; 46 | if ([request.URL rxr_isRexxarHttpScheme]) { 47 | request.URL = [request.URL rxr_urlByReplacingRexxarSchemeWithHttp]; 48 | } 49 | 50 | NSMutableArray *modes = [NSMutableArray array]; 51 | [modes addObject:NSDefaultRunLoopMode]; 52 | NSString *currentMode = [[NSRunLoop currentRunLoop] currentMode]; 53 | if (currentMode != nil && ![currentMode isEqualToString:NSDefaultRunLoopMode]) { 54 | [modes addObject:currentMode]; 55 | } 56 | _dataTask = [sessionDemux dataTaskWithRequest:request delegate:self modes:modes]; 57 | } 58 | return self; 59 | } 60 | 61 | - (void)resume 62 | { 63 | [self.dataTask resume]; 64 | } 65 | 66 | - (void)cancel 67 | { 68 | [self.sessionDemux performBlockWithTask:self.dataTask block:^{ 69 | self.schemeTask = nil; 70 | [self.dataTask cancel]; 71 | }]; 72 | } 73 | 74 | #pragma mark - NSURLSessionDataDelegate 75 | 76 | - (void)URLSession:(NSURLSession *)session 77 | dataTask:(NSURLSessionDataTask *)dataTask 78 | didReceiveResponse:(NSURLResponse *)response 79 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler 80 | { 81 | if (self.hasReceiveResponse) { 82 | NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:nil]; 83 | RXRLogObject *logObj = [[RXRLogObject alloc] initWithLogDescription:@"rxr_already_receive_response" error:error requestURL:dataTask.currentRequest.URL localFilePath:nil otherInformation:nil]; 84 | [RXRConfig rxr_logWithLogObject:logObj]; 85 | return; 86 | } 87 | NSHTTPURLResponse *URLResponse = nil; 88 | if ([response isKindOfClass:[NSHTTPURLResponse class]]) { 89 | URLResponse = (NSHTTPURLResponse *)response; 90 | URLResponse = [NSHTTPURLResponse rxr_responseWithURL:self.schemeTask.request.URL 91 | statusCode:URLResponse.statusCode 92 | headerFields:URLResponse.allHeaderFields 93 | noAccessControl:YES]; 94 | } 95 | 96 | [self.schemeTask didReceiveResponse:URLResponse ?: response]; 97 | completionHandler(NSURLSessionResponseAllow); 98 | self.hasReceiveResponse = YES; 99 | } 100 | 101 | - (void)URLSession:(NSURLSession *)session 102 | dataTask:(NSURLSessionDataTask *)dataTask 103 | didReceiveData:(NSData *)data 104 | { 105 | if (!self.hasReceiveResponse) { 106 | NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:nil]; 107 | RXRLogObject *logObj = [[RXRLogObject alloc] initWithLogDescription:@"rxr_receive_data_before_response" error:error requestURL:dataTask.currentRequest.URL localFilePath:nil otherInformation:nil]; 108 | [RXRConfig rxr_logWithLogObject:logObj]; 109 | return; 110 | } 111 | 112 | [self.schemeTask didReceiveData:data]; 113 | } 114 | 115 | - (void)URLSession:(NSURLSession *)session 116 | dataTask:(NSURLSessionDataTask *)dataTask 117 | willCacheResponse:(NSCachedURLResponse *)proposedResponse 118 | completionHandler:(void (^)(NSCachedURLResponse *_Nullable cachedResponse))completionHandler 119 | { 120 | completionHandler(proposedResponse); 121 | } 122 | 123 | - (void)URLSession:(NSURLSession *)session 124 | task:(NSURLSessionTask *)task 125 | willPerformHTTPRedirection:(NSHTTPURLResponse *)response 126 | newRequest:(NSURLRequest *)request 127 | completionHandler:(void (^)(NSURLRequest *_Nullable))completionHandler 128 | { 129 | NSMutableURLRequest *mutableRequest = [task.currentRequest mutableCopy]; 130 | [mutableRequest setURL:request.URL]; 131 | completionHandler(mutableRequest); 132 | } 133 | 134 | - (void)URLSession:(NSURLSession *)session 135 | task:(NSURLSessionTask *)task 136 | didCompleteWithError:(nullable NSError *)error 137 | { 138 | if (error == nil && !self.hasReceiveResponse) { 139 | error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:nil]; 140 | RXRLogObject *logObj = [[RXRLogObject alloc] initWithLogDescription:@"rxr_finish_before_response" error:error requestURL:task.currentRequest.URL localFilePath:nil otherInformation:nil]; 141 | [RXRConfig rxr_logWithLogObject:logObj]; 142 | } 143 | 144 | if (error == nil) { 145 | [self.schemeTask didFinish]; 146 | } else { 147 | if ([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { 148 | // Do nothing. 149 | } else { 150 | [self.schemeTask didFailWithError:error]; 151 | } 152 | } 153 | 154 | if ([self.delegate respondsToSelector:@selector(schemeTask:didCompleteWithError:)]) { 155 | [self.delegate schemeTask:self.schemeTask didCompleteWithError:error]; 156 | } 157 | } 158 | 159 | @end 160 | 161 | API_AVAILABLE(ios(11.0)) 162 | @interface RXRCustomSchemeHandler() 163 | 164 | @property(nonatomic, strong) NSMutableDictionary *runningTasks; 165 | @property(nonatomic, strong) dispatch_semaphore_t listSema; 166 | @property(nonatomic, strong) RXRURLSessionDemux *sessionDemux; 167 | 168 | @end 169 | 170 | @implementation RXRCustomSchemeHandler 171 | 172 | - (instancetype)init 173 | { 174 | self = [super init]; 175 | if (self) { 176 | _runningTasks = [NSMutableDictionary dictionary]; 177 | _listSema = dispatch_semaphore_create(1); 178 | 179 | NSURLSessionConfiguration *sessionConfiguration = [RXRConfig requestsURLSessionConfiguration]; 180 | _sessionDemux = [[RXRURLSessionDemux alloc] initWithSessionConfiguration:sessionConfiguration]; 181 | } 182 | return self; 183 | } 184 | 185 | - (void)dealloc 186 | { 187 | [_sessionDemux.session finishTasksAndInvalidate]; 188 | } 189 | 190 | - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask API_AVAILABLE(ios(11.0)) 191 | { 192 | RXRCustomSchemeDataTaskRunner *runner = [[RXRCustomSchemeDataTaskRunner alloc] initWithSchemeTask:urlSchemeTask sessionDemux:self.sessionDemux]; 193 | runner.delegate = self; 194 | NSString *taskID = [self taskIDForSchemeTask:urlSchemeTask]; 195 | dispatch_semaphore_wait(self.listSema, DISPATCH_TIME_FOREVER); 196 | self.runningTasks[taskID] = runner; 197 | dispatch_semaphore_signal(self.listSema); 198 | [runner resume]; 199 | } 200 | 201 | - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id )urlSchemeTask API_AVAILABLE(ios(11.0)) 202 | { 203 | NSString *taskID = [self taskIDForSchemeTask:urlSchemeTask]; 204 | dispatch_semaphore_wait(self.listSema, DISPATCH_TIME_FOREVER); 205 | [self.runningTasks[taskID] cancel]; 206 | self.runningTasks[taskID] = nil; 207 | dispatch_semaphore_signal(self.listSema); 208 | } 209 | 210 | - (NSString *)taskIDForSchemeTask:(id )urlSchemeTask API_AVAILABLE(ios(11.0)) 211 | { 212 | return [NSString stringWithFormat:@"%p", urlSchemeTask]; 213 | } 214 | 215 | - (void)schemeTask:(id)task didCompleteWithError:(NSError *)error 216 | { 217 | NSString *taskID = [self taskIDForSchemeTask:task]; 218 | dispatch_semaphore_wait(self.listSema, DISPATCH_TIME_FOREVER); 219 | self.runningTasks[taskID] = nil; 220 | dispatch_semaphore_signal(self.listSema); 221 | } 222 | 223 | @end 224 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRDataValidator.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRDataValidator.h 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 06/11/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #ifndef RXRDataValidator_h 10 | #define RXRDataValidator_h 11 | 12 | 13 | /** 14 | 可在 `RXRConfig` 中设置 `RXRDataValidator`,`Rexxar` 不提供默认实现。 15 | 目前只提供验证下载的 HTML file 的方法。 16 | */ 17 | @protocol RXRDataValidator 18 | 19 | #pragma mark - Downloading HTML files related validation 20 | /** 21 | 验证下载的 `fileData` 是否是合法的。 22 | 23 | @param fileURL 下载文件对应的 remote URL 24 | @param fileData 下载的文件数据 25 | @return 是否通过验证 26 | */ 27 | - (BOOL)validateRemoteHTMLFile:(nullable NSURL *)fileURL fileData:(nullable NSData *)fileData; 28 | 29 | /** 30 | 如果验证失败,是否停止继续下载其他文件。 31 | 32 | @return 是否停止继续下载其他文件 33 | */ 34 | - (BOOL)stopDownloadingIfValidationFailed; 35 | 36 | @end 37 | 38 | 39 | #endif /* RXRDataValidator_h */ 40 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRErrorHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRErrorHandler.h 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 21/12/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | FOUNDATION_EXPORT const NSString * _Nonnull rxrErrorUserInfoURLKey; 12 | FOUNDATION_EXPORT NSErrorDomain _Nonnull rxrHttpErrorDomain; 13 | FOUNDATION_EXPORT const NSInteger rxrHttpResponseErrorNotFound; 14 | FOUNDATION_EXPORT const NSInteger rxrHttpResponseURLProtocolError; 15 | 16 | @protocol RXRErrorHandler 17 | - (void)handleError:(nullable NSError *)error fromReporter:(nullable id)reporter; 18 | @end 19 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRErrorHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRErrorHandler.m 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 21/12/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | const NSString *rxrErrorUserInfoURLKey = @"rxrErrorUserInfoURLKey"; 12 | 13 | NSErrorDomain rxrHttpErrorDomain = @"rxrHttpErrorDomain"; 14 | const NSInteger rxrHttpResponseErrorNotFound = 404; 15 | 16 | // In order not to be conflicated with other official HTTP status code from 1xx to 5xx, we choose to use 999 to 17 | // indicate URLProtocol loading error. 18 | const NSInteger rxrHttpResponseURLProtocolError = 999; 19 | 20 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRLogger.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRLogger.h 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 07/11/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #ifdef DEBUG 10 | #define RXRLog(...) NSLog(@"[Rexxar] " __VA_ARGS__) 11 | #else /* DEBUG */ 12 | #define RXRLog(...) 13 | #endif /* DEBUG */ 14 | 15 | #define RXRDebugLog(...) RXRLog(@"[DEBUG] " __VA_ARGS__) 16 | #define RXRWarnLog(...) RXRLog(@"[WARN] " __VA_ARGS__) 17 | #define RXRErrorLog(...) RXRLog(@"[ERROR] " __VA_ARGS__) 18 | 19 | @import Foundation; 20 | @class RXRLogObject; 21 | 22 | typedef enum : NSUInteger { 23 | RXRLogTypeNoRoutesMapURLError, // 没有设置 RoutesMap 地址 24 | RXRLogTypeDownloadingRoutesError, // 下载 Routes 失败 25 | RXRLogTypeDownloadingHTMLFileError, // 下载 HTML file 失败 26 | RXRLogTypeValidatingHTMLFileError, // 验证下载的 HTML file 失败(需要提供 `RXRDataValidator`) 27 | RXRLogTypeFailedToCreateCacheDirectoryError, // 创建 cache 目录失败 28 | RXRLogTypeWebViewLoadingError, // WebView 加载失败 29 | RXRLogTypeNoRemoteHTMLForURI, // 在内存中的 route 列表里找不到 uri 对应的项(没有对应的 html 文件名) 30 | RXRLogTypeNoLocalHTMLForURI, // 在内存中的 route 列表里找不到 uri 对应的本地 html 文件 31 | RXRLogTypeWebViewLoad404, // 没有本地 html 时,webview 加载远程 html 出现 404 32 | RXRLogTypeWebViewLoadNot200, // WebView load 中返回了非 200 status code 33 | RXRLogTypeUnknown, 34 | } RXRLogType; 35 | 36 | /** 37 | 可在 `RXRConfig` 中设置,`Rexar` 不提供默认实现。如果设置了 `RXRLogger`,将会提供 `RXRLogType` 中所包含类型的记录。 38 | */ 39 | @protocol RXRLogger 40 | 41 | /** 42 | `RXRLogger` 目前只提供这一个方法,需要调用者实现。调用者获取到 `logObject` 后自己处理具体的 log 逻辑。 43 | 44 | @param logObject 由 `RXRLogObject` 封装的 log 记录。 45 | */ 46 | - (void)rexxarDidLogWithLogObject:(nonnull RXRLogObject *)logObject; 47 | 48 | @end 49 | 50 | FOUNDATION_EXPORT NSString *const _Nonnull logOtherInfoStatusCodeKey; 51 | FOUNDATION_EXPORT NSString *const _Nonnull logOtherInfoRoutesVersionKey; 52 | 53 | NS_ASSUME_NONNULL_BEGIN 54 | 55 | @interface RXRLogObject : NSObject 56 | 57 | @property (nonatomic, readonly) RXRLogType type; 58 | @property (nonatomic, readonly) NSString *logDescription; 59 | @property (nonatomic, readonly, nullable) NSError *error; 60 | @property (nonatomic, readonly, nullable) NSURL *requestURL; 61 | @property (nonatomic, readonly, nullable) NSString *localFilePath; 62 | @property (nonatomic, readonly, nullable) NSDictionary *otherInfomation; // 目前 Rexxar 只提供 `logOtherInfoStatusCodeKey` 63 | 64 | - (instancetype)initWithLogType:(RXRLogType)type 65 | error:(nullable NSError *)error 66 | requestURL:(nullable NSURL *)requestURL 67 | localFilePath:(nullable NSString *)localFilePath 68 | otherInformation:(nullable NSDictionary *)otherInformation; 69 | 70 | - (instancetype)initWithLogDescription:(nullable NSString *)description 71 | error:(nullable NSError *)error 72 | requestURL:(nullable NSURL *)requestURL 73 | localFilePath:(nullable NSString *)localFilePath 74 | otherInformation:(nullable NSDictionary *)otherInformation NS_DESIGNATED_INITIALIZER; 75 | @end 76 | 77 | NS_ASSUME_NONNULL_END 78 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRLogger.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRLogger.m 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 07/11/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRLogger.h" 10 | 11 | NSString *const logOtherInfoStatusCodeKey = @"status"; 12 | NSString *const logOtherInfoRoutesVersionKey = @"version"; 13 | 14 | static NSString *descriptionFromLogType(RXRLogType type) 15 | { 16 | switch (type) { 17 | case RXRLogTypeNoRoutesMapURLError: 18 | return @"no_routes_map_url"; 19 | 20 | case RXRLogTypeDownloadingRoutesError: 21 | return @"downloading_routes_error"; 22 | 23 | case RXRLogTypeDownloadingHTMLFileError: 24 | return @"downloading_HTML_file_error"; 25 | 26 | case RXRLogTypeValidatingHTMLFileError: 27 | return @"validating_HTML_file_error"; 28 | 29 | case RXRLogTypeFailedToCreateCacheDirectoryError: 30 | return @"failed_to_create_cache_directory_error"; 31 | 32 | case RXRLogTypeWebViewLoadingError: 33 | return @"webView_loading_error"; 34 | 35 | case RXRLogTypeNoLocalHTMLForURI: 36 | return @"no_local_html_for_uri"; 37 | 38 | case RXRLogTypeNoRemoteHTMLForURI: 39 | return @"no_remote_html_for_uri"; 40 | 41 | case RXRLogTypeWebViewLoad404: 42 | return @"webview_load_404"; 43 | 44 | case RXRLogTypeWebViewLoadNot200: 45 | return @"webview_load_not_200"; 46 | 47 | default: 48 | return @"Unknow rexxar error"; 49 | } 50 | } 51 | 52 | @implementation RXRLogObject 53 | 54 | - (instancetype)init 55 | { 56 | NSAssert(NO, @"Should call designated initializer"); 57 | 58 | return [self initWithLogDescription:nil 59 | error:nil 60 | requestURL:nil 61 | localFilePath:nil 62 | otherInformation:nil]; 63 | } 64 | 65 | - (instancetype)initWithLogType:(RXRLogType)type 66 | error:(NSError *)error 67 | requestURL:(NSURL *)requestURL 68 | localFilePath:(NSString *)localFilePath 69 | otherInformation:(NSDictionary *)otherInformation 70 | { 71 | if (self = [self initWithLogDescription:descriptionFromLogType(type) 72 | error:error 73 | requestURL:requestURL 74 | localFilePath:localFilePath 75 | otherInformation:otherInformation]) { 76 | _type = type; 77 | } 78 | 79 | return self; 80 | } 81 | 82 | - (instancetype)initWithLogDescription:(NSString *)description 83 | error:(NSError *)error 84 | requestURL:(NSURL *)requestURL 85 | localFilePath:(NSString *)localFilePath 86 | otherInformation:(NSDictionary *)otherInformation 87 | { 88 | if (self = [super init]) { 89 | _logDescription = [description copy]; 90 | _error = error; 91 | _requestURL = [requestURL copy]; 92 | _localFilePath = [localFilePath copy]; 93 | _otherInfomation = [otherInformation copy]; 94 | _type = RXRLogTypeUnknown; 95 | } 96 | 97 | return self; 98 | } 99 | @end 100 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRNSURLProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNSURLProtocol.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | @class RXRURLSessionDemux; 12 | 13 | @interface RXRNSURLProtocol : NSURLProtocol 14 | 15 | @property (nonatomic, strong) NSURLSessionTask *dataTask; 16 | @property (nonatomic, copy) NSArray *modes; 17 | 18 | #pragma mark - Public methods, do not override 19 | 20 | /** 21 | * 在startLoading中调用此方法 22 | */ 23 | - (void)beforeStartLoadingRequest; 24 | 25 | /** 26 | * 加载完成时调用此方法 27 | */ 28 | - (void)afterStopLoadingRequest; 29 | 30 | /** 31 | * 将该请求标记为可以忽略 32 | */ 33 | + (void)markRequestAsIgnored:(NSMutableURLRequest *)request; 34 | 35 | /** 36 | * 清除该请求 `可忽略` 标识 37 | */ 38 | + (void)unmarkRequestAsIgnored:(NSMutableURLRequest *)request; 39 | 40 | /** 41 | * 判断该请求是否是被忽略的 42 | */ 43 | + (BOOL)isRequestIgnored:(NSURLRequest *)request; 44 | 45 | /** 46 | * 注册 `RXRURLProtocol` 47 | * 48 | * @param clazz a subclass of `RXRURLProtocol` 49 | */ 50 | + (BOOL)registerRXRProtocolClass:(Class)clazz; 51 | 52 | /** 53 | * 反注册 `RXRURLProtocol` 54 | * 55 | * @param clazz a subclass of `RXRURLProtocol` 56 | */ 57 | + (void)unregisterRXRProtocolClass:(Class)clazz; 58 | 59 | /** 60 | * 实现 URLSession 共享和 URLProtocol client 回调的分发 61 | * 62 | * @return 共享的复用解析器 63 | */ 64 | + (RXRURLSessionDemux *)sharedDemux; 65 | 66 | @end 67 | 68 | /// 69 | /// 截获所有的 http / https 请求,然后内部使用 URLSession 重新发送请求 70 | /// 为了确保 URLProtocol 注册顺序,使用时必须优先注册这个类 71 | /// 72 | @interface RXRDefaultURLProtocol : RXRNSURLProtocol 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRNSURLProtocol.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNSURLProtocol.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import WebKit; 10 | 11 | #import "RXRNSURLProtocol.h" 12 | #import "RXRConfig.h" 13 | #import "RXRConfig+Rexxar.h" 14 | #import "NSURL+Rexxar.h" 15 | #import "RXRURLSessionDemux.h" 16 | #import "NSHTTPURLResponse+Rexxar.h" 17 | #import "RXRErrorHandler.h" 18 | #import "RXRWebViewController.h" 19 | 20 | static NSMutableDictionary *sRegisteredClassCounter; 21 | 22 | @interface RXRNSURLProtocol() 23 | 24 | @property (nonatomic, strong) WKWebView *webview; 25 | 26 | @end 27 | 28 | @implementation RXRNSURLProtocol 29 | 30 | + (RXRURLSessionDemux *)sharedDemux 31 | { 32 | static dispatch_once_t onceToken; 33 | static RXRURLSessionDemux *demux; 34 | 35 | dispatch_once(&onceToken, ^{ 36 | NSURLSessionConfiguration *sessionConfiguration = [RXRConfig requestsURLSessionConfiguration]; 37 | demux = [[RXRURLSessionDemux alloc] initWithSessionConfiguration:sessionConfiguration]; 38 | }); 39 | 40 | return demux; 41 | } 42 | 43 | - (void)startLoading 44 | { 45 | NSAssert(NO, @"Implement this method in a subclass."); 46 | } 47 | 48 | - (void)stopLoading 49 | { 50 | [self afterStopLoadingRequest]; 51 | 52 | [[self.class sharedDemux] performBlockWithTask:[self dataTask] block:^{ 53 | NSURLSessionTask *task = [self dataTask]; 54 | if (task != nil) { 55 | [self setDataTask:nil]; 56 | [task cancel]; 57 | } 58 | }]; 59 | } 60 | 61 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 62 | { 63 | return request; 64 | } 65 | 66 | #pragma mark - Public methods, do not override 67 | 68 | - (void)beforeStartLoadingRequest 69 | { 70 | NSString *ua = self.request.allHTTPHeaderFields[@"User-Agent"]; 71 | NSArray *comps = [ua componentsSeparatedByString:@" "]; 72 | NSString *webviewID = nil; 73 | for (NSString *comp in comps) { 74 | if ([comp hasPrefix:@"webviewID/"]) { 75 | webviewID = [comp stringByReplacingOccurrencesOfString:@"webviewID/" withString:@""]; 76 | } 77 | } 78 | self.webview = [RXRWebViewStore webViewForID:webviewID]; 79 | 80 | [RXRWebViewStore addInterceptor:self withWebViewID:webviewID]; 81 | } 82 | 83 | - (void)afterStopLoadingRequest 84 | { 85 | if (self.webview != nil) { 86 | NSString *webViewID = [RXRWebViewStore IDForWebView:self.webview]; 87 | [RXRWebViewStore removeInterceptor:self withWebViewID:webViewID]; 88 | } 89 | } 90 | 91 | + (void)markRequestAsIgnored:(NSMutableURLRequest *)request 92 | { 93 | NSString *key = NSStringFromClass([self class]); 94 | [NSURLProtocol setProperty:@YES forKey:key inRequest:request]; 95 | } 96 | 97 | + (void)unmarkRequestAsIgnored:(NSMutableURLRequest *)request 98 | { 99 | NSString *key = NSStringFromClass([self class]); 100 | [NSURLProtocol removePropertyForKey:key inRequest:request]; 101 | } 102 | 103 | + (BOOL)isRequestIgnored:(NSURLRequest *)request 104 | { 105 | NSString *key = NSStringFromClass([self class]); 106 | if ([NSURLProtocol propertyForKey:key inRequest:request]) { 107 | return YES; 108 | } 109 | return NO; 110 | } 111 | 112 | + (BOOL)registerRXRProtocolClass:(Class)clazz 113 | { 114 | NSParameterAssert([clazz isSubclassOfClass:[self class]]); 115 | 116 | BOOL result; 117 | NSInteger countForClass = [self _rxr_countForRegisteredClass:clazz]; 118 | if (countForClass <= 0) { 119 | if ([RXRConfig useCustomScheme]) { 120 | NSMutableArray *const mutableProtocolClasses = [([RXRConfig requestsURLSessionConfiguration].protocolClasses ?: @[]) mutableCopy]; 121 | [mutableProtocolClasses insertObject:clazz atIndex:0]; 122 | [RXRConfig requestsURLSessionConfiguration].protocolClasses = mutableProtocolClasses; 123 | result = YES; 124 | } else { 125 | result = [NSURLProtocol registerClass:clazz]; 126 | } 127 | if (result) { 128 | [self _rxr_setCount:1 forRegisteredClass:clazz]; 129 | } 130 | } else { 131 | [self _rxr_setCount:countForClass + 1 forRegisteredClass:clazz]; 132 | result = YES; 133 | } 134 | 135 | return result; 136 | } 137 | 138 | + (void)unregisterRXRProtocolClass:(Class)clazz 139 | { 140 | NSParameterAssert([clazz isSubclassOfClass:[self class]]); 141 | 142 | NSInteger countForClass = [self _rxr_countForRegisteredClass:clazz] - 1; 143 | if (countForClass <= 0) { 144 | if ([RXRConfig useCustomScheme]) { 145 | NSMutableArray *const mutableProtocolClasses = [([RXRConfig requestsURLSessionConfiguration].protocolClasses ?: @[]) mutableCopy]; 146 | [mutableProtocolClasses removeObjectIdenticalTo:clazz]; 147 | [RXRConfig requestsURLSessionConfiguration].protocolClasses = mutableProtocolClasses; 148 | } else { 149 | [NSURLProtocol unregisterClass:clazz]; 150 | } 151 | } 152 | 153 | if (countForClass >= 0) { 154 | [self _rxr_setCount:countForClass forRegisteredClass:clazz]; 155 | } 156 | } 157 | 158 | #pragma mark - Private methods 159 | 160 | + (NSInteger)_rxr_countForRegisteredClass:(Class)clazz 161 | { 162 | NSString *key = NSStringFromClass(clazz); 163 | if (key && sRegisteredClassCounter && sRegisteredClassCounter[key]) { 164 | return [sRegisteredClassCounter[key] integerValue]; 165 | } 166 | 167 | return 0; 168 | } 169 | 170 | + (void)_rxr_setCount:(NSInteger)count forRegisteredClass:(Class)clazz 171 | { 172 | if (!sRegisteredClassCounter) { 173 | sRegisteredClassCounter = [NSMutableDictionary dictionary]; 174 | } 175 | 176 | NSString *key = NSStringFromClass(clazz); 177 | if (key) { 178 | sRegisteredClassCounter[key] = @(count); 179 | } 180 | } 181 | 182 | #pragma mark - NSURLSessionTaskDelegate 183 | 184 | - (void)URLSession:(NSURLSession *)session 185 | task:(NSURLSessionTask *)task 186 | willPerformHTTPRedirection:(NSHTTPURLResponse *)response 187 | newRequest:(NSURLRequest *)request 188 | completionHandler:(void (^)(NSURLRequest *_Nullable))completionHandler 189 | { 190 | if ([self client] != nil && _dataTask == task) { 191 | NSMutableURLRequest *mutableRequest = [_dataTask.currentRequest mutableCopy]; 192 | [mutableRequest setURL:request.URL]; 193 | completionHandler(mutableRequest); 194 | } 195 | } 196 | 197 | - (void)URLSession:(NSURLSession *)session 198 | task:(NSURLSessionTask *)task 199 | didCompleteWithError:(nullable NSError *)error 200 | { 201 | if ([self client] != nil && (_dataTask == nil || _dataTask == task)) { 202 | if (error == nil) { 203 | [[self client] URLProtocolDidFinishLoading:self]; 204 | } else { 205 | [[self client] URLProtocol:self didFailWithError:error]; 206 | 207 | if ([RXRConfig rxr_canHandleError]) { 208 | [RXRConfig rxr_handleError:error fromReporter:self]; 209 | } 210 | } 211 | 212 | [self afterStopLoadingRequest]; 213 | } 214 | } 215 | 216 | #pragma mark - NSURLSessionDataDelegate 217 | 218 | - (void)URLSession:(NSURLSession *)session 219 | dataTask:(NSURLSessionDataTask *)dataTask 220 | didReceiveResponse:(NSURLResponse *)response 221 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler 222 | { 223 | if ([self client] != nil && [self dataTask] != nil && [self dataTask] == dataTask) { 224 | NSHTTPURLResponse *URLResponse = nil; 225 | if ([response isKindOfClass:[NSHTTPURLResponse class]]) { 226 | URLResponse = (NSHTTPURLResponse *)response; 227 | URLResponse = [NSHTTPURLResponse rxr_responseWithURL:URLResponse.URL 228 | statusCode:URLResponse.statusCode 229 | headerFields:URLResponse.allHeaderFields 230 | noAccessControl:YES]; 231 | } 232 | 233 | [[self client] URLProtocol:self 234 | didReceiveResponse:URLResponse ?: response 235 | cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 236 | completionHandler(NSURLSessionResponseAllow); 237 | } 238 | } 239 | 240 | - (void)URLSession:(NSURLSession *)session 241 | dataTask:(NSURLSessionDataTask *)dataTask 242 | didReceiveData:(NSData *)data 243 | { 244 | if ([self client] != nil && [self dataTask] == dataTask) { 245 | [[self client] URLProtocol:self didLoadData:data]; 246 | } 247 | } 248 | 249 | - (void)URLSession:(NSURLSession *)session 250 | dataTask:(NSURLSessionDataTask *)dataTask 251 | willCacheResponse:(NSCachedURLResponse *)proposedResponse 252 | completionHandler:(void (^)(NSCachedURLResponse *_Nullable cachedResponse))completionHandler 253 | { 254 | if ([self client] != nil && [self dataTask] == dataTask) { 255 | completionHandler(proposedResponse); 256 | } 257 | } 258 | 259 | @end 260 | 261 | 262 | @implementation RXRDefaultURLProtocol 263 | 264 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 265 | { 266 | // 不是 HTTP 请求,不处理 267 | if (![request.URL rxr_isHttpOrHttps]) { 268 | return NO; 269 | } 270 | // 请求被忽略(被标记为忽略或者已经请求过),不处理 271 | if ([self isRequestIgnored:request]) { 272 | return NO; 273 | } 274 | // 请求不是来自Rexxar浏览器,不处理 275 | if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"] || ![request.allHTTPHeaderFields[@"User-Agent"] containsString:@"Rexxar"]) { 276 | return NO; 277 | } 278 | return YES; 279 | } 280 | 281 | - (void)startLoading 282 | { 283 | [self beforeStartLoadingRequest]; 284 | 285 | NSMutableURLRequest *newRequest = nil; 286 | if ([self.request isKindOfClass:[NSMutableURLRequest class]]) { 287 | newRequest = (NSMutableURLRequest *)self.request; 288 | } else { 289 | newRequest = [self.request mutableCopy]; 290 | } 291 | 292 | [[self class] markRequestAsIgnored:newRequest]; 293 | 294 | NSMutableArray *modes = [NSMutableArray array]; 295 | [modes addObject:NSDefaultRunLoopMode]; 296 | NSString *currentMode = [[NSRunLoop currentRunLoop] currentMode]; 297 | if (currentMode != nil && ![currentMode isEqualToString:NSDefaultRunLoopMode]) { 298 | [modes addObject:currentMode]; 299 | } 300 | [self setModes:modes]; 301 | 302 | NSURLSessionTask *dataTask = [[[self class] sharedDemux] dataTaskWithRequest:newRequest delegate:self modes:self.modes]; 303 | [dataTask resume]; 304 | [self setDataTask:dataTask]; 305 | } 306 | 307 | @end 308 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRRoute.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRoute.h 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/20/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * `RXRRoute` 路由信息对象。 15 | */ 16 | @interface RXRRoute : NSObject 17 | 18 | /** 19 | * 以一个字典初始化路由信息对象。 20 | */ 21 | - (instancetype)initWithDictionary:(NSDictionary *)dict; 22 | 23 | /** 24 | * 匹配该路由的 URI 正则表达式。 25 | */ 26 | @property (nonatomic, readonly) NSRegularExpression *URIRegex; 27 | 28 | /** 29 | * 该路由对应的 html 文件地址。 30 | */ 31 | @property (nonatomic, readonly) NSURL *remoteHTML; 32 | 33 | @property (nonatomic, readonly) BOOL isPackageInApp; 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRRoute.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRoute.m 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/20/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRRoute.h" 10 | 11 | @implementation RXRRoute 12 | 13 | - (instancetype)initWithDictionary:(NSDictionary *)dict 14 | { 15 | if ( (self = [super init]) ) { 16 | _remoteHTML = [NSURL URLWithString:dict[@"remote_file"]]; 17 | _URIRegex = [NSRegularExpression regularExpressionWithPattern:dict[@"uri"] options:0 error:nil]; 18 | _isPackageInApp = [dict[@"pack_in_app"] boolValue]; 19 | } 20 | return self; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRRouteFileCache.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRouteFileCache.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/11/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * `RXRRouteCache` 提供对 Route files 的读取。 15 | * Route files 包括用于渲染 rexxar 页面的静态 html 文件。 16 | * 为何我们会自己实现一个缓存,而不使用 NSURLCache? 17 | * 因为获取 Route 信息有两个来源,要么从本地缓存(上线后发布,下载的资源会有本地缓存); 要么资源文件夹(上线时打入的)。这和 NSURLCache 缓存机制不同。 18 | * 1. 本地缓存; 19 | * 2. 资源:应用打包的资源文件中有一份, 这部分资源不会改变。 20 | * 21 | * `RXRRouteCache` offer the access method of Route files. 22 | * Route files include rexxar page 's static html file. 23 | * Why we write this cache instead of using NSURLCache? 24 | * It's because that there are two sources of Route files,local cache (create and save the downloaded resources in cache after app release) or resource file (in the release ipa): 25 | * 1. local cache: disk cache; 26 | * 2. resource file: a copy in ipa's resource bundle, this resource will not change. 27 | */ 28 | @interface RXRRouteFileCache : NSObject 29 | 30 | /** 31 | * cachePath, 如果是相对路径的话,则认为其是相对于应用缓存路径。 32 | */ 33 | @property (nonatomic, copy) NSString *cachePath; 34 | 35 | /** 36 | * Rexxar 资源地址, 会在打包应用时,打包进入 ipa。如果是相对路径的话,则认为其是相对于 main bundle 路径。 37 | */ 38 | @property (nonatomic, copy) NSString *resourcePath; 39 | 40 | /** 41 | * 单例方法,获取一个 RXRRouteFileCache 实例。 42 | * 43 | * Get RXRRouteFileCache Singleton instance. 44 | */ 45 | + (RXRRouteFileCache *)sharedInstance; 46 | 47 | /** 48 | * 存储 Route Map File,文件名为 `routes.json`。 49 | * 50 | * Save routes map file with file name : `routes.json`. 51 | */ 52 | - (void)saveRoutesMapFile:(NSData *)data; 53 | 54 | /** 55 | * 读取缓存中 Route Map File。 56 | * 57 | * Read routes map file in cache. 58 | */ 59 | - (nullable NSData *)cacheRoutesMapFile; 60 | 61 | /** 62 | * 读取资源文件中 Route Map File。 63 | * 64 | * Read routes map file in bundle resource. 65 | */ 66 | - (nullable NSData *)resourceRoutesMapFile; 67 | 68 | 69 | /** 70 | * 将 `url` 下载下来的资源数据,存入缓存。 71 | * 72 | * Save the route file with url. 73 | */ 74 | - (void)saveRouteFileData:(NSData *)data withRemoteURL:(NSURL *)url; 75 | 76 | /** 77 | * 从缓存中读取出 `url` 下载的资源。 78 | * 79 | * Read the route file according url. 80 | */ 81 | - (nullable NSData *)routeFileDataForRemoteURL:(NSURL *)url; 82 | 83 | /** 84 | * 获取远程 url 对于的本地 url。先在缓存文件夹中寻找,再在资源文件夹中寻找。如果在缓存文件和资源文件中都找不到对应的本地文件,返回 nil。 85 | * 86 | * Get the local url for remote url. Search the local file first from cache file, then from resource file. 87 | * If it dose not exist in cache file and resource file, return nil. 88 | */ 89 | - (nullable NSURL *)routeFileURLForRemoteURL:(NSURL *)url; 90 | 91 | /** 92 | * 清理缓存。 93 | * 94 | * Clean Cache。 95 | */ 96 | - (void)cleanCache; 97 | 98 | 99 | /** 100 | 获取缓存文件大小 101 | 102 | @return 缓存文件大小 103 | */ 104 | - (NSUInteger)cacheFileSize; 105 | 106 | @end 107 | 108 | NS_ASSUME_NONNULL_END 109 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRRouteFileCache.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRouteFileCache.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/11/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRRouteFileCache.h" 10 | #import "RXRConfig.h" 11 | 12 | #import "RXRLogger.h" 13 | #import "NSData+RXRDigest.h" 14 | #import "RXRLogger.h" 15 | #import "RXRConfig+Rexxar.h" 16 | 17 | static NSString * const RoutesMapFile = @"routes.json"; 18 | 19 | @implementation RXRRouteFileCache 20 | 21 | + (RXRRouteFileCache *)sharedInstance 22 | { 23 | static RXRRouteFileCache *instance = nil; 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | instance = [[RXRRouteFileCache alloc] init]; 27 | instance.cachePath = [RXRConfig routesCachePath]; 28 | instance.resourcePath = [RXRConfig routesResourcePath]; 29 | }); 30 | return instance; 31 | } 32 | 33 | - (instancetype)initWithCachePath:(NSString *)cachePath 34 | resourcePath:(NSString *)resourcePath 35 | { 36 | self = [super init]; 37 | if (self) { 38 | } 39 | return self; 40 | } 41 | 42 | #pragma mark - Save & Read methods 43 | 44 | - (void)setCachePath:(NSString *)cachePath 45 | { 46 | // cache dir 47 | if (!cachePath) { 48 | // 默认缓存路径:/.rexxar 49 | cachePath = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@".rexxar"]; 50 | } 51 | 52 | if (![cachePath isAbsolutePath]) { 53 | cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) 54 | firstObject] stringByAppendingPathComponent:cachePath]; 55 | } 56 | 57 | _cachePath = [cachePath copy]; 58 | 59 | NSError *error; 60 | [[NSFileManager defaultManager] createDirectoryAtPath:_cachePath 61 | withIntermediateDirectories:YES 62 | attributes:@{} 63 | error:&error]; 64 | if (error) { 65 | RXRDebugLog(@"Failed to create directory: %@", _cachePath); 66 | [RXRConfig rxr_logWithType:RXRLogTypeFailedToCreateCacheDirectoryError error:error requestURL:nil localFilePath:_cachePath userInfo:nil]; 67 | } 68 | } 69 | 70 | - (void)setResourcePath:(NSString *)resourcePath 71 | { 72 | // resource dir 73 | if (!resourcePath && [resourcePath length] > 0) { 74 | // 默认资源路径:/rexxar 75 | resourcePath = [[NSBundle mainBundle] pathForResource:@"rexxar" ofType:nil]; 76 | } 77 | 78 | if (![resourcePath isAbsolutePath]) { 79 | resourcePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:resourcePath]; 80 | } 81 | _resourcePath = [resourcePath copy]; 82 | } 83 | 84 | - (void)cleanCache 85 | { 86 | NSFileManager *manager = [NSFileManager defaultManager]; 87 | [manager removeItemAtPath:self.cachePath error:nil]; 88 | [manager createDirectoryAtPath:self.cachePath 89 | withIntermediateDirectories:YES 90 | attributes:@{} 91 | error:NULL]; 92 | } 93 | 94 | - (NSUInteger)cacheFileSize 95 | { 96 | return [self _rxr_fileSizeAtPath:self.cachePath]; 97 | } 98 | 99 | - (void)saveRoutesMapFile:(NSData *)data 100 | { 101 | NSString *filePath = [self.cachePath stringByAppendingPathComponent:RoutesMapFile]; 102 | if (data == nil) { 103 | [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 104 | } else { 105 | [data writeToFile:filePath atomically:YES]; 106 | } 107 | } 108 | 109 | - (NSData *)cacheRoutesMapFile 110 | { 111 | NSString *filePath = [self.cachePath stringByAppendingPathComponent:RoutesMapFile]; 112 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 113 | return [NSData dataWithContentsOfFile:filePath]; 114 | } 115 | 116 | return nil; 117 | } 118 | 119 | - (NSData *)resourceRoutesMapFile 120 | { 121 | NSString *filePath = [self.resourcePath stringByAppendingPathComponent:RoutesMapFile]; 122 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 123 | return [NSData dataWithContentsOfFile:filePath]; 124 | } 125 | 126 | return nil; 127 | } 128 | 129 | - (void)saveRouteFileData:(NSData *)data withRemoteURL:(NSURL *)url 130 | { 131 | NSString *filePath = [self _rxr_cachedRouteFilePathForRemoteURL:url]; 132 | if (data == nil) { 133 | [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 134 | } else { 135 | [data writeToFile:filePath atomically:YES]; 136 | } 137 | } 138 | 139 | - (NSData *)routeFileDataForRemoteURL:(NSURL *)url 140 | { 141 | NSString *filePath = [self routeFilePathForRemoteURL:url]; 142 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 143 | return [NSData dataWithContentsOfFile:filePath]; 144 | } 145 | 146 | return nil; 147 | } 148 | 149 | - (NSString *)routeFilePathForRemoteURL:(NSURL *)url 150 | { 151 | NSString *filePath = [self _rxr_cachedRouteFilePathForRemoteURL:url]; 152 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 153 | return filePath; 154 | } 155 | 156 | filePath = [self _rxr_resourceRouteFilePathForRemoteURL:url]; 157 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 158 | return filePath; 159 | } 160 | 161 | return nil; 162 | } 163 | 164 | - (NSURL *)routeFileURLForRemoteURL:(NSURL *)url 165 | { 166 | if (url == nil) { 167 | return nil; 168 | } 169 | 170 | NSString *filePath = [self routeFilePathForRemoteURL:url]; 171 | return [[NSFileManager defaultManager] fileExistsAtPath:filePath] ? [NSURL fileURLWithPath:filePath] : nil; 172 | } 173 | 174 | #pragma mark - Private methods 175 | 176 | - (NSString *)_rxr_cachedRouteFilePathForRemoteURL:(NSURL *)url 177 | { 178 | NSString *cacheKey = url.absoluteString; 179 | if (RXRConfig.URLCacheConverter != nil) { 180 | cacheKey = [RXRConfig.URLCacheConverter cacheKeyForURL:url]; 181 | } 182 | 183 | NSString *md5 = [[cacheKey dataUsingEncoding:NSUTF8StringEncoding] md5]; 184 | NSString *filename = [self.cachePath stringByAppendingPathComponent:md5]; 185 | return [filename stringByAppendingPathExtension:url.pathExtension]; 186 | } 187 | 188 | - (NSString *)_rxr_resourceRouteFilePathForRemoteURL:(NSURL *)url 189 | { 190 | NSString *filename = nil; 191 | NSArray *pathComps = url.pathComponents; 192 | if (pathComps.count > 2) { // 取后两位作为文件路径 193 | filename = [[pathComps subarrayWithRange:NSMakeRange(pathComps.count - 2, 2)] componentsJoinedByString:@"/"]; 194 | } else { 195 | filename = url.path; 196 | } 197 | return [self.resourcePath stringByAppendingPathComponent:filename]; 198 | } 199 | 200 | - (NSUInteger)_rxr_fileSizeAtPath:(NSString *)path 201 | { 202 | NSFileManager *manager = [NSFileManager defaultManager]; 203 | NSUInteger totalSize = 0; 204 | NSArray *contents = [manager contentsOfDirectoryAtPath:path error:nil]; 205 | 206 | for (NSString *name in contents) { 207 | NSString *itemPath = [path stringByAppendingPathComponent:name]; 208 | NSDictionary *attrs = [manager attributesOfItemAtPath:itemPath error:nil]; 209 | NSFileAttributeType type = [attrs objectForKey:NSFileType]; 210 | if (!type) { 211 | continue; 212 | } 213 | if ([type isEqualToString:NSFileTypeDirectory]) { 214 | totalSize += [self _rxr_fileSizeAtPath:itemPath]; 215 | } else if ([attrs objectForKey:NSFileSize]) { 216 | totalSize += [[attrs objectForKey:NSFileSize] unsignedIntegerValue]; 217 | } 218 | } 219 | 220 | return totalSize; 221 | } 222 | 223 | @end 224 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRRouteManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRouteManager.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/11/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | #import 11 | 12 | @class RXRRoute; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | typedef NS_ENUM(NSInteger, RXRRouteUpdateState) { 17 | RXRRouteUpdateStateFailed, // 更新失败 18 | RXRRouteUpdateStateSuccess, // 更新成功 19 | RXRRouteUpdateStateCancelled, // 更新取消 20 | }; 21 | 22 | /** 23 | * `RXRRouteManager` 提供了对路由信息的管理和使用接口。 24 | */ 25 | @interface RXRRouteManager : NSObject 26 | 27 | /** 28 | * uri 和 html 对应关系的路由表。 29 | * 30 | * 路由表读取路径优先级: 31 | * - 缓存路由表。 32 | * - 配置中地址的根目录下的路由表。 33 | * 34 | * 路由表更新策略: 35 | * - 对象创建后。 36 | * - 当通过 `htmlForURI:` 没有找到目标 html 时。 37 | */ 38 | @property (readonly, nullable) NSArray *routes; 39 | 40 | /* 41 | * 当前 routes 版本。 42 | */ 43 | @property (nonatomic, readonly, nullable) NSString *routesVersion; 44 | 45 | /** 46 | * 读取 Routes Map 信息的 URL 地址。路由表应该由服务器提供。 47 | */ 48 | @property (nonatomic, copy) NSURL *routesMapURL; 49 | 50 | @property (nonatomic, weak, nullable) id dataValidator; 51 | 52 | /** 53 | * 单例方法,获取一个 RXRRouteManager 实例。 54 | */ 55 | + (RXRRouteManager *)sharedInstance; 56 | 57 | /** 58 | * 设置缓存地址。如果是相对路径的话,则认为其是相对于应用缓存路径 59 | */ 60 | - (void)setCachePath:(NSString *)cachePath; 61 | 62 | /** 63 | * 设置 rexxar 资源地址。如果是相对路径的话,则认为其是相对于 main bundle 路径。 64 | */ 65 | - (void)setResoucePath:(NSString *)resourcePath; 66 | 67 | /** 68 | * 查找 uri 对应的本地 html 文件 URL。先查 Cache,再查 Resource 69 | */ 70 | - (nullable NSURL *)localHtmlURLForURI:(NSURL *)uri; 71 | 72 | /** 73 | * 查找 uri 对应的服务器上 html 文件。 74 | */ 75 | - (nullable NSURL *)remoteHtmlURLForURI:(NSURL *)uri; 76 | 77 | /** 78 | * 立即同步路由表。 79 | * 80 | * @param completion 同步完成后的回调,可以为 nil 81 | */ 82 | - (void)updateRoutesWithCompletion:(nullable void (^)(RXRRouteUpdateState state))completion; 83 | 84 | - (NSComparisonResult)compareVersion:(NSString *)version toVersion:(NSString *)otherVersion; 85 | 86 | @end 87 | 88 | NS_ASSUME_NONNULL_END 89 | 90 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRURLSessionDemux.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRURLSessionDemux.h 3 | // Rexxar 4 | // 5 | // Created by XueMing on 31/03/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | 实现 url protocol session 共享,确保每个 url protocol client 触发和回调在同一个线程里。 15 | */ 16 | @interface RXRURLSessionDemux : NSObject 17 | 18 | - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration; 19 | 20 | @property (nonatomic, copy, readonly) NSURLSessionConfiguration *sessionConfiguration; 21 | @property (nonatomic, strong, readonly) NSURLSession *session; 22 | 23 | - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request 24 | delegate:(id)delegate 25 | modes:(nullable NSArray *)modes; 26 | 27 | - (void)performBlockWithTask:(NSURLSessionTask *)task 28 | block:(dispatch_block_t)block; 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRURLSessionDemux.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRURLSessionDemux.m 3 | // Rexxar 4 | // 5 | // Created by XueMing on 31/03/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRURLSessionDemux.h" 10 | 11 | @interface RXRURLSessionDemuxTask : NSObject 12 | 13 | - (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id)delegate modes:(NSArray *)modes; 14 | 15 | @property (nonatomic, strong, readonly) NSURLSessionDataTask *task; 16 | @property (nonatomic, weak, readonly) id delegate; 17 | @property (nonatomic, strong, readonly) NSThread *thread; 18 | @property (nonatomic, copy, readonly) NSArray *modes; 19 | 20 | - (void)performBlock:(dispatch_block_t)block; 21 | - (void)invalidate; 22 | 23 | @end 24 | 25 | @interface RXRURLSessionDemuxTask () 26 | 27 | @property (nonatomic, strong) NSURLSessionDataTask *task; 28 | @property (nonatomic, weak) id delegate; 29 | @property (nonatomic, strong) NSThread *thread; 30 | @property (nonatomic, copy) NSArray *modes; 31 | 32 | @end 33 | 34 | @implementation RXRURLSessionDemuxTask 35 | 36 | - (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id)delegate modes:(NSArray *)modes 37 | { 38 | self = [super init]; 39 | if (self != nil) { 40 | _task = task; 41 | _delegate = delegate; 42 | _thread = [NSThread currentThread]; 43 | _modes = [modes copy]; 44 | } 45 | return self; 46 | } 47 | 48 | - (void)performBlock:(dispatch_block_t)block 49 | { 50 | NSAssert(_delegate != nil, nil); 51 | NSAssert(_thread != nil, nil); 52 | 53 | if (_delegate != nil && _thread != nil) { 54 | [self performSelector:@selector(performBlockOnClientThread:) onThread:_thread withObject:[block copy] waitUntilDone:YES modes:[_modes copy]]; 55 | } 56 | } 57 | 58 | - (void)performBlockOnClientThread:(dispatch_block_t)block 59 | { 60 | NSAssert([NSThread currentThread] == _thread, nil); 61 | block(); 62 | } 63 | 64 | - (void)invalidate 65 | { 66 | _delegate = nil; 67 | _thread = nil; 68 | } 69 | 70 | @end 71 | 72 | 73 | @interface RXRURLSessionDemux () 74 | 75 | @property (nonatomic, copy ) NSURLSessionConfiguration *sessionConfiguration; 76 | @property (nonatomic, strong) NSURLSession *session; 77 | @property (nonatomic, strong) NSOperationQueue *sessionDelegateQueue; 78 | @property (nonatomic, strong) NSMutableDictionary *demuxTasks; 79 | 80 | @end 81 | 82 | @implementation RXRURLSessionDemux 83 | 84 | - (instancetype)init 85 | { 86 | return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; 87 | } 88 | 89 | - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration 90 | { 91 | self = [super init]; 92 | if (self) { 93 | NSString *sessionName = [NSString stringWithFormat:@"%@.%@.%p.URLSession", [[NSBundle mainBundle] bundleIdentifier], NSStringFromClass([self class]), self]; 94 | NSString *delegateQueueName = [NSString stringWithFormat:@"%@.delegateQueue", sessionName]; 95 | 96 | _sessionConfiguration = [sessionConfiguration copy]; 97 | _sessionConfiguration.timeoutIntervalForRequest = 30; 98 | _demuxTasks = [NSMutableDictionary dictionary]; 99 | _sessionDelegateQueue = [[NSOperationQueue alloc] init]; 100 | _sessionDelegateQueue.maxConcurrentOperationCount = 1; 101 | _sessionDelegateQueue.name = delegateQueueName; 102 | _session = [NSURLSession sessionWithConfiguration:_sessionConfiguration delegate:self delegateQueue:_sessionDelegateQueue]; 103 | _session.sessionDescription = sessionName; 104 | } 105 | return self; 106 | } 107 | 108 | - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request 109 | delegate:(id)delegate 110 | modes:(nullable NSArray *)modes 111 | { 112 | if ([modes count] == 0) { 113 | modes = @[NSDefaultRunLoopMode]; 114 | } 115 | 116 | NSURLSessionDataTask *dataTask = [_session dataTaskWithRequest:request]; 117 | RXRURLSessionDemuxTask *demuxTask = [[RXRURLSessionDemuxTask alloc] initWithTask:dataTask delegate:delegate modes:modes]; 118 | 119 | @synchronized (self) { 120 | _demuxTasks[@([dataTask taskIdentifier])] = demuxTask; 121 | } 122 | 123 | return dataTask; 124 | } 125 | 126 | - (void)performBlockWithTask:(NSURLSessionTask *)task block:(dispatch_block_t)block 127 | { 128 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 129 | if (demuxTask) { 130 | [demuxTask performBlock:block]; 131 | } else if (block) { 132 | block(); 133 | } 134 | } 135 | 136 | - (RXRURLSessionDemuxTask *)demuxTaskForTask:(NSURLSessionTask *)task 137 | { 138 | RXRURLSessionDemuxTask *demuxTask = nil; 139 | 140 | @synchronized (self) { 141 | demuxTask = [self.demuxTasks objectForKey:@([task taskIdentifier])]; 142 | } 143 | 144 | return demuxTask; 145 | } 146 | 147 | - (void)URLSession:(NSURLSession *)session 148 | task:(NSURLSessionTask *)task 149 | willPerformHTTPRedirection:(NSHTTPURLResponse *)response 150 | newRequest:(NSURLRequest *)newRequest 151 | completionHandler:(void (^)(NSURLRequest *))completionHandler 152 | { 153 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 154 | 155 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) { 156 | [demuxTask performBlock:^{ 157 | [demuxTask.delegate URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler]; 158 | }]; 159 | } else { 160 | completionHandler(newRequest); 161 | } 162 | } 163 | 164 | - (void)URLSession:(NSURLSession *)session 165 | task:(NSURLSessionTask *)task 166 | didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 167 | completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler 168 | { 169 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 170 | 171 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) { 172 | [demuxTask performBlock:^{ 173 | [demuxTask.delegate URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; 174 | }]; 175 | } else { 176 | completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); 177 | } 178 | } 179 | 180 | - (void)URLSession:(NSURLSession *)session 181 | task:(NSURLSessionTask *)task 182 | needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler 183 | { 184 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 185 | 186 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:task:needNewBodyStream:)]) { 187 | [demuxTask performBlock:^{ 188 | [demuxTask.delegate URLSession:session task:task needNewBodyStream:completionHandler]; 189 | }]; 190 | } else { 191 | completionHandler(nil); 192 | } 193 | } 194 | 195 | - (void)URLSession:(NSURLSession *)session 196 | task:(NSURLSessionTask *)task 197 | didSendBodyData:(int64_t)bytesSent 198 | totalBytesSent:(int64_t)totalBytesSent 199 | totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend 200 | { 201 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 202 | 203 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)]) { 204 | [demuxTask performBlock:^{ 205 | [demuxTask.delegate URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend]; 206 | }]; 207 | } 208 | } 209 | 210 | - (void)URLSession:(NSURLSession *)session 211 | task:(NSURLSessionTask *)task 212 | didCompleteWithError:(NSError *)error 213 | { 214 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:task]; 215 | 216 | @synchronized (self) { 217 | [self.demuxTasks removeObjectForKey:@(demuxTask.task.taskIdentifier)]; 218 | } 219 | 220 | // Call the delegate if required. In that case we invalidate the task info on the client thread 221 | // after calling the delegate, otherwise the client thread side of the -performBlock: code can 222 | // find itself with an invalidated task info. 223 | 224 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) { 225 | [demuxTask performBlock:^{ 226 | [demuxTask.delegate URLSession:session task:task didCompleteWithError:error]; 227 | [demuxTask invalidate]; 228 | }]; 229 | } else { 230 | [demuxTask invalidate]; 231 | } 232 | } 233 | 234 | - (void)URLSession:(NSURLSession *)session 235 | dataTask:(NSURLSessionDataTask *)dataTask 236 | didReceiveResponse:(NSURLResponse *)response 237 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler 238 | { 239 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:dataTask]; 240 | 241 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) { 242 | [demuxTask performBlock:^{ 243 | [demuxTask.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler]; 244 | }]; 245 | } else { 246 | completionHandler(NSURLSessionResponseAllow); 247 | } 248 | } 249 | 250 | - (void)URLSession:(NSURLSession *)session 251 | dataTask:(NSURLSessionDataTask *)dataTask 252 | didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask 253 | { 254 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:dataTask]; 255 | 256 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:dataTask:didBecomeDownloadTask:)]) { 257 | [demuxTask performBlock:^{ 258 | [demuxTask.delegate URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask]; 259 | }]; 260 | } 261 | } 262 | 263 | - (void)URLSession:(NSURLSession *)session 264 | dataTask:(NSURLSessionDataTask *)dataTask 265 | didReceiveData:(NSData *)data 266 | { 267 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:dataTask]; 268 | 269 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) { 270 | [demuxTask performBlock:^{ 271 | [demuxTask.delegate URLSession:session dataTask:dataTask didReceiveData:data]; 272 | }]; 273 | } 274 | } 275 | 276 | - (void)URLSession:(NSURLSession *)session 277 | dataTask:(NSURLSessionDataTask *)dataTask 278 | willCacheResponse:(NSCachedURLResponse *)proposedResponse 279 | completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler 280 | { 281 | RXRURLSessionDemuxTask *demuxTask = [self demuxTaskForTask:dataTask]; 282 | 283 | if ([demuxTask.delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) { 284 | [demuxTask performBlock:^{ 285 | [demuxTask.delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler]; 286 | }]; 287 | } else { 288 | completionHandler(proposedResponse); 289 | } 290 | } 291 | 292 | @end 293 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRViewController+Router.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRViewController+Router.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/26/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRViewController.h" 10 | #import "RXRRouteManager.h" 11 | 12 | @implementation RXRViewController (Router) 13 | 14 | #pragma mark - Route File Interface 15 | 16 | + (void)updateRouteFilesWithCompletion:(void (^)(RXRRouteUpdateState state))completion 17 | { 18 | RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; 19 | [routeManager updateRoutesWithCompletion:completion]; 20 | } 21 | 22 | + (BOOL)isRouteExistForURI:(NSURL *)uri 23 | { 24 | RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; 25 | NSURL *remoteHtml = [routeManager remoteHtmlURLForURI:uri]; 26 | if (remoteHtml) { 27 | return YES; 28 | } 29 | return NO; 30 | } 31 | 32 | + (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri 33 | { 34 | RXRRouteManager *routeManager = [RXRRouteManager sharedInstance]; 35 | NSURL *localHtml = [routeManager localHtmlURLForURI:uri]; 36 | if (localHtml) { 37 | return YES; 38 | } 39 | return NO; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRViewController.h 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/4/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @protocol RXRWidget; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | * `RXRViewController` 是一个 Rexxar Container。 18 | * 它提供了一个使用 web 技术 html, css, javascript 开发 UI 界面的容器。 19 | */ 20 | @interface RXRViewController : RXRWebViewController 21 | 22 | /** 23 | * 对应的 uri。 24 | */ 25 | @property (nonatomic, copy) NSURL *uri; 26 | 27 | /** 28 | * activities 代表该 Rexxar Container 可以响应的协议。 29 | */ 30 | @property (nonatomic, copy) NSArray> *widgets; 31 | 32 | /** 33 | * 初始化一个RXRViewController。 34 | * 35 | * @param uri 该页面对应的 uri。 36 | * 37 | * @discussion 会根据 uri 从 Route Map File 中选择对应本地 html 文件加载。如果无本地 html 文件,则从服务器加载 html 资源。 38 | * 在 WKWebView 中,远程 URL 需要注意跨域问题。 39 | */ 40 | - (instancetype)initWithURI:(NSURL *)uri; 41 | 42 | /** 43 | * 初始化一个RXRViewController。 44 | * 45 | * @param uri 该页面对应的 uri。 46 | * @param htmlFileURL 该页面对应的 html file url。 47 | * 48 | * @discussion 会根据 uri 从 Route Map File 中选择对应本地 html 文件加载。如果无本地 html 文件,则从服务器加载 html 资源。 49 | * 在 WKWebView 中,远程 URL 需要注意跨域问题。 50 | */ 51 | - (instancetype)initWithURI:(NSURL *)uri htmlFileURL:(nullable NSURL *)htmlFileURL NS_DESIGNATED_INITIALIZER; 52 | 53 | /** 54 | * 重新加载 WebView。 55 | */ 56 | - (void)reloadWebView; 57 | 58 | /** 59 | * 通知 WebView 页面显示,缺省会在 viewWillAppear 里调用。本方法可以由业务层自主定制向 WebView 通知 onPageVisible 的时机。 60 | */ 61 | - (void)onPageVisible; 62 | 63 | /** 64 | * 通知 WebView 页面消失,缺省会在 viewDidDisappear 里调用。本方法可以由业务层自主定制向 WebView 通知 onPageInvisible 的时机。 65 | */ 66 | - (void)onPageInvisible; 67 | 68 | /** 69 | * 调用 WebView 的一个 JavaScript 函数,并传入一个 json 串作为参数。 70 | * 71 | * @param function 调用的函数。 72 | * @param jsonParameter 传递的参数,json 串。 73 | */ 74 | - (void)callJavaScript:(NSString *)function jsonParameter:(nullable NSString *)jsonParameter; 75 | 76 | @end 77 | 78 | 79 | #pragma mark - Public Route Methods 80 | 81 | /** 82 | * 暴露出 Route 相关的接口。 83 | */ 84 | @interface RXRViewController (Router) 85 | 86 | /** 87 | * 更新 Route Files。 88 | * 89 | * @param completion 更新完成后将执行这个 block。 90 | */ 91 | + (void)updateRouteFilesWithCompletion:(nullable void (^)(RXRRouteUpdateState state))completion; 92 | 93 | /** 94 | * 判断路由表是否存在对应于 uri 的 route 信息。 95 | * 96 | * @param uri 待判断的 uri。 97 | */ 98 | + (BOOL)isRouteExistForURI:(NSURL *)uri; 99 | 100 | /** 101 | * 判断本地(缓存,或预置资源中)是否已经下载了存在对应于 uri 的 route 信息的资源。 102 | * 103 | * @param uri 待判断的 uri。 104 | */ 105 | + (BOOL)isLocalRouteFileExistForURI:(NSURL *)uri; 106 | 107 | @end 108 | 109 | NS_ASSUME_NONNULL_END 110 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRWebViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRWebViewController.h 3 | // Rexxar 4 | // 5 | // Created by XueMing on 15/05/2017. 6 | // Copyright © 2017 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @protocol RXRWebViewDelegate 14 | 15 | @optional 16 | - (BOOL)webView:(WKWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(WKNavigationType)navigationType; 17 | - (void)webViewDidStartLoad:(WKWebView *)webView; 18 | - (void)webViewDidFinishLoad:(WKWebView *)webView; 19 | - (void)webView:(WKWebView *)webView didFailLoadWithError:(nullable NSError *)error; 20 | - (void)webViewDidTerminate:(WKWebView *)webView; 21 | @end 22 | 23 | @interface RXRWebViewController : UIViewController 24 | 25 | @property (nonatomic, readonly) WKWebView *webView; 26 | @property (nonatomic, assign) BOOL shouldRegisterWebViewCustomSchemes; // default is YES; 27 | 28 | - (void)loadRequest:(NSURLRequest *)request; 29 | - (CGRect)webViewFrame; 30 | - (void)initWebView; 31 | - (void)openExternalURL:(NSURL *)url completionHandler:(void (^ __nullable)(BOOL success))completion; 32 | 33 | @end 34 | 35 | @interface RXRWebViewStore: NSObject 36 | 37 | + (NSString *)IDForWebView:(WKWebView *)webView; 38 | + (void)setWebView:(WKWebView *)webView withWebViewID:(NSString *)webViewID; 39 | + (WKWebView *)webViewForID:(NSString *)webViewID; 40 | 41 | + (void)addInterceptor:(NSURLProtocol *)interceptor withWebViewID:(NSString *)webViewID; 42 | + (void)removeInterceptor:(NSURLProtocol *)interceptor withWebViewID:(NSString *)webViewID; 43 | + (NSArray *)interceptorsForWebViewID:(NSString *)webViewID; 44 | 45 | @end 46 | 47 | NS_ASSUME_NONNULL_END 48 | -------------------------------------------------------------------------------- /Rexxar/Core/RXRWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRWidget.h 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/5/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | 12 | @class RXRViewController; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | /** 16 | * `RXRWidget` 是一个 Widget 协议。 17 | * 实现 RXRWidget 协议的类将完成一个 Web 对 Native 的功能调用。 18 | */ 19 | @protocol RXRWidget 20 | 21 | /** 22 | * 判断该 Widget 是否要对该 URL 做出反应。 23 | * 24 | * @param URL 对应的 URL。 25 | */ 26 | - (BOOL)canPerformWithURL:(NSURL *)URL; 27 | 28 | /** 29 | * 对该 URL,执行 Widget 的各项准备工作。 30 | * 31 | * @param URL 对应的 URL。 32 | */ 33 | - (void)prepareWithURL:(NSURL *)URL; 34 | 35 | /** 36 | * 执行 Widget 的操作。 37 | * 38 | * @param controller 执行该 Widget 的 Controller。 39 | */ 40 | - (void)performWithController:(RXRViewController *)controller; 41 | 42 | @end 43 | 44 | NS_ASSUME_NONNULL_END 45 | -------------------------------------------------------------------------------- /Rexxar/Decorator/RXRDecorator.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRDecorator.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/17/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * `RXRDecorator` 是一个请求装修器协议。请求装修器代表了一个可用于修改 http 请求的类的协议。 15 | * 符合该协议的类可以用于修改 Rexxar-Container 内发出的 Http 请求。 16 | */ 17 | @protocol RXRDecorator 18 | 19 | /** 20 | * 判断是否应该拦截侦听该请求 21 | * 22 | * @param request 对应请求 23 | */ 24 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request; 25 | 26 | /** 27 | 对该请求的修改动作 28 | 29 | @param originalRequest 修改前的 request 30 | @return 修改后的 request 31 | */ 32 | - (NSURLRequest *)decoratedRequestFromOriginalRequest:(NSURLRequest *)originalRequest; 33 | 34 | @optional 35 | 36 | /** 37 | * 准备执行对该请求的修改动作 38 | * 39 | * @param request 对应请求 40 | */ 41 | - (void)prepareWithRequest:(NSURLRequest *)request; 42 | 43 | @end 44 | 45 | NS_ASSUME_NONNULL_END 46 | -------------------------------------------------------------------------------- /Rexxar/Decorator/RXRRequestDecorator.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRequestDecorator.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 7/1/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | #import "RXRDecorator.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | /** 16 | * `RXRRequestDecorator` 是一个具体的请求装修器。 17 | * 通过该装修器对 Rexxar-Conntainer 中发出的请求作修改。增加其 url 参数,以及增添自定义 header。 18 | */ 19 | @interface RXRRequestDecorator : NSObject 20 | 21 | /** 22 | * 需要为请求增添的自定义 header。 23 | */ 24 | @property (nonatomic, copy) NSDictionary *headers; 25 | 26 | /** 27 | * 需要为请求增添的 url 参数。 28 | */ 29 | @property (nonatomic, copy) NSDictionary *parameters; 30 | 31 | /** 32 | * 初始化一个请求装修器。 33 | * 34 | * @param headers 需要为请求增添的自定义 header 35 | * @param parameters 需要为请求增添的 url 参数 36 | */ 37 | - (instancetype)initWithHeaders:(NSDictionary *)headers 38 | parameters:(NSDictionary *)parameters; 39 | 40 | @end 41 | 42 | NS_ASSUME_NONNULL_END 43 | -------------------------------------------------------------------------------- /Rexxar/Decorator/RXRRequestDecorator.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRequestDecorator.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 7/1/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRURLRequestSerialization.h" 10 | #import "RXRRequestDecorator.h" 11 | 12 | #import "NSURL+Rexxar.h" 13 | 14 | @interface RXRRequestDecorator () 15 | 16 | @property (nonatomic, strong) RXRHTTPRequestSerializer *requestSerializer; 17 | 18 | @end 19 | 20 | @implementation RXRRequestDecorator 21 | 22 | - (instancetype)init 23 | { 24 | return [self initWithHeaders:@{} parameters:@{}]; 25 | } 26 | 27 | - (instancetype)initWithHeaders:(NSDictionary *)headers 28 | parameters:(NSDictionary *)parameters 29 | { 30 | self = [super init]; 31 | if (self) { 32 | _headers = [headers copy]; 33 | _parameters = [parameters copy]; 34 | _requestSerializer = [[RXRHTTPRequestSerializer alloc] init]; 35 | } 36 | return self; 37 | } 38 | 39 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request 40 | { 41 | // 只处理 Http 和 https 请求 42 | if (![request.URL rxr_isHttpOrHttps]) { 43 | return NO; 44 | } 45 | 46 | // 不处理静态资源文件 47 | if ([[self class] _rxr_isStaticResourceRequest:request]) { 48 | return NO; 49 | } 50 | 51 | return YES; 52 | } 53 | 54 | - (NSURLRequest *)decoratedRequestFromOriginalRequest:(NSURLRequest *)originalRequest 55 | { 56 | NSMutableURLRequest *mutableRequest = [originalRequest mutableCopy]; 57 | 58 | // Request headers 59 | [self.headers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 60 | if ([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]]){ 61 | [mutableRequest setValue:obj forHTTPHeaderField:key]; 62 | } 63 | }]; 64 | 65 | // Request url parameters 66 | NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.parameters]; 67 | [self _rxr_addQuery:mutableRequest.URL.query toParameters:parameters]; 68 | 69 | // Note: mutableRequest.URL.query has been added to the paramters, _requestSerializer will generate a new NSURLRequest 70 | // object from the parameters every time when it decorates a request. If we don't remove query from URL, request may 71 | // contain duplicated query string if the original request is decorated more than 2 times. 72 | NSURLComponents *comp = [[NSURLComponents alloc] initWithURL:mutableRequest.URL resolvingAgainstBaseURL:NO]; 73 | comp.query = nil; 74 | mutableRequest.URL = comp.URL; 75 | 76 | return [_requestSerializer requestBySerializingRequest:mutableRequest 77 | withParameters:parameters 78 | error:nil]; 79 | } 80 | 81 | #pragma mark - Private methods 82 | 83 | - (void)_rxr_addQuery:(NSString *)query toParameters:(NSMutableDictionary *)parameters 84 | { 85 | if (!parameters) { 86 | return; 87 | } 88 | 89 | for (NSString *pair in [query componentsSeparatedByString:@"&"]) { 90 | NSArray *keyValuePair = [pair componentsSeparatedByString:@"="]; 91 | if (keyValuePair.count != 2) { 92 | continue; 93 | } 94 | 95 | NSString *key = [keyValuePair[0] stringByRemovingPercentEncoding]; 96 | if (parameters[key] == nil) { 97 | parameters[key] = [keyValuePair[1] stringByRemovingPercentEncoding]; 98 | } 99 | } 100 | } 101 | 102 | 103 | + (BOOL)_rxr_isStaticResourceRequest:(NSURLRequest *)request 104 | { 105 | NSString *extension = request.URL.pathExtension; 106 | if ([extension isEqualToString:@"js"] || 107 | [extension isEqualToString:@"css"] || 108 | [extension isEqualToString:@"html"]) { 109 | return YES; 110 | } 111 | return NO; 112 | } 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /Rexxar/Decorator/RXRRequestInterceptor.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRequestInterceptor.h 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 09/03/2017. 6 | // Copyright © 2017 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRNSURLProtocol.h" 10 | #import "RXRDecorator.h" 11 | #import "RXRProxy.h" 12 | 13 | /** 14 | * `RXRRequestInterceptor` 是一个 Rexxar-Container 的请求侦听器。 15 | * 这个侦听器用于修改请求,比如增添请求的 url 参数,添加自定义的 http header。 16 | * 17 | */ 18 | @interface RXRRequestInterceptor : RXRNSURLProtocol 19 | 20 | @property (class, nonatomic, copy, nullable) NSArray> *decorators; 21 | @property (class, nonatomic, copy, nullable) NSArray> *proxies; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Rexxar/Decorator/RXRRequestInterceptor.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRequestInterceptor.m 3 | // Rexxar 4 | // 5 | // Created by bigyelow on 09/03/2017. 6 | // Copyright © 2017 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | #import "RXRRequestInterceptor.h" 12 | #import "RXRURLSessionDemux.h" 13 | #import "RXRConfig.h" 14 | 15 | static NSArray> *_decorators; 16 | static NSArray> *_proxies; 17 | 18 | @implementation RXRRequestInterceptor 19 | 20 | #pragma mark - Properties 21 | 22 | + (NSArray> *)decorators 23 | { 24 | return _decorators; 25 | } 26 | 27 | + (void)setDecorators:(NSArray> *)decorators 28 | { 29 | _decorators = [decorators copy]; 30 | } 31 | 32 | + (NSArray> *)proxies 33 | { 34 | return _proxies; 35 | } 36 | 37 | + (void)setProxies:(NSArray> *)proxies 38 | { 39 | _proxies = [proxies copy]; 40 | } 41 | 42 | #pragma mark - Superclass methods 43 | 44 | + (BOOL)canInitWithRequest:(NSURLRequest *)request 45 | { 46 | // 请求被忽略(被标记为忽略或者已经请求过),不处理 47 | if ([self isRequestIgnored:request]) { 48 | return NO; 49 | } 50 | // 请求不是来自浏览器,不处理 51 | if (![request.allHTTPHeaderFields[@"User-Agent"] hasPrefix:@"Mozilla"]) { 52 | return NO; 53 | } 54 | 55 | for (id proxy in _proxies) { 56 | if ([proxy shouldInterceptRequest:request]) { 57 | return YES; 58 | } 59 | } 60 | 61 | for (id decorator in _decorators) { 62 | if ([decorator shouldInterceptRequest:request]){ 63 | return YES; 64 | } 65 | } 66 | 67 | return NO; 68 | } 69 | 70 | - (void)startLoading 71 | { 72 | [self beforeStartLoadingRequest]; 73 | 74 | for (id proxy in _proxies) { 75 | if ([proxy shouldInterceptRequest:self.request]) { 76 | NSURLResponse *response = [proxy responseWithRequest:self.request]; 77 | if (response != nil) { 78 | NSData *data = [proxy responseDataWithRequest:self.request]; 79 | if (data != nil) { 80 | [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 81 | [self.client URLProtocol:self didLoadData:data]; 82 | [self.client URLProtocolDidFinishLoading:self]; 83 | if ([proxy respondsToSelector:@selector(proxyDidFinishWithRequest:)]) { 84 | [proxy proxyDidFinishWithRequest:self.request]; 85 | } 86 | return; 87 | } 88 | } 89 | } 90 | } 91 | 92 | NSMutableURLRequest *newRequest = [self _rxr_decorateRequest:self.request]; 93 | NSMutableArray *modes = [NSMutableArray array]; 94 | [modes addObject:NSDefaultRunLoopMode]; 95 | 96 | NSString *currentMode = [[NSRunLoop currentRunLoop] currentMode]; 97 | if (currentMode != nil && ![currentMode isEqualToString:NSDefaultRunLoopMode]) { 98 | [modes addObject:currentMode]; 99 | } 100 | [self setModes:modes]; 101 | 102 | [NSURLProtocol setProperty:@([NSDate timeIntervalSinceReferenceDate]) forKey:@"StartTime" inRequest:newRequest]; 103 | NSURLSessionTask *dataTask = [[[self class] sharedDemux] dataTaskWithRequest:newRequest delegate:self modes:self.modes]; 104 | [dataTask resume]; 105 | [self setDataTask:dataTask]; 106 | } 107 | 108 | - (NSMutableURLRequest *)_rxr_decorateRequest:(NSURLRequest *)request 109 | { 110 | NSMutableURLRequest *newRequest = nil; 111 | 112 | if ([request isKindOfClass:[NSMutableURLRequest class]]) { 113 | newRequest = (NSMutableURLRequest *)request; 114 | } else { 115 | newRequest = [request mutableCopy]; 116 | } 117 | 118 | for (id decorator in _decorators) { 119 | if ([decorator shouldInterceptRequest:newRequest]) { 120 | if ([decorator respondsToSelector:@selector(prepareWithRequest:)]) { 121 | [decorator prepareWithRequest:newRequest]; 122 | } 123 | newRequest = [[decorator decoratedRequestFromOriginalRequest:newRequest] mutableCopy]; 124 | } 125 | } 126 | 127 | // 由于在 iOS9 及以下版本对 WKWebView 缓存支持不好,所有的请求不使用缓存 128 | if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] == NSOrderedAscending) { 129 | [newRequest setValue:nil forHTTPHeaderField:@"If-None-Match"]; 130 | [newRequest setValue:nil forHTTPHeaderField:@"If-Modified-Since"]; 131 | } 132 | 133 | [[self class] markRequestAsIgnored:newRequest]; 134 | 135 | return newRequest; 136 | } 137 | 138 | #pragma mark - NSURLSessionDelegate 139 | 140 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task 141 | willPerformHTTPRedirection:(NSHTTPURLResponse *)response 142 | newRequest:(NSURLRequest *)request 143 | completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler 144 | { 145 | NSMutableURLRequest *newRequest = [task.currentRequest mutableCopy]; 146 | [newRequest setURL:request.URL]; 147 | 148 | newRequest = [self _rxr_decorateRequest:newRequest]; 149 | completionHandler(newRequest); 150 | } 151 | 152 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 153 | { 154 | [super URLSession:session task:task didCompleteWithError:error]; 155 | 156 | NSNumber *startNum = [NSURLProtocol propertyForKey:@"StartTime" inRequest:task.originalRequest]; 157 | NSTimeInterval startTime = [startNum doubleValue]; 158 | if (RXRConfig.didCompleteRequestBlock) { 159 | RXRConfig.didCompleteRequestBlock(task.originalRequest.URL, task.response, error, [NSDate timeIntervalSinceReferenceDate] - startTime); 160 | } 161 | } 162 | 163 | @end 164 | -------------------------------------------------------------------------------- /Rexxar/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Rexxar/Proxy/RXRProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRProxy.h 3 | // Rexxar 4 | // 5 | // Created by XueMing on 2019/3/5. 6 | // Copyright © 2019 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | 本地服务代理,请求可以被代理时,从本地返回数据,否则继续发送原来的请求 15 | */ 16 | @protocol RXRProxy 17 | 18 | /** 19 | * 判断是否应该拦截侦听该请求 20 | * 21 | * @param request 对应请求 22 | */ 23 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request; 24 | 25 | /** 26 | * 当可以代理请求时,返回 NSURLResponse 对象;不能代理时返回空。 27 | */ 28 | - (nullable NSURLResponse *)responseWithRequest:(NSURLRequest *)request; 29 | 30 | /** 31 | * 当可以代理请求是,返回代理内容;不能代理时返回空。 32 | */ 33 | - (nullable NSData *)responseDataWithRequest:(NSURLRequest *)request; 34 | 35 | @optional 36 | 37 | /** 38 | * 代理工作完成时 39 | */ 40 | - (void)proxyDidFinishWithRequest:(NSURLRequest *)request; 41 | 42 | @end 43 | 44 | NS_ASSUME_NONNULL_END 45 | -------------------------------------------------------------------------------- /Rexxar/Rexxar.h: -------------------------------------------------------------------------------- 1 | // 2 | // Rexxar.h 3 | // Rexxar 4 | // 5 | // Created by XueMing on 11/10/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #ifndef _REXXAR_ 10 | #define _REXXAR_ 11 | 12 | #import "RXRConfig.h" 13 | #import "RXRViewController.h" 14 | 15 | #import "RXRWidget.h" 16 | 17 | #import "RXRNSURLProtocol.h" 18 | 19 | #import "RXRContainerInterceptor.h" 20 | #import "RXRContainerAPI.h" 21 | 22 | #import "RXRRequestInterceptor.h" 23 | #import "RXRDecorator.h" 24 | #import "RXRRequestDecorator.h" 25 | 26 | #import "NSURL+Rexxar.h" 27 | #import "NSDictionary+RXRMultipleItems.h" 28 | #import "NSString+RXRURLEscape.h" 29 | #import "NSHTTPURLResponse+Rexxar.h" 30 | 31 | #if DSK_WIDGET 32 | #import "RXRModel.h" 33 | #import "RXRNavTitleWidget.h" 34 | #import "RXRAlertDialogWidget.h" 35 | #import "RXRPullRefreshWidget.h" 36 | #endif 37 | 38 | #endif /* _REXXAR_ */ 39 | -------------------------------------------------------------------------------- /Rexxar/Widget/Model/RXRAlertDialogData.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRAlertDialogData.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRModel.h" 10 | 11 | /** 12 | * `RXRAlertDialogButton` 对话框上按钮的数据对象。 13 | */ 14 | @interface RXRAlertDialogButton : RXRModel 15 | 16 | /** 17 | * 按钮的标题文字。 18 | */ 19 | @property (nonatomic, copy, readonly) NSString *text; 20 | 21 | /** 22 | * 按按钮后将执行的动作。 23 | */ 24 | @property (nonatomic, copy, readonly) NSString *action; 25 | 26 | @end 27 | 28 | /** 29 | * `RXRAlertDialogData` 对话框的数据对象。 30 | */ 31 | @interface RXRAlertDialogData : RXRModel 32 | 33 | /** 34 | * 对话框的标题。 35 | */ 36 | @property (nonatomic, copy, readonly) NSString *title; 37 | 38 | /** 39 | * 对话框的消息。 40 | */ 41 | @property (nonatomic, copy, readonly) NSString *message; 42 | 43 | /** 44 | * 对话框的按钮。 45 | */ 46 | @property (nonatomic, readonly) NSArray *buttons; 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Rexxar/Widget/Model/RXRAlertDialogData.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRAlertDialogData.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRAlertDialogData.h" 10 | 11 | @implementation RXRAlertDialogButton 12 | 13 | - (NSString *)text 14 | { 15 | return [self.dictionary objectForKey:@"text"]; 16 | } 17 | 18 | - (NSString *)action 19 | { 20 | return [self.dictionary objectForKey:@"action"]; 21 | } 22 | 23 | @end 24 | 25 | 26 | @implementation RXRAlertDialogData 27 | 28 | - (NSString *)title 29 | { 30 | return [self.dictionary objectForKey:@"title"]; 31 | } 32 | 33 | - (NSString *)message 34 | { 35 | return [self.dictionary objectForKey:@"message"]; 36 | } 37 | 38 | - (NSArray *)buttons 39 | { 40 | NSMutableArray *result = [NSMutableArray array]; 41 | NSArray *array = [self.dictionary objectForKey:@"buttons"]; 42 | for (id dic in array) { 43 | if ([dic isKindOfClass:[NSDictionary class]]) { 44 | RXRAlertDialogButton *button = [[RXRAlertDialogButton alloc] initWithDictionary:dic]; 45 | if (button) { 46 | [result addObject:button]; 47 | } 48 | } 49 | } 50 | return result; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Rexxar/Widget/Model/RXRModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRModel.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * `RXRModel` 数据对象。 15 | * Web 对 Native 调用时可能会出发一些结构化的数据。 16 | * RXRModel 提供了对这些数据的更简便的访问方法。 17 | */ 18 | @interface RXRModel : NSObject 19 | 20 | /** 21 | * 数据对象的 json 字符串形式。 22 | */ 23 | @property (nonatomic, readonly, copy) NSString *string; 24 | 25 | /** 26 | * 数据对象的字典形式。 27 | */ 28 | @property (nonatomic, strong) NSMutableDictionary *dictionary; 29 | 30 | /** 31 | * 以 json 字符串初始化数据对象。 32 | * 33 | * @param theJsonStr 字符串 34 | */ 35 | - (id)initWithString:(NSString *)theJsonStr; 36 | 37 | /** 38 | * 以字典初始化数据对象。 39 | * 40 | * @param theDictionary 字典 41 | */ 42 | - (id)initWithDictionary:(NSDictionary *)theDictionary; 43 | 44 | @end 45 | 46 | NS_ASSUME_NONNULL_END 47 | -------------------------------------------------------------------------------- /Rexxar/Widget/Model/RXRModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRModel.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 6/28/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRModel.h" 10 | 11 | @implementation RXRModel 12 | 13 | - (id)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | self.dictionary = [NSMutableDictionary dictionary]; 18 | } 19 | return self; 20 | } 21 | 22 | - (id)initWithDictionary:(NSDictionary *)theDictionary 23 | { 24 | self = [self init]; 25 | if (self) { 26 | if (![theDictionary isKindOfClass:[NSDictionary class]]) { 27 | theDictionary = nil; 28 | } 29 | self.dictionary = [[NSMutableDictionary alloc] initWithDictionary:theDictionary]; 30 | } 31 | return self; 32 | } 33 | 34 | - (id)initWithString:(NSString *)theJsonStr 35 | { 36 | if (!theJsonStr || [theJsonStr length] <= 0) { 37 | return nil; 38 | } 39 | 40 | NSData *jsonStrData = [theJsonStr dataUsingEncoding:NSUTF8StringEncoding]; 41 | if (!jsonStrData) { 42 | return nil; 43 | } 44 | 45 | NSError *error = nil; 46 | id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonStrData 47 | options:kNilOptions 48 | error:&error]; 49 | if (error) { 50 | return nil; 51 | } 52 | 53 | NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:jsonObject]; 54 | if (!dic) { 55 | return nil; 56 | } 57 | 58 | self = [self initWithDictionary:dic]; 59 | 60 | return self; 61 | } 62 | 63 | - (NSString *)string 64 | { 65 | if (self.dictionary) { 66 | NSData *data = [NSJSONSerialization dataWithJSONObject:self.dictionary options:kNilOptions error:nil]; 67 | NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 68 | return result; 69 | } 70 | return nil; 71 | } 72 | 73 | - (void)setString:(NSString *)theJsonStr 74 | { 75 | NSError *error = nil; 76 | id jsonObject = [NSJSONSerialization JSONObjectWithData:[theJsonStr dataUsingEncoding:NSUTF8StringEncoding] 77 | options:kNilOptions 78 | error:&error]; 79 | if (error) { 80 | return; 81 | } 82 | 83 | NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:jsonObject]; 84 | if (!dic) { 85 | return; 86 | } 87 | self.dictionary = dic; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRAlertDialogWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRAlertDialogWidget.h 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/6/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRWidget.h" 10 | 11 | /** 12 | * `RXRAlertDialogWidget` 实现弹出一个对话框的功能。 13 | */ 14 | @interface RXRAlertDialogWidget : NSObject 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRAlertDialogWidget.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRAlertDialogWidget.m 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/6/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRAlertDialogWidget.h" 10 | #import "RXRViewController.h" 11 | #import "RXRAlertDialogData.h" 12 | #import "NSDictionary+RXRMultipleItems.h" 13 | #import "NSURL+Rexxar.h" 14 | 15 | @interface RXRAlertDialogWidget () 16 | 17 | @property (nonatomic, weak) RXRViewController *rexxarViewController; 18 | @property (nonatomic, strong) RXRAlertDialogData *alertDialogData; 19 | 20 | @end 21 | 22 | 23 | @implementation RXRAlertDialogWidget 24 | 25 | - (BOOL)canPerformWithURL:(NSURL *)URL 26 | { 27 | NSString *path = URL.path; 28 | if (path && [path isEqualToString:@"/widget/alert_dialog"]) { 29 | return YES; 30 | } 31 | return NO; 32 | } 33 | 34 | - (void)prepareWithURL:(NSURL *)URL 35 | { 36 | NSString *string = [[URL rxr_queryDictionary] rxr_itemForKey:@"data"]; 37 | self.alertDialogData = [[RXRAlertDialogData alloc] initWithString:string]; 38 | } 39 | 40 | - (void)performWithController:(RXRViewController *)controller 41 | { 42 | 43 | self.rexxarViewController = controller; 44 | 45 | if (!self.alertDialogData) { 46 | return; 47 | } 48 | 49 | __weak typeof(self) weakSelf = self; 50 | 51 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:self.alertDialogData.title 52 | message:self.alertDialogData.message 53 | preferredStyle:UIAlertControllerStyleAlert]; 54 | 55 | for (RXRAlertDialogButton *button in [self.alertDialogData buttons]) { 56 | UIAlertAction *action = [UIAlertAction actionWithTitle:button.text 57 | style:UIAlertActionStyleDefault 58 | handler:^(UIAlertAction *alertAction) { 59 | [weakSelf.rexxarViewController.webView evaluateJavaScript:button.action completionHandler:nil]; 60 | }]; 61 | [alert addAction:action]; 62 | } 63 | 64 | [self.rexxarViewController presentViewController:alert animated:YES completion:nil]; 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRNavTitleWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNavTitleWidget.h 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/5/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRWidget.h" 10 | 11 | /** 12 | * `RXRNavTitleWidget` 实现对导航的标题进行设置。 13 | */ 14 | @interface RXRNavTitleWidget : NSObject 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRNavTitleWidget.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNavTitleWidget.m 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/5/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRNavTitleWidget.h" 10 | #import "RXRViewController.h" 11 | #import "NSDictionary+RXRMultipleItems.h" 12 | #import "NSURL+Rexxar.h" 13 | 14 | @interface RXRNavTitleWidget () 15 | 16 | @property (nonatomic, copy) NSString *title; 17 | 18 | @end 19 | 20 | 21 | @implementation RXRNavTitleWidget 22 | 23 | - (BOOL)canPerformWithURL:(NSURL *)URL 24 | { 25 | NSString *path = URL.path; 26 | if (path && [path isEqualToString:@"/widget/nav_title"]) { 27 | return YES; 28 | } 29 | return NO; 30 | } 31 | 32 | - (void)prepareWithURL:(NSURL *)URL 33 | { 34 | self.title = [[URL rxr_queryDictionary] rxr_itemForKey:@"title"]; 35 | } 36 | 37 | - (void)performWithController:(RXRViewController *)controller 38 | { 39 | if (controller) { 40 | controller.title = self.title; 41 | } 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRPullRefreshWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRPullRefreshWidget.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/5/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRWidget.h" 10 | 11 | /** 12 | * `RXRPullRefreshWidget` 实现下拉刷新。 13 | */ 14 | @interface RXRPullRefreshWidget : NSObject 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Rexxar/Widget/RXRPullRefreshWidget.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRPullRefreshWidget.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/5/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | @import WebKit; 10 | 11 | #import "RXRPullRefreshWidget.h" 12 | #import "RXRViewController.h" 13 | #import "NSURL+Rexxar.h" 14 | #import "NSDictionary+RXRMultipleItems.h" 15 | 16 | @interface RXRPullRefreshWidget () 17 | 18 | @property (nonatomic, strong) UIRefreshControl *refreshControl; 19 | @property (nonatomic, copy) NSString *action; 20 | @property (nonatomic, assign) BOOL onRefreshStart; 21 | 22 | @end 23 | 24 | 25 | @implementation RXRPullRefreshWidget 26 | 27 | - (BOOL)canPerformWithURL:(NSURL *)URL 28 | { 29 | NSString *path = URL.path; 30 | if (path && [path isEqualToString:@"/widget/pull_to_refresh"]) { 31 | return YES; 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)prepareWithURL:(NSURL *)URL 37 | { 38 | NSDictionary *queryItems = [URL rxr_queryDictionary]; 39 | self.action = [queryItems rxr_itemForKey:@"action"]; 40 | } 41 | 42 | - (void)performWithController:(RXRViewController *)controller 43 | { 44 | if ([self.action isEqualToString:@"enable"] && !self.refreshControl.isRefreshing) { 45 | // Web 通知该页面有下拉组件 46 | if (!self.refreshControl) { 47 | self.refreshControl = [self _rxr_refreshControllerWithScrollView:controller.webView]; 48 | } 49 | } else if ([self.action isEqualToString:@"complete"]) { 50 | // Web 通知下拉动作完成 51 | [self.refreshControl endRefreshing]; 52 | self.onRefreshStart = NO; 53 | } 54 | } 55 | 56 | #pragma mark - Private 57 | 58 | - (UIRefreshControl *)_rxr_refreshControllerWithScrollView:(WKWebView *)webView 59 | { 60 | UIScrollView *scrollView = webView.scrollView; 61 | UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; 62 | [scrollView addSubview:refreshControl]; 63 | [refreshControl addTarget:self action:@selector(_rxr_refresh:) forControlEvents:UIControlEventValueChanged]; 64 | return refreshControl; 65 | } 66 | 67 | - (void)_rxr_refresh:(UIRefreshControl *)refreshControl 68 | { 69 | UIView *view = [[refreshControl superview] superview]; 70 | if ([view isKindOfClass:[WKWebView class]] && !self.onRefreshStart) { 71 | self.onRefreshStart = YES; 72 | WKWebView *webView = (WKWebView *)view; 73 | [webView evaluateJavaScript:@"window.Rexxar.Widget.PullToRefresh.onRefreshStart()" completionHandler:nil]; 74 | } 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /RexxarDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RexxarDemo 4 | // 5 | // Created by XueMing on 11/10/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.rootViewController = UINavigationController(rootViewController: RoutesViewController()) 20 | window?.makeKeyAndVisible() 21 | 22 | // Config Rexxar 23 | let routesMapURL = "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/routes.json" 24 | RXRConfig.setRoutesMapURL(URL(string: routesMapURL)!) 25 | RXRConfig.setRoutesCachePath("com.douban.RexxarDemo") 26 | RXRConfig.setRoutesResourcePath("hybrid") 27 | RXRConfig.setUserAgent("Mozilla/5.0 AppleWebKit/605.1.15 com.douban.frodo/6.11.0 Rexxar/1.2.100 iOS/12.1") 28 | RXRViewController.updateRouteFiles(completion: nil) 29 | 30 | return true 31 | } 32 | 33 | func applicationWillResignActive(_ application: UIApplication) { 34 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 35 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 36 | } 37 | 38 | func applicationDidEnterBackground(_ application: UIApplication) { 39 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 40 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 41 | } 42 | 43 | func applicationWillEnterForeground(_ application: UIApplication) { 44 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | func applicationDidBecomeActive(_ application: UIApplication) { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | func applicationWillTerminate(_ application: UIApplication) { 52 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /RexxarDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /RexxarDemo/Bridge-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Briding-Header.h 3 | // Example 4 | // 5 | // Created by Tony Li on 11/4/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | 14 | #import "RXRToastWidget.h" 15 | #import "RXRNavMenuWidget.h" 16 | #import "RXRGeoContainerAPI.h" 17 | #import "RXRLogContainerAPI.h" 18 | 19 | -------------------------------------------------------------------------------- /RexxarDemo/ContainerAPI/RXRGeoContainerAPI.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRGeoContainerAPI.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RXRGeoContainerAPI : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /RexxarDemo/ContainerAPI/RXRGeoContainerAPI.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRGeoContainerAPI.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRGeoContainerAPI.h" 10 | 11 | @implementation RXRGeoContainerAPI 12 | 13 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request 14 | { 15 | // https://rexxar-container/api/event_location 16 | if ([request.URL rxr_isHttpOrHttps] && 17 | [request.URL.host isEqualToString:@"rexxar-container"] && 18 | [request.URL.path hasPrefix:@"/api/geo"]) { 19 | 20 | return YES; 21 | } 22 | return NO; 23 | } 24 | 25 | - (NSURLResponse *)responseWithRequest:(NSURLRequest *)request 26 | { 27 | return [NSHTTPURLResponse rxr_responseWithURL:request.URL statusCode:200 headerFields:nil noAccessControl:YES]; 28 | } 29 | 30 | - (NSData *)responseData 31 | { 32 | // It's just a demo here. 33 | // You can implement your own geo service to get the real current city data. 34 | NSDictionary *dictionary = @{@"name": @"北京", 35 | @"letter": @"beijing", 36 | @"longitude": @(116.41667), 37 | @"latitude": @(39.91667)}; 38 | NSError *error; 39 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary 40 | options:NSJSONWritingPrettyPrinted 41 | error:&error]; 42 | return jsonData; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /RexxarDemo/ContainerAPI/RXRLogContainerAPI.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRLogContainerAPI.h 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/18/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | #import 12 | 13 | @interface RXRLogContainerAPI : NSObject 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /RexxarDemo/ContainerAPI/RXRLogContainerAPI.m: -------------------------------------------------------------------------------- 1 | // 2 | // FRDRXRLogContainerAPI.m 3 | // Frodo 4 | // 5 | // Created by GUO Lin on 5/18/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import "RXRLogContainerAPI.h" 10 | 11 | @implementation RXRLogContainerAPI 12 | 13 | - (BOOL)shouldInterceptRequest:(NSURLRequest *)request 14 | { 15 | // https://rexxar-container/api/event_location 16 | if ([request.URL rxr_isHttpOrHttps] && 17 | [request.URL.host isEqualToString:@"rexxar-container"] && 18 | [request.URL.path hasPrefix:@"/api/log"] && 19 | [request.HTTPMethod.uppercaseString isEqualToString:@"GET"] && 20 | [request.URL.query containsString:@"_rexxar_method=POST"]) { 21 | 22 | return YES; 23 | } 24 | return NO; 25 | } 26 | 27 | - (NSURLResponse *)responseWithRequest:(NSURLRequest *)request 28 | { 29 | return [NSHTTPURLResponse rxr_responseWithURL:request.URL statusCode:200 headerFields:nil noAccessControl:YES]; 30 | } 31 | 32 | 33 | - (NSData *)responseData 34 | { 35 | NSDictionary *dictionary = @{}; 36 | NSError *error; 37 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary 38 | options:NSJSONWritingPrettyPrinted 39 | error:&error]; 40 | return jsonData; 41 | } 42 | 43 | - (void)performWithRequest:(NSURLRequest *)request completion:(void (^)(void))completion 44 | { 45 | NSData *data = request.HTTPBody; 46 | NSString *encodeStr = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; 47 | NSString *decodeStr = [encodeStr rxr_decodingStringUsingURLEscape]; 48 | 49 | NSArray *keyValues = [decodeStr componentsSeparatedByString:@"&"]; 50 | if (keyValues.count > 0) { 51 | NSMutableDictionary *form = [NSMutableDictionary dictionary]; 52 | for (NSString *keyValue in keyValues) { 53 | NSArray *array = [keyValue componentsSeparatedByString:@"="]; 54 | if (array.count == 2) { 55 | [form setObject:array[1] forKey:array[0]]; 56 | } 57 | } 58 | 59 | if ([form rxr_itemForKey:@"event"]) { 60 | NSLog(@"Log event:%@, label:%@", [form rxr_itemForKey:@"event"], [form rxr_itemForKey:@"label"]); 61 | } 62 | } 63 | 64 | if (completion) { 65 | completion(); 66 | } 67 | } 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /RexxarDemo/FullRXRViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullRXRViewController.swift 3 | // RexxarDemo 4 | // 5 | // Created by GUO Lin on 8/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FullRXRViewController: RXRViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | view.backgroundColor = UIColor.white 16 | 17 | // Widgets 18 | let pullRefreshWidget = RXRPullRefreshWidget() 19 | let titleWidget = RXRNavTitleWidget() 20 | let alertDialogWidget = RXRAlertDialogWidget() 21 | let toastWidget = RXRToastWidget() 22 | let navMenuWidget = RXRNavMenuWidget() 23 | widgets = [titleWidget, alertDialogWidget, pullRefreshWidget, toastWidget, navMenuWidget] 24 | 25 | // Decorators 26 | let headers = ["Customer-Authorization": "Bearer token"] 27 | let parameters = ["apikey": "apikey value"] 28 | let requestDecorator = RXRRequestDecorator(headers: headers, parameters: parameters) 29 | RXRRequestInterceptor.decorators = [requestDecorator] 30 | RXRNSURLProtocol.registerRXRProtocolClass(RXRRequestInterceptor.self) 31 | 32 | // ContainerAPIs 33 | let geoContainerAPI = RXRGeoContainerAPI() 34 | let logContainerAPI = RXRLogContainerAPI() 35 | RXRContainerInterceptor.containerAPIs = [geoContainerAPI, logContainerAPI] 36 | RXRNSURLProtocol.registerRXRProtocolClass(RXRContainerInterceptor.self) 37 | } 38 | 39 | deinit { 40 | RXRNSURLProtocol.unregisterRXRProtocolClass(RXRContainerInterceptor.self) 41 | RXRNSURLProtocol.unregisterRXRProtocolClass(RXRRequestInterceptor.self) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RexxarDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | None 26 | CFBundleURLSchemes 27 | 28 | douban 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSRequiresIPhoneOS 35 | 36 | NSAppTransportSecurity 37 | 38 | NSAllowsArbitraryLoads 39 | 40 | 41 | UILaunchStoryboardName 42 | LaunchScreen 43 | UIRequiredDeviceCapabilities 44 | 45 | armv7 46 | 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | UISupportedInterfaceOrientations~ipad 54 | 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationPortraitUpsideDown 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /RexxarDemo/Library/FRDToast/FRDToast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FRDToast.swift 3 | // FRDToast 4 | // 5 | // Created by 李俊 on 15/11/11. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let toastStartY: CGFloat = 50 12 | private let toastFinalY: CGFloat = 80 13 | private let miniToastShowTime: TimeInterval = 1.5 14 | private let horizonalMargin: CGFloat = 25 15 | 16 | @objc public enum FRDToastMaskType: Int { 17 | case `default` // allow user interactions while Toast is displayed 18 | case clear // don't allow user interactions 19 | } 20 | 21 | public class FRDToast: NSObject { 22 | 23 | /** 24 | 设置文本字体,如果不设置该属性,缺省为 HelveticaNeue-Medium 字体。 25 | */ 26 | public static var titleFont = UIFont(name:"HelveticaNeue-Medium", size:15) { 27 | didSet { 28 | sharedToast.toastView.titleFont = titleFont 29 | } 30 | } 31 | 32 | private static let sharedToast = FRDToast() 33 | 34 | private lazy var toastView: ToastView = { 35 | let view = ToastView() 36 | view.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin] 37 | return view 38 | }() 39 | 40 | private lazy var overlayView: UIControl = { 41 | 42 | let application = UIApplication.shared 43 | let window = application.delegate?.window ?? nil 44 | 45 | var windowBounds = CGRect.zero 46 | if let bounds = window?.bounds { 47 | windowBounds = bounds 48 | } 49 | 50 | let view = UIControl(frame: windowBounds) 51 | view.backgroundColor = UIColor.clear 52 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 53 | view.isUserInteractionEnabled = false 54 | return view 55 | }() 56 | 57 | private var fadeOutTimer: Timer? 58 | private var toastShowTime = miniToastShowTime 59 | private var isFadeIn = false 60 | private var isFadeOut = false 61 | 62 | private func showToast(_ title: String, color: UIColor, maskType: FRDToastMaskType, image: UIImage?, loadingAnimateOrNot: Bool) { 63 | 64 | let application = UIApplication.shared 65 | let window = application.delegate?.window ?? nil 66 | 67 | var windowBounds = CGRect.zero 68 | if let bounds = window?.bounds { 69 | windowBounds = bounds 70 | } 71 | 72 | overlayView.frame = windowBounds 73 | if overlayView.superview == nil { 74 | for window in UIApplication.shared.windows { 75 | let windowOnMainScreen = window.screen == UIScreen.main 76 | let windowIsVisible = !window.isHidden && window.alpha > 0 77 | let windowLevelNormal = window.windowLevel == UIWindow.Level.normal 78 | 79 | if windowOnMainScreen && windowIsVisible && windowLevelNormal { 80 | window.addSubview(overlayView) 81 | break 82 | } 83 | } 84 | } else { 85 | overlayView.superview?.bringSubviewToFront(overlayView) 86 | } 87 | 88 | switch maskType { 89 | case .default: 90 | overlayView.isUserInteractionEnabled = false 91 | case .clear: 92 | overlayView.isUserInteractionEnabled = true 93 | } 94 | 95 | toastShowTime = displayDurationForTitle(title) 96 | if fadeOutTimer != nil { 97 | fadeOutTimer?.invalidate() 98 | } 99 | 100 | if toastView.superview != nil && !isFadeIn { 101 | let newToastView = ToastView() 102 | newToastView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin] 103 | newToastView.updateContent(title, color: color, image: image, loadingAnimateOrNot: loadingAnimateOrNot) 104 | switchToastViewWithAnimation(newToastView) 105 | } else { 106 | toastView.updateContent(title, color: color, image: image, loadingAnimateOrNot: loadingAnimateOrNot) 107 | addToastViewToOverLayView(toastView, center: nil) 108 | showToastWithAnimation() 109 | } 110 | } 111 | 112 | private func addToastViewToOverLayView(_ toastView: ToastView, center: CGPoint?) { 113 | let toastViewSize = toastView.sizeThatFits(CGSize(width: (overlayView.bounds.width - 2 * horizonalMargin), height: 0)) 114 | toastView.bounds = CGRect(x: 0, y: 0, width: toastViewSize.width, height: toastViewSize.height) 115 | 116 | if toastView.superview == nil { 117 | overlayView.addSubview(toastView) 118 | toastView.alpha = 0 119 | let centerX = overlayView.bounds.width / 2 120 | toastView.center = (center != nil ? center! : CGPoint(x: centerX, y: toastStartY + toastView.bounds.height/2)) 121 | } 122 | } 123 | 124 | private func showToastWithAnimation() { 125 | if isFadeIn { 126 | return 127 | } 128 | isFadeIn = true 129 | isFadeOut = false 130 | UIView.animate(withDuration: TimeInterval(0.5 * (1 - toastView.alpha)), delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in 131 | self.toastView.center.y = toastFinalY + self.toastView.bounds.height/2 132 | self.toastView.alpha = 1 133 | }, completion: { (_) -> Void in 134 | if self.toastView.alpha == 1 { 135 | self.isFadeIn = false 136 | if self.toastView.loadingAnimateOrNot { 137 | self.toastView.startLoadingAnimation() 138 | return 139 | } 140 | 141 | self.fadeOutTimer = Timer(timeInterval: self.toastShowTime, target: self, selector: #selector(self.dismiss), userInfo: nil, repeats: false) 142 | RunLoop.main.add(self.fadeOutTimer!, forMode: RunLoop.Mode.common) 143 | } 144 | }) 145 | } 146 | 147 | private func switchToastViewWithAnimation(_ newToastView: ToastView) { 148 | let oldToastView = toastView 149 | if oldToastView.loadingAnimateOrNot { 150 | oldToastView.stopLoadingAnimation() 151 | } 152 | 153 | toastView = newToastView 154 | newToastView.alpha = 0 155 | addToastViewToOverLayView(newToastView, center: oldToastView.center) 156 | 157 | if isFadeOut || isFadeIn { 158 | oldToastView.removeFromSuperview() 159 | newToastView.alpha = oldToastView.alpha 160 | showToastWithAnimation() 161 | return 162 | } 163 | 164 | UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in 165 | oldToastView.alpha = 0 166 | }, completion: { (_) -> Void in 167 | if self.toastView.alpha == 0 { 168 | oldToastView.removeFromSuperview() 169 | } 170 | }) 171 | 172 | UIView.animate(withDuration: TimeInterval(0.5 * (1 - newToastView.alpha)), delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in 173 | newToastView.alpha = 1 174 | }, completion: nil) 175 | 176 | showToastWithAnimation() 177 | } 178 | 179 | @objc private func dismiss() { 180 | if isFadeOut || toastView.alpha == 0 { 181 | return 182 | } 183 | isFadeIn = false 184 | isFadeOut = true 185 | if toastView.loadingAnimateOrNot { 186 | toastView.stopLoadingAnimation() 187 | } 188 | 189 | UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { () -> Void in 190 | self.toastView.center.y = toastStartY 191 | self.toastView.alpha = 0 192 | }, completion: { (_) -> Void in 193 | if self.toastView.alpha == 0 { 194 | self.isFadeOut = false 195 | self.toastView.removeFromSuperview() 196 | self.overlayView.removeFromSuperview() 197 | self.fadeOutTimer = nil 198 | } 199 | }) 200 | } 201 | 202 | private func showStaticToast(_ status: String, color: UIColor, image: UIImage?) { 203 | showToast(status, color: color, maskType: .default, image: image, loadingAnimateOrNot: false) 204 | } 205 | 206 | private func displayDurationForTitle(_ title: String) -> TimeInterval { 207 | let nsTitle = title as NSString 208 | let time = max(TimeInterval(nsTitle.length)*0.06 + 0.5, miniToastShowTime) 209 | return min(time, 5.0) 210 | } 211 | } 212 | 213 | // MARK: public function for show 214 | 215 | public extension FRDToast { 216 | 217 | /** 218 | 灰色,用于展示信息的提示。 219 | 220 | - Parameter status: 文本信息 221 | */ 222 | @objc 223 | class func showInfo(_ status: String) { 224 | showInfo(status, image: nil) 225 | } 226 | 227 | /** 228 | 灰色,用于展示带图片的信息提示。 229 | 230 | - Parameter status: 文本信息 231 | - Parameter image: 图片 232 | */ 233 | @objc 234 | class func showInfo(_ status: String, image: UIImage?) { 235 | let color = UIColor(hex: 0x494949, alpha: 0.96) 236 | FRDToast.sharedToast.showStaticToast(status, color: color, image: image) 237 | } 238 | 239 | /** 240 | 绿色,用于成功的提示。 241 | 242 | - Parameter status: 文本信息 243 | */ 244 | @objc 245 | class func showSuccess(_ status: String) { 246 | showSuccess(status, image: nil) 247 | } 248 | 249 | /** 250 | 绿色,用于带图片的成功的提示。 251 | 252 | - Parameter status: 文本信息 253 | - Parameter image: 图片 254 | */ 255 | @objc 256 | class func showSuccess(_ status: String, image: UIImage?) { 257 | let color = UIColor(hex: 0x42bd56, alpha: 0.96) 258 | FRDToast.sharedToast.showStaticToast(status, color: color, image: image) 259 | } 260 | 261 | /** 262 | 红色,用于失败、警告信息,比如某项操作失败,密码错误等。 263 | 264 | - Parameter status: 展示的文本信息 265 | */ 266 | @objc 267 | class func showError(_ status: String) { 268 | showError(status, image: nil) 269 | } 270 | 271 | /** 272 | 红色,用于带图片的失败、警告信息,比如某项操作失败,密码错误等。 273 | 274 | - Parameter status: 展示的文本信息 275 | - Parameter image: 图片 276 | 277 | */ 278 | @objc 279 | class func showError(_ status: String, image: UIImage?) { 280 | let color = UIColor(hex: 0xff4055, alpha: 0.96) 281 | FRDToast.sharedToast.showStaticToast(status, color: color, image: image) 282 | } 283 | 284 | /** 285 | 显示一个自己定制的 Toast。 286 | 287 | - Parameter status: 展示的文本信息 288 | - Parameter image: 图片 289 | - Parameter backgroundColor: 背景色 290 | - Parameter maskType: 交互类型 291 | */ 292 | @objc 293 | class func show(_ status: String, backgroundColor: UIColor, image: UIImage?, maskType: FRDToastMaskType) { 294 | FRDToast.sharedToast.showStaticToast(status, color: backgroundColor, image: image) 295 | } 296 | 297 | /** 298 | 使 Toast 消失。 299 | */ 300 | @objc 301 | class func dismiss() { 302 | FRDToast.sharedToast.dismiss() 303 | } 304 | 305 | /** 306 | 检查 Toast 的可见性。 307 | */ 308 | @objc 309 | class func isVisible() -> Bool { 310 | let toastView = FRDToast.sharedToast.toastView 311 | return toastView.superview != nil && toastView.alpha == 1.0 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /RexxarDemo/Library/FRDToast/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // Frodo 4 | // 5 | // Created by 李俊 on 15/12/7. 6 | // Copyright © 2015年 Douban Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let animationDuration: TimeInterval = 2.4 12 | 13 | @objc class LoadingView: UIView { 14 | 15 | var lineWidth: CGFloat = 5 { 16 | didSet { 17 | setNeedsLayout() 18 | } 19 | } 20 | 21 | var strokeColor = UIColor(hex: 0x42BD56) { 22 | didSet { 23 | ringLayer.strokeColor = strokeColor.cgColor 24 | rightPointLayer.strokeColor = strokeColor.cgColor 25 | leftPointLayer.strokeColor = strokeColor.cgColor 26 | } 27 | } 28 | 29 | fileprivate let ringLayer = CAShapeLayer() 30 | fileprivate let pointSuperLayer = CALayer() 31 | fileprivate let rightPointLayer = CAShapeLayer() 32 | fileprivate let leftPointLayer = CAShapeLayer() 33 | fileprivate var isAnimating = false 34 | 35 | init(frame: CGRect, color: UIColor?) { 36 | super.init(frame: frame) 37 | strokeColor = color ?? strokeColor 38 | 39 | ringLayer.contentsScale = UIScreen.main.scale 40 | ringLayer.strokeColor = strokeColor.cgColor 41 | ringLayer.fillColor = UIColor.clear.cgColor 42 | ringLayer.lineCap = CAShapeLayerLineCap.round 43 | ringLayer.lineJoin = CAShapeLayerLineJoin.bevel 44 | 45 | layer.addSublayer(ringLayer) 46 | layer.addSublayer(pointSuperLayer) 47 | 48 | rightPointLayer.strokeColor = strokeColor.cgColor 49 | rightPointLayer.lineCap = CAShapeLayerLineCap.round 50 | pointSuperLayer.addSublayer(rightPointLayer) 51 | 52 | leftPointLayer.strokeColor = strokeColor.cgColor 53 | leftPointLayer.lineCap = CAShapeLayerLineCap.round 54 | pointSuperLayer.addSublayer(leftPointLayer) 55 | 56 | NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) 57 | } 58 | 59 | required init?(coder aDecoder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | override func layoutSubviews() { 64 | super.layoutSubviews() 65 | 66 | let centerPoint = CGPoint(x: bounds.width/2, y: bounds.height/2) 67 | let radius = bounds.width/2 - lineWidth 68 | let path = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle:CGFloat(-Double.pi), endAngle: CGFloat(Double.pi * 0.6), clockwise: true) 69 | 70 | ringLayer.lineWidth = lineWidth 71 | ringLayer.path = path.cgPath 72 | ringLayer.frame = bounds 73 | 74 | let x = bounds.width/2 - CGFloat(sin(Double.pi * 50.0/180.0)) * radius 75 | let y = bounds.height/2 - CGFloat(sin(Double.pi * 40.0/180.0)) * radius 76 | let rightPoint = CGPoint(x: bounds.width - x, y: y) 77 | let leftPoint = CGPoint(x: x, y: y) 78 | 79 | let rightPointPath = UIBezierPath() 80 | rightPointPath.move(to: rightPoint) 81 | rightPointPath.addLine(to: rightPoint) 82 | rightPointLayer.path = rightPointPath.cgPath 83 | rightPointLayer.lineWidth = lineWidth 84 | 85 | let leftPointPath = UIBezierPath() 86 | leftPointPath.move(to: leftPoint) 87 | leftPointPath.addLine(to: leftPoint) 88 | leftPointLayer.path = leftPointPath.cgPath 89 | leftPointLayer.lineWidth = lineWidth 90 | 91 | pointSuperLayer.frame = bounds 92 | } 93 | 94 | func startAnimation() { 95 | 96 | if isAnimating { return } 97 | pointSuperLayer.isHidden = false 98 | 99 | let keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.216 as Double), NSNumber(value: 0.396 as Double), NSNumber(value: 0.8 as Double), NSNumber(value: 1 as Int32)] 100 | 101 | // pointSuperLayer animation 102 | 103 | let pointKeyAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z") 104 | pointKeyAnimation.duration = animationDuration 105 | pointKeyAnimation.repeatCount = Float.infinity 106 | pointKeyAnimation.values = [0, (2 * Double.pi * 0.375 + 2 * Double.pi), (4 * Double.pi), (4 * Double.pi), (4 * Double.pi + 0.3 * Double.pi)] 107 | pointKeyAnimation.keyTimes = keyTimes 108 | pointSuperLayer.add(pointKeyAnimation, forKey: nil) 109 | 110 | // ringLayer animation 111 | 112 | let ringAnimationGroup = CAAnimationGroup() 113 | 114 | let ringKeyRotationAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z") 115 | ringKeyRotationAnimation.values = [0, (2 * Double.pi), (Double.pi/2 + 2 * Double.pi), (Double.pi/2 + 2 * Double.pi), (4 * Double.pi)] 116 | ringKeyRotationAnimation.keyTimes = keyTimes 117 | ringAnimationGroup.animations = [ringKeyRotationAnimation] 118 | 119 | let ringKeyStartAnimation = CAKeyframeAnimation(keyPath: "strokeStart") 120 | ringKeyStartAnimation.values = [0, 0.25, 0.35, 0.35, 0] 121 | ringKeyStartAnimation.keyTimes = keyTimes 122 | ringAnimationGroup.animations?.append(ringKeyStartAnimation) 123 | 124 | let ringKeyEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd") 125 | ringKeyEndAnimation.values = [1, 1, 0.9, 0.9, 1] 126 | ringKeyEndAnimation.keyTimes = keyTimes 127 | ringAnimationGroup.animations?.append(ringKeyEndAnimation) 128 | 129 | ringAnimationGroup.duration = animationDuration 130 | ringAnimationGroup.repeatCount = Float.infinity 131 | ringLayer.add(ringAnimationGroup, forKey: nil) 132 | 133 | // pointAnimation 134 | 135 | let rightPointKeyAnimation = CAKeyframeAnimation(keyPath: "lineWidth") 136 | rightPointKeyAnimation.values = [lineWidth, lineWidth, lineWidth * 1.4, lineWidth * 1.4, lineWidth] 137 | rightPointKeyAnimation.keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.21 as Double), NSNumber(value: 0.29 as Double), NSNumber(value: 0.88 as Double), NSNumber(value: 0.96 as Double)] 138 | rightPointKeyAnimation.duration = animationDuration 139 | rightPointKeyAnimation.repeatCount = Float.infinity 140 | rightPointLayer.add(rightPointKeyAnimation, forKey: nil) 141 | 142 | let leftPointKeyAnimation = CAKeyframeAnimation(keyPath: "lineWidth") 143 | leftPointKeyAnimation.values = [lineWidth, lineWidth, lineWidth * 1.4, lineWidth * 1.4, lineWidth] 144 | leftPointKeyAnimation.keyTimes = [NSNumber(value: 0 as Double), NSNumber(value: 0.31 as Double), NSNumber(value: 0.39 as Double), NSNumber(value: 0.8 as Double), NSNumber(value: 0.88 as Double)] 145 | leftPointKeyAnimation.duration = animationDuration 146 | leftPointKeyAnimation.repeatCount = Float.infinity 147 | leftPointLayer.add(leftPointKeyAnimation, forKey: nil) 148 | 149 | isAnimating = true 150 | } 151 | 152 | func stopAnimation() { 153 | pointSuperLayer.removeAllAnimations() 154 | ringLayer.removeAllAnimations() 155 | rightPointLayer.removeAllAnimations() 156 | leftPointLayer.removeAllAnimations() 157 | 158 | isAnimating = false 159 | } 160 | 161 | func setPercentage(_ percent: CGFloat) { 162 | pointSuperLayer.isHidden = true 163 | ringLayer.strokeEnd = percent 164 | } 165 | 166 | @objc fileprivate func appWillEnterForeground() { 167 | if isAnimating { 168 | isAnimating = false 169 | startAnimation() 170 | } 171 | } 172 | 173 | override func willMove(toWindow newWindow: UIWindow?) { 174 | if newWindow != nil && isAnimating { 175 | isAnimating = false 176 | startAnimation() 177 | } 178 | } 179 | 180 | deinit { 181 | NotificationCenter.default.removeObserver(self) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /RexxarDemo/Library/FRDToast/ToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastView.swift 3 | // FRDToast 4 | // 5 | // Created by 李俊 on 15/11/11. 6 | // Copyright © 2015年 Douban Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let horizonalMargin: CGFloat = 25 12 | private let imageTitleMargin: CGFloat = 5 13 | private let verticalMargin: CGFloat = 10 14 | 15 | class ToastView: UIView { 16 | 17 | var titleFont = UIFont(name:"HelveticaNeue-Medium", size:15) { 18 | didSet { 19 | label.font = titleFont 20 | } 21 | } 22 | 23 | fileprivate var image: UIImage? { 24 | didSet { 25 | guard let image = image else { 26 | imageView?.removeFromSuperview() 27 | imageView = nil 28 | return 29 | } 30 | 31 | if imageView == nil { 32 | imageView = UIImageView() 33 | addSubview(imageView!) 34 | } 35 | imageView?.image = image 36 | } 37 | } 38 | 39 | fileprivate var loadingView: LoadingView? 40 | fileprivate var imageView: UIImageView? 41 | fileprivate let label: UILabel 42 | 43 | var loadingAnimateOrNot: Bool = false { 44 | didSet { 45 | if !loadingAnimateOrNot { 46 | loadingView?.removeFromSuperview() 47 | loadingView = nil 48 | return 49 | } 50 | 51 | if loadingView == nil { 52 | loadingView = LoadingView(frame: .zero, color: UIColor.white) 53 | loadingView?.lineWidth = 2 54 | addSubview(loadingView!) 55 | } 56 | } 57 | } 58 | 59 | override init(frame: CGRect) { 60 | label = UILabel(frame: frame) 61 | super.init(frame: frame) 62 | 63 | label.font = titleFont 64 | label.textColor = UIColor.white 65 | label.textAlignment = .center 66 | label.numberOfLines = 3 67 | addSubview(label) 68 | 69 | layer.shadowColor = UIColor(hex: 0x000000).cgColor 70 | layer.shadowOpacity = 0.3 71 | layer.shadowOffset = CGSize(width: 0, height: 0) 72 | } 73 | 74 | required init?(coder aDecoder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | override func sizeThatFits(_ size: CGSize) -> CGSize { 79 | var labelMaxWidth = size.width - horizonalMargin * 2 80 | var width: CGFloat = horizonalMargin * 2 81 | if image != nil || loadingAnimateOrNot { 82 | let lineHeight = label.font.lineHeight 83 | var imageViewSize = CGSize.zero 84 | if let image = image { 85 | imageViewSize = computeLableSideViewSize(lineHeight, width: size.width, imageContentSize: image.size) 86 | } 87 | 88 | if loadingAnimateOrNot { 89 | imageViewSize = computeLableSideViewSize(lineHeight, width: size.width, imageContentSize: CGSize(width: lineHeight, height: lineHeight)) 90 | } 91 | 92 | let imageViewWidth = imageViewSize.width 93 | width += (imageViewWidth + imageTitleMargin) 94 | labelMaxWidth -= (imageViewWidth + imageTitleMargin) 95 | } 96 | 97 | let maxSize = CGSize(width: labelMaxWidth, height: size.height) 98 | let labelSize = label.sizeThatFits(maxSize) 99 | width += labelSize.width 100 | return CGSize(width: width, height: labelSize.height + 2 * verticalMargin) 101 | } 102 | 103 | override func layoutSubviews() { 104 | super.layoutSubviews() 105 | layer.cornerRadius = bounds.height / 2 106 | 107 | var labelMaxWidth = bounds.width - horizonalMargin * 2 108 | var x = horizonalMargin 109 | if image != nil || loadingAnimateOrNot { 110 | let lineHeight = label.font.lineHeight 111 | var sideViewSize = CGSize.zero 112 | if let image = image { 113 | sideViewSize = computeLableSideViewSize(lineHeight, width: bounds.width, imageContentSize: image.size) 114 | imageView?.frame = CGRect(x: x, y: (bounds.height - sideViewSize.height) / 2, width: sideViewSize.width, height: sideViewSize.height) 115 | } 116 | 117 | if loadingAnimateOrNot { 118 | sideViewSize = computeLableSideViewSize(lineHeight, width: bounds.width, imageContentSize: CGSize(width: lineHeight, height: lineHeight)) 119 | loadingView?.frame = CGRect(x: x, y: (bounds.height - sideViewSize.height) / 2, width: sideViewSize.width, height: sideViewSize.height) 120 | } 121 | 122 | x += (sideViewSize.width + imageTitleMargin) 123 | labelMaxWidth -= (sideViewSize.width + imageTitleMargin) 124 | } 125 | 126 | let maxSize = CGSize(width: labelMaxWidth, height: 0) 127 | let labelSize = label.sizeThatFits(maxSize) 128 | label.frame = CGRect(x: x, y: (bounds.height - labelSize.height) / 2, width: labelSize.width, height: labelSize.height) 129 | } 130 | 131 | fileprivate func computeLableSideViewSize(_ height: CGFloat, width: CGFloat, imageContentSize: CGSize) -> CGSize { 132 | let width = imageContentSize.width / imageContentSize.height * height 133 | let labelSize = label.sizeThatFits(CGSize(width: width - horizonalMargin * 2 - width - imageTitleMargin, height: 0)) 134 | if labelSize.height > height { 135 | return computeLableSideViewSize(labelSize.height, width: width, imageContentSize: imageContentSize) 136 | } 137 | 138 | return CGSize(width: width, height: height) 139 | } 140 | 141 | } 142 | 143 | // MARK: internal method 144 | 145 | extension ToastView { 146 | 147 | func updateContent(_ title: String, color: UIColor, image toastImage: UIImage?, loadingAnimateOrNot bool: Bool) { 148 | label.text = title 149 | backgroundColor = color 150 | image = toastImage 151 | loadingAnimateOrNot = bool 152 | setNeedsLayout() 153 | } 154 | 155 | func startLoadingAnimation() { 156 | loadingView?.startAnimation() 157 | } 158 | 159 | func stopLoadingAnimation() { 160 | loadingView?.stopAnimation() 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /RexxarDemo/Library/FRDToast/UIColor+helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+helper.swift 3 | // FRDToast 4 | // 5 | // Created by GUO Lin on 7/13/16. 6 | // Copyright © 2015年 Douban Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | convenience init(hex rgbHexValue: UInt, alpha: CGFloat) { 14 | self.init(red: ((CGFloat)((rgbHexValue & 0xFF0000) >> 16))/255.0, 15 | green: ((CGFloat)((rgbHexValue & 0xFF00) >> 8))/255.0, 16 | blue: ((CGFloat)(rgbHexValue & 0xFF))/255.0, 17 | alpha: alpha) 18 | } 19 | 20 | convenience init(hex rgbHexValue: UInt) { 21 | self.init(hex: rgbHexValue, alpha: 1.0) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /RexxarDemo/PartialRXRViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PartialRexxarViewController.swift 3 | // RexxarDemo 4 | // 5 | // Created by GUO Lin on 5/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PartialRexxarViewController: UIViewController { 12 | 13 | var rexxarURI: URL 14 | var childRexxarViewController: RXRViewController 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | init(URI: URL) { 21 | rexxarURI = URI 22 | childRexxarViewController = FullRXRViewController(uri: rexxarURI) 23 | 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | view.backgroundColor = UIColor.lightGray 30 | childRexxarViewController.view.backgroundColor = UIColor.white 31 | 32 | addChild(childRexxarViewController) 33 | childRexxarViewController.view.frame = CGRect(x: 0, 34 | y: 100, 35 | width: view.frame.size.width, 36 | height: 500) 37 | view.addSubview(childRexxarViewController.view) 38 | childRexxarViewController.didMove(toParent: self) 39 | } 40 | 41 | override func viewWillAppear(_ animated: Bool) { 42 | super.viewWillAppear(animated) 43 | childRexxarViewController.beginAppearanceTransition(true, animated: animated) 44 | } 45 | 46 | override func viewDidAppear(_ animated: Bool) { 47 | super.viewDidAppear(animated) 48 | childRexxarViewController.endAppearanceTransition() 49 | } 50 | 51 | override func viewWillDisappear(_ animated: Bool) { 52 | super.viewWillDisappear(animated) 53 | childRexxarViewController.beginAppearanceTransition(false, animated: animated) 54 | } 55 | 56 | override func viewDidDisappear(_ animated: Bool) { 57 | super.viewDidDisappear(animated) 58 | childRexxarViewController.endAppearanceTransition() 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /RexxarDemo/Resource/hybrid/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "deploy_time": "Sun, 09 Oct 2016 05:54:22 GMT", 5 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-c775298867.html", 6 | "uri": "douban://douban.com/rexxar_demo[/]?.*" 7 | } 8 | ], 9 | "partial_items": [ 10 | { 11 | "deploy_time": "Sun, 09 Oct 2016 05:54:22 GMT", 12 | "remote_file": "https://raw.githubusercontent.com/douban/rexxar-web/master/example/dist/rexxar/demo-c775298867.html", 13 | "uri": "douban://partial.douban.com/rexxar_demo/_.*" 14 | } 15 | ], 16 | "deploy_time": "Sun, 09 Oct 2016 05:54:22 GMT" 17 | } 18 | -------------------------------------------------------------------------------- /RexxarDemo/RoutesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutesViewController.swift 3 | // RexxarDemo 4 | // 5 | // Created by Tony Li on 11/25/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RoutesViewController: UITableViewController { 12 | 13 | fileprivate let URIs = [URL(string: "douban://douban.com/rexxar_demo")!, 14 | URL(string: "douban://partial.douban.com/rexxar_demo/_.s")!] 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | navigationController?.navigationBar.isTranslucent = false; 19 | 20 | title = "URIs" 21 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 22 | } 23 | 24 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 25 | return URIs.count 26 | } 27 | 28 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 29 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 30 | cell.textLabel?.text = URIs[(indexPath as NSIndexPath).row].absoluteString 31 | return cell 32 | } 33 | 34 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 35 | let uri = URIs[(indexPath as NSIndexPath).row] 36 | if (indexPath as NSIndexPath).row == 0 { 37 | 38 | let controller = FullRXRViewController(uri: uri) 39 | navigationController?.pushViewController(controller, animated: true) 40 | } else if (indexPath as NSIndexPath).row == 1 { 41 | let controller = PartialRexxarViewController(URI: uri) 42 | navigationController?.pushViewController(controller, animated: true) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/Model/Menu/RXRMenuItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRMenuItem.h 3 | // Frodo 4 | // 5 | // Created by Tony Li on 11/25/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | #import 12 | 13 | @interface RXRMenuItem : RXRModel 14 | 15 | @property (nonatomic, copy, readonly) NSString *type; 16 | @property (nonatomic, copy, readonly) NSString *title; 17 | @property (nonatomic, copy, readonly) UIColor *color; 18 | @property (nonatomic, copy, readonly) NSURL *uri; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/Model/Menu/RXRMenuItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRMenuItem.m 3 | // Frodo 4 | // 5 | // Created by Tony Li on 11/25/15. 6 | // Copyright © 2015 Douban Inc. All rights reserved. 7 | // 8 | 9 | 10 | #import "RXRMenuItem.h" 11 | 12 | @implementation RXRMenuItem 13 | 14 | - (NSString *)type 15 | { 16 | return [self.dictionary objectForKey:@"type"]; 17 | } 18 | 19 | - (NSString *)title 20 | { 21 | return [self.dictionary objectForKey:@"title"]; 22 | } 23 | 24 | - (NSString *)color 25 | { 26 | return [self.dictionary objectForKey:@"color"]; 27 | } 28 | 29 | - (NSURL *)uri 30 | { 31 | return [NSURL URLWithString:[self.dictionary objectForKey:@"uri"]]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/RXRNavMenuWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNavMenuWidget.h 3 | // RexxarDemo 4 | // 5 | // Created by GUO Lin on 5/5/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RXRNavMenuWidget : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/RXRNavMenuWidget.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRNavMenuWidget.m 3 | // RexxarDemo 4 | // 5 | // Created by GUO Lin on 5/5/16. 6 | // Copyright © 2016 Douban Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | #import "RXRNavMenuWidget.h" 14 | #import "RXRMenuItem.h" 15 | 16 | 17 | @interface RXRNavMenuWidget () 18 | 19 | @property (nonatomic, copy) NSArray *menuItems; 20 | 21 | @end 22 | 23 | 24 | @implementation RXRNavMenuWidget 25 | 26 | - (BOOL)canPerformWithURL:(NSURL *)URL 27 | { 28 | NSString *path = URL.path; 29 | if (path && [path isEqualToString:@"/widget/nav_menu"]) { 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | - (void)prepareWithURL:(NSURL *)URL 36 | { 37 | NSString *string = [[URL rxr_queryDictionary] rxr_itemForKey:@"data"]; 38 | NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; 39 | NSArray *itemJSONs = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; 40 | if ([itemJSONs isKindOfClass:[NSArray class]] && itemJSONs.count > 0) { 41 | NSMutableArray *menuItems = [NSMutableArray array]; 42 | for (id JSON in itemJSONs) { 43 | if ([JSON isKindOfClass:[NSDictionary class]]) { 44 | [menuItems addObject:[[RXRMenuItem alloc] initWithDictionary:JSON]]; 45 | } 46 | } 47 | self.menuItems = [menuItems copy]; 48 | } 49 | } 50 | 51 | - (void)performWithController:(RXRViewController *)controller 52 | { 53 | if (!self.menuItems || self.menuItems.count == 0) { 54 | return; 55 | } 56 | 57 | NSMutableArray *items = [NSMutableArray array]; 58 | [self.menuItems enumerateObjectsUsingBlock:^(RXRMenuItem *menu, NSUInteger idx, BOOL *stop) { 59 | UIBarButtonItem *item = [self _rxr_buildMenuItem:menu]; 60 | item.tag = idx; 61 | [items addObject:item]; 62 | }]; 63 | controller.navigationItem.rightBarButtonItems = items; 64 | } 65 | 66 | 67 | #pragma mark - Private methods 68 | 69 | - (void)_rxr_buttonItemAction:(UIBarButtonItem *)item 70 | { 71 | RXRMenuItem *menu = self.menuItems[item.tag]; 72 | NSLog(@"Action go to uri: %@", menu.uri); 73 | } 74 | 75 | - (UIBarButtonItem *)_rxr_buildMenuItem:(RXRMenuItem *)menu 76 | { 77 | UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:menu.title 78 | style:UIBarButtonItemStylePlain 79 | target:self 80 | action:@selector(_rxr_buttonItemAction:)]; 81 | return item; 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/RXRToastWidget.h: -------------------------------------------------------------------------------- 1 | // 2 | // RXRToastWidget.h 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RXRToastWidget : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /RexxarDemo/Widget/RXRToastWidget.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRToastWidget.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 8/19/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | #import "RXRToastWidget.h" 13 | 14 | #import "RexxarDemo-Swift.h" 15 | 16 | @interface RXRToastWidget () 17 | 18 | @property (nonatomic, copy) NSString *level; 19 | @property (nonatomic, copy) NSString *message; 20 | 21 | @end 22 | 23 | 24 | @implementation RXRToastWidget 25 | 26 | - (BOOL)canPerformWithURL:(NSURL *)URL 27 | { 28 | NSString *path = URL.path; 29 | if (path && [path isEqualToString:@"/widget/toast"]) { 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | 36 | - (void)prepareWithURL:(NSURL *)URL 37 | { 38 | NSDictionary *queryItems = [URL rxr_queryDictionary]; 39 | self.level = [queryItems rxr_itemForKey:@"level"]; 40 | self.message = [queryItems rxr_itemForKey:@"message"]; 41 | } 42 | 43 | - (void)performWithController:(RXRViewController *)controller 44 | { 45 | if ([self.level isEqualToString:@"info"]) { 46 | [FRDToast showSuccess:self.message]; 47 | } else if ([self.level isEqualToString:@"error"]) { 48 | [FRDToast showInfo:self.message]; 49 | } else if ([self.level isEqualToString:@"fatal"]) { 50 | [FRDToast showError:self.message]; 51 | } 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /RexxarTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /RexxarTests/RXRRouteFileCacheTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RXRRouteFileCacheTests.m 3 | // Rexxar 4 | // 5 | // Created by GUO Lin on 5/12/16. 6 | // Copyright © 2016 Douban.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "RXRCacheFileInterceptor.h" 12 | #import "RXRRouteFileCache.h" 13 | #import "RXRConfig.h" 14 | 15 | @interface RXRRouteFileCacheTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation RXRRouteFileCacheTests 20 | 21 | - (void)setUp 22 | { 23 | NSString *resourcePath = [[NSBundle bundleForClass:self.class] pathForResource:@"www" ofType:nil]; 24 | [RXRConfig setRoutesResourcePath:resourcePath]; 25 | 26 | [RXRConfig setRoutesCachePath:[[NSUUID UUID] UUIDString]]; 27 | [RXRCacheFileInterceptor registerRXRProtocolClass:[RXRCacheFileInterceptor class]]; 28 | } 29 | 30 | + (void)tearDown 31 | { 32 | [RXRCacheFileInterceptor unregisterRXRProtocolClass:[RXRCacheFileInterceptor class]]; 33 | } 34 | 35 | + (NSURLSession *)session 36 | { 37 | if ([RXRConfig useCustomScheme]) { 38 | return [NSURLSession sessionWithConfiguration:[RXRConfig requestsURLSessionConfiguration]]; 39 | } 40 | return [NSURLSession sharedSession]; 41 | } 42 | 43 | - (void)testCacheJS 44 | { 45 | NSURL *resourceURL = [NSURL URLWithString:@"http://img3.doubanio.com/f/shire/3d5cb5d1155d18c20ab9bd966387432a8a9f2008/js/core/_init_.js"]; 46 | 47 | XCTestExpectation *expect = [self expectationWithDescription:@"Resource cached"]; 48 | [[[self.class session] dataTaskWithRequest:[self webResourceRequest:resourceURL] 49 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 50 | 51 | if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL]) { 52 | [expect fulfill]; 53 | } 54 | }] resume]; 55 | 56 | [self waitForExpectationsWithTimeout:3 handler:nil]; 57 | } 58 | 59 | - (void)testCacheCss 60 | { 61 | // 404 URL 62 | NSURL *resourceURL = [NSURL URLWithString:@"https://img3.doubanio.com/f/shire/7e852f2bb1782270ae227988d79adc8e7acb1e30/css/frontpage/_init_.css"]; 63 | 64 | XCTestExpectation *expect = [self expectationWithDescription:@"Resource cached"]; 65 | [[[self.class session] dataTaskWithRequest:[self webResourceRequest:resourceURL] 66 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 67 | 68 | if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL] == nil) { 69 | [expect fulfill]; 70 | } 71 | }] resume]; 72 | 73 | [self waitForExpectationsWithTimeout:3 handler:nil]; 74 | } 75 | 76 | - (void)testNoCacheResource 77 | { 78 | NSURL *resourceURL = [NSURL URLWithString:@"http://cdn.staticfile.org/jquery/2.1.1-rc2/jquery.js"]; 79 | 80 | XCTestExpectation *expect = [self expectationWithDescription:@"Resource should not be cached"]; 81 | [[[self.class session] dataTaskWithRequest:[self webResourceRequest:resourceURL] 82 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 83 | 84 | if (data && [[RXRRouteFileCache sharedInstance] routeFileURLForRemoteURL:resourceURL]) { 85 | [expect fulfill]; 86 | } 87 | }] resume]; 88 | 89 | [self waitForExpectationsWithTimeout:30 handler:nil]; 90 | } 91 | 92 | - (NSMutableURLRequest *)webResourceRequest:(NSURL *)url 93 | { 94 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 95 | [request addValue:@"Mozilla" forHTTPHeaderField:@"User-Agent"]; 96 | return request; 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /RexxarTests/RXRRouteManagerTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RouteTests.m 3 | // Rexxar 4 | // 5 | // Created by Tony Li on 11/24/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | #import 9 | 10 | #import "RXRRouteManager.h" 11 | #import "RXRRouteFileCache.h" 12 | #import "RXRViewController.h" 13 | #import "RXRConfig.h" 14 | #import "RXRRoute.h" 15 | 16 | @interface RXRRouteManagerTests : XCTestCase 17 | 18 | @end 19 | 20 | 21 | @implementation RXRRouteManagerTests 22 | 23 | - (void)setUp 24 | { 25 | [RXRConfig setRoutesCachePath:[[NSUUID UUID] UUIDString]]; 26 | NSString *resourcePath = [[NSBundle bundleForClass:self.class] pathForResource:@"www" ofType:nil]; 27 | [RXRConfig setRoutesResourcePath:resourcePath]; 28 | 29 | NSURL *routesMapURL = [NSURL URLWithString:@"https://rexxar.douban.com/api/routes"];; 30 | [RXRConfig setRoutesMapURL:routesMapURL]; 31 | } 32 | 33 | - (void)testRoutes 34 | { 35 | NSURL *uri = [NSURL URLWithString:@"douban://douban.com/subject_collection/123"]; 36 | 37 | [RXRViewController updateRouteFilesWithCompletion:NULL]; 38 | 39 | [self expectationForPredicate:[self predicateForURI:uri routable:YES] 40 | evaluatedWithObject:[NSObject new] 41 | handler:nil]; 42 | 43 | [self expectationForPredicate:[self predicateForURI:[NSURL URLWithString:@"douban://douban.com/foo"] routable:NO] 44 | evaluatedWithObject:[NSObject new] 45 | handler:nil]; 46 | 47 | [self waitForExpectationsWithTimeout:10 handler:nil]; 48 | } 49 | 50 | - (void)testLocalRoutes 51 | { 52 | NSString *uri = @"douban://douban.com/subject_collection/123"; 53 | BOOL found = NO; 54 | 55 | NSURL *remoteHtmlURL = [[RXRRouteManager sharedInstance] remoteHtmlURLForURI:[NSURL URLWithString:uri]]; 56 | if (remoteHtmlURL) { 57 | found = YES; 58 | } 59 | XCTAssertTrue(found); 60 | } 61 | 62 | - (void)testCompareVersion 63 | { 64 | XCTAssert([[RXRRouteManager sharedInstance] compareVersion:@"6.36.0" toVersion:@"6.36.1"] == NSOrderedAscending); 65 | XCTAssert([[RXRRouteManager sharedInstance] compareVersion:@"6.36.0" toVersion:@"6.36.0"] == NSOrderedSame); 66 | XCTAssert([[RXRRouteManager sharedInstance] compareVersion:@"6.36.0" toVersion:@"6.35.100"] == NSOrderedDescending); 67 | XCTAssert([[RXRRouteManager sharedInstance] compareVersion:@"6.36.4" toVersion:@"6.36.4.1"] == NSOrderedAscending); 68 | } 69 | 70 | - (void)testInitializeRoutesFromResource 71 | { 72 | // Resource routes version: 6.4.0 73 | NSString *jsonWithLowVersion = @"{\"version\": \"6.3.0\", \"items\": []}"; 74 | NSString *jsonWithNoVersion = @"{\"items\": []}"; 75 | for (NSString *json in @[jsonWithNoVersion, jsonWithLowVersion]) { 76 | NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; 77 | [[RXRRouteFileCache sharedInstance] saveRoutesMapFile:data]; 78 | 79 | XCTAssertNotNil([[RXRRouteFileCache sharedInstance] cacheRoutesMapFile]); 80 | XCTAssertNotNil([[RXRRouteFileCache sharedInstance] resourceRoutesMapFile]); 81 | 82 | RXRRouteManager *manager = [[RXRRouteManager alloc] init]; 83 | manager.routesMapURL = [RXRConfig routesMapURL]; 84 | 85 | XCTAssertNil([[RXRRouteFileCache sharedInstance] cacheRoutesMapFile]); 86 | 87 | XCTAssertTrue(manager.routes.count == 1); 88 | XCTAssertTrue([manager.routesVersion isEqualToString:@"6.4.0"]); 89 | } 90 | } 91 | 92 | - (void)testInitializeRoutesFromCache 93 | { 94 | NSString *json = @"{\"version\": \"6.6.0\", \"items\": []}"; 95 | NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; 96 | [[RXRRouteFileCache sharedInstance] saveRoutesMapFile:data]; 97 | 98 | RXRRouteManager *manager = [[RXRRouteManager alloc] init]; 99 | manager.routesMapURL = [RXRConfig routesMapURL]; 100 | 101 | XCTAssertNotNil([[RXRRouteFileCache sharedInstance] cacheRoutesMapFile]); 102 | XCTAssertNotNil([[RXRRouteFileCache sharedInstance] resourceRoutesMapFile]); 103 | 104 | XCTAssertTrue(manager.routes.count == 0); 105 | XCTAssertTrue([manager.routesVersion isEqualToString:@"6.6.0"]); 106 | } 107 | 108 | - (NSPredicate *)predicateForURI:(NSURL *)uri routable:(BOOL)routable 109 | { 110 | return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * bindings) { 111 | id instance = [[RXRRouteManager sharedInstance] remoteHtmlURLForURI:uri]; 112 | return routable ? instance != nil : instance == nil; 113 | }]; 114 | } 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /RexxarTests/RexxarTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // RexxarTests.m 3 | // RexxarTests 4 | // 5 | // Created by Tony Li on 11/20/15. 6 | // Copyright © 2015 Douban.Inc. All rights reserved. 7 | // 8 | #import 9 | 10 | #import "Rexxar.h" 11 | #import "RXRRequestInterceptor.h" 12 | #import "RXRRouteFileManager.h" 13 | #import "RequestDecorator.h" 14 | 15 | @interface RexxarTests : XCTestCase 16 | 17 | @property (nonatomic, readonly) RXRRouteFileManager *routeFileManager; 18 | @property (nonatomic, readonly) id decorater; 19 | 20 | @end 21 | 22 | @implementation RexxarTests 23 | 24 | - (void)setUp 25 | { 26 | NSURL *routesMapURL = [NSURL URLWithString:@"http://rexxar.douban.com/api/routes"]; 27 | _routeFileManager = [[RXRRouteFileManager alloc] initWithRoutesMapURL:routesMapURL 28 | cacheDirectory:[[NSUUID UUID] UUIDString] 29 | resourceDirectory:[NSBundle bundleForClass:self.class].bundlePath]; 30 | } 31 | 32 | - (void)testInterceptAPI 33 | { 34 | NSURL *url = [NSURL URLWithString:@"http://frodo.douban.com/jsonp/subject_collection/movie_free_stream/items?os=ios&loc_id=108288&start=0&count=18&_=1448948380006&callback=jsonp1"]; 35 | XCTAssertTrue([RXRRequestIntercepter isRequestInterceptable:[self webResourceRequest:url]]); 36 | 37 | url = [NSURL URLWithString:@"http://frodo.douban.com/api/v2/recommend_feed"]; 38 | XCTAssertTrue([RXRRequestIntercepter isRequestInterceptable:[self webResourceRequest:url]]); 39 | } 40 | 41 | - (NSMutableURLRequest *)webResourceRequest:(NSURL *)url 42 | { 43 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 44 | [request addValue:@"Mozilla" forHTTPHeaderField:@"User-Agent"]; 45 | return request; 46 | } 47 | 48 | @end 49 | 50 | -------------------------------------------------------------------------------- /RexxarTests/www/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6.4.0", 3 | "items":[ 4 | {"remote_file":"subject_collection.html","uri":"douban:\/\/douban.com\/subject_collection\/(\\w+)[\/]?.*"} 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/images/Rexxar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douban/rexxar-ios/cbad1a5f3c86a3c74ecdf4020892452354d81acf/docs/images/Rexxar.png --------------------------------------------------------------------------------