前提
要用 turbolinks-iOS 來寫一個(gè) Hybrid 的 App 的前提是你的網(wǎng)站使用了 turbolinks, 如何使用 turbolinks, 請看這里
安裝
通過 Cocoapods 或者 Carthage 安裝,都是直接引用了 github 上的地址。
github "turbolinks/turbolinks-ios" "3.0"
或者
pod 'Turbolinks', :git => 'https://github.com/turbolinks/turbolinks-ios.git', branch: 'swift-3.0'
turbolinks-iOS 是使用純 swift 寫的,支持 iOS 8.0 以上的版本。如果項(xiàng)目已經(jīng)使用了 swift 3.0, 需要使用 3.0 的分支。雖然官方官方說 3.0的分支他們沒有在生產(chǎn)環(huán)境下用過,但是我們用下來沒有什么問題。
使用
首先建議把 turbolinks-iOS 的代碼都下載下來,先跑一下他的demo,demo 雖然很簡單,但是基本上把能碰到的情況都寫上去了,比如網(wǎng)頁請求失敗,遇到需要登錄的情況等。
創(chuàng)建Session
使用 turbolinks-iOS,最重要的一個(gè)概念就是 session, session 控制著網(wǎng)頁的前進(jìn)和返回,也是網(wǎng)頁和原生代碼之間交互的通道。創(chuàng)建 session, 設(shè)置 delegate, 同時(shí)設(shè)置好 processPool。 如果你有多個(gè) session, 并且想要在多個(gè) session 之間共享 cookies,那就需要?jiǎng)?chuàng)建一個(gè)共享的pool,比如通過 singleton 的方式。
fileprivate lazy var webViewConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(self, name: "turbolinksDemo")
configuration.processPool = self.webViewProcessPool
configuration.applicationNameForUserAgent = "TurbolinksDemo"
return configuration
}()
fileprivate lazy var session: Session = {
let session = Session(webViewConfiguration: self.webViewConfiguration)
session.delegate = self
return session
}()
實(shí)現(xiàn) Delegate
iOS 里適合做前進(jìn)后退這件事最合適的就是 UINavigationController 了,因此通常會(huì)在 navigationController 中新建 session 實(shí)例,并把自身作為這個(gè)session 的 delegate。作為 session 的delegate,必須實(shí)現(xiàn)兩個(gè)方法。
- <code>func session(_ session: Session, didProposeVisitToURL URL: URL, withAction action: Action)</code>, 網(wǎng)頁上任何的訪問新網(wǎng)頁的事件,都會(huì)回調(diào)這個(gè)方法。
func session(_ session: Session, didProposeVisitToURL URL: URL, withAction action: Action) {
let visitable = JDXLVisitableViewController(url: URL)
if action == .Advance {
pushViewController(visitable, animated: true)
} else if action == .Replace {
if viewControllers.count == 1 {
var controllers = viewControllers
controllers[0] = visitable
setViewControllers(controllers, animated: false)
} else {
popViewController(animated: false)
pushViewController(visitable, animated: false)
}
}
// DO NOT FORGET TO CALL visit
session.visit(visitable)
}
代碼很直觀,每次訪問一個(gè)新的URL時(shí),都根據(jù) action 值來判斷是 push 進(jìn)一個(gè)新的 viewcontroller, 還是替換當(dāng)前的 viewcontroller。 替換也就是 pop 出當(dāng)前的controller,push進(jìn)新的。
在處理完 viewcontroller 之間的關(guān)系之后,還要調(diào)用 session.visit(visitable),通知背后的 WKWebView 訪問新的地址。
- <code>func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError)</code>
這是當(dāng)請求失敗時(shí),處理回調(diào)的函數(shù)。 比如服務(wù)器端返回 401, 可以彈出登陸界面。 網(wǎng)絡(luò)不好,或者404時(shí),可以顯示自定義的錯(cuò)誤頁面。
func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) {
NSLog("ERROR: %@", error)
guard let visitableViewController = visitable as? JDXLVisitableViewController, let errorCode = ErrorCode(rawValue: error.code) else { return }
switch errorCode {
case .httpFailure:
let statusCode = error.userInfo["statusCode"] as! Int
switch statusCode {
case 400:
....
//do something
case 401:
presentAuthenticationViewController(visitable: visitableViewController)
case 404:
visitableViewController.presentError(error: .HTTPNotFoundError)
default:
visitableViewController.presentError(error: TurbolinkError(HTTPStatusCode: statusCode))
}
case .networkFailure:
visitableViewController.presentError(error: .NetworkError)
}
}
處理 Form 提交
Turbolinks 默認(rèn)不接受標(biāo)準(zhǔn)的 HTML Form提交, 原因如下:
By default, Turbolinks for iOS prevents standard HTML form submissions. This is because a form submission often results in redirection to a different URL, which means the Visitable view controller’s URL would change in place.
Instead, we recommend submitting forms with JavaScript using XMLHttpRequest, and using the response to tell Turbolinks where to navigate afterwards. See Redirecting After a Form Submission in the Turbolinks documentation for more details.
官網(wǎng)的這句話就是我 pull request 的,_, 就不翻譯了。 改用 Ajax 的方式提交 Form, 在返回的結(jié)果中, 調(diào)用 <code> Turbolinks.visit(some_url) </code> 來指向新的頁面。
Turbolinks 和 Turbolinks-iOS 是如何通信的
Turbolinks-iOS 使用的 WKWebView 如何捕獲網(wǎng)頁上的頁面訪問事件,比如點(diǎn)擊一個(gè) a 標(biāo)簽 ? 通過 WKUserScript。
當(dāng) webview 加載的頁面 'document ready' 時(shí), Turbolinks-iOS 通過 WKUserScript 加載一個(gè)定制的 JS 文件.
let bundle = Bundle(for: type(of: self))
let source = try! String(contentsOf: bundle.url(forResource: "WebView", withExtension: "js")!, encoding: String.Encoding.utf8)
let userScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
configuration.userContentController.addUserScript(userScript)
configuration.userContentController.add(self, name: "turbolinks")
這個(gè)JS文件做了什么? 他把自己作為一個(gè) adapter 綁定到 Turbolinks 上了。
function WebView(controller, messageHandler) {
this.controller = controller
this.messageHandler = messageHandler
controller.adapter = this
}
//init
this.webView = new WebView(Turbolinks.controller, webkit.messageHandlers.turbolinks)
因?yàn)榫W(wǎng)頁是基于Turbolinks,因此網(wǎng)頁上有任何"風(fēng)吹草動(dòng)"都會(huì)通過controller,傳到這個(gè)adapter上來。比如,訪問一個(gè)新的地址是:
visit: (location, options = {}) ->
location = Turbolinks.Location.wrap(location)
if @applicationAllowsVisitingLocation(location)
if @locationIsVisitable(location)
action = options.action ? "advance"
@adapter.visitProposedToLocationWithAction(location, action)
else
window.location = location
而 adapter 在通過 webkit.messageHandlers 把消息傳遞給 iOS,比如: <code>visitRequestStarted</code>
visitProposedToLocationWithAction: function(location, action) {
this.postMessage("visitProposed", { location: location.absoluteURL, action: action })
},
postMessage: function(name, data) {
this.messageHandler.postMessage({ name: name, data: data || {} })
},
這個(gè) messageHandler 就是 初始化傳進(jìn)來的 <code>webkit.messageHandlers.turbolinks</code>
iOS 原生代碼,再通過 WKScriptMessageHandler 捕獲這些事件后,通過 VisitDelegate 和 SessionDelegate,
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let message = ScriptMessage.parse(message) else { return }
switch message.name {
//....some codes....
case .VisitProposed:
delegate?.webView(self, didProposeVisitToLocation: message.location!, withAction: message.action!)
//....some other codes...
case .ErrorRaised:
let error = message.data["error"] as? String
NSLog("JavaScript error: %@", error ?? "<unknown error>")
}
}
session 就是這里的這個(gè)delegate,并且在這個(gè)delegate的實(shí)現(xiàn)方法中
func webView(_ webView: WebView, didProposeVisitToLocation location: URL, withAction action: Action) {
delegate?.session(self, didProposeVisitToURL: location, withAction: action)
}
這就回到了最初實(shí)現(xiàn)的 SessionDelegate 了,就是我們的 UINavigationController
視頻介紹
這是在2016年的 Rails Conference 上,Turbolinks 和 Turbolinks-iOS 主要貢獻(xiàn)者 Sam Stephenson 做的一個(gè)演講,值得一看,Youtube 地址