turbolinks源碼分析(轉(zhuǎn))

Turbolinks5 是用 Coffeescript 編寫.

學(xué)習(xí) Turbolinks5 能夠讓你:

  • 從根本上掌握瀏覽器加載網(wǎng)頁時的處理流程
  • 掌握 Turbolinks5 的核心原理, 學(xué)會如何模塊化一個 "大" 的前端項目
  • 跟著老鳥學(xué)會如何分析源代碼

準備工作

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

準備你的編輯器, 推薦 atom 或 sublime text

  • 找到 Turbolinks.start() 入口

老鳥提示, 在研究代碼之前, 明確你研究對象的適用范圍非常重要, 大部分時間先看文檔是一個非常有效的熟悉項目架構(gòu)的手段.

  • 所以推薦提前閱讀它的 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() 是真正的入口.

老鳥提示, 用空間想像力從靜態(tài)的代碼中抽出運行時各個類或組件的關(guān)系, 這是閱讀代碼的精粹. 必要時, 可以動用動態(tài)的 debug 工具進行動態(tài)分析.

controller 在做什么

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

  start: ->

    unless @started

      addEventListener("click", @clickCaptured, true)

      addEventListener("DOMContentLoaded", @pageLoaded, false)

      @scrollManager.start()

      @startHistory()

      @started = true

      @enabled = true

暫時不用關(guān)心非骨干的代碼, 我們看到最重要的入口已經(jīng)出現(xiàn):
clickCaptured 函數(shù)掛載到了全局的 click 事件, pageLoaded 函數(shù)掛載到了 DOMContentLoaded 事件
DOMContentLoaded 與 Load 事件區(qū)別在于前者不繼續(xù)等待 css, image 加載完成即觸發(fā), 后者等頁面完全加載后觸發(fā).

從這里, 我們已經(jīng)看到第一個關(guān)鍵實現(xiàn):

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

這時我們已經(jīng)到了 visit() 這個入口了. 繼續(xù)往下看:

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

最后來到 controller.coffee 的 startVisitToLocationWithAction:

  startVisit: (location, action, properties) ->

    @currentVisit?.cancel()

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

    @currentVisit.start()

    @notifyApplicationAfterVisitingLocation(location)

我們可以看出以下信息:

  • 用戶點擊一個鏈接后, 實際上, Turbolinks5 創(chuàng)建了一個 Visit 的實例, 然后調(diào)用了 .start() 來啟動具體的訪問過程.

這時也可以基本分析出 controller 的作用了:

  • controller 是所有相關(guān)類的一個容器, 通過它來關(guān)聯(lián)各個模塊, 但 Visit 是特例, 它每次訪問都產(chǎn)生一個新的實例, 并存儲在 @currentVisit

Visit 真正的訪問

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

這是真正的處理流程:

  1. 發(fā)送 HTTP Request( 異步, 注意 Javascript 里面請求默認都是異步 )

  2. 更新瀏覽器歷史( 通過 History API )

  3. 加載 cache 頁面

是時候分道揚鑣了.

HttpRequest

http_request.coffee 里面研究一下, HTTP Request 是如何發(fā)送的:

  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

果然不出預(yù)料, 通過 XMLHttpRequest 對象, 發(fā)起了一個異步請求. 設(shè)定了回調(diào). 我們暫時不看異常處理, 直接看 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 的命名在框架里是非常常見的, 它代表一個代理人, 將請求轉(zhuǎn)給對應(yīng)的接口. 這里明顯就是原來的 Visit 實例. 這樣設(shè)計能夠讓 HttpRequest 對象不依賴于具體的實現(xiàn)類( 比如 visit ), 更為通用.

繼續(xù)分析, 就發(fā)現(xiàn)它最后調(diào)用了

  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 之后的動作, 可以看出它調(diào)用了 @controller.render 接口. 先不繼續(xù)往 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 進行了.

我們最終需要進入最關(guān)鍵的 render 函數(shù)

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()

我們轉(zhuǎn)了一圈, 最終找到了 render 的實際入口. 我們看到 render 做了以下幾件事:

  1. 合并頭

  2. 替換 body

  3. 一些雜項

繼續(xù)看 mergeHead

  mergeHead: ->

    @copyNewHeadStylesheetElements()

    @copyNewHeadScriptElements()

    @removeCurrentHeadProvisionalElements()

    @copyNewHeadProvisionalElements()

非常明顯的命名.

我們繼續(xù)從 head_details.coffee 中分析到具體操作:

document.head.appendChild(element)

也就是 mergeHead 也就是同步了頭部信息, 并將其加載起來. 注意這里在明確理解 Javascript 操作 script 標簽元素的作用.( 會自動異步取回 src 屬性中的內(nèi)容并執(zhí)行 )

同理, replaceBody 的操作關(guān)鍵是:

    for replaceableElement in @getNewBodyScriptElements()

      element = @createScriptElement(replaceableElement)

      replaceableElement.parentNode.replaceChild(element, replaceableElement)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子给赞,更是在濱河造成了極大的恐慌玖绿,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機愧哟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哼蛆,“玉大人蕊梧,你說我怎么就攤上這事∪椋” “怎么了肥矢?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我甘改,道長旅东,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任十艾,我火速辦了婚禮抵代,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忘嫉。我一直安慰自己荤牍,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布庆冕。 她就那樣靜靜地躺著康吵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪访递。 梳的紋絲不亂的頭發(fā)上晦嵌,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音拷姿,去河邊找鬼惭载。 笑死,一個胖子當著我的面吹牛响巢,可吹牛的內(nèi)容都是我干的描滔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抵乓,長吁一口氣:“原來是場噩夢啊……” “哼伴挚!你這毒婦竟也來了靶衍?” 一聲冷哼從身側(cè)響起灾炭,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎颅眶,沒想到半個月后蜈出,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡涛酗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年铡原,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片商叹。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡燕刻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出剖笙,到底是詐尸還是另有隱情卵洗,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布弥咪,位于F島的核電站过蹂,受9級特大地震影響十绑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酷勺,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一本橙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脆诉,春花似錦甚亭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至潜的,卻和暖如春骚揍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啰挪。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工信不, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亡呵。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓抽活,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锰什。 傳聞我的和親對象是個殘疾皇子下硕,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容