0、前言


寫此篇文章的動機
身為一個”前”數位行銷人員,幫網站串接GTM,連接數據是一個非常常見的事!
因此在學習ROR的時候,想當然也想要嘗試把GTM裝在ROR的網站上,不過在安裝前,發現Rails的Turbolinks
用以往的GTM安裝方法,會讓網站產生BUG,因此決心來徹底了解Turbolinks到底是什麼東西?
並且希望最後能成功把GTM成功裝上ROR的網站!
參考文章
Rails Turbolinks™ 5 深度研究
軒軒的家 - Turbolinks 5 教學翻譯



Turbolinks 是Javascript程式,是源自 Ruby on Rails 的 Web 加載優化方案
通常會搭配Ruby On Rails一起做使用,目的是用來讓網頁切換更快速,主要是透過fetch HTML 直接抽換的方式來避免直接換頁要重新發送請求


1-1、Turbolinks怎麼做到換頁?

1-1-1、xhr替換

點擊A頁面的連結後(連到pageB),會用ajax的方式去抓pageB的內容物
首先把頁面A的body抽掉,並把頁面B的body(XXX),塞到pageA的body
再來把頁面A的header留著,並看頁面B的header中,有哪些是A頁面header不存在的,會把不存在的塞到pageA的header
最後把頁面A的URL,換成頁面B的URL

> pageA                        pageB
> 
> --------------   merge       ----------
> |  header    |   <------     | header |
> |            |               |        |
> --------------   ajax        ----------
> | link pageB |   ------>     | XXX  |
> |            |               | XXX  |
> |            |   replace     | XXX  |
> |            |   <------     | XXX  |
> --------------               --------
> 

當你前往新的頁面,Turbolinks 會搜尋任何在新頁面下 head 的 script 元件,如果當前頁面不存在某些元件
然後會新頁面的元件,附加在當前頁面的 head 地方,你可以利用這種方式來動態載入需要的額外 JavaScript 檔案


1-1-2、原理

Turbolinks會去攔截畫面上所有的超連結(a-link),如果今天他發現這個超連結是站內的超連結,他就會用xhr的方式,把連結的頁面抓回來


Ps. 普通你點一個超連結,會觸發的網頁事件是DOMContentLoaded,不過在Turbolinks底下,沒有這個事件,因為根本沒有換頁的行為,所以也就根本沒有觸發DOMContentLoaded的事件


1、先抓到所有超連結
2、所有超連結點擊的時候,先暫停預設行為
3、最後用fetch方法,去把點擊的站內網頁內容抓過來

> const links = document.querySelectorAll("a")
> 
> links.forEach((link) => {
>     link.addEventListener("click", () => {
>         e.preventDefault()
> 
>         fetch(.....)
>         .then()
>         .catch()
>     })
> })

2、turbolinks的生命週期

2-1、cache是什麼


在講解turbolinks的獨特生命週期前,要先介紹一下cache是什麼
快取(cache, web cache, HTTP cache)是暫時存放 HTTP 回應的組件。只要滿足特定要求,就能在接下來的 HTTP 請求派上用場。
from MDN


2-2、visit

剛剛前面有說到,Turbolinks 攔截同個網域下所有a的連結,當你點擊該連結,Turbolinks會取消預設動作,
取而代之,Turbolinks 利用 History API 來改變瀏覽器的 URL, 使用 XMLHttpRequest 請求新頁面, 然後將結果 render 成 HTML
在 render 時, Turbolinks 替換當前的 <body> 的全部元件, 並且合併 <head> 的內容. JavaScript 的 window 和 document 物件
以及 HTML <html> 元件, 在每一個 render 之間則會保留起來.

那visit是啥呢?其實就是每個連結切換的動作,代表一個visit(Each Navigation is a Visit)
也就是說,從點擊頁面A的連結(連結到B)後,到turbolinks把網頁B頁面的內容render到A頁面上,這整個流程就是一個Visit

Ps. Turbolinks 的導覽模型代表採取一個 action 來 visit 一個 location (URL)
Ps. 包含改變瀏覽器歷史、發起網路請求、從快取回存某個頁面、render最後的回傳、更新捲動位置。


visit有兩種型態
1、application visit,代表 advance 或 replace 的 action
2、restoration visit,代表 restore 這個 action


2-3、application visit

一個 application visit 一定會發起一個網路請求,當收到回應Turbolinks會render該HTML,以完成當次visit.


什麼時候會觸發application visit呢?
只要點擊一個 Turbolinks 的連結,或由程式呼叫 Turbolinks.visit(location),就可以初始化Application visits


使用者點擊連結後~render畫面出來前,到底發生了哪些事?

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-4、restoration visit

