實作簡易的 Client Side Router

路由

一言蔽之,路由是匹配 URL 和 內容的流程。

以前,路由是由後端在處理。我們透過瀏覽器直到看到頁面的流程如下:

  1. 假設伺服器架在 godlike0108.github.io 這個網域
  2. 瀏覽器造訪伺服器下的某個 URL(例如: godlike0108.github.io/tetris )
  3. 伺服器路由解析 /tetris 路徑
  4. 伺服器回傳與 /tetris 路徑相對應的服務(執行函式、或回傳一個靜態頁面)

這麼做的壞處是,每次跳轉新頁面時,都得回傳一整個新的頁面文件;但有的時候,頁面間的差異可能僅僅是某些資料的不同,重新載入整份文件顯得浪費,也造成使用者體驗較差。

前端路由

後來有了 AJAX 技術,我們得以透過請求資料的方式,在相同頁面直接抽換部分內容,而不用透過後端路由載入整份新的頁面,單頁應用程式(Single Page Application, SPA)就發展了起來。

SPA 簡單來說,就是伺服器只提供給你單一頁面,要呈現不同內容,則以 AJAX 請求抽換內容來實現。

但有個問題,由於所有資料都在同一個 URL 底下,使用者可能在 SPA 內點啊點的,好不容易找到需要的資料,下次需要看同一筆資料時,不能透過 URL 直接連到該資料,還是只能進入首頁,點啊點的…

也就是說,「不同資料具有相對應的 URL 」還是有其必要性。因此,前端路由技術因應而生。

前端路由原理

相較於後端路由切換 URL 時會呼叫伺服器的服務來回傳頁面,前端路由切換 URL 時,是直接利用寫在前端的 JS 檔來替換 DOM 和資料。

因此合理的流程如下:

  1. 先定義各 URL 會對應到的處理函式
  2. 在切換 URL 時解析 URL 並觸發對應的處理函式
  3. 函式替換內容與 DOM

問題是:

  1. 程式碼怎麼改變 URL ?
  2. 程式碼怎麼知道 URL 何時被改變了?

因此我們需要一套能夠與 URL 互動的工具,而瀏覽器提供了兩種與 URL 互動的方式,分別為 Hash 跟 History API ,於是產生兩種不同的實作方式。

利用 Hash 實作前端路由

流程:

  1. 定義各 URL 會對應到的處理函式
  2. window.location.hash 取得 URL
  3. <a href="#/<URL名稱>></a> 更動 URL 可觸發 hashchange 事件
  4. 監聽 hashchange 事件,當事件觸發時執行與該 URL 對應的函式

原理:
# Hash 符號代表某個在頁面上的錨點,當造訪的 URL 是同頁面的錨點時,頁面不會重新載入。

若瀏覽器當前的 URL 內含有錨點時,使用 window.location.hash 能夠取得該錨點的字串值: #<錨點名稱>

瀏覽器提供 hashchange 事件,當錨點改變時會被觸發。

核心程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// route handler
function router() {
// assign router view
view = view || document.getElementById('view');
// assign url (hash version)
let url = window.location.hash.slice(1) || '/';
// use url to match current route
let route = routes[url];
// if view and route exist, render route content in view
if(view && route.callback) {
view.innerHTML = route.callback(route.templateID);
}
}

// listen to event when load or hashchange
addEventListener('load', router);
addEventListener('hashchange', router);

完整程式碼連結

利用 History API 實作前端路由

流程:

  1. 定義各 URL 會對應到的處理函式
  2. window.location.pathname 取得 URL
  3. window.history.pushState()window.history.replaceState() 更動 URL 可觸發 popstate 事件
  4. 監聽 popstate 事件,當事件觸發時執行與該 URL 對應的函式

原理:
目前主要瀏覽器都會提供 History API 讓我們能夠操縱當次瀏覽器訪問過的 URL 記錄。 與記錄相關的資料被存在 window.history 物件中。

history.pushState()history.replaceState() 分別可以讓我們新增一個 URL 記錄、以及替換當前的 URL 記錄。被新增在記錄中的 URL ,用上一頁/下一頁可以瀏覽到。

當瀏覽狀態改變時, popstate 事件會被觸發,例如當「回到上一頁」、或造訪新的網站時。

很重要的特性是,更動瀏覽記錄並不等於瀏覽狀態改變,單純更動瀏覽記錄並不會讓瀏覽器直接跳轉到新的 URL 回傳的頁面,因此我們可以利用此特性製作前端路由。

核心程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// route handler
const router = {
// init is used when we directly enter the app
init(path) {
// update the url
history.replaceState(null, null, `${origin}${path}`);
// assign router view
view = view || document.getElementById('view');
// update the content
let route = routes[path];
view.innerHTML = route.callback(route.templateID);
},
// go is used when we trigger links in th app
go (path) {
// update the url
history.pushState(null, null, `${origin}${path}`);
// update the content
let route = routes[path];
view.innerHTML = route.callback(route.templateID);
}
}

// Init the router when first time enter the URL
router.init(location.pathname);
let navbar = document.querySelector('.navbar');
navbar.addEventListener('click', e => {
if(e.target.tagName === 'A') {
e.preventDefault();
router.go(e.target.getAttribute('href'))
}
})

完整程式碼連結

Reference

Hash 實作前端 Router
History API 實作前端 Router
現代前端 Router 實作(猛)