× 說心事 政治時事 生活 運動 汽車 機車 自行車 相機 科技 理財 旅遊美食 娛樂 健康 美麗時尚 人際關係 文學故事 《思書》
吃喝玩樂 《思書》


Rails Turbolinks™ 5 深度研究


紅寶鐵軌客
來關注...
關注/停止關注:紅寶鐵軌客
關注有什麼好處?:當作者有新文章發佈時,「思書」就會自動通知您,讓您更容易與作者互動。
×
現在就加入《思書》,你就可以關注本作者了!
《思書》是一個每個人的寫作與論壇平台,特有的隱私管理,讓你寫作不再受限,討論更深入真實,而且免費。 趕快來試試!
還未加入《思書》? 現在就登錄! 已經加入《思書》── 登入
Rails 使用中,折磨中,享受中......
407   0  
2017/10/13


不管你是愛、還是恨(說得有點超過),但是真的很多 Rails 人乾脆把 Turbolinks 關掉了。

我還是繼續使用中...

什麼是 Turbolinks? 好處?

Turbolinks 是一個 Javascript 程式,它的目地就是讓網頁切換時更快速,怎麼做到的?基本上就是把瀏覽器的跳轉切掉,交由 Turbolinks 來管,用 history API 來改變 URL,及 當網頁切換跳轉時,

  • <body> 中的內容:用 XMLHttpRequest 取得後,Render 它
  • <head> 中的內容:只執行有新增的,其他的就保留

Rails 內定就是開啟 Turbolinks,除非你把它給關了,原因:大概是很多人都認為 Rails 慢吧。

先說:Turbolinks 5.0 中如何使用 javascript

每一個使用 JavaScript 的,window 跟 document objects load 都是一個很重要的起始點,但是在 Turbolinks 中,這兩個 object 的狀態就變成不確定了,應為瀏覽器的頁面跳轉已經被 Turbolinks 接管了,所以如果是用:window.onloadDOMContentLoaded 或是 jQuery 的 ready 就要改成

document.addEventListener("turbolink:load", function() {
  // ...
})

這也是大家會在網路上找解答時,最多的答案,也是文件上的標準作法,但是,很快,就會發現,在很多時候,這還是有問題,這並不能完全取代 window.onloadDOMContentLoaded 或是 jQuery 的 ready

很多人會使用 JQuery 的 event 來觸發 Turbolink:load,如下:

