大家好,我們今天來點特別的,前面我們在實做Router的篇章中,成功運用正規表示式比對網址hash的部份,來找出對應的路徑和元件。不過有使用過框架的大大們就知道,Router裡可以可以在路徑中設定動態變數,來獲取真正得到的資料並顯示在頁面中,這聽起來似乎有點抽象,不如我們直接進入主題吧!
這邊先用簡單例子做示範,相信你應該都看過這樣類型的網址:
「 https://www.xxxxxxxx.com/?page=3 」
在網址末端有個「?page=3」,代表是在第3頁的意思。這個我們通常稱作網址參數,也叫query string。
如果我們在Chrome瀏覽器打開console,輸入location執行,會得到location物件的相關資訊,其中可以看到之前熟悉的href屬性,網址是剛剛輸入的「 https://www.xxxxxxxx.com/?page=3 」。再往下一點點就可以看到search屬性,這裡的值就是網址參數,與hash一樣,有設定值會返回「?」,沒有則是空值。
除了上述的類型,相信你一定還看過這樣的網址:
「https://www.xxxxxxxx.com/3」
注意到後面的數字嗎?這裡也是頁數,只不過與網址合而為一,所以稱為URL參數。
用剛剛的例子,在console裡輸入location執行,可以發現有個地方是URL參數的部份,沒錯,就是pathname屬性,會返回host(網域)後面的字串。
舉了上述的例子,就是想讓你知道網址其實也能傳遞變數。假設我們想要做個列表頁面,總共有上千至上萬筆資訊,不太可能一次顯示全部,這時候就需要靠網址參數來做分頁,分頁功能除了要事先規劃好每頁有多少筆,也要得到總筆數。
再進一步了解,假設有個商品內容頁面,商品除了有不同的分類,還有不同的編號,若照傳統方式一頁一頁做,那可能會做到崩潰。這裡就可以善用網址參數來設定類型及編號,當想要得到這兩個資訊時,從網址上就可以得到,然後利用這個資訊向後端請求商品資料。
這邊說說另外一個應用,就是我們所熟知的表單,在form標籤中,設定method傳輸方式為「get」時,表示會透過網址傳遞表單的資訊,不過因為是在網址上可以看到,所以不適合放入敏感(如密碼)或大量的資訊,這時可以改用「post」的傳輸方式,夾帶在body封包裡。
React實現前端路由有個重要的套件叫做React-router-dom。我們直接前往官方網站,來看看React router是怎麼設定網址參數:
先看橘色框框的地方,觀察Route裡path的地方,是不是跟網址列上的網址很相像?在末端有個「:username」,這個就是URL參數,搭配前面加上冒號會變成username變數,之後要調用可以在元件內使用props.match.params.username,如同藍色框框的地方,等元件內拿到這個資訊,就可以拿來向後端請求特定的資料。這個資訊會隨著網址變化也會得到不同的內容,所以前端Router除了匹配路徑和對應元件外,也會解析網址列設定的變數來讓元件內取得。
回到我們前面實做Router的地方[DAY16]手動打造SPA - 建立Router與Route(下篇),還記得當時只有做到完全符合條件才進行匹配Route中設定對應的元件嗎? 如果這邊也想要實做帶入URL參數,並傳入至元件內使用呢?
首先我們把之前實做Router的完整程式碼貼出來:
src/routes/Router.js
// 引入路徑模組
import { Route } from './Route'
// 找不到頁面
import { NotFound } from '../pages/NotFound'
//引入監聽處理
import { addListener, removeAllListeners } from '../utils/eventListerer'
// 找出對應元件
// path:目前路徑,routes:路徑設定
const getComponent = (path, routes) => {
let result = routes.find((r) => r.path.match(new RegExp(`^${path}$`))) || {}
return result
}
export const Router = () => {
// 0.若沒有hash則強制加入預設路徑
if (!location.hash) {
location.hash = '#/'
}
// 1.得到目前路徑(對應routes)
const path = location.hash.slice(1).toLowerCase()
// 2.找出對應頁面
// ES6 解構賦值
const { component = NotFound } = getComponent(path, Route)
// 3.將元件內容渲染至畫面
document.querySelector('#wrapper').innerHTML = component.render()
// 4.元件render後呼叫
'mount' in component ? component.mount() : null
// 5.處理監聽事件
// 取消全部監聽事件
removeAllListeners()
// 註冊元件內監聽事件
'listener' in component
? Object.keys(component.listener).forEach((type) =>
addListener(type, component.listener[type])
)
: null
}
可以看到之前對應元件的程式在Router函式裡的第二步,其中使用getComponent來獲得對應的元件,傳入參數(path,Routes)的部份,利用location.hash作為path,以及事先設定好的Routes模組傳入至內部進行比對。所以可以先專注在下面這段函式就好。
// 找出對應元件
// path:目前路徑,routes:路徑設定
const getComponent = (path, routes) => {
let result = routes.find((r) => r.path.match(new RegExp(`^${path}$`))) || {}
return result
}
因為需要對Router做威力加強版本,過程會比較複雜,首先依序規劃出開發步驟:
const getComponent = (path, routes) => {
// STEP1:若路徑後有斜線,將斜線拿掉處理(排除#根目錄)
// STEP2:比對完全符合路徑
let result =
routes.find((route) => route.path.match(new RegExp(`^${path}$`))) || {}
// STEP3:若無比對完全符合路徑,則找尋相似目錄,及判斷是否傳入參數
// STEP4:返回結果
return result
}
我們希望首先標準化網址的格式,可以跟Route中設定的path正常比對,所以第一步會先尋找除了根目錄外的網址路徑最後面是否有斜線,若有的話就移除。
// STEP1:若路徑後有斜線,將斜線拿掉處理(排除#根目錄)
if (path.slice(-1) === '/' && path.match(/\//g).length > 1){
path = path.slice(0, -1)
}
這裡就是原本的程式碼,把它移至第二步,作用是比對完全符合路徑的條件,如果有比對出來代表就不需要往下繼續匹配了。另外我們原本使用邏輯運算子做返回,若沒找出結果返回空物件{},這裡改成undefined,後面在判斷沒有比對結果會用到。
// STEP2:比對完全符合路徑
let result =
routes.find((route) => route.path.match(new RegExp(`^${path}$`))) || undefined
這是所有配置中最複雜的步驟,主要作用是當第二步沒有找到結果返回undefined時,會再與Route進行尋找比對,目的是希望找出有在Route裡設定URL參數(最前面搭配冒號:)以及模糊條件(最末端搭配問號?)。利用find迭代與網址路徑尋找第一個符合條件的結果,其中排除時搭配返回false,讓find提前可以再往下一個尋找比對。這裡重點會擺在兩個地方,一個是routesArry,另一個是urlArry,兩個資訊同時會處理為陣列後,排除無效值再用迴圈每層交互比對,比對重複運用到正規表示式。另外在返回route物件前有加入props屬性,讓URL參數一起設置到props中的屬性,屬性名稱與Route中設定的變數是一樣的。
// STEP3:若無比對完全符合路徑,則找尋相似目錄,及判斷是否傳入參數
if (!result) {
//尋找符合條件的route
result = routes.find((route) => {
//找尋設定參數目錄(:)
if (route.path.match(/\/:/g)) {
//傳入參數初始化
route.props = {}
//當前routes路徑目錄陣列(filter去除無效值)
let routesArry = route.path.split('/').filter((a) => a)
//當前網址列路徑目錄陣列(filter去除無效值)
let urlArry = path.split('/').filter((a) => a)
//逐個比對是否符合路徑
for (let i = 0; i < routesArry.length; i++) {
//若有設定傳入參數
if (routesArry[i].slice(0, 1) === ':') {
//檢查當前路由是否設定傳入參數及正規表示法比對
let regex = routesArry[i].match(/[^\:(.)^?]+/g)
//解構路由陣列
let [params, paramsRegex] = regex
//排除條件設定
if (
urlArry.length > routesArry.length || //當前網址列路徑數目多於routes
!paramsRegex || //若有設定正規表示法驗證
(!urlArry[i] && routesArry[i].slice(-1) !== '?') || //若網址不存在,檢查路徑是否設定模糊匹配
(urlArry[i] &&
!urlArry[i].match(new RegExp(`^${paramsRegex}$`, 'gm'))) //若網址存在,檢查是否符合路徑設定之正規表示法
) {
return false
}
//比對成功,設定傳遞參數值
route.props[params] = urlArry[i] || '' //將路徑參數導入組件
} else {
//若非相似目錄則比對是否完全相同(判斷是否持續比對)
if (routesArry[i] !== urlArry[i]) {
return false
}
}
}
return route
}
return false
})
}
是的,我們也把最後返回值放到第四步了,因為必須要返回物件格式,所以使用邏輯運算子,即使沒有比對出結果,還是回傳個空物件:
// STEP4:返回結果
return result || {}
Router改好了之後,我們試試也把Route模組做點改變,使用類似React Router在path設定URL參數的方式:
src/routes/Route.js
//引入Component
import { Home } from '../pages/Home'
import { Post } from '../pages/Post'
import { Product } from '../pages/Product'
//設定路徑並對應Component
export const Route = [
{ path: '/', component: Home },
{ path: '/post/:page([1-9][0-9]*)?', component: Post },
{
path: '/product/:category([A-z]*)/:productId([A-z0-9]*)?',
component: Product,
},
]
在路徑中可以發現,有些路徑中起始有冒號,有些中間小括弧夾著正規表示式,還有些是最後面夾帶問號,這個特定的格式組合用來設定與比對URL參數,說明如下:
如果你仔細看格式並與剛剛在Router裡getComponent函式中的第三步做比較,這個格式解析出來後,可以讓Router同時給賦予變數名稱屬性與值在props中,以及運用正規表示式來與網址做比對。
BTW,這裡我們偷偷加一個Product頁面,並使用兩個參數設置,用來最後看看傳入參數時的效果。
src/pages/Product
export const Product = {
render: (props) => {
return `
<div class="p-5">
<h3>商品頁</h3>
<h5>目前所在的分類是:${props.category}</h5>
<h5>目前商品的編號是:${props.productId}</h5>
</div>
`
},
}
前面都設定好了,然後要怎麼把網址參數導入到文件呢?這裡還需要在Router加入一點小設定:
src/routes/Router.js
export const Router = () => {
//...
// 2.找出對應頁面
// ES6 解構賦值
const { component = NotFound, props = {} } = getComponent(path, Route)
// 3.將元件內容渲染至畫面
document.querySelector('#wrapper').innerHTML = component.render(props)
//...
}
可以看到這裡在得到返回值時,我們在原本物件裡解構並宣告了「props」,繼承返回route物件中的props,由於這個props裡會包含URL參數的屬性與值,所以會再傳入至render方法中,當參數一起傳遞至元件內。為了看的更清楚,我們先用瀏覽器打開console,下面是在進入Post頁面時getComponent返回的完整物件格式,其中可以看到props屬性中的格式為{page:"3"}:
終於到了最後展示成果的時刻了! 我們先來試試Post頁面運行如何:
我們將原本/post改變為/post/3後,有沒有看到在console輸出{page:"3"} ?! 代表設定的URL參數page已經正確拿到了,這是成功的開始!
接著再進一步前往Product商品頁面,來看看同時在網址中設定兩個參數時顯示正不正常。首先進入/product/shoes,可以看到成功進來Product頁面了! 為了清楚展示,已經把兩個網址參數都直接呈現在頁面中,第一個是categroy代表商品分類,第二個productId代表商品編號,而第二個分類因為有設定可有可無的問號,所以即使沒有帶編號也還是可以進的去商品頁。從內容可以看到分類shoes是透過網址列動態設定的,當網址列的分類改成shirt時,分類也同時跟著改變了!
再來我們來輸入第二個URL參數,沒問題! 商品ID可以正常帶入。若我們故意打錯誤的格式,例如含有對應規則外的特殊符號,就會顯示找不到頁面。
以上就是今天實做的內容,我們已經用Router實現擷取網址設定的參數做應用,因為都是自己手動寫的,也許還有空間可以改進,不知各位大大有沒有其他更好的方式呢?希望客官們看的開心,明天見! (回神時竟然已經凌晨四點了@@)。
參考資料
1.React-router-dom | 原理解析
2.[Javascript] 初探Regex 正規表達式