├── .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 | [](https://travis-ci.org/douban/rexxar-ios)
4 | []()
5 | []()
6 | [](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 | [](https://travis-ci.org/douban/rexxar-ios)
4 | []()
5 | []()
6 | [](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
--------------------------------------------------------------------------------