就是要一起拖拖拉拉!(二) - 利用 DataTransfer API 實作拖拉元素間的資料傳遞

上週提到了瀏覽器拖拉時會陸續觸發的幾個事件,我們知道了對可拖曳物件 (draggable) 進行拖曳與放開時以及拖曳物件滑過目標物件上方時,都會觸發對應的事件。今天我想研究看看在事件與事件之間可以做什麼事情,瀏覽器提供了 DragEvent 這個 API ,讓我們可以在開始拖曳時夾帶資料,並在拖曳結束放開時取得該筆資料。

Outline

  • DataTransfer 物件
  • 實際應用 DataTransfer API

DataTransfer 物件

上次有提到在瀏覽器操作中的拖曳功能,必須要自己搭配事件處理來實作,但其實有一些拖曳行為是預設就是有效,像是網頁連結和圖片的拖曳,以及文字選取的拖曳(反白)。而在這些以外的元素如果要觸發拖曳就必須使用 draggable 屬性來達成。

而在拖放事件的最一開始, ondragstart 事件被觸發時,我們其實可以利用事件物件(也就是事件回呼函式內的第一個參數 event ) 裡面的 dataTransfer 物件來賦予被拖放物件想要挾帶的資料,使用方式如下:

let dragger = document.querySelectAll('#dragger') 

dragger.addEventListener("dragstart",function(e){
	e.dataTransfer.setData('text/plain', 'This text   may be dragged')
})

dataTransfer.setData

dataTransfer 物件負責處理拖曳行為之間的資料傳輸,這個物件裡面有兩個方法,分別是 setDatagetDatasetData 使用方式如下:

dataTransfer.setData(format, data); 

這個 API 有兩個參數,分別是:

  • format :想要挾帶的資料格式,使用的是網頁常見的 MIME 格式字串,如文字格式就是 text/plain ,關於 MIME 型別可以參考 MDN 說明
  • data : 想要挾帶的資料

dataTransfer.getData

而在拖曳行為結束觸發 drop 事件時,則可以反過來利用 dataTransfer.getData 來取得前面挾帶的資料。

dataTransfer.getData(format); 

記得必須透過同樣的格式字串來取得同一個參數。

DataTransfer.types

回傳在 ondragstart 時透過 setData API 所設定資料的資料格式,因為可能不會只設定一種格式的資料,所以會以陣列的方式來表示。

實際應用 Datatransfer API

接下來我們就試試看根據上面的說明,如何應用到實際的拖拉操作流程上,修改上次的範例來做說明,先做一個輸入框來設定拖曳時想要挾帶的數值。

let dragDataInput = document.querySelector('#dragDataInput')

<div class="row align-items-center">
    <div class="col-2 d-flex align-items-center">
            <p class="m-0">輸入想要挾帶的資料:</p>
    </div>
    <div class="col-10">

      <input type="text" id="dragDataInput" class="form-control">
    </div>
</div>

之後在原本的 dragststart 事件處理裡面,利用 dataTransfer 設定夾帶資料,這邊資料的格式就用字串做表示。

dragger.addEventListener("dragstart",function(e){
   dragTemp = e.target
  e.dataTransfer.setData('text/plain', dragDataInput.value)
})

後面一樣在 drop 事件就能夠透過 e.dataTransfer.setData 收到前面設定的資料了,我把它掛到拖曳完成的元素上。

dropper.addEventListener("drop",function(e){
	...
  let dragText = e.dataTransfer.getData('text/plain') 
  dragTemp.append(dragText)
  e.target.style.color="#fff"
	...
})

完整範例可以在這裡看到。

參考文章

https://developer.mozilla.org/zh-TW/docs/Web/API/HTML_Drag_and_Drop_API

就是要一起拖拖拉拉!(一) - 瀏覽器 Drag and Drop API 操作

Drag and Drop 在一些網頁產品裡面算是蠻常見的應用,使用起來的效果也常常讓人印相深刻,在像是 Trello、Asana、Cakeresume 等需要進行一些計畫安排或是畫面規劃等互動性較強的工具,都會看到它的蹤影,由於之前的工作內容剛好都沒有機會碰到,最近又剛好想要研究,今天就讓我們一起來研究相關的 API 吧。

