引言
我們?nèi)粘V惺褂?react
開發(fā)項目窃判,那么一定會跟 react-rouer
打交道,但是由于 react
路由的設(shè)計和用法惠窄,使得很多剛使用 react
的同學(xué)在接觸到react-router
的時候就會很蛋疼蒸眠,不知道其匹配的機(jī)制和原理。
追本溯源一直是我們學(xué)習(xí)和提升過程中一個很重要的過程杆融,本文就以 HashRouter
為例楞卡,結(jié)合最新的 react-hooks
語法, 一步步的實現(xiàn)一個屬于自己的 react-router
,看一看 react-router
到底應(yīng)該怎么實現(xiàn)蒋腮!
初始化項目
我們需要做一些前期的準(zhǔn)備工作淘捡,初始化項目這一步是必須的,我們通過
npx create-react-app my-react-router
腳手架工具初始化一個項目池摧,為了方便測試焦除,將里面沒用的文件刪除掉,保留一個比較干凈的 workspace
作彤。
新建 react-router-dom
的文件夾膘魄,存放我們自定義的 router
組件。
新建 views
文件夾竭讳,先自定義三個組件并將其導(dǎo)入到 App.js
中创葡,用于后期測試我們的頁面跳轉(zhuǎn),這里我已 Home
绢慢,Category
灿渴,Profile
三個組件為例。
Context api
react-router
最基本的用法如下:
<Router>
<div>
<Route path="/">
<Home />
</Route>
<Route path="/news">
<News />
</Route>
</div>
</Router>
我們在使用 Router
的時候呐芥,只要是被 Route
包裹的組件逻杖,我們都可以通過 props
屬性獲取到改組件的路由信息,而這些信息都是往往都是通過父組件 Router
傳遞過去的思瘟,這也是為什么要用 Router
包裹 Route
的原因荸百。
當(dāng) Route
嵌套過深,路由信息更為復(fù)雜的時候滨攻,為了解決嵌套路由之間的數(shù)據(jù)共享够话,以及狀態(tài)更新的問題,需要引入新的 api
光绕,即 createContext
和 useContext
女嘲,它可以實現(xiàn)跨組件的數(shù)據(jù)傳遞。
因此诞帐,我們在 react-router-dom
文件夾下創(chuàng)建一個 Context
文件欣尼,用于管理路由信息。
import React from 'react'
// 創(chuàng)建路由共享 context
const RouterContext = React.createContext({});
export default RouterContext;
關(guān)于 createContext
和 useContext
的用法停蕉,這里不在贅述愕鼓,不了解的小伙伴,這里給你們準(zhǔn)備了一篇文章慧起,供你參考菇晃。
Router 組件
上面我們大概了解了 react-router
的基本用法,即 Router
和 Route
組件蚓挤。
我們不妨也仿照著先來構(gòu)建這兩組件磺送。
我們在 react-router-dom
文件夾下新建驻子,index.js
入口文件 ,以及 HashRouter.js
和 Route.js
兩個組件文件估灿,并在入口文件導(dǎo)出崇呵,App.js
引入即可。
App.js
組件基本內(nèi)容如下:
import { HashRouter as Router , Route } from './react-router-dom'
function App () {
return (
<Router>
<div className="App">
<div className="">
<Route path={'/home'} component={Home} />
<Route path={'/category'} component={Category} />
<Route path={'/profile'} component={Profile} />
</div>
</div>
</Router>
);
}
Router.js
組件甲捏,主要是給子組件提供路由信息演熟,因為其基礎(chǔ)結(jié)構(gòu)如下:
import React from 'react';
import RouterContext from './Context';
const HashRouter = (props) => {
return <RouterContext.Provider value={{a : 1}}>
{ props.children }
</RouterContext.Provider>
}
export default HashRouter;
其中 props.children
就是 Router.js
組件所包裹的子組件的內(nèi)容。
- 初始化路由
如果初始化沒有 hash
值司顿,在初始化路由的時候默認(rèn)會被自動到導(dǎo)向到 #/
根路由上去芒粹,我們需要在 useEffect
中做處理
useEffect(()=>{
// 初始化路由信息
window.location.hash = window.location.hash || '/';
},[])
- 構(gòu)建
location
信息
路由信息里有一個非常重要的屬性 location
,里面包含了 pathname
大溜, hash
等參數(shù)化漆,因此我們需要通過 useState
構(gòu)建這么一個狀態(tài);
// location 狀態(tài)
const [location, setLocation] = useState({
pathname: window.location.hash.slice(1) || '/'
})
- 監(jiān)控
hash
變化
有了 location
的狀態(tài)钦奋,我們需要在 hashchange
的時候去更改它座云,需要在 useEffect
添加 onHashChange
事件,監(jiān)控 hash
變化付材,并更新 location
的值朦拖。
// hash 變化處理函數(shù)
const onHashChange = () => {
setLocation((location) => ({
...location,
pathname: window.location.hash.slice(1) || '/'
}))
}
useEffect(() => {
// 初始化路由信息
window.location.hash = window.location.hash || '/';
// 監(jiān)控 hash 變化
window.addEventListener('hashchange', onHashChange, false);
}, [])
- 傳遞
location
屬性
父組件已經(jīng)可以根據(jù) hash
狀態(tài)更新 location
的值,那么 Route
子組件需要根據(jù) pathname
的狀態(tài)去匹配對應(yīng)的 Component
厌衔,所以父組件需要通過 context
的值將 location
信息傳遞下去璧帝。
// 傳遞給子組件
const value = {
location
}
return <RouterContext.Provider value={value}>
{props.children}
</RouterContext.Provider>
Route 組件
上面父組件已經(jīng)通過 context
將路徑傳遞給了 Route
子組件,Route
子組件就可以通過useContext
去消費父組件提供的 location
富寿,進(jìn)而進(jìn)行路徑匹配睬隶,決定組件的渲染。
// 獲取 pathname 路徑
const { location: { pathname } } = useContext(RouterContext);
// 獲取 path 和 Component 屬性
const { path, component: Component } = props;
// 判斷路徑是否匹配页徐,匹配返回對應(yīng)的組件
if ( pathname === path ) return <Component/>;
// 不匹配返回 null
return null;
現(xiàn)在一個苏潜,基本的匹配就完成了,你可以手動在地址欄上輸入對應(yīng)的 hash
值变勇,就可以看到匹配的組件了恤左。
這么匹配顯然是有問題吧,因為它只能精確匹配搀绣,不能包含匹配飞袋,即路徑為 home/1
的時候匹配不到 Home
,這是有問題的豌熄,我們需要將匹配規(guī)則進(jìn)行修改,改成正則匹配物咳。
這里我們利用一個第三方的庫 Path-to-RegExp
來實現(xiàn)锣险,它的介紹和詳細(xì)用法請戳 這里
// path 轉(zhuǎn)正則進(jìn)行匹配
let reg = pathToRegexp(path, [], { end: false });
// 判斷路徑是否匹配蹄皱,匹配返回對應(yīng)的組件
let result = pathname.match(reg);
if ( result ) return <Component/>;
其中 end:false
表示不精確匹配。
ok芯肤,這樣一來就比較靠譜了~
雖然解決了上面的問題巷折,但我們訪問 home/1
的時候,會同時匹配 home
和 home/1
兩個路徑所對應(yīng)的組件崖咨,而我們就想只匹配一個的時候锻拘,我們需要加上 exact
參數(shù)。
我們上述代碼只需要簡單的修改即可击蹲,end
參數(shù)的值通過結(jié)構(gòu) props
的 exact
參數(shù)來獲取署拟,默認(rèn)為 false
即可。
// 獲取 path 和 Component 屬性
const { path, component: Component, exact = false } = props;
// path 轉(zhuǎn)正則進(jìn)行匹配
let reg = pathToRegexp(path, [], { end: exact });
Link 組件
我們目前實現(xiàn)的結(jié)果是可以通過手動輸入 hash
的值歌豺,可以跳轉(zhuǎn)到對應(yīng)的組件推穷,這樣顯然很不方便,我們希望的是可以通過點擊來完成這一功能类咧,需要提供一個 Link
組件馒铃。
先在 react-router-dom
目錄下新建 Link
組件,并在 index.js
中導(dǎo)出痕惋。
我們需要點擊跳轉(zhuǎn)区宇,那么就需要有一個跳轉(zhuǎn)的方法 push
, 該方法應(yīng)該是由 父組件 Router
提供的值戳,因此我們在 Router
組件新增一個 history
屬性议谷,里面有一個 push
方法。
// Rouer.js
// 傳遞給子組件
const value = {
location,
history : {
push(to){
window.location.hash = to;
}
}
}
然后在 Link
組件接收使用即可
const Link = (props) => {
// 獲取 push 方法
const { history: { push } } = useContext(RouterContext);
// 獲取跳轉(zhuǎn)路徑
const { to } = props;
// 跳轉(zhuǎn)
return <a onClick={() => push(to)}>{props.children}</a>
}
Redirect 組件
有時候我們希望用戶訪問到某一路由時重定向到其他路由述寡,例如我們?nèi)绻斎氩缓戏ǖ穆酚墒料叮ヅ洳怀晒Φ臅r候我們希望重定向到我們的首頁 /home
上去,就需要用到 Redirect
組件鲫凶。
App.js
組件稍做修改:
<Route path={'/home'} exact={true} component={Home}/>
<Route path={'/category'} component={Category}/>
<Route path={'/profile'} component={Profile}/>
<Redirect to={'/home'} />
在我們的 react-router-dom
文件夾下新建 Redirect.js
文件并在 index.js
導(dǎo)出禀崖。
Redirect
組件與 Link
類似,只不過它不做熱河渲染螟炫,加載完畢就直接跳轉(zhuǎn)波附。
const Redirect = (props) => {
// 獲取 push 方法
const { history: { push } } = useContext(RouterContext);
// 獲取跳轉(zhuǎn)路徑
const { to } = props;
// 重定向跳轉(zhuǎn)
useEffect(() => {
push(to);
}, [push, to]);
// 不渲染
return null;
}
這會,你隨便敲一個不存在路徑昼钻,就會幫你重定向到 home
頁面~
但是掸屡,你再點擊 category
、profile
路由的時候也會閃一下后重定向到 home
頁面然评。
這是因為仅财,我們并沒有做處理,雖然匹配到了 category
和 profile
路由碗淌,但是也并不妨礙繼續(xù)往下執(zhí)行盏求,就又到了 Redirect
組件抖锥,應(yīng)該說是我們無論如何匹配都會重定向到 home
頁面,這顯然是不合理的碎罚,需要通過 Swtich
組件進(jìn)行包裹磅废。
Swtich 組件
Swtich
組件要實現(xiàn)的功能就是當(dāng)我們匹配到了路徑之后,就不需要繼續(xù)匹配荆烈,解決上述重定向的問題拯勉。
我們在 react-router-dom
文件夾下新建 Swtich.js
文件并在 index.js
導(dǎo)出。
實現(xiàn)思路就是憔购,循環(huán)遍歷 Switch
的子組件宫峦,判斷當(dāng)前路徑是否與子組件的路由匹配,匹配成功后返回即可倦始。
const Switch = (props) => {
// 獲取 pathname 路徑
const { location: { pathname } } = useContext(RouterContext);
// 子組件
const children = props.children;
// 循環(huán)遍歷進(jìn)行匹配
for ( let i = 0; i < children.length; i++ ) {
// 子組件
let child = children[i];
// 子組件的路徑(redirect 沒有 path 屬性)
let path = child.props.path || '';
// 路由正則
let reg = pathToRegexp(path, [], { end: false });
// 如果匹配成功
if( reg.test(pathname )) return child;
}
// 匹配不成功
return null;
}
現(xiàn)在只要在我們的 Route
上用 Switch
包裹一下斗遏,就可以解決上述問題了!
現(xiàn)在鞋邑,我們的基本路由已經(jīng)實現(xiàn)了一大半了诵次,贊!
組件Props
目前我們的跳轉(zhuǎn)只能靠 Link
或者 Redirect
組件來實現(xiàn)枚碗,實際上我們并不希望這樣逾一,我們希望每個組件都可以獲取 Router
組件的 history
以及 location
等屬性,方便子組件的使用肮雨。
實現(xiàn)方式也非常簡單遵堵,只需要將我們的 Route
稍作修改即可,在渲染 Component
的時候以 props
的方式傳遞給子組件怨规。
// 傳遞給子組件的數(shù)據(jù)
const { location, history } = useContext(RouterContext);
const componentProps = {
location,
history,
match: {}
}
if ( result ) return <Component {...componentProps}/>;
這樣陌宿,子組件就可以獲取 history
,location
等參數(shù)波丰,即可以在組件中通過 props.history.push
方法進(jìn)行跳轉(zhuǎn)了壳坪。
參數(shù)路由
類似 /detail/:id
這種帶參數(shù)的路由實際生產(chǎn)是非常常見的,我們要動態(tài)獲取 id
的值掰烟,好在 path-to-regexp
這個包給我們提供了解析路由的參數(shù)的方法爽蝴,只需要照搬即可。
將 Route.js
組件修改一下纫骑,
const Route = (props) => {
// 獲取 pathname 路徑
const { location: { pathname } } = useContext(RouterContext);
// 獲取 path 和 Component 屬性
const { path, component: Component, exact = false } = props;
// path 轉(zhuǎn)正則進(jìn)行匹配
let keys = [];
let reg = pathToRegexp(path, keys, { end: exact });
// 判斷路徑是否匹配蝎亚,匹配返回對應(yīng)的組件
let result = pathname.match(reg);
let [url, ...values] = result || [];
// 傳遞給子組件的數(shù)據(jù)
const { location, history } = useContext(RouterContext);
const componentProps = {
location,
history,
match: {
path: pathname,
params: keys.reduce((obj, current, idx) => {
obj[current['name']] = values[idx]
return obj;
}, {}),
url
}
}
if ( result ) return <Component {...componentProps}/>;
// 不匹配返回 null
return null;
}
通過 keys
查詢匹配的 name
屬性值,通過 values
查詢匹配的 name
屬性值對應(yīng)的value
值先馆,之后將其放到 match
對象中发框,一并傳遞給子組件,子組件就可以在 props
的 match
中查詢到該值煤墙。
結(jié)語
至此梅惯,我們已經(jīng)實現(xiàn)了一個簡易版的 react-router
顾患, 有沒有感到一絲欣慰?
相信你能閱讀到這里个唧,對 react-router
有了一個較深的理解,當(dāng)你深入了解之后發(fā)現(xiàn) react-router
的實現(xiàn)非常巧妙设预,并不是特別復(fù)雜徙歼,當(dāng)然,這些基礎(chǔ)功能也只是冰山一角鳖枕,還有更多更強(qiáng)大功能等著你去實現(xiàn)魄梯。
代碼已上傳github,需要的小伙伴自行查閱宾符!