react-router v4 路由規則解析

瀟湘待雨發表於2019-03-03

前言

react-router升級到4之後,跟前面版本比有了很大的差別。
例如包的拆分,動態路由等詳細的差別就不說了,各位大神的總結也很到位,詳細可以點選看看,All About React Router 4這篇文章
此外還有個差別是路由規則的變化。 一直有著上個版本的習慣,所以稍微複雜的路由,配起來的時候簡直痛不欲生。
痛定思痛,要好好了解下其依賴的匹配規則,即path-to-regexp

本文期望讀者是對react-router有過使用的同學,不然本文省略了太多東西,可能看起來可能有點太亂。

path-to-regexp 是什麼

其文件一句話介紹很簡潔明瞭: 將路徑字串(如/user/:name)轉換為正規表示式。react-router matchPath就是基於其來匹配了。

使用

var pathToRegexp = require(`path-to-regexp`)

// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
複製程式碼

引數:

  • path: 字串、字串陣列、正規表示式
  • keys 可選 由在path裡找到的key組成的陣列
  • options 可選 由下面幾部分組成:
    1. 敏感匹配 預設false,當為true時,正則將區分大小寫
    2. 嚴格模式 預設false 為true,將會匹配可選的緊跟的分隔符
    3. end 預設true 正則是否匹配至字串結尾
    4. start 預設true 是否從字串開始進行匹配
    5. 高階選項(用於非路徑名稱字串,例如主機名稱hostname)
      1. 分隔符 預設每段的分隔符是`/`
      2. 結尾字元 可選欄位或欄位列表、用於作為結束字元
      3. 分隔符列表 解析時要當做分隔符考慮的欄位列表 預設‘./’

還是直接看官方例子吧

// 匹配的path中關鍵字,得到由其組成的陣列
// 簡而言之,就是匹配的結果,增加該引數,可以更方便的使用和分析
var keys = []
var re = pathToRegexp(`/foo/:bar`, keys)
// 執行結果,轉換之後的正則就如下
// re = /^/foo/([^/]+?)/?$/i
// 得到的路由相關資訊
keys = [
    { 
        // 路由path中的引數名稱
        name: `bar`, 
        // 字首,分隔符等    
        prefix: `/`, 
        delimiter: `/`, 
        optional: false, 
        repeat: false, 
        pattern: `[^\/]+?` 
    }
]
複製程式碼

這樣看起來應該清楚一下,下面繼續看使用規則

規則

最簡單的例子(結合react-router-config 路由最簡單的路由可以如下, 各欄位含義就不提了,本文只關注匹配規則):

const routes = [
  { component: Root,  
    routes: [
      { 
        //只匹配/  
        path: `/`,
        exact: true,
        component: Home
      }
    ]
  }
]  
複製程式碼

看起來也不過爾爾,簡單匹配就完了,但是如果要是有比較複雜的路徑的話,例如有這麼一個路徑:`/a/1/3.html` 其實/1/3都是可以省略的也是可選的,也就是說如下面這樣:

`/a/1/3.html`
`/a.html`
`/a/2.html`
複製程式碼

先不要急著寫,這種當然是要有按照相應規則來匹配了,先看下對應規則:

引數

路徑引數將會被用來定義引數和匹配關鍵字列表(即我們的keys)

命名引數