Outline

  • 基本概念
  • 宣告 draggable 與 droppable 元素
  • 可拖曳 (Draggble) 元素上的事件
  • 可拖曳 (Droppable) 元素上的事件
  • 實作 Drag and Drop 範例
  • 總結
  • 參考資料

基本概念

瀏覽器的拖拉功能基本上是由一連串的事件觸發而組成。在要被拖拉的元素上要先以 draggable的 屬性宣告,告訴瀏覽器這個元素是可以被拖移的,而在能夠放下拖拉元素的另外一個元素,相對的也必須以 dropabble 屬性宣告,才能夠接收拖移過來的元素內容。

而這一連串拖移的動作又可以拆分為幾個發生的時間段,說到這裡,對瀏覽器比較有概念的人應該可以猜到了。沒錯,這些時間段在瀏覽器內都是一個對應會被觸發的事件 (以下使用 Camelcase 做表示):

  • 開始拖移 (onDragStart)
  • 拖移中 (onDrag)
  • 拖移進入某個元素 (onDragEnter)
  • 拖移經過某個某個元素 (onDragOver)
  • 拖移離開某個元素 (onDragLeave)
  • 結束拖移 (onDragOver)

而拖拉期間與元素互動的方式,將會由開發者利用這些事件觸發來決定,詳細可以參考 MDN 官方網站,接下來會試著實作出基本拖放功能範例,以此說明一些常用到的拖拉事件觸發是如何發生。

宣告 draggable 與 droppable 元素

首先我們要先做出一個可拖曳的跟可放置的元素。分別在兩個素上將 draggable 屬性與 dropabble 屬性宣告為 true (這個專案使用 Bootstrap 作為輔助,後面會有範例)。

<div class="container"> 
  <div class="row justify-content-center"> 
    <div draggable="true" class=" box box-dragger"></div>
    <div droppable="true" class="box box-dropper"></div>
  </div>
</div>

之後,我們來看看上述提到的每個事件代表的意義與被觸發的時機,才能在實作時知道什麼時候該用什麼事件來搭配完成功能。

可拖曳 (Draggble) 元素上的事件

DragStart 事件

這個事件只會在被拖曳元素剛開始被拖曳時被觸發一次,我們可以試著在 .box-dragger 元素上宣告 dragstart 的事件監聽,接著就可以試著拖曳看看。

dragger.addEventListener("dragstart",function(){
 console.log("drag start!!!!")   
})

DragEnd 事件

dragend 事件會在拖曳結束時被觸發,應該很好理解,利用以下的程式碼片段:

dragger.addEventListener("dragend",function(){
 console.log("drag end!!!!")   
})

之後試著拖曳 .box-dragger 元素後再放開就可以看到上述的事件被觸發。

Drag 事件

drag 事件會在可以拖曳元素被拖曳期間持續被觸發:

dragger.addEventListener("drag",function(){
 console.log("draging!!")   
})

可以看見一如上面說的 dragstart 事件只會被觸發一次,而 drag 事件會在拖曳期間持續被觸發。

可放置 (Droppable) 元素上的事件

DragEnter 、DragLeave 事件

這兩個事件的觸發分別會在被拖曳元素進入可放置元素被拖曳元素離開可拖曳元素時發生:

dragger.addEventListener("dragenter",function(){
  console.log("dragenter")
})

dragger.addEventListener("dragleave",function(){
  console.log("dragleave")
})

試著把元素拖曳進可放置元素再離開,可以發現 dragenterdragleave 分別被觸發了一次。

DragOver 事件

dragover 這個事件名稱代表的意義應該不難理解,意思是當可放置元素上有拖曳中的元素經過,就會持續觸發,有點像是 CSS 裡面的 :hover 所代表的使用情境,試著使用下面的程式碼試試看這個事件的綁定吧:

dropper.addEventListener("dragover",function(){
  console.log("dragover")
})

順利的話應該會看到以下結果:

Drop 事件

在 MDN 裡面, drop 事件的定義是「一個元素或文字選取區塊被放置至一個有效的放置目標時觸發。」,但想要順利的觸發這個事件必須要注意的一點是,在 drop行為發生之前的 dragover 事件的預設行為會阻止 drag 行為的完成,也會讓 drop 事件無法被觸發。

因此必須在 dragover事件裡面停止預設的行為,這可以用 preventDefault 方法來達到。對這個預設行為有興趣的可以先參考官方說明,這部分預計在下一篇文章會有更詳細的說明。

dropper.addEventListener("dragover",function(e){
   e.preventDefault()
})
dropper.addEventListener("drop",function(e){
  console.log("drop!")
})

實作 Drag and Drop 範例

現在就讓我以前面的說明為例,來看看如何做出能夠可以把 dragger 拖放到 dropper 元素內的拖曳功能吧,我們已經宣告完 draggable 以及 droppable 的元素了,接下來只要利用事件處理就能夠完成拖拉互動了。

現在我們有兩個方塊,左邊藍色方塊是前面提到的 dragger 右邊則是 dropper ,目標是讓藍色方塊可以被拖曳拉到右邊另一個方塊,而一個很基本的構想是在 drop 事件觸發時,拿掉原本的舊藍色方塊,並把藍色方塊複製一份,放到右邊橘色方塊裡面

我們可以用另一個常見的 Web API 叫做 appendChild ,原本是用來在元素內掛載新的元素,不過如果所掛載的對象是一個已經存在的元素,就會變成是移動元素的效果(驚喜不驚喜意外不意外?沒關係我也是現在才發現),可以參考 W3School 的範例

所以在 dragstart 發生時,我先試著把藍色方塊的內容暫存到另外一個變數裡面。

let dragger = document.querySelector(".box-dragger")
let dropper = document.querySelector(".box-dropper")
let dragTemp;

dragger.addEventListener("dragstart",function(e){
  dragTemp = e.target
})

接下來要記得上面提到的,在 dragover 裡面要阻止瀏覽器預設行為。

dropper.addEventListener("dragover",function(e){
  e.preventDefault()
})

這個時候 drop 事件已經可以順利被觸發,只要在 drop 事件觸發時,在 event.target 也就是 dropper 裡面掛載藍色方塊的元素,就可以完成移動。

上面會看到兩個方塊沒有對齊是因為我一開始有給一些 margin ,不過從開發者工具裡面可以確定藍色方塊已經被拖曳放到橘色方塊之中。

再搭配適合的事件觸發以及適當的樣式調整,像是 dragenter 時改變顏色、 dragleave 時復原,就可以讓拖曳的元素經過可放置元素時產生顏色的改變來提示使用者。

最後在 drop 事件觸發並掛載後,調整一下元素的 spacing 可以看到比較好看的效果了。

想了解完整程式碼的話,我把這個 Demo 放在 Codepen

總結

其實拖拉互動效果並沒有非常複雜,可以想成瀏覽器提供給我們從拖拉開始到結束,一連串的時間段,讓我們可以在期間自由地做互動效果的調整,只要知道什麼時候該做哪些處理,基本上就是網頁元素的互動而已。

而想要達成拖拉互動效果也並不是只有一種方法,也沒有最正確的答案,只要熟悉DOM 元素的操作,能達到心中想要的效果我相信都是可以的。下一章節一樣會針對瀏覽器這個拖拉功能做稍微深入一點的研究,希望今天的主題能夠帶給你一些新的啟發,那麼下次見啦!

參考資料

個人技術站一把罩!部落格建置大全(二)- 將 Github Page 串上自己的域名

上週提到了使用 Hexo 這個工具來架設個人部落格,並放到自己 Github Page 上的方法。這次我們要來看看怎麼把架好部落格的 Github Page ,串上自己擁有的域名( ex. blabla.com ) ,所以在這篇文章內將會對 DNS 與相關設定有一定部分的講解跟介紹。有興趣的人可以先用 https://www.cowboybebop.space/ 來觀看設定完成的結果,這個域名應該還會保留 1~2 個月左右。

Outline

  • 域名購買與域名商介紹
  • 什麼是 DNS ?
  • A Record 與 CNAME
  • A Record 與 CNAME 設定
  • Github Page 的域名設定
  • 總結
  • 參考資料

域名購買與域名商介紹

首先在開始進行串接之前,你要先有域名,而域名可以透過域名商來做購買,這些域名商有很多,相同域名在不同商家的價格也會不太ㄧ樣,或是某些域名只能在某些域名商買得到,這邊推薦幾個我自己用過的:Gandi網路中文Google Domains 。這篇文章會用 Gandi 來做示範,但其實這些網站上購買的流程都差不多。

在網站上購買完域名之後,就可以在 Domains 頁面看到自己擁有的域名。點擊進入各個域名就可以前往個別的設定頁面,今天會以我之前買的多出來的域名 cowboybebop.space 為例。

找到 「 DNS Records 」的分頁,會看到很多,會看到一堆很像神秘魔法咒語的設定值,今天不會逐一介紹,但這邊我們有必要先了解一下什麼是 DNS。

什麼是 DNS ?

DNS (Domain Name Server) ,直翻成中文是網域名稱伺服器,可以理解為「負責處理域名的伺服器」,為什麼域名還會需要處理呢?我們在使用網際網路瀏覽網頁時所看到的內容,其實是從你使用的裝置(手機、電腦或平板)連出去對另外一台裝置(也就是伺服器)索取回來的。而兩者之間的溝通,則是以 IP 位址來找到正確的伺服器,IP 位置就像現實世界的門牌地址ㄧ樣,讓其他人可以順利找到對應的位置。

但是我們每天經常使用的網站這麼多,像是 Facebook 、Gmail 、Dcard、Slack,如果世界上所有的網站,都使用 IP 位置來做位置的辨別,那麼你可以想像,我們可能要熟記好幾種數字組合才能夠維持正常的生活。

DNS 的出現就是為了解決這個問題,如同我們現在在生活中所經歷的,只要在搜尋列上輸入 facebook.com 就能夠快速前往臉書的網站,這是因為 DNS 透過給每個 IP 位置取個名字,把這些原本只有電腦能夠理解的位置數據,變成人類也很好記憶、理解的名稱,讓各種網站應用能夠更加融入我們的生活中。

A Record 與 CNAME

DNS 相關的設定其實有很多,我們就是透過這些設定來告訴 DNS 如何將域名配對到正確的 IP 位址。今天會介紹其中最常會碰到的兩個設定值:

  • A Record : A 代表 Address ,也就是紀錄 IP Address 與網域名稱的配對,這個紀錄是在設定 Domain Name 時最重要的項目。

  • CNAME : CNAME 是網域名稱的別名,用來將子域名指向另外一個主機的域名,最常見的就是將 www.abc.com (子域名) 指向 abd.com(購買的主域名) ,避免使用者因為意外而找不到網站(夠買主域名後,就可以從域名商的設定後台,新增子域名,是不需要另外付費的)。

  • ALIAS : 與 CNAME 記錄很相像,差別在 CNAME 別名只能用於子域名,而 ALIAS 記錄能夠用在主域名,但這麼做會影響對主域名的域名解析,所以不能與 A 紀錄同時使用。

在了解什麼是 A Record 與 CNAME 與 ALIAS 之後,就讓我們繼續往下設定。要達到串接域名的效果其實不只有一種方式,為了幫助理解以下提供兩種可能的方案。

方案一:直接透過 A Record 設定

因為今天的目標是要將自己的域名可以接到 Github Page。如果只是單純的想要把主域名 ( cowboybebop.space ) 串到 Github Page 上,只要將它指向 Github 伺服器 的 IP ,也就是:

185.199.108.153

就可以讓域名順利解析為 Github 主機的位址了,這部分的相關資訊可以在 Github 官方說明文件內找到。而因為我們不只要找到 github.io 還要找到我們的個人頁面 moojitsai.github.io ,這部分必須在 Github Page 的 Repo 內做對應的設定,會在接下來的內容中提到。

方案二:CNAME 搭配 ALIAS 設定

設定 CNAME 的目的是因為有時候我們希望使用者是透過加上 www 前綴(www.cowboybebop.space ) 的網站域名進入我們的網站,在這種情況下只要用戶不記得或不知道要輸入 www. 前綴,就會沒辦法順利找到我們的網站應用,所以我們必須要將這些用戶重新導向到我們想要的域名。

