大家好,昨天我們建立了Route並設定好路徑與匹配對應的元件,今天目標要來繼續完成Router。Router作為判斷符合設定的路徑並匹配對應的Component,可以說是整個SPA重要的中樞神經,也是本系列文章的精華重點。
昨天建立完Route後,一樣在routes目錄裡建立路由Router.js:
src/routes/Router.js:
// 引入路徑模組
import { Route } from './Route'
export const Router = () => {
// 1.得到目前路徑(對應route)
// 2.找出對應頁面
// 3.將元件內容渲染至畫面
}
首先宣告Router為一個function作為模組輸出,給index.js進入點使用。為了說明清楚,這邊分三步驟來解析:
在開始我們需要得到目前對應元件真正的路徑,由於路徑的產生方式是透過location.hash方法,如此會帶有hash(#),所以需要整理一下,才能去比對Route每個元素裡的path屬性:
src/routes/Router.js:
export const Router = () => {
// 1.得到目前路徑(對應route)
const path = location.hash.slice(1).toLowerCase()
// 2.找出對應頁面
// 3.將元件內容渲染至畫面
}
location.hash得到的值會是一個字串,拿掉前面的#可以用slice(1),然後統一轉小寫就能取得真實的對應路徑了。
接著我們想要透過剛剛宣告的path與Route來做比對,並找出對應的元件,所以另外設計一個函式來處理這件事:
src/routes/Router.js:
import { Route } from './Route'
// 找出對應元件
// path:目前路徑,routes:路徑設定
const getComponent = (path, routes) => {
let result = routes.find((r) => r.path.match(new RegExp(`^${path}$`))) || {}
return result
}
export const Router = () => {
//...
為了找到route裡第一個符合條件的結果,這邊使用的是ES6的find()方法,不免俗地來看一下MDN的說明:
find() 方法會回傳第一個滿足所提供之測試函式的元素值。否則回傳 undefined。
這邊宣告result來判斷得到的結果,以迭代比對routes裡每個元素的path屬性,比對採用的方式是正規表達式match方法,若有找到結果,result會得到整個Component物件,沒有則是空物件,最後再返回result。
接著我們回到Router函式裡繼續完成第二步:
src/routes/Router.js:
export const Router = () => {
// 1.得到目前路徑(對應route)
const path = location.hash.slice(1).toLowerCase()
// 2.找出對應頁面
// ES6 解構賦值
const { component } = getComponent(path, Route)
// 3.將元件內容渲染至畫面
}
可以先看右半邊,第一步宣告的path與Route陣列當作參數,傳至getComponent函式得到結果後,在左半邊可以看到宣告常數component外面包著{...},這是使用了ES6的解構賦值:
解構賦值 (Destructuring assignment) 語法是一種 JavaScript 運算式,可以把陣列或物件中的資料解開擷取成為獨立變數。
透過這樣的宣告式把物件資料解開,所以這邊宣告的component在成為變數同時,會得到getComponent返回物件裡component屬性的值,也就是對應path的Component物件。
最後一步是產生畫面,既然我們已經拿到對應的Component物件,那麼要產生畫面也不是難事了:
src/routes/Router.js:
export const Router = () => {
// 1.得到目前路徑(對應route)
const path = location.hash.slice(1).toLowerCase()
// 2.找出對應頁面
// ES6 解構賦值
const { component } = getComponent(path, Route)
// 3.將元件內容渲染至畫面
document.querySelector('#wrapper').innerHTML = component.render()
}
從剛剛宣告component的物件裡取出render屬性的值,也就是要產生的HTML,然後用querySelector選擇器操作DOM寫入HTML,這樣render就完成了! 最後我們來把所有的元件串連起來,讓它們能夠正常運作。
回到進入點index.js,由於已經將路由模組化,這裡只要將路由與監聽事件搭配使用就可以了。前面我們有提到監聽的事件類型有兩種時機,當網址產生變化及畫面加載完畢時,所以這邊增加hashchange與load類型的監聽事件,並觸發Router程序:
src/index.js:
//引入scss
import './scss/style.scss'
//Router
import { Router } from './routes/Router'
//監聽hash變化&加載完畢事件
window.addEventListener('hashchange', Router)
window.addEventListener('load', Router)
完成後我們來看看成果(記得執行npm run build後用live-server開啟檢視):
可以看到切換上下頁面時,內容跟著改變了,這是好的開始!不過你知道的,往往事情很難第一次就成功,可以發現當你把hash拿掉時,這樣的場景似乎有些熟悉也不意外:
這裡就是昨天賣的關子,也是之前系列有提到的情境。因為一開始沒有在網址列提供hash,導致location.hash無法對應route裡的path,所以我們在Router一開始增加第零步,檢查是否有hash值:
src/routes/Router.js:
export const Router = () => {
// 0.若沒有hash則強制加入預設路徑
if (!location.hash) {
location.hash = '/'
}
// 1.得到目前路徑(對應route)
//...
}
一開始沒有hash值,會直接強制加入hash根目錄路徑,用戶即使不帶入hash也能正常訪問到主頁。另外,location.hash = '#/'也是可以的,使用location.hash方法除了空值以外的,開頭都會自帶「#」。
以上結果正常運作。接著再來看看另一個情況,當網址列輸入的路徑對應不到元件時:
在網址列輸入不存在Route裡設定的/about路徑時發現,咦?為什麼會跑到home頁面? 看看console log出現的錯誤訊息,原來是因為找不到對應元件回傳空物件,因為找不到render屬性可以執行,造成程式碼中止,所以讓畫面還停留在原本的home page。一般其實這種狀況會顯示「找不到頁面」,所以我們希望指定一個預設物件,在Router找不到對應的元件時,會使用這個預設物件來做render,這邊來建立另一個新的Component:
src/pages/NotFound.js
// named export
export const NotFound = {
render: () => {
return `<h1>This page not found!</h1>`
},
}
然後Router引入這個Component模組,回到第二步給component賦予預設值:
src/routes/Router.js:
// 找不到頁面
import { NotFound } from '../pages/NotFound'
//...
export const Router = () => {
…
// 2.找出對應頁面
// ES6 解構賦值
const { component = NotFound } = getComponent(path, Route)
//...
}
到這裡就算完成所有Router配置了,我們來看看最後的成果:
回顧這幾天的實做,一開始我們建立了Component物件,並使用javascript模組化的方式輸出匯入,接著建立Route和Router,搭配監聽事件動態判斷路徑及對應Component,最後在主頁渲染出內容。到這邊手動實做SPA的基礎內容算是告一段落了,明天之後會來說說有關SPA進階的處理。
(連假要開始趕進度了...@@)
參考資料: