Turbolinks-iOS 上路实用手册 · Turbolinks Part 3

前提

要用 turbolinks-iOS 来写一个 Hybrid 的 App 的前提是你的网站使用了 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 以上的版本。如果项目已经使用了 swift 3.0, 需要使用 3.0 的分支。虽然官方官方说 3.0的分支他们没有在生产环境下用过,但是我们用下来没有什么问题。

使用

首先建议把 turbolinks-iOS 的代码都下载下来,先跑一下他的demo,demo 虽然很简单,但是基本上把能碰到的情况都写上去了,比如网页请求失败,遇到需要登录的情况等。

创建Session

使用 turbolinks-iOS,最重要的一个概念就是 session, session 控制着网页的前进和返回,也是网页和原生代码之间交互的通道。创建 session, 设置 delegate, 同时设置好 processPool。 如果你有多个 session, 并且想要在多个 session 之间共享 cookies,那就需要创建一个共享的 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
}()

实现 Delegate

iOS 里适合做前进后退这件事最合适的就是 UINavigationController 了,因此通常会在 navigationController 中新建 session 实例,并把自身作为这个 session 的 delegate。作为 session 的 delegate,必须实现两个方法。

  1. func session(_ session: Session, didProposeVisitToURL URL: URL, withAction action: Action), 网页上任何的访问新网页的事件,都会回调这个方法。
  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)
  }

代码很直观,每次访问一个新的URL时,都根据 action 值来判断是 push 进一个新的 viewcontroller, 还是替换当前的 viewcontroller。 替换也就是 pop 出当前的 controller,push 进新的。

在处理完 viewcontroller 之间的关系之后,还要调用 session.visit(visitable),通知背后的 WKWebView 访问新的地址。

  1. func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, withError error: NSError) 这是当请求失败时,处理回调的函数。 比如服务器端返回 401, 可以弹出登陆界面。 网络不好,或者404时,可以显示自定义的错误页面。
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 默认不接受标准的 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.

官网的这句话就是我 pull request 的,^_^, 就不翻译了。 改用 Ajax 的方式提交 Form, 在返回的结果中, 调用 Turbolinks.visit(some_url) 来指向新的页面。

Turbolinks-iOS 使用的 WKWebView 如何捕获网页上的页面访问事件,比如点击一个 a 标签 ? 通过 WKUserScript。

当 webview 加载的页面 ‘document ready’ 时, Turbolinks-iOS 通过 WKUserScript 加载一个定制的 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")

这个JS文件做了什么? 他把自己作为一个 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)

因为网页是基于Turbolinks,因此网页上有任何”风吹草动”都会通过 controller,传到这个 adapter 上来。比如,访问一个新的地址是:

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,比如: visitRequestStarted

visitProposedToLocationWithAction: function(location, action) {
  this.postMessage("visitProposed", { location: location.absoluteURL, action: action })
},

postMessage: function(name, data) {
  this.messageHandler.postMessage({ name: name, data: data || {} })
},

这个 messageHandler 就是 初始化传进来的 webkit.messageHandlers.turbolinks

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 就是这里的这个delegate,并且在这个delegate的实现方法中

func webView(_ webView: WebView, didProposeVisitToLocation location: URL, withAction action: Action) {
  delegate?.session(self, didProposeVisitToURL: location, withAction: action)
}

这就回到了最初实现的 SessionDelegate 了,就是我们的 UINavigationController

视频介绍

这是在2016年的 Rails Conference 上,Turbolinks 和 Turbolinks-iOS 主要贡献者 Sam Stephenson 做的一个演讲,值得一看,Youtube 地址

Upload Image or File Ajaxly with Pure Javascript

随着Web技术的不断发展,XMLHttpRequest的进度和浏览器的支持,现在已经可以用用Ajax的方式实现图片/文件的上传,不需要任何插件或者库了。

FormData来实现上传FormData

$input = $('input[type=file]')[0]
if $input.files.length > 0
	formData = new FormData()
	formData.append('image[url]', $input.files[0])
	$.ajax(
	    url: 'URL',
	    data: formData,
	    cache: false,
	    contentType: false,
	    processData: false,
	    type: 'POST',
	    beforeSend: ->
	    success: ->
	  )

例子还是用jQuery来实现ajax请求,其中的设置很重要,

contentType: false
processData: false

这两个设置,不能忘掉

兼容性?

这个API的兼容性怎么样? 已经很好了,除了IE8,9,10,其它基本都支持, 可以看这里: http://caniuse.com/#search=FormData

试试?

Rails Basic: Use of Acceptance

