手動實現(xiàn)一個 react-router-dom

引言

我們?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光绕,即 createContextuseContext女嘲,它可以實現(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)于 createContextuseContext 的用法停蕉,這里不在贅述愕鼓,不了解的小伙伴,這里給你們準(zhǔn)備了一篇文章慧起,供你參考菇晃。

Router 組件

上面我們大概了解了 react-router 的基本用法,即 RouterRoute 組件蚓挤。

我們不妨也仿照著先來構(gòu)建這兩組件磺送。

我們在 react-router-dom 文件夾下新建驻子,index.js 入口文件 ,以及 HashRouter.jsRoute.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 的時候,會同時匹配 homehome/1 兩個路徑所對應(yīng)的組件崖咨,而我們就想只匹配一個的時候锻拘,我們需要加上 exact 參數(shù)。

我們上述代碼只需要簡單的修改即可击蹲,end 參數(shù)的值通過結(jié)構(gòu) propsexact 參數(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 頁面~

但是掸屡,你再點擊 categoryprofile 路由的時候也會閃一下后重定向到 home 頁面然评。

這是因為仅财,我們并沒有做處理,雖然匹配到了 categoryprofile 路由碗淌,但是也并不妨礙繼續(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}/>;

這樣陌宿,子組件就可以獲取 historylocation 等參數(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 對象中发框,一并傳遞給子組件,子組件就可以在 propsmatch 中查詢到該值煤墙。

結(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,需要的小伙伴自行查閱宾符!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末酿秸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子魏烫,更是在濱河造成了極大的恐慌辣苏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哄褒,死亡現(xiàn)場離奇詭異稀蟋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)呐赡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門退客,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人链嘀,你說我怎么就攤上這事萌狂。” “怎么了怀泊?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵茫藏,是天一觀的道長。 經(jīng)常有香客問我包个,道長刷允,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任碧囊,我火速辦了婚禮树灶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糯而。我一直安慰自己天通,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布熄驼。 她就那樣靜靜地躺著像寒,像睡著了一般烘豹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诺祸,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天携悯,我揣著相機(jī)與錄音,去河邊找鬼筷笨。 笑死憔鬼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胃夏。 我是一名探鬼主播轴或,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼仰禀!你這毒婦竟也來了照雁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤答恶,失蹤者是張志新(化名)和其女友劉穎饺蚊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悬嗓,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡卸勺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了烫扼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曙求。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖映企,靈堂內(nèi)的尸體忽然破棺而出悟狱,到底是詐尸還是另有隱情,我是刑警寧澤堰氓,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布挤渐,位于F島的核電站,受9級特大地震影響双絮,放射性物質(zhì)發(fā)生泄漏浴麻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一囤攀、第九天 我趴在偏房一處隱蔽的房頂上張望软免。 院中可真熱鬧,春花似錦焚挠、人聲如沸膏萧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榛泛。三九已至蝌蹂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間曹锨,已是汗流浹背孤个。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留沛简,地道東北人硼身。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像覆享,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子营袜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355