iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 16
1
自我挑戰組

不用前端框架 手把手打造基礎SPA網站系列 第 16

[DAY16]手動打造SPA - 建立Router與Route(下篇)

大家好,昨天我們建立了Route並設定好路徑與匹配對應的元件,今天目標要來繼續完成Router。Router作為判斷符合設定的路徑並匹配對應的Component,可以說是整個SPA重要的中樞神經,也是本系列文章的精華重點。

建立Router

昨天建立完Route後,一樣在routes目錄裡建立路由Router.js:

src/routes/Router.js:

// 引入路徑模組
import { Route } from './Route'
 
export const Router = () => {
  // 1.得到目前路徑(對應route)
  
  // 2.找出對應頁面
  
  // 3.將元件內容渲染至畫面
}

首先宣告Router為一個function作為模組輸出,給index.js進入點使用。為了說明清楚,這邊分三步驟來解析:

  1. 得到目前路徑(對應route)

在開始我們需要得到目前對應元件真正的路徑,由於路徑的產生方式是透過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),然後統一轉小寫就能取得真實的對應路徑了。

  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物件。

  1. 將元件內容渲染至畫面

最後一步是產生畫面,既然我們已經拿到對應的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進階的處理。

/images/emoticon/emoticon13.gif

(連假要開始趕進度了...@@)

參考資料:

  1. Build a very basic SPA JavaScript router
  2. JavaScript 陣列處理方法 [filter(), find(), forEach(), map(), every(), some(), reduce()]
  3. 從ES6開始的JavaScript學習生活-解構賦值

上一篇
[DAY15]手動打造SPA - 建立Router與Route(上篇)
下一篇
[DAY17]進階應用 - 幫你的SPA套上Bootstrap
系列文
不用前端框架 手把手打造基礎SPA網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言