Turbolinks會從cache render畫面,而不用發出請求,否則,他會從網路取得一份新的資料


什麼時候會觸發restoration visit呢?
當你使用瀏覽器的前進或後退按鈕來瀏覽時,Turbolinks會自動啟動restoration visit.


使用者點擊前進/後退的按鈕後,發生了哪些事情?

1、把 cache 的內容 render 出來:render a copy of the page from cache without making a request
2、回到先前 cache 時的停留點:returns to this saved position

Ps. Turbolinks在每一頁要離開時,會自動儲存捲軸位置,並且當返回瀏覽時會回復卷軸位置

2-5、visit完整生命週期


Turbolinks 發出允許您追蹤導航生命週期和響應頁面加載的事件,除非另有說明,Turbolinks會在document object上觸發事件。

Turbolinks emits events that allow you to track the navigation lifecycle and respond to page loading
Except where noted, Turbolinks fires events on the document object.


2-5-1、完整事件列表(Full List of Events)

> turbolinks:click          單擊啟用 Turbolinks 的連結時觸發(fires when you click a Turbolinks-enabled link)
> turbolinks:before-visit   在 visit 某個位置之前觸發(fires before visiting a location)
> turbolinks:visit          visit 開始後立即觸發(fires immediately after a visit starts)
> turbolinks:request-start  在 Turbolinks 發出獲取頁面的請求之前觸發( fires before Turbolinks issues a network request to fetch the page)
> turbolinks:request-end    請求完成後觸發(fires after the network request completes)
> turbolinks:before-cache   在 Turbolinks 將當前頁面保存到緩存之前觸發( fires before Turbolinks saves the current page to cache)
> turbolinks:before-render  在 render 頁面之前觸發(fires before rendering the page)
> turbolinks:render         在 Turbolinks render 頁面後觸發。該事件在應用程序訪問緩存位置期間觸發兩次:
>                           一次是在呈現緩存版本之後(once after rendering the cached version)
>                           另一次是在呈現新版本之後(and again after rendering the fresh version)
> turbolinks:load           在初始頁面加載後觸發一次,並在每次 Turbolinks 訪問後觸發一次(fires once after the initial page load, and again after every Turbolinks visit)

2-5-2、application visit(點擊turbolinks下的連結)

> 無快取                            有快取(之前有拜訪過此頁面)

> 1、turbolinks:click               1、turbolinks:click                 -> 連結點擊
> 2、turbolinks:before-visit        2、turbolinks:before-visit          -> 觸發visit前
> 3、turbolinks:request-start       3、turbolinks:request-start         -> 開始提交需求
> 4、turbolinks:visit               4、turbolinks:visit                 -> 觸發visit
> 5、turbolinks:request-end         5、turbolinks:before-cache          -> 無快取、結束需求。 有快取、快取前
> 6、turbolinks:before-cache        6、turbolinks:before-render         -> 無快取、快取前。   有快取、渲染前
> 7、turbolinks:before-render       7、turbolinks:render                -> 無快取、渲染前。   有快取、渲染
> 8、turbolinks:render              8、turbolinks:request-end           -> 無快取、渲染。    有快取、結束需求
> 9、turbolinks:load                9、turbolinks:before-render         -> 無快取、load。    有快取、渲染前
>                                  10、turbolinks:render                ->                 有快取、渲染
>                                  11、turbolinks:load                  ->                 有快取、load
>                                   

2-5-3、restoration visit(點下前進/倒退鍵)

> 原先網頁內容"沒"改變                 原先網頁內容"有"改變

> 1、turbolinks:visit               1、turbolinks:request-start         -> "沒"改變、觸發visit。 "有"改變、開始提交需求
> 2、turbolinks:before-cache        2、turbolinks:visit                 -> "沒"改變、快取前。    "有"改變、觸發visit
> 3、turbolinks:before-render       3、turbolinks:request-end           -> "沒"改變、渲染前。    "有"改變、結束需求
> 4、turbolinks:render              4、turbolinks:before-cache          -> "沒"改變、渲染。      "有"改變、快取前
> 5、turbolinks:load                5、turbolinks:before-render         -> "沒"改變、load。     "有"改變、渲染前
>                                   6、turbolinks:render                ->                    "有"改變、渲染
>                                   7、turbolinks:load                  ->                    "有"改變、load
>                                   

2-5-4、按下f5或者Refresh按鈕

> turbolinks:load 

2-5-5、重點整理

從上面的生命週期可以發現,不管是application visit還是restoration visit,最大的差異就在於cache的有無

舉個例子,現在有兩個頁面,一個是A頁面,裡面有一個B頁面的連結,假設先前沒有拜訪過B頁面,點該連結的時候,會先觸發request-end後,才觸發cache,相反的,如果今天有拜訪過B頁面,觸發request-start後,就會觸發cache。

上面的例子是application visit,那restoration visit呢?一樣舉個例子,假設今天先進到A頁面、點擊A頁面的B頁面連結後,把B頁面的內容置換到A頁面(url也換了),接著點擊瀏覽器的上一頁,而他觸發的是before-cache,不會再從server端提交需求,並拿到回應。

但!假設今天按上一頁回到A頁面的時候,如果上一頁的內容改變了(跟cache裡面存取的A頁面不一樣),turbolinks就會再重新跑一個提交需求,並把頁面存取到cache裡面。

3、Idempotent是什麼?

既然知道turbolinks的生命週期,也知道turbolinks執行的時候,造成JS發生詭異的行為,是因為cache和server內容不一樣,那要怎麼避免不一樣呢?這個turbolinks文件中也有說到:掛在Turbolinks上的程式,必須要是:Idempotent

3-1、Idempotent詳解


Idempotent是什麼?
在數學裡,Idempotent有兩種主要的定義。
1、在某二元運算下,Idempotent元素是指被自己重複運算(或對於函數是為複合)的結果等於它自己的元素。例如,乘法下唯一兩個Idempotent實數為0和1。
2、某一元運算為Idempotent的時,其作用在任一元素兩次後會和其作用一次的結果相同。例如,高斯符號便是Idempotent的。
from wiki

不過看完wiki,還是不知道他在說啥,決定直接引用紅寶鐵軌客的翻譯 他基本上是一個數學與電腦科學的專用字,是指這個程式或是數學運算式必須要每次執行結果都一樣!


3-2、沒有符合Idempotent例子

知道Idempotent的意思後,我們來舉個例子,如果今天程式碼沒有符合Idempotent,會發生什麼問題? 我有一個頁面,頁面上想要顯示使用者的當地時間,而這個頁面有很多元件,想要把這些元件,按照時間做分配(1/20的文章在一塊,1/22的文章在一塊)

如果今天我用data-timestamp屬性註記一組元件,代表元件的UTC(世界時間標準)建立時間,接著我用一組JS的函式,來查詢這個頁面所有組件的時間,並把所有時間轉成使用者的當地時間,最後在每一個新的一天插入日期的標頭。

如果今天你在turbolinks下實作這組函式,會發現一個大問題!!大家可以想一下會發生啥問題,給大家想3s~

… 1S
… 2S
… 3S

我們來實際走一次流程,假設我今天進到這個頁面,JS的函式如預期般的運作,並且會根據不同的日期,幫網頁內容插入不同日期的標頭,畫面正常的顯示出來,使用者接著點擊了另外一個頁面,Turbolinks保存一個頁面轉換後的副本到cache(有插入標頭的),當你按下返回鍵(上一頁)後,再次啟動turbolinks:load,此時你的JS又會再次插入標頭一次!

3-3、這裡會發生兩個問題!

3-3-1、第一個問題

假如今天你只是單純把時間印在A頁面上,像下面這樣,發生的問題就是,因為網頁是直接抓cache的內容,所以當下的時間點是第一次進入A頁面的時間點(第二次進入A頁面,顯示時間點是錯的)

> <%= Time.now %>

> 第一次進到A畫面
> 時間點顯示 - 2023-01-28 18:13:55

> 第二次進到A畫面(1分鐘後)
> 時間點顯示 - 2023-01-28 18:13:55(完全沒變)

3-3-2、第二個問題

如果你有用JS去更改Time.now,把時區更改成每個使用者登入的時區(美國人登入就改成美國時區、日本人登入就改成日本時區),會造成標頭被再次插入(出現了兩次),

3-4、用Idempotent解決問題

!!這一段我自己也不懂!!

一個讓轉換成為idempotent的技巧是,在每一次設定data屬性時,無論你之前是否已經設定都要檢查。 當 Turbolinks 從快取回存你的頁面時,這一些屬性仍然會存在,然後在你的轉換 function 中偵測這些屬性是否已經有處理過。

另外一個比較可靠的技巧是簡單的在轉換 function 本身檢查。 在上面的按日期分類的例子,意思就是在插入新的資料之前,檢查 div 元件的 date 是否已經存在,這可以優雅的處理尚未被原始轉換程式處理的新增元件.


雖然這樣說,但是Idempotent其實蠻難解決的,原因下面會提到