命名引數通過如下形式定義: 在引數前面加上引號,例如:‘:foo’。預設情況下,在path的該區域結束之前的部分都會被匹配到(預設的話也就是兩個//之間為一個區域,例如/:foo/,那麼:foo 部分就是一個區域(segment))。

var re = pathToRegexp(`/:foo/:bar`)
// 對應的匹配key陣列如下
keys = [
    { name: `foo`, prefix: `/`, ... },
    { name: `bar`, prefix: `/`, ... }
]
// 對於下面的path,執行結果
re.exec(`/test/route`)
//=> [`/test/route`, `test`, `route`]
複製程式碼

引數修飾符

可選

引數字尾可以加上一個可選標識即`?`,表明該引數可選,這樣情況下該部分引數如果沒有也不正確匹配,只不過在匹配結果裡值為undefined

var re = pathToRegexp(`/:foo/:bar?`)
keys = [
    { name: `foo`, ... }, 
    { 
    name: `bar`, 
    delimiter: `/`, 
    // 匹配key陣列第二部分就為true,表明該引數可選
    optional: true, 
    repeat: false }
]
// 可省略候選bar對應的部分
re.exec(`/test`)
//=> [`/test`, `test`, undefined]

re.exec(`/test/route`)
//=> [`/test`, `test`, `route`]
複製程式碼
0-n

當然引數可以以*結尾,標識該部分引數0-n(可以類比正則)。每個匹配都會將字首(/)考慮進去,即/已經不是預設的區塊分割了,這也是跟?的區別。看例子比較清晰

var re = pathToRegexp(`/:foo*`)
// keys = [{ name: `foo`, delimiter: `/`, optional: true, repeat: true }]

re.exec(`/`)
//=> [`/`, undefined]
// 主要看這裡,這時候/baz的內容同樣被當成 foo的value組成部分了,直接和前面的一起輸出
re.exec(`/bar/baz`)
//=> [`/bar/baz`, `bar/baz`]
複製程式碼

對比下?修飾符,應該比較清楚了。

var re = pathToRegexp(`/:foo?`)

// 直接認為是不匹配的,輸出為null
re.exec(`/bar/baz`)
//=> null
複製程式碼
1-n

引數以+結尾時,表明該部分引數至少為1,同樣會將分隔符計算進來。可以對比下上面與*的區別

var re = pathToRegexp(`/:foo+`)
// keys = [{ name: `foo`, delimiter: `/`, optional: false, repeat: true }]
// 此時/ 的路由已經不能匹配了,至少有一個引數
re.exec(`/`)
//=> null
// 這裡倒是跟*一樣
re.exec(`/bar/baz`)
//=> [`/bar/baz`, `bar/baz`]
複製程式碼
自定義匹配引數

所有的引數都可以提供自定義的匹配規則,來覆蓋預設規則([^/]+),如下匹配數字的例子:

// 這裡自定的規則就是我們的數字匹配了(d+) 
var re = pathToRegexp(`/icon-:foo(\d+).png`)
// keys = [{ name: `foo`, ... }]

re.exec(`/icon-123.png`)
//=> [`/icon-123.png`, `123`]

re.exec(`/icon-abc.png`)
//=> null
複製程式碼

注意:自定義規則中反斜槓()前面需要再加一個反斜槓,例如上線的例子(d+)(這裡跟正則不太一致,記得別混淆)

未命名引數

未命名的引數當然也是可行的,即只包含修飾符的群組。和命名引數的功能一樣,只不過其name不是對應的key而是數字下標

// 第二個區塊,匹配的是所有字元.*,顯然是未命名的
var re = pathToRegexp(`/:foo/(.*)`)
keys = [
{ name: `foo`, ... }, 
// name就是0了,再有一個則按順序排列    
{ name: 0, ... }]
// 結果沒什麼差別。
re.exec(`/test/route`)
//=> [`/test/route`, `test`, `route`]  
複製程式碼

注意: react-router v4 不再處理querystring了,大家可以使用各種工具來處理,自己擼個工具也行。

到這裡引數部分已經結束了,回到上面的部分,/a/1/3.html。後面兩個引數可選。
具體規則可以如下配置。

const routes = [
  { component: Root,  
    routes: [
      {  
        path: `/a(/)?:num1?(/)?:num2?(/)?`,
        exact: true,
        component: Home
      }
    ]
  }
]  
複製程式碼

是不是感覺日了那什麼,有這麼複雜嗎,來我們仔細看看有沒有這麼複雜。

  1. /a是固定的,可以不變,第一部分確定。
  2. 後面這個1對應num1,且可選 /a/:num1?
  3. 3對應num2,同樣可選 /a/:num1?/:num2?.html

看起來應該是這樣。那麼來試一試吧。

var re = pathToRegexp(`/a/:num1?/:num2?.html`)
// 第一種情況是滿足的,並且正確的得到value了。 3,4
console.log(re.exec(`/a/3/4.html`)) 
// [ `/a/3/4.html`, `3`, `4`, index: 0, input: `/a/3/4.html` ]
// 這裡看起來沒問題,但是我們第一個匹配num1 是 undefined
// 這樣順序就亂了,這裡應該是num1而非num2
console.log(re.exec(`/a/4.html`))
// [ `/a/4.html`, undefined, `4`, index: 0, input: `/a/4.html` ]

// 直接不能匹配了
console.log(re.exec(`/a.html`))
// null
複製程式碼

這裡的問題就在於連續兩個可選引數的情況下,單純的使用?就不滿足了。

按照上面的表示式,匹配的應該是第一個引數可選,但只有一個引數時,4.html連著一起,認為是num2的value了。
上面的表示式轉換為正則之後如下,有興趣可以研究下:
這裡的4.html命中的是後面的([^/]+?)?.html(?:/)?$

/^/a(?:/([^/]+?))?/([^/]+?)?.html(?:/)?$/i
複製程式碼

對著上面的文件思考下,可以自定義可選引數,那麼我們可不可以這樣來試試(講真的,開始真是試的):

指明字首也是可選,表明.html不是跟最後一個區塊緊密相連,這樣應該可以滿足要求

var re = pathToRegexp(`/a(/)?:num1?(/)?:num2?.html`)
console.log(re.exec(`/a/0/4.html`))
//[ `/a/0/4.html`, `/`, `0`, `/`, `4`, index: 0, input: `/a/0/4.html` ]
// 滿足需求,這樣4其實為num2的value 
console.log(re.exec(`/a/4.html`))
//[ `/a/4.html`,`/`,`4`,undefined,undefined,index: 0,input: `/a/4.html` ]
// 第三種滿足情況
console.log(re.exec(`/a.html`))
// [ `/a.html`,undefined,undefined,undefined,undefined,index: 0,input: `/a.html` ]
複製程式碼

這樣總算滿足需求了。

方法

有以下這麼幾個,這裡就不詳細介紹了。

  • Parse 返回一個字串和keys的陣列。
  • Compile (“Reverse” Path-To-RegExp) 將字串轉換為有效路徑。
  • 其他的參考官網

結束語

到這裡關於react-router V4 路由規則部分的解析就結束了。起因也是自己在配置路由時有點懵,不想就那樣跟著別人的路由配完就完了。知其然也要知其所以然,應該是我們技術人員一直秉承的一個態度,所以自己總結了一下,拋磚引玉,以供自己記憶和有需要的同學參考。
更多我的部落格請移步

參考文章

相關文章