React 中的一些 Router 必備知識點

2022-01-10 前端Q

本文首發於政採雲前端團隊博客:React 中的一些 Router 必備知識點

https://www.zoo.team/article/react-router前言

每次開發新頁面的時候,都免不了要去設計一個新的 URL,也就是我們的路由。其實路由在設計的時候不僅僅是一個由幾個簡單詞彙和斜槓分隔符組成的連結,偶爾也可以去考慮有沒有更「優雅」的設計方式和技巧。而在這背後,路由和組件之間的協作關係是怎樣的呢?於是我以 React 中的 Router 使用方法為例,整理了一些知識點小記和大家分享~

React-Router基本用法

通常我們使用 React-Router (https://reactrouter.com/native/guides/quick-start) 來實現 React 單頁應用的路由控制,它通過管理 URL,實現組件的切換,進而呈現頁面的切換效果。

其最基本用法如下:

import { Router, Route } from 'react-router';
render((
  <Router>
    <Route path="/" component={App}/>
  </Router>
), document.getElementById('app'));

亦或是嵌套路由:

在 React-Router V4 版本之前可以直接嵌套,方法如下:

<Router>
  <Route path="/" render={() => <div>外層</div>}>
      <Route path="/in" render={() => <div>內層</div>} />
  </Route>
</Router>

上面代碼中,理論上,用戶訪問 /in 時,會先加載 <div>外層</div>,然後在它的內部再加載 <div>內層</div>。

然而實際運行上述代碼卻發現它只渲染出了根目錄中的內容。後續對比 React-Router 版本發現,是因為在 V4 版本中變更了其渲染邏輯,原因據說是為了踐行 React 的組件化理念,不能讓 Route 標籤看起來只是一個標籤(奇怪的知識又增加了)。

現在較新的版本中,可以使用 Render 方法實現嵌套。

<Route
  path="/"
  render={() => (
    <div>
      <Route
        path="/"
        render={() => <div>外層</div>}
      />
      <Route
        path="/in"
        render={() => <div>內層</div>}
      />
      <Route
        path="/others"
        render={() => <div>其他</div>}
      />
    </div>
  )}
/>

此時訪問 /in 時,會將「外層」和「內層」一起展示出來,類似地,訪問 /others 時,會將「外層」和「其他」一起展示出來。

路由傳參小 Tips

在實際開發中,往往在頁面切換時需要傳遞一些參數,有些參數適合放在 Redux 中作為全局數據,或者通過上下文傳遞,比如業務的一些共享數據,但有些參數則適合放在 URL 中傳遞,比如頁面類型或詳情頁中單據的唯一標識 id。在處理 URL 時,除了問號帶參數的方式,React-Router 能幫我們做什麼呢?在這其中,Route 組件的 path 屬性便可用於指定路由的匹配規則。

場景 1 

描述:就想讓普普通通的 URL 帶個平平無奇的參數

那麼,接下來我們可以這樣幹:

Case A:路由參數

path="/book/:id"

我們可以用冒號 + 參數名字的方式,將想要傳遞的參數添加到 URL 上,此時,當參數名字(本 Case 中是 id)對應的值改變時,將被認為是不同 URL。

Case B:查詢參數

path="/book"

如果想要在頁面跳轉的時候問號帶參數,那麼 path 可以直接設計成既定的樣子,參數由跳轉方拼接。在跳轉時,有兩種形式帶上參數。其一是在 Link 組件的 to 參數中通過配置字符串並用問號帶參數,其二是 to 參數可以接受一個對象,其中可以在 search 欄位中配置想要傳遞的參數。

<Link to="/book?id=111" />
// 或者
<Link to={{
  pathname: '/book',
  search: '?id=111',
}}/>

此時,假設當前頁面 URL 中的 id 由 111 修改為 222 時,該路由對應的組件(在上述例子中就是 React-Route 配置時 path="/book" 對應的頁面/組件 )會更新,即執行 componentDidUpdate 方法,但不會被卸載,也就是說,不會執行 componentDidMount 方法。

Case C:查詢參數隱身式帶法

path="/book"

path 依舊設計成既定的樣子,而在跳轉時,可以通過 Link 中的 state 將參數傳遞給對應路由的頁面。

<Link to={{
  pathname: '/book',
  state: { id: 111 }
}}/>

但一定要注意的是,儘管這種方式下查詢參數不會明文傳遞了,但此時頁面刷新會導致參數丟失(存儲在 state 中的通病),So,灰常不推薦~~(其實不想明文可以進行加密處理,但一般情況下敏感信息是不建議放在 URL 中傳遞的~)

場景 2 

描述:編輯/詳情頁,想要共用一個頁面,URL 由不同的參數區分,此時我們希望,參數必須為 edit、detail、add 中的 1 個,不然需要跳轉到 404 Not Found 頁面。

path='/book/:pageType(edit|detail|add)'

如果不加括號中的內容 (edit|detail|add),當傳入錯誤的參數(比如用戶誤操作、隨便拼接 URL 的情況),則頁面不會被 404 攔截,而是繼續走下去開始渲染頁面或調用接口,但此時很有可能導致接口傳參錯誤或頁面出錯。

場景 3 

描述:新增頁和編輯頁辣麼像,我的新增頁也想和編輯/詳情共用一個頁面。但是新增頁不需要 id,編輯/詳情頁需要 id,使用同一個頁面怎麼辦?

path='/book/:pageType(edit|detail|add)/:id?'

別急,可以用 ? 來解決,它意味著 id 不是一個必要參數,可傳可不傳。

場景 4 

描述:我的 id 只能是數字,不想要字符串怎麼辦?

path='/book/:id(\\\d+)'

此時 id 不是數字時,會跳轉 404,被認為 URL 對應的頁面找不到啦。

底層依賴

有了這麼多場景,那 Router 是怎樣實現的呢?其實它底層是依賴了 path-to-regexp (https://github.com/pillarjs/path-to-regexp/tree/v1.7.0) 方法。

var pathToRegexp = require('path-to-regexp')
// pathToRegexp(path, keys, options)
// 示例
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]

 

delimiter:重複參數的定界符,默認是 '/',可配置

一些其他常用的路由正則通配符:

如果忘記寫參數名字,而只寫了路由規則,比如下述代碼中 /:foo 後面的參數:

var re = pathToRegexp('/:foo/(.*)', keys)
// 匹配除「\n」之外的任何字符
// keys = [{ name: 'foo', ... }, { name: 0, ...}]
re.exec('/test/route')
//=> ['/test/route', 'test', 'route']

它也會被正確解析,只不過在方法處理的內部,未命名的參數名會被替換成數組下標。

取路由參數

path 帶的參數,可以通過 this.props.match 獲取

例如:

// url 為 /book/:pageType(edit|detail|add)
const { match } = this.props;
const { pageType } = match.params;

由於有 #,# 之後的所有內容都會被認為是 hash 的一部分,window.location.search 是取不到問號帶的參數的。

比如:http://aaa.bbb.com/book-center/#/book/list?id=123

那麼在 React-Router 中,問號帶的參數,可以通過 this.props.location (官方牆推 👍)獲取。個人理解是因為 React-Router 幫我們做了處理,通過路由和 hash 值(window.location.hash)做了解析的封裝。

例如:

// url 為 /book?pageType=edit
const { location } = this.props;
const searchParams = location.search; // ?pageType=edit

實際列印 props 參數發現,this.props.history.location 也可以取到問號參數,但不建議使用,因為 React 的生命周期(componentWillReceiveProps、componentDidUpdate)可能使它變得不可靠。(原因可參考:https://blog.csdn.net/zrq1210/article/details/108403772)

在早期的 React-Router 2.0 版本是可以用 location.query.pageType 來獲取參數的,但是 V4.0 去掉了(有人認為查詢參數不是 URL 的一部分,有人認為現在有很多第三方庫,交給開發者自己去解析會更好,有個對此討論的 Issue,有興趣的可以自行獲取 😊 https://github.com/ReactTraining/react-router/issues/4410)

針對上一節中場景 1 的 Case C,查詢參數隱身式帶法時(從 state 裡帶過去的),在 this.props.location.state 裡可以取到(不推薦不推薦不推薦,刷新會沒~)

Switch

<div>
  <Route
    path="/router/:type"
    render={() => <div>影像</div>}
  />
  <Route
    path="/router/book"
    render={() => <div>圖書</div>}
  />
</div>

如果 <Route /> 是平鋪的(用 div 包裹是因為 Router 下只能有一個元素),輸入 /router/book 則影像和圖書都會被渲染出來,如果想要只精確渲染其中一個,則需要 Switch

<Switch>
  <Route
    path="/router/:type"
    render={() => <div>影像</div>}
  />
  <Route
    path="/router/book"
    render={() => <div>圖書</div>}
  />
</Switch>

Switch 的意思便是精準的根據不同的 path 渲染不同 Route 下的組件。但是,加了 Switch 之後路由匹配規則是從上到下執行,一旦發現匹配,就不再匹配其餘的規則了。因此在使用的時候一定要「百般小心」。

上面代碼中,用戶訪問 /router/book 時,不會觸發第二個路由規則(不會展示「圖書」),因為它會匹配 /router/:type 這個規則。因此,帶參數的路徑一般要寫在路由規則的底部。

路由的基本原理

路由做的事情:管控 URL 變化,改變瀏覽器中的地址。

Router 做的事情:URL 改變時,觸發渲染,渲染對應的組件。

URL 有兩種,一種不帶 #,一種帶 #,分別對應 Browse 模式和 Hash 模式。

一般單頁應用中,改變 URL,但是不重新加載頁面的方式有兩類:

Case 1(會觸發路由監聽事件):點擊 前進、後退,或者調用的 history.back( )、history.forward( )

Case 2(不會觸發路由監聽事件):組件中調用 history.push( ) 和 history.replace( )

於是參考「源碼解析 」這一次徹底弄懂 React-Router 路由原理(https://blog.csdn.net/zl_alien/article/details/109231294) 一文,針對上述兩種 Case,以及這兩種 Case 分別對應的兩種模式,作出如下總結。

 

圖片來源:「源碼解析 」這一次徹底弄懂 React-Router 路由原理

Browser 模式

Case 1:

URL 改變,觸發路由的監聽事件 popstate,then,監聽事件的回調函數 handlePopState 在回調中觸發 history 的 setState 方法,產生新的 location 對象。state 改變,通知 Router 組件更新 location 並通過 context 上下文傳遞,匹配出符合的 Route 組件,最後由 <Route /> 組件取出對應內容,傳遞給渲染頁面,渲染更新。

/* 簡化版的 handlePopState (監聽事件的回調) */
const handlePopState = (event)=>{
     /* 獲取當前location對象 */
    const location = getDOMLocation(event.state)
    const action = 'POP'
     /* transitionManager 處理路由轉換 */
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
        if (ok) {
          setState({ action, location })
        } else {
          revertPop(location)
        }
    })
}

Case 2: 以 history.push 為例,首先依據你要跳轉的 path 創建一個新的 location 對象,然後通過 window.history.pushState (H5 提供的 API )方法改變瀏覽器當前路由(即當前的 url),最後通過 setState 方法通知 Router,觸發組件更新。

const push = (path, state) => {
   const action = 'PUSH'
   /* 創建location對象 */
   const location = createLocation(path, state, createKey(), history.location)
   /* 確定是否能進行路由轉換 */
   transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
   ... // 此處省略部分代碼
   const href = createHref(location)
   const { key, state } = location
   if (canUseHistory) {
     /* 改變 url */
     globalHistory.pushState({ key, state }, null, href)
     if (forceRefresh) {
       window.location.href = href
     } else {
       /* 改變 react-router location對象, 創建更新環境 */
       setState({ action, location })
     }
   } else {
     window.location.href = href
   }
 })
}

