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)

非常清晰的命名, 讓我們能夠很快明白這里的邏輯.

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容