通常會在域名前面加上 www. 當作網站位置是為了提醒用戶,這是一個公開的網站,藉此提升體驗。但其實加或不加,並不影響網站的功能性,只要有對應正確的設定即可,因為加上了 www. 前綴的域名,相對於主域名來說只是子域名而已,只是站在 SEO 或是追蹤流量的角度,一般還是建議兩種方式選一種(要不要加上 www.) 並且統一使用,否則 Google 爬蟲會將這兩種域名視為兩個不同的網站。

在方案二中,我想要以將原來沒有 www. 前綴的主域名統一導向到有 www. 前綴的子域名的方式來完成:

cowboybebop.space/    -----自動導向到------->    www.cowboybebop.space/

就像上面提到的,ALIAS 記錄可以用在主域名的別名設定,這個設定剛好可以達成上面提到的導向到具有 www. 前綴域名的效果。因此我們要這麼做:

  1. 將主域名的 ALIAS 設為加上 www. 前綴的子域名
  2. 將加上 www. 前綴的子域名, CNAME 設為 Github Page 的 URL

提供下圖做為參考,只要看第一跟最後一筆紀錄就可以了,其他是系統預設的設定。

透過這樣的方式,我們就能夠完美的將有 www. 與沒有 www. 前綴的域名都導向到 Github Page 頁面囉!而且統一使用的會是 www.cowboybebop.space,也符合前面提到的建議使用方式!

Github Page 設定

在 Github Page 對應 Repo 內的 Setting 頁面裡的 「 GitHub Pages 區塊」,可以看到 Custom Domain 欄位,在這邊我們必需告訴 Github 我們想要將自己的 Github Page 與哪一個域名串接,才能夠讓我們自己的域名,正確對應到原來個人的 Github Page Url。

設定完之後,記得把 「Enforce Http」的設定打勾,這樣一來,我們的域名就自動有了 https 憑證,不需要在自己簽發,因為 Github 幫你做掉了,這個功能超方便!

設定完成後應該可以在 https://www.cowboybebop.space/ 看到頁面了!

總結

域名相關的設定對於網頁相關工作者來說,初期並不是那麼容易了解,因為其中還有許多細節跟環節,像是 DNS 相關的設定也不只有今天介紹的三種,還有其他適用於特殊使用情境的設定,只不過比較特定。像是 ALIAS 紀錄我也是在寫這篇文章的期間才發現,並知道怎麼設定的。

而因為每次 DNS 設定更新之後,必須等待 DNS 伺服器解析後才會生效,因此來來回回也失敗了好幾次才終於達成目標,這部分是我覺得最麻煩的。也感謝我的顧問團隊 Max 的幫助。那麼這個 Github Page 的部落格系列就暫時到這邊,希望大家都能夠有記錄自己成長過程的專屬頁面囉,那麼就下次見,有發現什麼有趣的東西再來跟大家分享!

參考資料

Managing a custom domain for your GitHub Pages site

CNAME 紀錄

為什麼越來越多的網站域名不加「www」前綴?

我室友 Max

個人技術站一把罩!部落格建置大全(一)- 使用 Hexo 搭配 Github Page 建置自己的部落格

這是我參加六角全馬鐵人挑戰的第二週,在比賽的一開始,就讓我來分享如何在 Github Page 上面架設自己的個人頁面,並串上自己購買的網域名稱(如果有的話)。相信各位工程師們多少都會聽過或看過其他工程師們使用 Medium 當作自己的部落格,對其他人分享自己經歷及技術,或是成長過程中領悟到的見解。

但如果你想對這個屬於自己的空間有更高的掌握度,或是想做一些比較客製化的排版視覺,那麼架設個人專屬的部落會是一個不錯的選擇。因此這邊推薦使用 Hexo ,作為今天介紹的主角。當然,方法沒有絕對,你也可以選擇自己從零開始開發,但我認為工程師的職涯如此漫長,有很多問題等我們解決,因此在適當的時機使用適合的工具,有時候是必要的。

Outline

  • Hexo 介紹及專案建置
  • Hexo 專案架構
  • Github Page
  • 將 Hexo 專案部署到 Github Page 上
  • 總結

Hexo 介紹及專案建置

Hexo 是一套可以快速幫你建置個人部落格的工具,在官方提供的頁面你可以找到很多別人做好的部落格模板,並直接套用到自己的專案。

首先讓我們先把 hexo 的建置工具裝到系統內,使用以下指令:

npm install -g hexo-cli

之後就可以利用 hexo 指令創造一個新的部落格專案。

npm init <要創造的 Hexo 專案資料夾名稱>

接下來進入剛剛建好的專案資料夾再用這個指令把 hexo 本地測試 server 架起來。

hexo server 或 hexo s

沒錯,這樣一來在本地就可以看見即時的變更囉,很方便吧!

Hexo 專案結構

這邊先介紹一下 Hexo 的資料夾分佈,以及功能。首先在根目錄可以看到幾個比較重要資料夾:

  • public : 存放編譯後的靜態 html 檔案,基本上不會需要改動裡面的內容

  • scaffolds:存放文章模板的地方,新增文章的時候可以選擇要用哪一種模板,以我為例,在寫 JS 文章跟後端 Ruby on Rails 文章的時候就可以用不同模板來產生不同分類與不同標籤的文章,不用每次都另外再改。使用模板來新增文章的參考指令:

      hexo new <<Scaffold Name>>  <<Article Name>>
    
  • source : 存放部落格文章原始檔案, Hexo 內的文章通常以 Markdonw 來表示內容,而 Markdown 在很多地方都通用,非常方便。

  • themes : Hexo 官網可以選擇許多別人做好的主題,在官網找到喜歡的主題後,就可以下載並放到這個資料夾,然後記得在根目錄的 _config.yml 檔案裡的 theme 設定改為對應的主題名(資料夾名稱) ,以這個範例來說就是 landscape ,而對應的主題資料夾裡面則包含了外觀相關的原始碼(如 HTML / CSS / JS),建議在必要的時候再去修改這些原始碼,否則盡量修改對應主題資料夾裡面的 _config.yml 檔案(與根目錄的同名設定檔不同)會比較好。

Github Page

什麼是 Github Page ? 在對 Hexo 專案內容有了基本的了解後,讓我們繼續往下看。 Github Page 是 Github 提供的、能讓開發者利用 git 的形式直接配置好靜態頁面的功能,非常好用,許多實驗性的作品或專案也都會透過這樣的方式來呈現,而今天我們就會嘗試將 Hexo 用 Github Page 的方式來作部署。

首先在 Github 上創建一個新的 Repository ,Repository的名字依照官方說明,必須遵循以下規則

( 記得 username 是你自己的 Github 帳號,不要輸入錯了 ):

<username>.github.io

然後就會得的一個新的 Repository ,待會我們 Github Page 就會是以這個 Repository 的內容為主來做對外顯示。

其實剛剛輸入的 Repository 名稱 <username>.gtihub.io 就會是你個人Github Page 的網址,可以直接透過瀏覽器輸入網址找到,但因為目前還是空的,所以還不會有東西,我們先在電腦本地將這份 Repository clone 下來 :

https://github.com/moojitsai/moojitsai.github.io.git

並新增 index.html 檔案做個測試,因為 Github 預設會去尋找這個檔名的檔案作為進入點。

新增完成後只要再用 git push 推回剛剛的 Repository 上,就會有 index.html 檔案,應該就可以從你的個人 Gtihub Page網址看到了(如果沒有看到再來問我)。

將 Hexo 專案部署到 Github Page 上

建立完新 Gtihub Page 後,我們來看看怎麼把這整個部落格專案放到 Github Page 上面,其實不難, Hexo 大多幫你做好了,只要設定檔配置正確就沒問題。

首先找到 _config.yml 這個檔案,然後在 deploy 這個設定下輸入你對應的 Github Page 的 Repo 位置,並把 type 寫為 git ,就完成基本設置了:

然後在部落格專案目錄底下裝上官方提供的 Git 部署套件 hexo-deployer-git

npm install hexo-deployer-git --save

最後因為我們文章內容並不是網頁可以直接看得懂的格式,像文章內容就是用 Markdown 來撰寫,所以在部署上 Github 之前我們要先產生部落格專案所需要的靜態檔,使用以下指令來產生:

hexo generate 或是 hexo g 

完成後就可以部署了:

hexo deploy 或 hexo d

總結

