turbolinks源码分析(转)

Turbolinks5 是用 Coffeescript 编写.

学习 Turbolinks5 能够让你:

  • 从根本上掌握浏览器加载网页时的处理流程
  • 掌握 Turbolinks5 的核心原理, 学会如何模块化一个 "大" 的前端项目
  • 跟着老鸟学会如何分析源代码

准备工作

clone 项目:
git clone https://github.com/turbolinks/turbolinks

准备你的编辑器, 推荐 atom 或 sublime text

  • 找到 Turbolinks.start() 入口

老鸟提示, 在研究代码之前, 明确你研究对象的适用范围非常重要, 大部分时间先看文档是一个非常有效的熟悉项目架构的手段.

  • 所以推荐提前阅读它的 README.

开始

入口非常简单:

Turbolinks.start = ->

  if installTurbolinks()

    Turbolinks.controller ?= createController()

    Turbolinks.controller.start()

installTurbolinks = ->

  window.Turbolinks ?= Turbolinks

  moduleIsInstalled()

createController = ->

  controller = new Turbolinks.Controller

  controller.adapter = new Turbolinks.BrowserAdapter(controller)

  controller

moduleIsInstalled = ->

  window.Turbolinks is Turbolinks

Turbolinks.start() if moduleIsInstalled()

可以了解到以下信息:

  • Turbolinks 会挂载到全局的 window.Turbolinks 对象, 单例( 即全局只有一个 ).
  • Turbolinks.controller 是核心, 也是单例的( 全局只有一个 ).
  • Turbolinks.controller.start() 是真正的入口.

老鸟提示, 用空间想像力从静态的代码中抽出运行时各个类或组件的关系, 这是阅读代码的精粹. 必要时, 可以动用动态的 debug 工具进行动态分析.

controller 在做什么

我们跳入 controller.coffee, 找到 start() 方法:

  start: ->

    unless @started

      addEventListener("click", @clickCaptured, true)

      addEventListener("DOMContentLoaded", @pageLoaded, false)

      @scrollManager.start()

      @startHistory()

      @started = true

      @enabled = true

暂时不用关心非骨干的代码, 我们看到最重要的入口已经出现:
clickCaptured 函数挂载到了全局的 click 事件, pageLoaded 函数挂载到了 DOMContentLoaded 事件
DOMContentLoaded 与 Load 事件区别在于前者不继续等待 css, image 加载完成即触发, 后者等页面完全加载后触发.

从这里, 我们已经看到第一个关键实现:

如何绑定了 a 元素的事件: addEventListener

这时我们已经到了 visit() 这个入口了. 继续往下看:

visit() -> @adapter.visitProposedToLocationWithAction -> @controller.startVisitToLocationWithAction

最后来到 controller.coffee 的 startVisitToLocationWithAction:

  startVisit: (location, action, properties) ->

    @currentVisit?.cancel()

    @currentVisit = @createVisit(location, action, properties)

    @currentVisit.start()

    @notifyApplicationAfterVisitingLocation(location)

我们可以看出以下信息:

  • 用户点击一个链接后, 实际上, Turbolinks5 创建了一个 Visit 的实例, 然后调用了 .start() 来启动具体的访问过程.

这时也可以基本分析出 controller 的作用了:

  • controller 是所有相关类的一个容器, 通过它来关联各个模块, 但 Visit 是特例, 它每次访问都产生一个新的实例, 并存储在 @currentVisit

Visit 真正的访问

start() -> @adapter.visitStarted(this) -> visit.issueRequest(); visit.changeHistory(); visit.loadCachedSnapshot()

这是真正的处理流程:

  1. 发送 HTTP Request( 异步, 注意 Javascript 里面请求默认都是异步 )

  2. 更新浏览器历史( 通过 History API )

  3. 加载 cache 页面

是时候分道扬镳了.

HttpRequest