$(document).on('turbolinks:load', function() {....}

or

$(document).on('ready turbolinks:load', function() {....}

我不建議用 JQuery,隨然我發現大部分運用都沒問題,但是,有時候,它會觸發好多次,為什麼? JQuery 的 event 跟 Javascript 的 event 本來就是兩個不同的東西有關,但是真是因為如此嗎?也懶得查了,跟 Turbolinks 的 preview 可能有關,這就是我討厭 Turbolinks 之處,當你使用時,最好追蹤一下它的觸發的次數,我的狀況是改成 native JS 的 addEventListener 就好了,不只我,還有很多人跟我一樣,有空可以看看。

Rails 5: how to use $(document).ready() with turbo-links — Turbolinks prevents normal $(document).ready() events from firing on all page visits besides the initial load, as discussed here and here. None of the solutions in the linked answers work with Rail... Stack Overflow

越是難用,我們就要用會它!

Turbolinks 基本上就是把 <a href> 給改道了,所以當使用者按了同站內的連結後,他開始做網頁拼圖,這個改變,也造成了兩種新的提取 (Loading)不同,這在以後 Rails 中使用 Javascript ,造成了很大的問題。

一切都是 visit (提取)

這是使用 Turbolinks 時,最重要的概念!在使用 Turbolinks 後,要記得,所有的瀏覽器同網域中的前進、後退以及點選連結時,Turbolinks 都會介入,改成 Turbolinks 自己的「visit 」,例如:當點選連結時,會執行的是 Turbolinks.visit(href location)。由於不同的 visit (提取) 行為,Turbolinks 的 visit 分為兩種,這兩種新的 visit 提取就是:(及,它的基本生命週期行為)

  1. application visit - 基本上是指使用者按了網頁中的一個連結,前進到這個網頁,與提取新的網頁
    1. 發出一個網路的需求:request a network request
    2. 如果有 cache,就先把它 render 出來:render a preview of the page from cache
    3. 移到先前 cache 的停留點:scroll to the anchored element
    4. 等到 server 回應內容,就再把它 render 出來:response arrived, renders HTML
    5. 改瀏覽器的歷史紀錄:change to the browser’s history
  2. restoration visit - 就是當使用者按了倒退鍵,前進鍵,回復到舊的網頁,或是類似的行為
    1. 把 cache 的內容 render 出來:render a copy of the page from cache without making a request
    2. 回到先前 cache 時的停留點:returns to this saved position

用文字說明真的不容易懂,所以我就實際上做了個 javascript 來 log 追蹤 Turbolinks 的行為,就用以下這簡單的程式來追蹤:

$(document).on('turbolinks:load', function() {
console.log("turbolinks:load");
});
...
$(document).on('turbolinks:before-visit', function() {
console.log("turbolinks:before-visit");
});

以下是一個 Application visit 時的 tracking:

turbolinks:click
turbolinks:before-visit
turbolinks:request-start
turbolinks:visit
turbolinks:request-end
turbolinks:before-cache
turbolinks:before-render
turbolinks:render
turbolinks:load

以下是一個另一個 Application visit 時的 tracking,差別是這個頁面有拜訪過,也就是有 cache 過,也有使用很大的 Javascript code,執行比較久,這個 JS code 也會改變頁面內容: 

turbolinks:click
turbolinks:before-visit
turbolinks:request-start
turbolinks:visit
turbolinks:before-cache
turbolinks:before-render
turbolinks:render
turbolinks:request-end
turbolinks:before-render
turbolinks:render
turbolinks:load

以下是一個 restoration visit (按倒退鍵),原先網頁的內容都沒變

turbolinks:visit
turbolinks:before-cache
turbolinks:before-render
turbolinks:render
turbolinks:load

以下是一個 restoration visit (按倒退鍵),但是,原來網頁的內容已經改變了

turbolinks:request-start
turbolinks:visit
turbolinks:request-end
turbolinks:before-cache
turbolinks:before-render
turbolinks:render
turbolinks:load

以下是一個 restoration visit (按前進鍵),原先網頁的內容都沒變。

turbolinks:visit
turbolinks:before-cache
turbolinks:before-render
turbolinks:render
turbolinks:load

只按 Refresh,也就是 page reload,就只有

turbolinks:load

這些事件的名稱就如同它的行為,在 Turbolinks 的文件中也有很清楚的說明,從這些事件中,我們可以觀察到 Turbolinks 在 application 和 restoration visit 中,是有很大的不同的,其中,最重要的關鍵就是有沒有 cached 的內容,而會造成很大困擾的就是到底什麼樣的內容被 cached 了,這跟它 cache 的時間點有很大的關係,而它的時間點就顯示在 turbolinks:before-cache,這個事件會在 turbolinks 要開始 cache 之前觸發,所以只要仔細看這個事件的發生點,就知道 Turbolinks 存的內容是什麼了。

Application visit 中,有 cache 與無就很不同,無 cache 時,會 cache 的內容是 server 回應的內容,但是有 cache 時,就是 server 回應前的內容。 Restoration visit,也就是一般按了倒退鍵時,Turbolinks 也會盡量只讀 cache 而不再從 server 要資料,這在 application visit 中也是先用 cache,再等資料回來時改頁面,這都很符合 Turbolinks 的基本精神,先顯示 cache 內容,讓使用者感覺網頁速度很好,再慢慢等 server 資料回來,再改內容 。 以上的事件的列表,應該也很清楚的說明 Turbolinks 的行為,與它如何做網頁拼圖的。 

<Body> 跟 <head> 的行為不同

在 Turbolinks 文件中,針對 <head> 內,還有幾個重點:

  1. 基本上 Turbolinks 會自動加入 head 內新增的 <script> 內容,下載新的執行新的,不像 <body> 內會整個更新改變、script 執行。
  2. Cache 是使用 cloneNode(true),也就是說所有的事件 event listeners 跟相關資料不會儲存。

我還找到另一篇很棒的介紹,值得一看:

TURBOLINKS' LIFECYCLE EXPLAINED

基本上這好文就是把 turbolink 的生命週期用另一種方式說明,我把重點筆記如下:

  1. VISITING AN UNCACHED PAGE
    1. HTTP/AJAX
    2. turbolinks:before-cache 
    3. load cached content
    4. turbolinks:before-render
    5. load HTTP/AJAX content
    6. turbolinks:render
    7. turbolinks:load
  2. VISITING A CACHED PAGE
    1. HTTP/AJAX
    2. turbolinks:before-cache
    3. turbolinks:before-render
    4. replace the body and header tags with the cached content loaded from the server
    5. turbolinks: render
    6. if AJAX do return content...
      1. turbolinks:before-render (for the cached version)
      2. turbolinks:render
    7. turbolinks:load

這篇文章建議了一些在使用 react 時,可以使用的不同事件,我不是很同意,但還是筆記了一下: 

如果 setup / teardown 執行後的 DOM 行為怪異,就用以下的方式:

  • document.addEventListener('turbolinks:load', this.setup(), {once: true})
  • document.addEventListener('turbolinks:render', this.setup())
  • document.addEventListener('turbolinks:before-render', this.teardown())

 

如果 setup / teardown 執行後的 DOM 行為還好,很正常,你也不在乎 cache 的內容與 javascript 的互動,就用以下的方式:

  • document.addEventListener('turbolinks:load', this.setup())
  • document.addEventListener('turbolinks:before-cache', this.teardown()

不過,我覺得真的沒那麼簡單,很多時候應該也只能試試看,當一個 javascript 在 Turbolinks 中執行有詭異行為時,非常大的機率就是因為 cache 的內容與 server 上的不同,這在 Turbolinks 的文件中也有一段說明或要求,要求加掛在 Turbolinks 上的程式,必須要是:Idempotence,一個難懂的字,這基本上是一個數學與電腦科學的專用字,是指這個程式或是數學運算式必須要每次執行結果都一樣!

一定要知道的 Idempotence

這樣的要求很簡單合理嗎?其實不容易做到,基本上,只要任何一個程式執行後,把原先參考的資料改了,就很有可能會錯,更討厭的是,如果你的 javascript 程式是自己開發,那還容易做到Idempotence,如果是在網路上找的,就辛苦了。 題外話:這也說明 Rails 的開發人員最好是 full stack,如果前端、後端加UX,光這個問題會很難溝通。

把這個 Idempotence 難題再加困難一點,別忘了 Turbolinks 有兩種 visit,除了往前走,還有 Restoration visit,也就是使用者按倒退,這時,大部分的機率 Turbolinks 會直接把 cached 的內容抓出來,不會再到 server 上讀,如果你的網頁內容是被 javascript 改過的,特別是改了 Javascript 參考的 DOM,恭喜,你的程式一定會變的行為很詭異,這樣的實例太多了:Javascript 改時區時間顯示、新增或移動了 DOM 的位置與內容......

真的山窮水盡無路時,繞開關掉 Turbolinks 吧

是的,Turbolinks 的美,來自 Cache,但是麻煩也因它而起!

以上這個 Idempotence 的要求,是 Turbolinks 使用的必須,但是,有時就是真的很難做到,那有沒有必殺技? 沒有,最後,只有兩個爛方法解決真的山窮水盡時:

  1. 關掉 Turbolinks cache,其實就是在那一網頁把 Turbolinks 給關了。
  2. Reload 網頁。 

我知道這兩個方法很爛,如果要用這兩個方法,不就等於把 Turbolinks 關掉? 但,也不全那麼爛啦,因為可以只關掉部分有問題的頁面,而且,當真的山窮水盡無法解決時,這是可以救急的。 

要如何關掉 Turbolinks cache,我們再來看一下 cache 的行為:當 Cache 發生前,Turbolinks 會發出 turbolinks:before-cache 事件,當網頁還是顯示 cache 時的資料時,<html> 的 attribute 會加入 data-turbolinks-preview,但是我發現只有在 application visit 已經有 cache 的網頁才會有這 attribute,其他不會,可以用以下的方法知道網頁還在 cached 的內容:

if (document.documentElement.hasAttribute("data-turbolinks-preview")) {
// Turbolinks is displaying a preview
}

網頁不要使用顯示 cache 的內容,就用 no-preview,  關掉 Turbolinks 的 cache 就是 no-cache,兩個都是加在 <head> 內,以下是關掉的方法:

<head>
  ...
  <meta name="turbolinks-cache-control" content="no-cache">
</head>

如何讓連結 <a> 把要呼叫的網頁 Turbolinks cache 關掉,在 rails 中很間單,但是有點被搞得很昏,在 Turbolinks 5.0 前,好像都是用:

<%= link_to ...., 'data-no-turbolink' => true %>

但是在 5.0 後,是要這樣寫的:

<%= link_to ...., data:{ turbolinks: "false" } %>

設成 true 就會打開。  再來,就是 assets 控制了,很多人都是用:data-turbolinks-track = true,這是 Turbolink 5.0 前,或是現在稱為 classic 的用法了,在 turbolinks 5 上面用,不會顯示問題,5.0 是用 data-turbolinks-track = "reload",但我不知道 true 會不會等於 reload,應該會吧,要是不會,很多老的 rails 程式應該會有有問題,但是好像沒聽說。

data-turbolinks-track = "reload" 是唯一在文件上看到的設定,基本上是要讓 Turbolinks 無條件 reload assets,用法如下,只要 Assets 跟設定的不同,就會 reload:

<head>
...
<link rel="stylesheet" href="/application-258e88d.css" data-turbolinks-track="reload">
<script src="/application-cbd3cd4.js" data-turbolinks-track="reload"></script>
</head>

如果你在 <body> 中間有 <script> 時,Turbolinks 會執行兩次,原因是第一次是為 preview 執行,第二次是當最終內容確定後,這很討厭,你有兩個方法做:

  1. 是確實的讓網頁內容 Idempotence,也就是說,每次執行 <script> 時,結果都一樣,網頁內容沒有被改變。
  2. 或是,把 preview 關掉:<meta name="turbolinks-cache-control" content="no-preview">

實務上,有很多外部的 javascript 是會改變 HTML 內容的,也就是說不是 Idempotence,這時,你就可能可以用 preview 關掉解決,下面的連結有更清楚的說明:

<script> elements in a page’s <body> will be evaluated twice. · Issue #167 · turbolinks/turbolinks — To reproduce this: add <script>console.log('hi')</script> at the end of a page , 2.reload the page, 'hi' will be printed once 3., go to another page and then go back, ... GitHub

我還蠻建議關掉 preview 的,畢竟很多瀏覽器都已經有內建的 cache 了,關掉 cache 有一個好處是 body 內的 javascript 就不會執行兩次了。

以上是對關掉 Turbolinks cache 或是 preview 的說明,應該算清楚了,那,瀏覽器 reload 頁面呢? 這一篇文章說有 535種 reload 方法,我還蠻支持用這個方法繞開 Turbolinks 問題的,只要是用在確定出問題的網頁,再清楚的排除不會出問題的狀況,就還是可以在大部分的情況下保有 Turbolinks 的使用者快速感,只有在必要的頁面與出問題的狀況下 reload page,而 reload page,就跟關掉 Turbolinks 的基本行為是一樣的,當然,會再慢一點,但是,如果處理得好,影響的就會是有限的頁面。

幾的常用的 reload page 方式:

  • location.reload() 
  • location=location 
  • location.assign(location) 
  • document.execCommand('Refresh') 

我愛 Turbolinks!

有人討厭就會有人愛,一堆人討厭 Turbolinks 要把它關掉,就是有人要把 Turbolink 找回來,什麼時候 Turbolinks 會不見呢?redirect_to 就會啊,很簡單啦:

  • js: Turbolinks.visit('http://xxx.com')
  • rails: redirect_to path, turbolinks: true

有一個特殊的 replace 用法,這對資料更新時,很好用,它不像前面兩個 visit 會新增新的 browser visit 歷史紀錄,它可以「replace」掉最後一筆,所以就不會在更新一筆資料時有 update 跟 show 兩個 visit 歷史紀錄:

  • <a href="/show" data-turbolinks-action="replace">Show</a>
  • Turbolinks.visit("/show", { action: "replace" })

實務上,我想大部分的人會要用 Turbolinks.visit 的原因是為了要保存 window 的狀況。

Script 不會重複下載執行:Turbolinks 會攜帶 Window 的內容

我是在說什麼啊,對不起啦,跳來跳去,這個 Window 指的是 javascipt 中的東東,還記得我在一開始說:<head> 中的內容:只執行有新增的,其他的就保留,這個保留就會讓內容留在 window 這個 global object 中,例如:當你的 <head> 中有 <script scr ... > 時,Turbolinks 下載跟執行它以後,當 visit 下一頁時,這個 script 並沒有消失,它會被 Turbolinks 保留,這樣的好處就是不用再下載跟執行一次,這確實會省下不少時間,所以如果你的網頁都是用 Turbolinks visit,你 head 中的內容都不會重複執行,這真的很棒,直到,你用了像是 redirect_to 或是 user 按了 refresh,這時 turbolinks 的內容才會 reload 了,對於 window global object,我有另一篇文章,也許可以幫助瞭解:

Javascript 的 Global object - window — 這是一個 JavaScript 的小品,我只是發現很少人在討論這個,就順手寫了下來。 Javascript global object...
Scrivinor 思書: 紅寶鐵軌客

Turbolinks 一樣支援 async 跟 defer,我也有另篇介紹:

Javascript 的 async 跟 defer 有什麼不同 — 網站,少不了要用到外部的 Javascript,fontawesome、google analytics、adsense 等等,通常都...
Scrivinor 思書: 紅寶鐵軌客

<body> 中的 script 就不一樣了,它可是會被 Turbolinks 重新每次執行的,這時如果 turbolinks 的 preview 沒關掉,它就會執行兩次,所以一般我不建議將 script 寫在 body 內,建議把 javascript 寫成 assets UJS 用 Turbolinks:load 來觸發。

後記:

不知不覺就寫了一堆,實際上是我自己想要把這些記錄下來,都是一些我認為以後會用到的設定與用法,Turbolinks 本來就不應該會簡單好用,它基本上就是一個 SPA,如果,網頁速度感不是那麼重要,開發時間很短,關掉它會是一個正確的決定!

我遇到的 Turbolinks 5 問題:

以下是我將已經遇到的「怪問題」列出,希望對大家有幫助。

Form & Submit: 

一個很簡單的網頁,裡面只有一個 form,但是當我用 jQuery 的 submit( ) listen 這個 form submit 時,卻發現從別的網頁連到這頁時,jQuery submit( ) 不會觸發,但是只要 refresh 網頁後,就一切正常了,解決的方法是在連進來的連結中,把 Turbulinks 關掉,很簡單:

link_to "a Form" forms_path, data: { turbolinks: false }

其他,還有很多 Turbolinks 5 使用上的問題,我都把它放在下面這分類了:

Rails 實作 - Turbolinks 5 — 思書 好書
Scrivinor 思書

我最後,放棄了

最後,我終於放棄 Turbolinks了,以下這篇獻給跟我一樣放棄落跑的人,我努力過了,Enough is enough ...

Turbolinks 實例:最後一根稻草,我終於跟 Turbolinks 說再見了 — 上個禮拜,我可愛的客戶有個要求,他們要追蹤一個行銷活動的點擊,以前我都是自己直接寫在網站後台,或是用 Google Analytics...
Scrivinor 思書: 紅寶鐵軌客

註解與參考:

  1. 脱线道士维克多 - Turbolinks 5: 个人技术博客

喜歡作者的文章嗎?馬上按「關注」,當作者發佈新文章時,思書™就會 email 通知您。

思書是公開的寫作平台,創新的多筆名寫作方式,能用不同的筆名探索不同的寫作內容,無限寫作創意,如果您喜歡寫作分享,一定要來試試! 《 加入思書》

思書™是自由寫作平台,本文為作者的個人立場與意見。


文章資訊

本文摘自:
分類於:
標籤:
日期:
創作於:2017/10/13,最後更新於:2019/04/29。


分享:
關於作者

很久以前就是個「寫程式的」,其實,什麼程式都不熟⋯⋯
就,這會一點點,那會一點點⋯⋯




參與討論!
×
現在就加入《思書》,馬上參與討論!
《思書》是一個每個人的寫作與論壇平台,特有的隱私管理,用筆名來區隔你討論內容,讓你的討論更深入,而且免費。 趕快來試試!
還未加入《思書》? 現在就登錄! 已經加入《思書》── 登入