今天的內容主要是紀錄部落格架設的過程,因為當初在使用 Hexo 的某些設定及功能時,某些官網寫的資料並不是那麼明確,還是要自己實驗過或是去看原始碼才比較會知道怎麼做,因此還是寫了一篇文章記錄下來,順便分享給有興趣的各位,下一篇要分享的是如何把今天設置完放到 Github Page 的網頁與自己購買的域名做串接,應該會蠻有趣的!可以先看看我最近串好的域名: https://www.muji.dev

參考文章

Hexo 官網

Managing a custom domain for your GitHub Pages site

Github Page 說明

瀏覽器的時光機—歷史堆疊、 pushState 與 replaceState API

或許 pushStatereplaceState 這兩個詞你可能沒聽過,但是瀏覽器「上一頁、下一頁」功能你一定不陌生,其實這兩個 API 是瀏覽器提供給開發者操作瀏覽紀錄用的,透過這兩個 API 並搭配事件處理,我們就可以將瀏覽器預設的「上一頁下一頁」修改成我們想要的客製化結果。

  • 使用情境說明
  • pushState / replaceState 與點擊新連結有什麼不一樣
  • pushState / replaceState 與 Stack 結構
  • pushState / replaceState 使用方式
  • onpopstate 事件
  • 一個栗子

使用情境說明

最近接到了必須修改瀏覽器歷史紀錄行為的需求,所以順便寫了篇文章整理下來。 在公司負責的產品是類似 AWS 那樣資源控制的後台介面,前端部分雖然使用了 React 作為主要工具,但架構上有點像是由 Webpack 打包出靜態檔, 之後交由後端 Server 來處理前端內容,所以無法直接將路由控制交給 React 作使用。

什麼意思?一般 SPA 原理是由 JS 產生動態內容並即時掛到 DOM 上,但這個產品流程上是這樣:

  1. 在開發階段由 Webpack 將源碼打包成瀏覽器看得懂的程式碼,一個工具模組對應一個頁面
  2. 在產品運行階段由 Node Server 來處理剛剛 Webpack 打包好的靜態檔案
  3. 因此除非在後端做更詳細的路由修改,否則無法作更複雜的路由變化(ex.巢狀路由)

而在這種情況下如果某一工具模組需要以多個頁面來進行資料的設定流程,就沒辦法像一般我們所習慣的使用框架的 Router 函式庫 — React-Router 或是 Vue-Router 讓路由與頁面動態地做搭配,因此 pushState 及 replaceState 這兩個 API 就特別適合這個時候拿出來使用。

(可能還是可以,只是在當時沒有太多時間做調查的情況下,我當下所做的決定就是使用這兩個 API 來解決)

首先讓我們先來了解 pushStatereplaceState 在做什麼事情。

pushState / replaceState 與點擊連結有什麼不一樣

兩種方式都會改變瀏覽器網址列的內容,但 pushState 與 replaceState 差別在於**會不會發送新的 Http Request,**這點應該不難發現,一般我們點擊新連結時,因為會發送 Request ,對後端重新要求一份的 HTML 內容,因此這個時候會看到瀏覽器重新刷新了頁面。

相對的 pushState 與 replaceState 這兩個 API 則單純只會修改網址列的內容,而不會刷新頁面,只是如果使用者在網址列按下 Enter 或重新整理的話,就一樣會發出 Request 。這與單純點擊的效果相同。

瀏覽器記錄與 Stack 結構

關於今天提到的兩個 API,如果查詢 JS 的 MDN 文件的話,關於 pushState 會得到以下的說明:

In an HTML document, the history.pushState() method adds a state to the browser's session history stack.

replaceState 則是 :

The replaceState() method modifies the current history entry, replacing it with the state objects, title, and URL passed in the method parameters.

讀完這兩段雖然可能沒辦法馬上弄清楚說明的意思,但從內容我們可以看到一個蠻重要的部分:「 history stack.」這是否說明瀏覽器記錄與資料結構裡的堆疊 ( Stack ) 有關?沒錯,瀏覽器裡的歷史紀錄就是以堆疊的形式儲存下來供使用者作使用,首先讓我們來看看堆疊是什麼,可以看看以下的模型圖。