一个非常基本的功能是在用户注册的是时候,要求用户同意某个协议,实现也非常简单,一个checkbox就可以搞定,但是正因为很简单,有好多网站却做得不好。

常犯的错误有以下两个:

  1. 只在前端验证
  2. 没有设置相应的label

前者的问题显而易见的,后者的问题是必须点击checkbox才能选中,这个在手机上用起来使用性就很差。

看看Rails是怎么玩的?

在Model中,加入

validates_acceptance_of :terms_of_service

在View中,使用(假定你使用form helper method)

f.text_field :terms_of_service

That’s it!

生成的html代码是标准的,验证实在服务器端完成的,All Good! 而且不需要在数据里加入额外的列。

如果你model中,已经有column来存储这个值了,只需要使用 accept参数,

validates_acceptance_of :terms_of_service, accept: true, message: '必须接受'

越是简单的事情,越是要做正确!

Rails4 ActiveRecord 不同的赋值方法

Rails 提供了多种设置Model属性的方法,方法之间又各有异同,有的会出发回调,有的不会,有的会对所属对象其它属性也产生影响。 因此理解方法之间的区别就显得很重要。

Cheat Sheet

最方便的先来,cheetsheet表,方便查询:

Method Uses Default Accessor Saved to Database Validations Callbacks Touches updated_at Readonly check
attribute= Yes No n/a n/a n/a n/a
write_attribute No No n/a n/a n/a n/a
update_attribute Yes Yes No Yes Yes Yes
attributes= Yes No n/a n/a n/a n/a
update Yes Yes Yes Yes Yes Yes
update_column No Yes No No No Yes
update_columns No Yes No No No Yes
User::update Yes Yes Yes Yes Yes Yes
User::update_all No Yes No No No No

user.name =

这是最常用的赋值方法,这个也是Rails默认生成的赋值方法。赋值后,对应的属性会被标记为dirty, 脏数据,但是并没有更新到数据里去。

调用save会把数据更新到数据库。调用reload会丢弃脏数据。

user.write_attribute(:name, ‘feng’)

这是上面那个赋值方法会调用的方法, 这个方法也不会更新数据库。

user.update_attribute(:name, ‘feng’)

这个方法会直接更新数据到数据库,而且会忽略到所有的验证,直接更新数据库。

  • 所有更新会直接到数据库
  • 所有的验证会被跳过

user.attributes = { name: ‘feng’ }

这个赋值方法会根据右边传入的哈希,对相应的属性进行赋值。其它的属性不会有变动。

user.assign_attributes { name: 'feng' }

user.update(name: ‘feng’)

在Rails 3中,这个方法叫update_attributes, 这个方法会更新对象,进行验证,然后更新到数据库.这方法会把所属对象中别的脏数据也更新到数据库。

user.update_columns(name: ‘feng’)

这个方法会生成 SQL Update,直接更新到数据库,跳过所有的数据验证和回调。

user.update_column(:name, ‘feng’)

跟上面的方法类似

User.update(1, name: ‘feng’)

这是一个类方法

这个方法的第一个参数是 id, 后面是更新的属性hash。 第一个参数,可以是一个数组, 一组 id

User.update_all(name: ‘feng’)

批量更新,be careful!

Sublime Text不可或缺的技巧2

之前写过一篇sublime text的文章,介绍快捷键和使用技巧,现在又学到一些新的,来介绍下

选中当前单词: ⌘ + D

在光标所在位置,按下 ⌘ + D, 可以选中当前单词,跟双击效果一样

选中下一个匹配单词: ⌘ + D

选中当前单词的时候,按住 ⌘ 键,再按下 D, 就可以选中下一个匹配的单词,再按下,就匹配再下一个。

cmd+ds

当前光标最近的Tag: ⌘ + ⇧ + K

在编辑html时,有时想把一个标签从 p 改到 div, 当然可以先改完开头,再来改关闭,但其实可以更快,就是用 ⌘ + ⇧ + K,

cmdshiftk

选中括号之间的内容: ⌘ + ⇧ + Space

选中所处的内容块之间的所有内容

cmdshiftspace

整行上下移: CTRL + ⌘ + ↑ / ↓

整行上移或者下移,

ctrlcmdarrow

复制文字或者正行: ⌘ + ⇧ + D

如果选中了文字,会复制文字,不然会复制当前正好的内容

cmdshiftd

粘贴时,自动缩进: ⇧ + ⌘ + V

这个好有用

shiftcmdv

包裹所选文字: CTRL + ⇧ + W

把所选文字加个 span 标签

ctrlshiftw