http_request.coffee 里面研究一下, HTTP Request 是如何发送的:

  createXHR: ->

    @xhr = new XMLHttpRequest

    @xhr.open("GET", @url, true)

    @xhr.timeout = @constructor.timeout * 1000

    @xhr.setRequestHeader("Accept", "text/html, application/xhtml+xml")

    @xhr.setRequestHeader("Turbolinks-Referrer", @referrer)

    @xhr.onprogress = @requestProgressed

    @xhr.onload = @requestLoaded

    @xhr.onerror = @requestFailed

    @xhr.ontimeout = @requestTimedOut

    @xhr.onabort = @requestCanceled

果然不出预料, 通过 XMLHttpRequest 对象, 发起了一个异步请求. 设定了回调. 我们暂时不看异常处理, 直接看 requestLoaded

  requestLoaded: =>

    @endRequest =>

      if 200 <= @xhr.status < 300

        @delegate.requestCompletedWithResponse(@xhr.responseText, @xhr.getResponseHeader("Turbolinks-Location"))

      else

        @failed = true

        @delegate.requestFailedWithStatusCode(@xhr.status, @xhr.responseText)

@delegate 是什么鬼? 实际上, delegate 的命名在框架里是非常常见的, 它代表一个代理人, 将请求转给对应的接口. 这里明显就是原来的 Visit 实例. 这样设计能够让 HttpRequest 对象不依赖于具体的实现类( 比如 visit ), 更为通用.

继续分析, 就发现它最后调用了

  loadResponse: ->

    if @response?

      @render ->

        @cacheSnapshot()

        if @request.failed

          @controller.render(error: @response, @performScroll)

          @adapter.visitRendered?(this)

          @fail()

        else

          @controller.render(snapshot: @response, @performScroll)

          @adapter.visitRendered?(this)

          @complete()

这就是最终 HttpRequest 之后的动作, 可以看出它调用了 @controller.render 接口. 先不继续往 render 里走. 回到上一个分支点.

老鸟提示, 好的命名能够极大程度降低阅读代码的工作量, 不要一路追到底, 明确了一个接口的含义后, 可以往其他重要的入口分析. 比如 render 就是一个非常清晰的含义, 我们几乎不分析也能明白它的作用.

loadCachedSnapshot

  loadCachedSnapshot: ->

    if snapshot = @getCachedSnapshot()

      isPreview = @shouldIssueRequest()

      @render ->

        @cacheSnapshot()

        @controller.render({snapshot, isPreview}, @performScroll)

        @adapter.visitRendered?(this)

        @complete() unless isPreview

非常妙, cache page 最终加载也通过 @controller.render 进行了.

我们最终需要进入最关键的 render 函数

controller.coffee

  render: (options, callback) ->

    @view.render(options, callback)

进入 view.coffee

  renderSnapshot: (snapshot, callback) ->

    Turbolinks.SnapshotRenderer.render(@delegate, callback, @getSnapshot(), Turbolinks.Snapshot.wrap(snapshot))

进入 snapshot_renderer.coffee

  render: (callback) ->

    if @trackedElementsAreIdentical()

      @mergeHead()

      @renderView =>

        @replaceBody()

        @focusFirstAutofocusableElement()

        callback()

    else

      @invalidateView()

我们转了一圈, 最终找到了 render 的实际入口. 我们看到 render 做了以下几件事:

  1. 合并头

  2. 替换 body

  3. 一些杂项

继续看 mergeHead

  mergeHead: ->

    @copyNewHeadStylesheetElements()

    @copyNewHeadScriptElements()

    @removeCurrentHeadProvisionalElements()

    @copyNewHeadProvisionalElements()

非常明显的命名.

我们继续从 head_details.coffee 中分析到具体操作:

document.head.appendChild(element)

也就是 mergeHead 也就是同步了头部信息, 并将其加载起来. 注意这里在明确理解 Javascript 操作 script 标签元素的作用.( 会自动异步取回 src 属性中的内容并执行 )

同理, replaceBody 的操作关键是:

    for replaceableElement in @getNewBodyScriptElements()

      element = @createScriptElement(replaceableElement)

      replaceableElement.parentNode.replaceChild(element, replaceableElement)

非常清晰的命名, 让我们能够很快明白这里的逻辑.

原著-- 深圳市百分之八十科技有限公司 李亚飞

你可能感兴趣的:(turbolinks源码分析(转))