iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 23
0
自我挑戰組

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

[DAY23]進階應用 - 將網址參數傳入元件

大家好,我們今天來點特別的,前面我們在實做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-router-dom為例

React實現前端路由有個重要的套件叫做React-router-dom。我們直接前往官方網站,來看看React router是怎麼設定網址參數:

先看橘色框框的地方,觀察Route裡path的地方,是不是跟網址列上的網址很相像?在末端有個「:username」,這個就是URL參數,搭配前面加上冒號會變成username變數,之後要調用可以在元件內使用props.match.params.username,如同藍色框框的地方,等元件內拿到這個資訊,就可以拿來向後端請求特定的資料。這個資訊會隨著網址變化也會得到不同的內容,所以前端Router除了匹配路徑和對應元件外,也會解析網址列設定的變數來讓元件內取得。

改造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
}
  1. 第一步:排除網址中路徑斜線

我們希望首先標準化網址的格式,可以跟Route中設定的path正常比對,所以第一步會先尋找除了根目錄外的網址路徑最後面是否有斜線,若有的話就移除。

  // STEP1:若路徑後有斜線,將斜線拿掉處理(排除#根目錄)
  if (path.slice(-1) === '/' && path.match(/\//g).length > 1){
    path = path.slice(0, -1)
  }
  1. 第二步:比對完全符合路徑

這裡就是原本的程式碼,把它移至第二步,作用是比對完全符合路徑的條件,如果有比對出來代表就不需要往下繼續匹配了。另外我們原本使用邏輯運算子做返回,若沒找出結果返回空物件{},這裡改成undefined,後面在判斷沒有比對結果會用到。

  // STEP2:比對完全符合路徑
  let result =
    routes.find((route) => route.path.match(new RegExp(`^${path}$`))) || undefined
  1. 第三步:若無比對出完全符合路徑則進行網址參數匹配

這是所有配置中最複雜的步驟,主要作用是當第二步沒有找到結果返回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
    })
  }
  1. 第四步:返回結果

是的,我們也把最後返回值放到第四步了,因為必須要返回物件格式,所以使用邏輯運算子,即使沒有比對出結果,還是回傳個空物件:

// 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參數,說明如下:

  • 冒號 ::表示有設定URL參數,前面固定會出現「:」,在冒號後才是真正變數名稱。
  • 小括弧 ():括弧內放入正規表示式(regex),之後可以跟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"}:

URL參數帶入元件成果

終於到了最後展示成果的時刻了! 我們先來試試Post頁面運行如何:

我們將原本/post改變為/post/3後,有沒有看到在console輸出{page:"3"} ?! 代表設定的URL參數page已經正確拿到了,這是成功的開始!

接著再進一步前往Product商品頁面,來看看同時在網址中設定兩個參數時顯示正不正常。首先進入/product/shoes,可以看到成功進來Product頁面了! 為了清楚展示,已經把兩個網址參數都直接呈現在頁面中,第一個是categroy代表商品分類,第二個productId代表商品編號,而第二個分類因為有設定可有可無的問號,所以即使沒有帶編號也還是可以進的去商品頁。從內容可以看到分類shoes是透過網址列動態設定的,當網址列的分類改成shirt時,分類也同時跟著改變了!

再來我們來輸入第二個URL參數,沒問題! 商品ID可以正常帶入。若我們故意打錯誤的格式,例如含有對應規則外的特殊符號,就會顯示找不到頁面。

以上就是今天實做的內容,我們已經用Router實現擷取網址設定的參數做應用,因為都是自己手動寫的,也許還有空間可以改進,不知各位大大有沒有其他更好的方式呢?希望客官們看的開心,明天見! (回神時竟然已經凌晨四點了@@)。

/images/emoticon/emoticon13.gif

參考資料
1.React-router-dom | 原理解析
2.[Javascript] 初探Regex 正規表達式


上一篇
[DAY22]進階應用 - 元件內部共用函式調用
下一篇
[DAY24]番外篇-Bootstrap實用元件介紹
系列文
不用前端框架 手把手打造基礎SPA網站30

尚未有邦友留言

立即登入留言