由于公司內(nèi)部OA 項目將turbolinks升級到了5,但是有許多的js還是采用著之前的寫法肢扯,結(jié)果一大堆的莫名其妙的頁面渲染問題暴露出來妒茬,總結(jié)一下遇到的坑担锤,以及整理網(wǎng)上各路大神給出的解決方案,備忘蔚晨。
要不要用 Turbolinks5
必須用起來, 既然是 Rails5應(yīng)用, 強烈推薦使用 Turbolinks5來構(gòu)建你的應(yīng)用. 在用好它的前提下, 它的用戶體驗速度趕超用 AngularJS, ReactJS, VueJS構(gòu)建的單頁應(yīng)用( 在詳細頁里面有解釋 ). 而學(xué)習(xí)成本實際上很低, 一個團隊只要一個人掌握即可. 而且我希望通過我的文章你就能完全掌握好它.
實際上, Turbolinks 用的好, 能夠讓 Rails 應(yīng)用比 AngularJS 或 ReactJS 構(gòu)建的單頁應(yīng)用還要快. 而學(xué)習(xí)成本又極低. 說不定你看完此文就明白了. 本文嘗試解答以下問題:
- Turbolinks5 的出現(xiàn)背景
- Turbolinks5 是否應(yīng)該使用, 在什么樣的情況下使用?
- Turbolinks5 與 Turbolinks-classic 有哪些區(qū)別?
- Turbolinks5 典型問題與解決方法
- Turbolinks5 技術(shù)內(nèi)冪
Turbolinks 背景
首先, 應(yīng)該先解讀一下 Turbolinks5 出現(xiàn)的背景與意義, 以此可以基本推出它解決問題的思路與采用的技術(shù).
Turbolinks5 是一個 "簡單" 的單頁應(yīng)用 JS 客戶端框架, 能夠讓你輕易地實現(xiàn) "單頁面" 應(yīng)用, 換言之, 它某種程度與 AngularJS, ReactJS 一樣, 都是為了提升客戶端體驗更快. 不同的地方是, 它是足夠 "簡單" 的方案, 幾乎只需要提供一行代碼即可:
//=require 'turbolinks'
它解決問題的思路與 Rails的理念是一致的:
- 通過簡單直白的思路解決 80% 的問題, 剩下的交給你. 我們來看看它如何解決問題.
一個直白的公式: 網(wǎng)頁加載速度 = 下載資源速度 + 解析資源速度
"單頁面應(yīng)用" 快的秘訣就在于它同時減少了下載資源的大小( 除卻第一次加載模板后, 后續(xù)全部使用 JSON API), 以及極大提高了解析資源的速度( 通過 JSON 數(shù)據(jù)就能更新頁面).
Turbolinks 無法解決下載資源的大小的問題, 卻可以通過幾乎不影響原有網(wǎng)頁架構(gòu)的情況下極大提高解析資源的速度.
為了理解 Turbolinks 的工作原理, 我們先來看一下在 chrome 瀏覽器中, 網(wǎng)頁是如何被加載的.
- 下載 index.html
- 解析 head 標簽中的 link 與 script 標簽, 如果是帶有 src屬性, 阻塞其他邏輯執(zhí)行, 繼續(xù)去下載對應(yīng)的資源并執(zhí)行. 如果沒帶, 則直接執(zhí)行其中的代碼邏輯.
- 渲染 body 標簽的內(nèi)容, 并解析執(zhí)行 body 中的 script 標簽.
- 全部執(zhí)行完畢, 執(zhí)行 DOMContentLoaded 事件綁定的邏輯.
第一次加載時網(wǎng)頁執(zhí)行跟上述是一致, 之后 Turbolinks 會綁定 Body 下所有的 a 元素的 click事件, 切換頁面時, Turbolinks 將會接管瀏覽器的頁面加載過程, 采用以下方式:
- 異步加載新頁面的 index.html
- 解析 head 標簽中的 link 與 script 標簽, 識別其中帶有 data-turbolinks-track
的屬性, 如果 src有變化( 可能性很小 ), 則重載所有頁面. 如果沒有變化, 則不進行任何操作. - 解析 head 標簽中新的 link 與 script 標簽, 加載并執(zhí)行.
- 用新頁面的 body 替換老的 body 中的內(nèi)容, 并執(zhí)行其中的 script 腳本.
這樣一來, Turbolinks 能夠絕大時間里避免每次重復(fù)的 head 其中的 css 與 javascript 標簽的解析與加載時間( 這個時間往往很耗時 ).
Turblinks5 與 Turbolinks-classic 的異同
Turblinks5 與 Turbolinks-classic 核心理念沒有變化, 最大的區(qū)別在于更清晰明確了流程, 以及更清晰的事件觸發(fā), 以及重構(gòu)了代碼.
對使用者影響最大的可能就是事件了.
- page:change -> turbolinks:load
- 不需要繼續(xù)綁定 ready事件了
- script 的追蹤標簽 由 data-turbolinks-track -> data-turbolinks-track="reload"
現(xiàn)在請直接使用 Turbolinks5, 更簡單明了. 以下均在 Turbolinks5 的基礎(chǔ)上進行講解與分析.
Turbolinks5 不是免費的
Turbolinks5 能夠讓你在極小的改動下提升網(wǎng)頁的加載速度, 但也帶來了新的問題: 頁面執(zhí)行邏輯的變化要求你的 javascript 邏輯盡可能的冪等( 即重復(fù)執(zhí)行也不影響最終的結(jié)果 ). 否則的話, 需要你理解 Turbolinks5 的工作原理并針對進行調(diào)整. 這便是本文最大的價值所在.
最佳實踐:
將所有 javascript 腳本打包為一個 application.js, 放在 head 中, 并用 'data-turbolinks-track="reload"' 追蹤.
但有的時候, 我們必須打破這個最佳, 比如第三方組件, 這時候我們該如何處理?
典型問題一: 為什么刷新就好了, 從其他頁面點擊過來就有問題?
注意到啟用了 Turbolinks5 的頁面, 瀏覽器的加載流程與 Turbolinks5 去加載的流程有大的差異. 如果沒有注意到一些冪等或依賴的 JS 問題, 就會出現(xiàn)這種問題.
遇到這種情況, 要具體問題具體分析.
典型問題二: head 中添加了新的 script, body 中有 script 對其有依賴
在典型的第三方插件中, 會要求你在 head 中添加它們的源, 在 body 中添加插件的初始化操作. 注意, 這種情況在 Turblinks5 中會有依賴加載的問題.
例:
// page1
<head>
<script src='application.js' data-turbolinks-track='reload'></script>
<script src='plugin.js'></script>
</head>
<body>
<script> window.plugin.init(); </script>
</body>
// plugin.js
window.plugin = { init: function(){ // init code here }}
這種寫法在標準的瀏覽器加載時是非常正常的, 但是在 Turbolinks5 流程里就有問題了:
plugin.js在 Turbolinks5 中執(zhí)行是用的類似于
script = document.createElement('script')script.src = 'plugin.js'
這個加載過程是異步的, 所以往往在 body 中的 script 標簽執(zhí)行時, plugin.js還沒有下載完, 所以執(zhí)行就會出現(xiàn)變量未定義錯誤.在這種情況下, 建議將其改為
// page1
<head>
<script src='application.js' data-turbolinks-track='reload'></script>
</head>
<body>
<script>
$.getScript('plugin.js', function(){
window.plugin.init();
});
</script>
</body>
典型問題三: body 中更新某個 DOM 節(jié)點會導(dǎo)致頁面緩存重復(fù)的問題
這也是一個非常容易犯的錯誤, 舉個例子
// page1
<head>
<script src='application.js' data-turbolinks-track='reload'></script>
</head>
<body>
<p id='el'>hello world</p>
<a href='page2'>page2</a>
<script>
$('#el').append('+123');
</script>
</body>
這個問題非常難于發(fā)現(xiàn), 但很容易犯錯. 采用這個頁面的寫法之后, 就能觸發(fā)一個 bug:
- 打開 page1
- 點擊進入 page2
- 點擊瀏覽器的 "后退" 按鈕
- 點擊瀏覽器的 "前進" 按鈕
- 再次 "后退" 按鈕
bug 出現(xiàn), 頁面上 #el元素變?yōu)?'hello world+123+123' 了. 如果你繼續(xù) "后退", "前進" 將出現(xiàn)重復(fù)的 +123 這樣的錯誤.想像一下, 如果將上述簡單代碼換成一個圖表插件, 插件將會導(dǎo)致重復(fù)的圖表渲染. 這個 bug 還是非常嚴重的.
如何修復(fù)?
有兩種方法:
- 關(guān)閉緩存, 在這個特定的頁面的 head 放置 <meta name="turbolinks-cache-control" content="no-cache">的 meta 標記以關(guān)閉當前頁面的緩存, 這樣將強制每一次后退必須從服務(wù)端拉取最新的頁面.
- 利用 turbolinks:before-cache事件, 在緩存前重置這個元素, 例如這里可以用
$(document).on('turbolinks:before-cache', function(){
$('#el').text('hello world');
});
來解決這個問題.
這種情況與我們接下來要講的緩存機制有關(guān). 接下來我們要深入理解 Turbolinks5 的機制, 達到掌控它的能力.
Turbolinks5 的緩存機制
Turbolinks5 在某種程度上比使用 "AngularJS", "VueJS", "ReactJS" 構(gòu)建的單頁應(yīng)用更快. 這得益于它的緩存機制設(shè)計.
Turbolinks5 在每一次訪問頁面后, 都會緩存當前頁面, 默認最多緩存 20 個. 緩存頁面有兩個用途:
- 使用瀏覽器后退, 前進時, 直接從緩存中取出對應(yīng)的頁面并渲染.
- 通過 a 元素點擊時, Turbolinks5 會率先從緩存中取出頁面, 渲染出來, 然后再通過 XMLHttpRequest 取得服務(wù)器最新的頁面, 再替換掉緩存頁, 并渲染最新的頁面.
這個時候請注意第一種情況, Turbolinks5 使用的是 cloneNode(true)
來緩存頁面, 這樣將導(dǎo)致它替換頁面時丟失掉所有的事件綁定, 它必須重新解析執(zhí)行其中的 script
腳本才能讓緩存頁面正常工作. 這時候如果處理不當就會出現(xiàn)上一段落第 3 個典型問題.
充分利用 Turbolinks5 的緩存機制, 能夠讓我們的頁面訪問速度超出 "單頁應(yīng)用", 所以, 我不建議你輕易的關(guān)閉它.
深入 Turbolinks5 的處理流程
我們再來深入的分析 Turbolinks5 在不同的情況的加載過程.
瀏覽器第一次加載, 或點擊刷新: 這種情況保持與瀏覽器的加載順序一致.
點擊瀏覽器后退或前進: 直接調(diào)取緩存頁面并顯示, 不再拉取服務(wù)端數(shù)據(jù).
點擊頁面的 a 元素: 先嘗試拉取緩存, 如果有, 渲染緩存頁面, 然后同時拉取服務(wù)端新頁面并替換緩存; 如果沒有, 則異步拉取服務(wù)端新頁面, 緩存之并渲染新頁面.
為了更清晰展示這個流程, 我花了幾個小時整理了一個流程圖:
還要詳細說明一下如何渲染頁面( 無論是緩存頁面還是新頁面 ):
- 檢查 head 中是否有標記過 reload 的 script 的 src 有變化, 如果有, 則完全刷新頁面了.
- head 中如果有新的 script 標簽, 就異步加載它( 通過 Element.src = ''的方式 )
- 執(zhí)行 body 中的 script 標簽
通過這個分析, 我們可以推斷出, 如果同時有緩存頁和新頁面, 其中的 script 標簽就可能被執(zhí)行兩次.
除了以上描述的標準流程外, Turbolinks5 還有幾個選項可以定制它的流程:
- 定義如何臨時關(guān)閉緩存
- 定義如何緩存頁面
- 定義如何管理歷史記錄
- 定義如何臨時關(guān)閉 Turbolinks5
關(guān)于這些選項, 可以閱讀它的源碼和官方主頁了解更多: https://github.com/turbolinks/turbolinks
如果想了解源代碼:http://www.reibang.com/p/622571b33eca
原文資料鏈接 Turbolinks5 概述及實現(xiàn)原理