Hash 模式

Case 1:

增加監聽,當 URL 的 Hash 發生變化時,觸發 hashChange 註冊的回調,回調中去進行相類似的操作,進而展示不同的內容。

window.addEventListener('hashchange',function(e){
  /* 監聽改變 */
})

Case 2:

history.push 底層調用 window.location.hash 來改變路由。history.replace 底層是調用 window.location.replace 改變路由。然後 setState 通知改變。

從一些參考資料中顯示,出於兼容性的考慮(H5 的方法 IE10 以下不兼容),路由系統內部將 Hash 模式作為創建 History 對象的默認方法。(此處若有疑議,歡迎指正~)

Dva/Router

在實際項目中發現,Link,Route 都是從 dva/router 中引進來的,那麼,Dva 在這之中做了什麼呢?

答案:貌似沒有做特殊處理,Dva 在 React-Router 上做了上層封裝,會默認輸出 React-Router (https://github.com/ReactTraining/react-router) 接口。

我們對 Router 做過的一些處理

Case 1:

項目代碼的 src 目錄下,不管有多少文件夾,路由一般會放在同一個 router.js 文件中維護,但這樣會導致頁面太多時,文件內容會越來越長,不便於查找和修改。

因此我們可以做一些小改造,在 src 下的每個文件夾中,創建自己的路由配置文件,以便管理各自的路由。但這種情況下 React-Router 是不能識別的,於是我們寫了一個 Plugin 放在 Webpack 中,目的是將各個文件夾下的路由匯總,並生成 router-config.js 文件。之後,將該文件中的內容解析成組件需要的相關內容。插件實現方式可了解本團隊另一篇文章:手把手帶你入門Webpack Plugin。

Case 2:

路由的 Hash 模式雖然兼容性好,但是也存在一些問題:

因此公司內部做了一次 Hash 路由轉 Browser 路由的改造。

如原有連結為:http://aaa.bbb.com/book-center/#/book/list?id=123

改造方案為:

通過新增以下配置代碼去掉 #

import createHistory from 'history/createBrowserHistroy';
const app = dva({
  history: createHistory({
    basename: '/book-center',
  }),
  onError,
});

同時,為了避免用戶訪問舊頁面出現 404 的情況,前端需要在 Redirect 中配置重定向以及在 Nginx 中配置舊的 Hash 頁面轉發。

Case 3:

在實際項目中,其實我們也會去考慮用戶未授權時路由跳轉、頁面 404 時路由跳轉等不同情況,以下 Case 和代碼僅供讀者參考~

<Switch>
  {
    getRoutes(match.path, routerData).map(item =>
     (
       // 用戶未授權處理,AuthorizedRoute 為項目中自己實現的處理組件
       <AuthorizedRoute
         {...item}
         redirectPath="/exception/403"
       />
     )
    )
  }
  // 默認跳轉頁面
  <Redirect from="/" exact to="/list" />
  // 頁面 404 處理
  <Route render={props => <NotFound {...props} />} />
</Switch>

參考連結

「源碼解析 」這一次徹底弄懂react-router路由原理 (https://blog.csdn.net/zl_alien/article/details/109231294)

react-router v4 路由規則解析 (https://www.cnblogs.com/pqjwyn/p/9936153.html)

二級動態路由的解決方案 (https://aibokalv.oschina.io/myarticle/2017/04/01/20170401%E4%BA%8C%E7%BA%A7%E5%8A%A8%E6%80%81%E8%B7%AF%E7%94%B1%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/)

我組建了一個氛圍特別好的騰訊內推社群,如果你對加入騰訊感興趣的話(後續有計劃也可以),我們可以一起進行面試相關的答疑、聊聊面試的故事、並且在你準備好的時候隨時幫你內推。下方加 winty 好友回復「面試」即可。


相關焦點

  • 手寫React-Router源碼,深入理解其原理
    看起來我們要搞懂react-router-dom的源碼還必須得去看react-router和history的源碼,現在我們手上有好幾個需要搞懂的庫了,為了看懂他們的源碼,我們得先理清楚他們的結構關係。之所以把他們放在一起,是因為他們之前有很強的依賴關係:react-router:是React-Router的核心庫,處理一些共用的邏輯react-router-config:是React-Router的配置處理,我們一般不需要使用react-router-dom
  • React-Router v6 新特性解讀及遷移指南
    新鉤子useRoutes代替react-router-config。1. <Switch>重命名為<Routes>該頂級組件將被重命名。但是,其功能大部分保持不變(嗨,瞎折騰)。
  • 「譯」React Router v6 中非常實用的4個API
    import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';function App() { return ( <BrowserRouter> <Routes> <Route path="/" element=
  • 一名 Vue 程式設計師總結的 React 基礎
    主要是會帶來命名衝突,相互依賴,不方便維護等一些缺點。高階組件其實就是處理 react 組件的函數,簡單理解就是和 ES6 中提供的 export/import 作用相似,不同點在於:高階組件會進行加工後再導出你需要的東西。
  • 60+ 實用 React 工具庫,助力你高效開發!
    最近看到一些實用的React工具庫,總結了一下分享給大家,避免重複造輪子。希望對你有所幫助~一、基礎1. React Infinite ScrollerReact Infinite Scroller 用於在React項目中無限滾動加載內容。
  • 【奇妙的IT術語】路由器router
    於是,router這時也就順理成章,變成了:a device that forwards data packets between computer networks 一個可以在計算機網絡中遞送數據包的設備。它的主要作用還是在對「數據包」的路徑規劃上,「router」也因此而得名。
  • 使用React、Electron、Dva、Webpack、Node.js、Websocket快速構建跨平臺應用
    /index.html'], vendor: ['react']        }忽略Electron中的代碼,不用webpack打包(因為Electron中有後臺模塊代碼,打包就會報錯)externals: [ (function () {
  • 超性感的React Hooks(三):useState
    總體感覺是,學會使用並不算難,但能用好卻並不簡單。索性拉了一個React Hooks的討論群,抽空時間在群裡糾正大家的使用方式。如果持續關注群消息,能夠學到許多正確的使用方式。感興趣的同學速度進群,如果提示滿人,公眾號回復React Hooks獲取進群方式。
  • 從 Vue2.0 到 React17 —— React 開發入門
    前言找工作時發現有一些公司是以React作為技術棧的,而且薪資待遇都不錯,為了增加生存的籌碼,所以還是得去學一下React,增加一項求生技能。因為我用Vue2.0開發項目已經四年了,故用Vue2.0開發項目的思路來學習React。
  • React.js 性能分析
    要感謝 Brian Vaughn 的努力,React 在其 調度包 中引入了對這個功能的試驗支持,更詳細的說明可以點擊 這裡 查看。一個交互追蹤,需要包含一個描述(例如:添加購物車按鈕被點擊)、一個時間戳和一個回調函數,在回調函數中你可以定義一些和該交互相關的邏輯。在「影片排期應用」中就有一個添加電影到播放列表的「+」號按鈕,這個就是一個交互按鈕。
  • React 輪播動畫探索
    slidesPerView={1} direction="horizontal" onSlideChange={() => console.log("slide change")} autoplay > {dataList.map((item) => renderBubble
  • 新人教版:八年級上冊數學必考知識點歸納!期末複習必備的好資料
    新人教版:八年級上冊數學必考知識點歸納!期末複習必備的好資料經過初一一年的學習生活後,相信不少同學對數學這門科目已經有了新的認識,與小學階段不同,初中數學學習難度更大,需要掌握的公式定理和概念性知識也就更多,所以同學們必須要重視起來才行。而且初二這個時間點非常關鍵,承上啟下,是能間接影響到中考最後的成敗,因此相關的知識點內容必須要掌握牢固。
  • 【英】新版 React DevTools 簡介
    react-domreact-nativeHow do I get the new DevTools?React DevTools is available as an extension for Chrome and Firefox.
  • 通過與React的簡單對比來入門Flutter
    'import ReactDOM from 'react-dom'import App from '.context) => DetailPage(true),        },      ),    );  }}React代碼import {BrowserRouter, Switch, Route} from 'react-router-dom
  • || 附《react中文入門教程》電子版
    官網:https://react-bootstrap.github.io中文文檔: http://react.tgwoo.comGithub: https://github.com/react-bootstrap/react-bootstrap/Ant-design
  • 【Hooks】:[組]Awesome React Hooks
    /medium.com/@dispix/from-react-component-to-hooks-b50241334365React Hooks: What's going to happen to my tests?
  • React深入useEffect
    三、useEffect原理useEffect實際上是ReactCurrentDispatcher.current.useEffect(源碼解析會講到)useEffect原理可以簡單理解為:函數組件在掛載階段會執行MountEffect,維護hook的鍊表,同時專門維護一個effect的鍊表。
  • React v17.0 正式發布!
    並且,在 React 17 之前,如果在同一個頁面上使用不同的 React 版本(可以這麼做,但是有風險),會導致事件問題的出現,會有一些未知的風險。我們正在修復 React v17 中的許多問題。這意味著,當 React 18 或未來版本來臨時,你將有更多選擇。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.