React Router 用法
React Router 用法
一、DEMO
import React from "react"; import { HashRouter as Router, Route, Link } from 'react-router-dom' const First = () => <div>第一個示例的第【1】個路由,第一個路由在第一個和第二個url裡都會顯示,但不在第三個顯示</div> const Second = () => <div>第一個示例的第【2】個路由,只在第二個url裡顯示</div> const Third = () => <div>第三個示例</div> class BaseDemo extends React.Component { render() {return <div> <h3>React-router基礎示例</h3> <h3>路由資料被儲存在 this.props.match 裡,這是其中的值{JSON.stringify(this.props.match)}</h3> <Router> <div> {/* this.props.match.url 表示當前url */} <li><Link to={`${this.props.match.url}/1`}>示例1</Link></li> <li><Link to={`${this.props.match.url}/2`}>示例2</Link></li> <li><Link to={`${this.props.match.url}/3`}>示例3</Link></li> <Route path={`${this.props.match.url}/1`} component={First}/> <Route path={`${this.props.match.url}/2`} component={First}/> <Route path={`${this.props.match.url}/2`} component={Second}/> <Route path={`${this.props.match.url}/3`} component={Third}/> </div> </Router> </div> } }
二、路由巢狀
在頂級路由匹配到元件後,子元件裡面也可能有一個次級路由。
假如頂級路由的url為:/1,那麼次級路由匹配後的路徑一般來說是 /1/2;
但是假如當前路徑是 /1,然後次級路由裡有這樣一個標籤 <Link to="/2"}>示例2</Link>。
當我們點選這個標籤時,跳轉的 url 是 /2,而不是我們期望的 /1/2。因此我們需要拿到之前的 url /1,具體方法就是通過路由的 match 屬性來拿,於是就有了這種寫法:
<li><Link to={`${this.props.match.url}/2`}>示例2</Link></li>
意思就是跳轉到當前路徑,後面再拼接 /2
這個路徑。
相對應的,我們在 Route 標籤裡也要新增相同的內容:
<Route path={`${this.props.match.url}/2`} component={second}/>
示例(子路由是 ChildRouter):
import React from "react"; import { HashRouter as Router, Route, Link } from 'react-router-dom' const First = () => <div>第一個示例的第【1】個路由,第一個路由在第一個和第二個url裡都會顯示,但不在第三個顯示</div> const Second = () => <div>第一個示例的第【2】個路由,只在第二個url裡顯示</div> const ChildRouter = (route) => <div>第一個示例的第【3】個路由,只在第三個url裡顯示 <Router> <div> <h3>以下是子路由的屬性</h3> <p>{JSON.stringify(route)}</p> <li><Link to={`${route.match.url}/1`}>跳轉子1</Link></li> <li><Link to={`${route.match.url}/2`}>跳轉子2</Link></li> <hr/> {/* component 是一個React元件。 * 注意,元件是放在這個屬性裡,而不是 Route 包裹的裡面 * */} <Route path={`${route.match.url}/1`} component={() => <h3>這裡是子1</h3>}/> <Route path={`${route.match.url}/2`} component={() => <h3>這裡是子2</h3>}/> </div> </Router> </div> class RoutingNested extends React.Component { render() { return <div> <h3>React-router 路由巢狀</h3> <h3>路由資料被儲存在 this.props.match 裡,這是其中的值{JSON.stringify(this.props.match)}</h3> <Router> <div> {/* this.props.match.url 表示當前url */} <li><Link to={`${this.props.match.url}/1`}>示例1</Link></li> <li><Link to={`${this.props.match.url}/2`}>示例2</Link></li> <li><Link to={`${this.props.match.url}/3`}>示例3</Link></li> <hr/> <Route path={`${this.props.match.url}/1`} component={First}/> <Route path={`${this.props.match.url}/2`} component={Second}/> <Route path={`${this.props.match.url}/3`} component={ChildRouter}/> </div> </Router> </div> } }
三、props
react-router 的路由資訊,都儲存在元件的 props 裡。
之所以是存在 props 裡,是因為我們寫在父元件裡的,是 Route 標籤,我們需要顯示的元件,是作為 Route 標籤的屬性而傳進去的。
而我們的元件,作為 Route 標籤的子元件而存在,因此,路由資料通過 props 傳給我們的元件。
因此:
1、只有 Route 標籤裡傳入的元件,才能通過 props 屬性讀取路由屬性(除非你自己手動傳給子元件);
2、每個能讀取路由屬性的元件,其 match 屬性,獲得的是當前級別的路由的屬性(例如本級路由的 match.url = '/Params/2',那麼上級路由的 match.url = '/Params'
3、match.isExact:假如當前路徑和 route 標籤裡的 path 完全相同,該值為 true,否則為 false(例如當匹配到次級路由時,那麼上級路由的這個屬性則為 false,次級當前的為 true)(當 url 為 / 時顯示該元件,/a 不顯示元件,需要使用這個);
4、match 屬性的值,是根據當前路由(元件所在的 route 標籤)的層級而決定的;
5、location 屬性的值,在每個能讀取到這個屬性的路由元件,都是相同的;
6、類似 /1?a=1 這樣的路徑,其中 ?a=1,是通過 location.search 來獲取;
7、路由資訊,當路由變化時,是會跟著一起更新的,但並不是實時更新的;
假如我通過點選 <Link> 標籤,讓路由從 /a 跳轉到 /b ,也就是說,從顯示 A 元件到顯示 B 元件。會發生以下事情:
【1】如果 Link 標籤裡有一個 onClick 事件,那麼顯然可以拿到 location 屬性的值。
在該事件執行的這段時間,props.location 的值,是 url 更新之前的。
並且,window.location(也就是原生的),其 url 也是更新之前的;
【2】那什麼時候可以獲取到更新之後的 url 呢?
答案是路由更新後,所對應的那個元件,在掛載的時候,生命週期處於 componentWillMount 時,可以獲取到最新的 url。
因此如果需要第一時間在父元件內拿到更新後的值,那麼需要在父元件,將回調函式傳給子元件才可以實現。
實現原理:可以參考元件通訊,父元件將回調函式傳給表單元件,然後表單元件負責執行這個回撥函式,並將修改後的值作為引數傳給函式。
例如:
【1、先例行引入】
import React from "react";
import {HashRouter as Router, Link, Route} from 'react-router-dom'
【2、兩個子元件,分別點選顯示和直接顯示在頁面上】
class First extends React.Component { constructor() { super() this.log = this.log.bind(this) } render() { return <button onClick={this.log}>點選顯示路由資訊,點選後請檢視控制檯</button> } log() { console.log(this.props) } } const Second = props => <div> 函式元件顯示路由資訊:(這裡是本級 Route 標籤的部分資訊) <pre>{JSON.stringify(props, undefined, 4)}</pre> </div>
【3、父元件,負責對比其 props 與子元件不同】
class RoutingNested extends React.Component { constructor() { super() } render() { return <div> <h3>React-router 引數設定</h3> <h3>注意,這裡存的不是元件裡的路由資訊,而是上一級 Router 標籤的路由資訊</h3> <h3>路由資料被儲存在 this.props 裡,這是其中部分屬性 <pre>{JSON.stringify(this.props, undefined, 4)}</pre></h3> <Router> <div> <li> <Link to={`${this.props.match.url}/1?a=1`} onClick={() => { console.log('Link 標籤(跳轉到/1)的 onClick 事件', this.props.location) }}> 示例1 </Link> </li> <li> <Link to={`${this.props.match.url}/2`} onClick={() => { console.log('Link 標籤(跳轉到/2)的 onClick 事件', this.props.location) }}> 示例2 </Link> </li> <hr/> <Route path={`${this.props.match.url}/1`} component={First}/> <Route path={`${this.props.match.url}/2`} component={Second}/> </div> </Router> </div> } }
四、引數
React路由取引數,有兩種:
1、?a=1 :這種屬於 search 字串,在 location.search 裡取值;
2、/a/123 :這種需要從 match.params裡取值;
但無論哪種,路由獲取到的值,是跳轉後的那一刻的值,而不是實時更新的最新值。
具體來說:
例1:假如 Link 標籤跳轉路徑實時繫結輸入框的一個值(假如值是 abc),這個值作為引數傳遞;
點選跳轉後,子元件讀取到當前傳的值 abc;
此時修改【1】中輸入框的值為 def;
請問子元件讀取到的值此時是多少?abc 還是 def;
答案是 abc;
原因是當前路徑是 abc,這個值讀取到的是當前路徑的值,而不是將要跳轉的路徑的值,因此不是實時更新的(顯然,也不應該是實時更新的);
手動修改位址列的 url:
例2:假如手動修改 url 為 ggg,那麼請問讀取到的值是多少?
我還真去試了一下。答案是除非你修改後,按回車跳轉路徑,會讀取到最新的;
否則,依然保持為修改前 abc;
即使你重新觸發 render 方法(比如修改 state 來實現),依然獲取到的是 abc ,而不是 ggg;
獲取最新值:
例3:如果你想要獲取到新值,那麼請重新點選跳轉(綁定了新的 url 的 Link 標籤)即可;
重新跳轉後(假如跳轉到同一個頁面),url 改變了,那麼元件會重新載入麼?
答案是否定的,如果跳轉到同一個元件,僅是引數改變,那麼元件是不會重新載入的,即元件內的資料保持之前不變,只有傳遞的引數改變了(生命週期函式也不會重新執行);
---生命週期:
1、元件的生命週期函式,只會在第一次掛載的時候執行,如果前後跳轉是同一個元件,那麼該元件的生命週期函式不會重複執行;
2、但 state 的生命週期,會多次執行(只要父元件的 state 改變了,子元件的相關函式會被執行);
3、由於路由資訊是通過 props 傳值的,因此也會多次觸發;
4、不過沒有影響,傳的值依然是舊值(因為路由資訊沒變);
5、假如你在 state 生命週期函式裡做了一些什麼事情,可能需要注意一下會不會帶來不良影響(雖然一般不會);
示例:
【例行引入和子元件】
import React from "react"; import {HashRouter as Router, Link, Route} from 'react-router-dom' const First = props => <div> 第一個元件讀取引數(location.search) {props.location.search} </div> const Second = props => <div> 第二個元件讀取引數(match.params.myParams) {props.match.params.myParams} </div>
【父元件,負責配置路由和傳值】
class RoutingNested extends React.Component { constructor() { super() this.state = { firstNumber: 0, secondNumber: 0 } this.changeFirst = this.changeFirst.bind(this) this.changeSecond = this.changeSecond.bind(this) } render() { return <div> <h3>4、React-router 傳參</h3> <h3>請在對應的跳轉標籤裡,輸入你想要傳的值</h3> <Router> <div> <li> <Link to={`${this.props.match.url}/1?a=${this.state.firstNumber}`} onClick={() => { console.log('Link 標籤(跳轉到/1)的 onClick 事件', this.props.location) }}> 示例1 </Link> <input type="text" value={this.state.firstNumber} onChange={this.changeFirst}/> </li> <li> <Link to={`${this.props.match.url}/2/${this.state.secondNumber}`} onClick={() => { console.log('Link 標籤(跳轉到/2)的 onClick 事件', this.props.location) }}> 示例2 </Link> <input type="text" value={this.state.secondNumber} onChange={this.changeSecond}/> </li> <hr/> <Route path={`${this.props.match.url}/1`} component={First}/> <Route path={`${this.props.match.url}/2/:myParams`} component={Second}/> </div> </Router> </div> } changeFirst(e) { this.setState({ firstNumber: e.target.value }) } changeSecond(e) { this.setState({ secondNumber: e.target.value }) } }
五、父元件傳 【值】 or 【函式】 給子元件
1、路由傳參是通過 props 傳參的;
2、子元件,是寫在 Route 標籤的 component 屬性中的;
3、通過 Route 標籤繫結值,是無法將值,從父元件傳給子元件的;
解決方案:
1)元件可以是一個函式;如const MySecond = () => <div>1</div>;
2)Route 標籤的 component 屬性支援以上函式寫法(顯而易見);
3)我們可以將子元件巢狀到函式返回的元件中;
4)此時我們的元件結構如下:父元件 >> Route 標籤 >> 函式元件 >> 子元件;
①父元件和 Route 進行資料通訊沒意義;
②父元件沒辦法和函式元件通訊;
③但父元件可以直接和子元件通訊;
5)因此,父元件將值先繫結給子元件標籤,然後再返回函式元件;
6)父元件是可以和子元件通訊的,但唯一問題是此時,如何將路由資訊從函式元件傳給子元件(路由資訊從Route傳給了函式元件);
函式元件是可以拿到 props 的,通過 Object.assign() 將 props 和 父元件繫結給子元件的【函式/變數】混合到一起,再傳給子元件。
此時,子元件就同時拿到了 路由資料 和 父元件 傳給他的資料。
【傳一個物件給子元件】
唯一可能存在的問題是,這個資料怎麼傳給子元件(畢竟是一個物件),我們之前接觸的方法都是 k = v 方式傳給子元件,但顯然這個方法不能這麼做。
React也有解決方法,具體來說,利用 es6 的擴充套件運算子 ...
比較簡單的寫法是 <First {...props}/> ,將自動展開 props 並傳給 First 元件。
例如:
【例行引入依賴和子元件,子元件負責顯示值】
import React from "react"; import {HashRouter as Router, Link, Route} from 'react-router-dom' class First extends React.Component { render() { return <div>【1】當前 time 值為:{this.props.time}</div> } } const Second = (props) => <div> 【2】time(負數): {props.time * -1} </div>
【父元件】
class RoutingNested extends React.Component { constructor() { super() this.state = { time: 0 } } // 這個是生命週期,目的是為了測試 state 的傳遞 componentWillMount() { this.timer = setInterval(() => { this.setState({ time: this.state.time + 1 }) }, 1000) } // 解除安裝時,刪除定時器 componentWillUnmount() { clearInterval(this.timer) } render() { // 這個寫法和寫在元件裡,基本沒什麼區別,不過這樣寫感覺好看一些 const MySecond = props => { let obj = Object.assign({}, {time: this.state.time}, props) return <Second {...obj}/> } return <div> <h3>5、父元件傳參給子元件</h3> <p>父元件當前值為:{this.state.time}</p> <Router> <div> <li> <Link to={`${this.props.match.url}`}> 跳轉檢視傳參【1】 </Link> </li> <li> <Link to={`${this.props.match.url}/2`}> 跳轉示例【2】 </Link> </li> <hr/> {/* 這種是寫在元件裡,沒啥區別 */} <Route exact path={`${this.props.match.url}/`} component={props => { let obj = Object.assign({}, {time: this.state.time}, props) return <First {...obj}/> }}/> <Route path={`${this.props.match.url}/2`} render={MySecond}/> </div> </Router> </div> } }
六、path 和 url 的區別
在 match 屬性中,有 path 和 url 屬性,大部分時間他們是一樣的,那麼區別是什麼呢?
假如路由匹配路徑是 /Params/2/:myParams,實際跳轉路徑是 /Params/2/1(此時匹配成功)。
那麼;
url:/Params/2/1
path:/Params/2/:myParams
七、Link 標籤 to 屬性為物件時(路由資訊傳值)
在元件裡,每個元件的路由資料,都是各自獨立的。
在之前分析中,已知:
match 屬性的值,儲存的是該 Route 標籤的路由;
location 屬性的值,其中 url 和 path 不同 Route 元件中,值是相同的;
但【2】並不準確,準確的說,自帶的 hash , search , pathname 這三個屬性的值,是相同的;
假如你在裡面添加了其他資料,那麼結果就有所不同了。
例如 Link 標籤,他有一個屬性 to,可以用於路徑跳轉。
比較常見的是以下這種寫法:
<Link to={`${props.match.url}/`}>子路由</Link>
但是,to 的值,也可以用物件,例如這樣:
<Link to={{ pathname: `${this.props.match.url}/1`, myState: '這是我自定義的變數' }}>示例1</Link>
當路由資訊匹配時:
1、父元件的路由資訊為:
{ "match": { "path": "/RouteInfo", "url": "/RouteInfo", "isExact": false, "params": {} }, "location": { "pathname": "/RouteInfo/1", "search": "", "hash": "" }, "history": { "length": 9, "action": "POP", "location": { "pathname": "/RouteInfo/1", "search": "", "hash": "" } } }
2、被傳值的子元件的路由資訊:
{ "match": { "path": "/RouteInfo/1", "url": "/RouteInfo/1", "isExact": true, "params": {} }, "location": { "pathname": "/RouteInfo/1", "myState": "這是我自定義的變數", "search": "", "hash": "" }, "history": { "length": 24, "action": "PUSH", "location": { "pathname": "/RouteInfo/1", "myState": "這是我自定義的變數", "search": "", "hash": "" } } }
可以看到,被傳值的子元件的路由資訊,location 會多了一個屬性。
但是請注意,以下幾種情況會導致失去這些資訊:
重新整理頁面;
訪問更深一層的子元件(因為資訊被更新了);
重新整理後,訪問相同的 url。舉例來說,你訪問了該 url,傳值了也收到了,然後重新整理頁面,再點選該 url,是沒有的。原因是相同 url 跳轉;
八、重定向
重定向有兩種方式,第一種是通過 <Redirect>
標籤實現,第二種是通過程式設計式導航方式實現。
【<Redirect>
標籤】
<Redirect to={'/default'}/>
【程式設計式導航方式】
this.props.history.push('/default')
兩個的效果是一樣的
九、登入攔截
1、需要元件:
1)登入功能元件;
2)受保護元件(需要登入後才能訪問);
3)受保護元件的父元件(可選,如果 1 和 2 不是同一個路徑,則需要,否則可以不需要)(用於進入受保護元件之前,檢查登入資訊,不符合要求則跳轉到登入元件);
邏輯:(有父元件,登入和受保護元件不是同一個路徑)
2、嘗試訪問受保護元件;
1)進入父元件,檢查儲存登入資訊的物件,確認當前是否登入;
2)如果已登入,則允許訪問受保護元件;
3)如果未登入,則觸發重定向,進入登入元件;
4)登入元件輸入資訊,嘗試登入;
5)輸入資訊符合要求後(不符合則報錯),將資訊寫入儲存登入資訊的物件,並跳轉到受保護元件的路徑;
6)登入後,如果需要登出,則清除登入資訊,並再次進行頁面跳轉;
比較核心的就是幾點:
1、進入受保護頁面時,需要先檢查一下登入資訊;
2、登入和登出時,除了寫入和清除登入資訊之外,還需要進行一次路徑跳轉(登入 -> 受保護頁面,受保護頁面 -> 未登入時的預設頁面)(如何重定向參照【八】);
十、路由配置
const RouteConfig = [ { path: 'first', component: First, name: '第一個路由', routes: [ { path: '1', component: ChildOne, name: '1-1' } ] }, { path: 'second', component: Second, name: '第二個路由' } ]
解釋:
1、component 是元件;
2、path 是該元件對應的 url;
3、name 非必須,這裡是為了省事,自動生成 Link 標籤的描述文字才加的;
4、routes 可選,是該元件的子元件路由資訊。具體來說,就是將這個值傳給路由的子元件,然後子元件就可以拿這個生成自己的子路由資訊了;
單純看這個比較麻煩,我們先來看一個函式吧:
const createRouter = (routes, props) => ( <Router> <div> {/* 自動生成 Link 標籤 */} {createLink(routes, props)} <hr/> {/* 自動生成 Route 標籤 */} {createRoute(routes, props)} </div> </Router> ) createRouter(RouteConfig, props)
這個函式幹了三件事:
1、建立了一個 Router 標籤;
2、根據 routes (來源於上面的路由配置表),自動生成了多個 Link 標籤;
3、以及多個 Route 標籤;
預期上,這兩個生成標籤的函式,他們生成的 Link 標籤和 Route 標籤是一一對應的;
然後我們再分別看這兩個生成函式,先看第一個生成 Link 標籤的:
const createLink = (routes, props) => ( <ol> { routes.map(route => ( <li key={route.path}> <Link to={`${props.match.url}/${route.path}`}>{route.name}</Link> </li> )) } </ol> )
注意,createLink 傳入的第一個引數,是一個數組(參考上面的 RouteConfig 變數);
遍歷這個陣列,生成一個 li 標籤,內包裹一個 Link 標籤,其 to 屬性是當前 url(props.match.url),後面加上路由路徑 route.path,描述文字就是 route.name。
示例(map 返回陣列的一個元素):
<li key='first'> <Link to={`/first`}>第一個路由</Link> </li>
最後結果就是自動生成了導航欄。
然後我們再看另外一個生成 Route 標籤的函式:
const createRoute = (routes, props) => ( <React.Fragment> {routes.map((route, i) => { let obj = { key: i, ...route, path: `${props.match.url}/${route.path}`, component: props => { let obj = {routes: route.routes, ...props} return <route.component {...obj}/> } } return <Route {...obj}/> })} </React.Fragment> )
懂了上面那個後,這個自然而言也很容易懂了。
遍歷 routes ,取出 component 屬性(即該路徑對應的元件),
將當前子路由的路由配置表,如下:
routes: [ { path: '1', component: ChildOne, name: '1-1' } ]
混合到路由資訊裡(見 obj.component 屬性,如果看不懂,請參考上面【7】中,component 的屬性是一個函式的那個示例)。
這樣,對應的子元件,就可以拿到路由配置表中,該元件的子路由資訊了。
具體舉例來說,就是 First 這個元件,可以從 props 裡取出以下資料:
routes: [ { path: '1', component: ChildOne, name: '1-1' } ]
因此,子元件可以繼續呼叫上面兩個函式,來自動生成 Link 標籤,和 Route 標籤。
簡單總結一下上面的過程:
1、呼叫函式 createRouter ,傳參 路由配置表;
2、createRouter 函式會呼叫 自動生成 Link 標籤 和 自動生成 Route 標籤的函式;
3、createLink 函式,根據路由配置表,自動生成了當前層級的 Link 標籤;
4、createRoute 函式,根據路由配置表,自動生成了當前層級的 Route 標籤;
5、createRoute 函式,假如路由配置表某個對應元件,有 routes 屬性,則自動將這個屬性的值,傳給了該元件,於是該元件可以拿到自己的,這一層級的子元件路由配置表;