前面有說到,只要讓Turbolinks符合Idempotent,就不會出問題,但!!要符合Idempotent其實蠻難的,因為程式執行後,只要更改原先的資料或者移動DOM位置之類的,就會不符合Idempotent,因此真的發生太多詭異的JS的時候,就…把這個頁面的Turbolinks給關掉吧!畢竟想辦法先讓程式正常運行比較重要


關掉Turbolinks有兩個方法
1、關掉指定網頁的turbolinks cache
2、reload網頁


先來看一下cache 的行為:當 Cache 發生前,Turbolinks 會發出 turbolinks:before-cache 事件,當網頁還是顯示 cache 時的資料時,<html> 的 attribute 會加入 data-turbolinks-preview,但是我發現只有在 application visit 已經有 cache 的網頁才會有這 attribute,其他不會,因此要讓網頁不要顯示cache內容,只要把preview關掉就好,關掉 Turbolinks 的 cache 就是 no-cache,兩個都是加在 <head> 內,以下是關掉的方法:

4-1-1、關掉preview

> application.html.erb

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

這樣可能會有一點模糊,我實際舉個例子好了,假設我今天在index.html.erb,最下方新增一個JS的函式,這個函式可以在你進入此頁面的時候,在這個畫面上增加一個按鈕區塊

> index.html.erb

> <script>
>     var btn = document.createElement("button")
>     btn.innerHTML = "click me"
>     document.body.appendChild(btn)
> </script>

這樣當我們第一次進到這個頁面,就會有一個按鈕出現在畫面上,不過假如今天你進到站內另外一個畫面,接著再點上一頁/下一頁,就會發現按鈕會一次跑出兩個,會這樣就是因為原本頁面已經有cache的關係,要解決這件事只要把剛剛上面no-cache那一段加上去就可以解決。

剛剛說的是頁面的cache,今天如果直接在連結點下去時候,不要讓turbolinks運作,只要後面加上這一段,就可以把turbolinks關掉

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

4-1-3、小總結

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

4-2、reload

幾的常用的 reload page 方式:

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

5、串接GTM

終於!!前面講了這麼多的turbolinks的作用、生命週期,終於可以開始把rails網站接上GTM了~
Ps. 參考連結

> 原本放在head的程式碼

>  <!-- Google Tag Manager -->
> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
> new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
> j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
> 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
> })(window,document,'script','dataLayer','GTM-WWSRBV2');</script>
> <!-- End Google Tag Manager -->

> 原本放在body的程式碼

> <!-- Google Tag Manager (noscript) -->
> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WWSRBV2"
> height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
> <!-- End Google Tag Manager (noscript) -->

更改過後的head程式碼,把下面這一段,塞進head裡面就可以

<!-- Google Tag Manager trigger for Turbolinks -->
<script type="text/javascript">
$(document).on('page:change', function(){
var url = window.location.href;

dataLayer.push({
'event':'pageView',
'virtualUrl': url
});
});
</script>
<!-- End Google Tag Manager trigger for Turbolinks -->


<!-- Google Tag Manager -->
<noscript><iframe src="//www.googletagmanager.com/ns.html?id=[YOUR ID]"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','[YOUR ID]');</script>
<!-- End Google Tag Manager -->

雖然這樣就裝好了,但是我看紅寶鐵軌客的說明,這樣會遇到點擊連結後,GTM的debug console會不見,不過目前我還沒遇到(可能是專案還不夠大),之後遇到會在這邊補充!除了debug console不見以外,鐵軌客還有提出一些其他的問題,不過我目前遇到的專案也沒辦法體驗到(QQQ..希望可以趕快遇到大專案讓我試試),所以也只能等到遇到問題的時候再來補充這篇文章了。

前面有提到部分拿掉turbolinks,不過如果今天真的不想用turbolinks,要怎麼拿掉呢?

> 1、移除 turbolinks gem(移除完後記得bundle install)
> 2、移除:<meta name="turbolinks-cache-control" xxx>,這有可能在任何一頁,做個 search 吧!
> 3、javascripts/application.js -> 移除://= require turbolinks
> 4、assets/javascripts/ 中所有的 js 跟 coffee 都要改成:
>    js: $(document).on('turbolinks:load', function() {  ⇒  $(document).ready( function() {
>    coffee: jQuery(document).on 'turbolinks:load',  ⇒  $(document).ready
> 5、移除 'data-no-turbolink',data-turbolinks 屬性
> 6、Javascript 中:改寫任何有 Turbolinks.visit、Turbolinks.clearCache()、Turbolinks.xxx 的程式,

7、總結

終於!最後成功把ROR網站成功串接GTM了,並且也有把GA成功串接上,不過問題就是目前做的專案太小,所以沒辦法遇到軌道客說的一堆問題,希望之後能把這一塊補完!!