這是第 109 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客:React 中的一些 Router 必備知識點javascript
每次開發新頁面的時候,都免不了要去設計一個新的 URL,也就是咱們的路由。其實路由在設計的時候不只僅是一個由幾個簡單詞彙和斜槓分隔符組成的連接,偶爾也能夠去考慮有沒有更「優雅」的設計方式和技巧。而在這背後,路由和組件之間的協做關係是怎樣的呢?因而我以 React 中的 Router 使用方法爲例,整理了一些知識點小記和你們分享~html
一般咱們使用 React-Router 來實現 React 單頁應用的路由控制,它經過管理 URL,實現組件的切換,進而呈現頁面的切換效果。前端
其最基本用法以下:java
import { Router, Route } from 'react-router';
render((
<Router> <Route path="/" component={App}/> </Router>
), document.getElementById('app'));
複製代碼
亦或是嵌套路由:react
在 React-Router V4 版本以前能夠直接嵌套,方法以下:git
<Router>
<Route path="/" render={() => <div>外層</div>}> <Route path="/in" render={() => <div>內層</div>} /> </Route>
</Router>
複製代碼
上面代碼中,理論上,用戶訪問 /in
時,會先加載 <div>外層</div>
,而後在它的內部再加載 <div>內層</div>
。github
然而實際運行上述代碼卻發現它只渲染出了根目錄中的內容。後續對比 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
時,會將「外層」和「其餘」一塊兒展現出來。微信
在實際開發中,每每在頁面切換時須要傳遞一些參數,有些參數適合放在 Redux 中做爲全局數據,或者經過上下文傳遞,好比業務的一些共享數據,但有些參數則適合放在 URL 中傳遞,好比頁面類型或詳情頁中單據的惟一標識 id
。在處理 URL 時,除了問號帶參數的方式,React-Router 能幫咱們作什麼呢?在這其中,Route 組件的 path
屬性即可用於指定路由的匹配規則。
描述:就想讓普普統統的 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 中傳遞的~)
描述:編輯/詳情頁,想要共用一個頁面,URL 由不一樣的參數區分,此時咱們但願,參數必須爲 edit、detail、add 中的 1 個,否則須要跳轉到 404 Not Found 頁面。
path='/book/:pageType(edit|detail|add)'
複製代碼
若是不加括號中的內容 (edit|detail|add)
,當傳入錯誤的參數(好比用戶誤操做、隨便拼接 URL 的狀況),則頁面不會被 404 攔截,而是繼續走下去開始渲染頁面或調用接口,但此時頗有可能致使接口傳參錯誤或頁面出錯。
描述:新增頁和編輯頁辣麼像,個人新增頁也想和編輯/詳情共用一個頁面。可是新增頁不須要 id,編輯/詳情頁須要 id,使用同一個頁面怎麼辦?
path='/book/:pageType(edit|detail|add)/:id?'
複製代碼
別急,能夠用 ?
來解決,它意味着 id 不是一個必要參數,可傳可不傳。
描述:個人 id 只能是數字,不想要字符串怎麼辦?
path='/book/:id(\\\d+)'
複製代碼
此時 id 不是數字時,會跳轉 404,被認爲 URL 對應的頁面找不到啦。
有了這麼多場景,那 Router 是怎樣實現的呢?其實它底層是依賴了 path-to-regexp 方法。
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:重複參數的定界符,默認是 '/',可配置
一些其餘經常使用的路由正則通配符:
? 可選參數
* 匹配 0 次或屢次
+ 匹配 1 次或屢次
若是忘記寫參數名字,而只寫了路由規則,好比下述代碼中 /: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 是取不到問號帶的參數的。
那麼在 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)可能使它變得不可靠。(緣由可參考:blog.csdn.net/zrq1210/art…
在早期的 React-Router 2.0 版本是能夠用 location.query.pageType 來獲取參數的,可是 V4.0 去掉了(有人認爲查詢參數不是 URL 的一部分,有人認爲如今有不少第三方庫,交給開發者本身去解析會更好,有個對此討論的 Issue,有興趣的能夠自行獲取 😊 github.com/ReactTraini…
針對上一節中場景 1 的 Case C,查詢參數隱身式帶法時(從 state 裏帶過去的),在 this.props.location.state
裏能夠取到(不推薦不推薦不推薦,刷新會沒~)
<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 路由原理 一文,針對上述兩種 Case,以及這兩種 Case 分別對應的兩種模式,做出以下總結。
圖片來源:「源碼解析 」這一次完全弄懂 React-Router 路由原理
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
}
})
}
複製代碼
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 對象的默認方法。(此處如有疑議,歡迎指正~)
在實際項目中發現,Link,Route 都是從 dva/router
中引進來的,那麼,Dva 在這之中作了什麼呢?
答案:貌似沒有作特殊處理,Dva 在 React-Router 上作了上層封裝,會默認輸出 React-Router 接口。
Case 1:
項目代碼的 src 目錄下,無論有多少文件夾,路由通常會放在同一個 router.js 文件中維護,但這樣會致使頁面太多時,文件內容會愈來愈長,不便於查找和修改。
所以咱們能夠作一些小改造,在 src 下的每一個文件夾中,建立本身的路由配置文件,以便管理各自的路由。但這種狀況下 React-Router 是不能識別的,因而咱們寫了一個 Plugin 放在 Webpack 中,目的是將各個文件夾下的路由彙總,並生成 router-config.js 文件。以後,將該文件中的內容解析成組件須要的相關內容。插件實現方式可瞭解本團隊另外一篇文章: 手把手帶你入門 Webpack Plugin。
Case 2:
路由的 Hash 模式雖然兼容性好,可是也存在一些問題:
所以公司內部作了一次 Hash 路由轉 Browser 路由的改造。
如原有連接爲:aaa.bbb.com/book-center…
改造方案爲:
經過新增如下配置代碼去掉 #
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路由原理
開源地址 www.zoo.team/openweekly/ (小報官網首頁有微信交流羣)
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com