我們可以使用 pushpop 兩種方法分別對堆疊結構加入一筆新的資料或是取出最後一筆資料,因此,關於堆疊有一個很常見的描述就是「先進後出」。其他前端常用到的資料結構可以參考我之前鐵人賽的文章

所以堆疊套用在歷史紀錄上是怎麼回事呢?每當我們從同一個頁面點擊網址轉到新的頁面時,就是在對歷史紀錄的堆疊使用 push 方法新增一筆新的瀏覽紀錄 ,而當我們執行上一頁往前瀏覽時,就像是在使用 pop 方法取出最後一筆瀏覽紀錄(網頁位置)。

pushState / replaceState 使用方法

history.pushState(state, title, url);

現在讓我們回到這兩個 API 上 , pushState 與 replaceState 都接受三個參數,分別是:

  • state:每個 history stack 都會可以給一個 state 物件。
  • title : 更新後頁面的 title 標籤內容設定,不過根據官方文件說明,目前為止大部分瀏覽器都會忽略他,因此最安全的方法是傳入空字串,不做任何修改。
  • url : 執行該方法後想要更新的想要更新的 url

但是這兩個長得這麼像的方法在使用上有什麼不ㄧ樣呢?首先,前面提到這兩個 API 都只會更改網址列內容而不會發出新的 Request (刷新頁面),而它們在使用上其實也差不多,差別在改變歷史紀錄堆疊的方式而已。

pushState 在被呼叫之後會真的對瀏覽歷史紀錄堆疊新增一筆紀錄,所以如果用 console.loghistory.length (歷史紀錄堆疊的長度)印出來看的話會發現長度多了 1 。

replaceState 在呼叫後雖然ㄧ樣會改變網址列內容,但 history.length 的值卻不會有任何改變,這是因為如果用堆疊的方式來看歷史紀錄的話, replaceState 只會修改堆疊的最後一筆紀錄內容,也就是目前的網址列內容。

onpopstate 事件

在瀏覽器上一頁按鈕被執行時,堆疊的最上層,也就是最後一筆瀏覽紀錄會被取( pop ) 出,新的一筆紀錄網址會被更改到網址列內,而這時會觸發瀏覽器的內建事件 — popstate 事件。所以如果有些客製化功能想要搭配上一頁按鈕執行,就可以使用這個事件。使用:

window.onpopstate  = (event)=>{ //事件函式內容 }

就可以寫入自訂的事件內容。

一個栗子

說了這麼多,來舉個實際的例子看看運作方式如何吧,現在我有三個按鈕,每個按鈕點擊後各自會呼叫有不同參數的 pushState 方法,而這時因為是第一次進入頁面,所以 history.length 是 1 。

所以如果我依序點擊 first 、second 、 third 按鈕之後,應該會在瀏覽器歷史裡新增三筆紀錄堆疊。

可以從上圖看到目前的網址內容變成 third.html ,但頁面仍然是原本的內容,沒有刷新,而瀏覽紀錄堆疊長度也真的變成 4 。這時的堆疊裡應該分別是:

first.html -> first.html -> second.html -> third.html 

堆疊裡的最後一筆紀錄是 third.html ,所以現在如果點擊上ㄧ頁按鈕,理論上會回到 second.html 堆疊。

透過上圖可以看出確實如此,而且搭配 onpopstate 事件把 event 物件印出來可以看到在 pushState 時傳入的 state 物件的內容 也會隨著被 pop出來。還有一個可以注意的地方是我們在做以上這些操作時,都沒有任何頁面刷新的情況發生,但確實改變了瀏覽紀錄堆疊。

總結

上面總共提到了 pushState、replaceState 及 onpopstate 事件,也提到歷史紀錄堆疊的存放方式,還有pushState、replaceState 與一般點擊連結的差異,只要好好活用這些 API 方法的特性,就可以達成 主流框架 Router 如 React-Router 或 Vue-Router 那樣不發 Request 就能頁面的效果(其實推測一下的話這些 Router 函式庫裡面應該也是使用這些方法)。

那麼就寫到這邊,最近剛好有這樣的需求需要比較特別的解決方法,剛好看了一下以前沒有深究的部分,覺得蠻有趣的,就順便記錄跟整理下來,下次有不錯的東西再寫下來跟大家分享囉!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×