你不知道的 React Router 4
幾個月前,
React Router 4
發(fā)布,我能清晰地感覺到來自大量的修改
的不同聲音。誠然,我在學習React Router 4
的第一天,也是非常痛苦
的,但是,這并不是因為看它的API
,而是反復思考使用它的模式
和策略
,因為V4
的變化確實有點大,V3
的功能它都有,除此之外,還增加了一些特性
,我不能直接將使用V3
的心得直接遷移過來,現(xiàn)在,我必須重新審視router
和layout components
之間的關系
本篇文章不是把 React Router 4
的 API
再次呈現(xiàn)給讀者看,而是簡單介紹其中最常用的幾個概念,和重點講解我在實踐的過程中發(fā)現(xiàn)的比較好的 模式
和 策略
不過,在閱讀下文之前,你得首先保證以下的 概念
對你來說 并不陌生
React stateless(Functional) 組件
- ES6 的
箭頭函數(shù)
和它的隱式返回
- ES6 的
解構
- ES6 的
模板字符串
如果你就是那 萬中無一
的絕世高手,那么你也可以選擇直接 view demo
一個全新的 API
React Router
的早期版本是將 router
和 layout components
分開,為了徹底搞清楚 V4
究竟有什么不同,我們來寫兩個簡單的 example
就明白了
example app
就兩個 routes
,一個 home
,一個 user
在 V3
中
import React from "react";
import { render } from "react-dom";
import { Router, Route, IndexRoute, Link, browserHistory } from "react-router";
const PrimaryLayout = props =>
<div className="primary-layout">
<header>Our React Router 3 App</header>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/user">User</Link>
</li>
</ul>
<main>
{props.children}
</main>
</div>;
const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;
const App = () =>
<Router history={browserHistory}>
<Route path="/" component={PrimaryLayout}>
<IndexRoute component={HomePage} />
<Route path="/user" component={UsersPage} />
</Route>
</Router>;
render(<App />, document.getElementById("root"));
上篇文章給大家推薦了一個在線
react
編譯器 stackblitz,本篇文章再給大家推薦一個不錯的,codesandbox,專門針對react
且開源,正所謂,實踐是檢驗真理的唯一標準
,這也是一種良好的學習習慣
上面代碼中有幾個關鍵的點在 V4
中就不復存在了
- 集中式
router
- 通過
<Route>
嵌套,實現(xiàn)Layout
和page 嵌套
-
Layout
和page 組件
是作為router
的一部分
我們使用 V4
來實現(xiàn)相同的應用程序對比一下
import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Route, Link } from "react-router-dom";
const PrimaryLayout = () =>
<div className="primary-layout">
<header>Our React Router 4 App</header>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/User">User</Link>
</li>
</ul>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/user" component={UsersPage} />
</main>
</div>;
const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;
const App = () =>
<BrowserRouter>
<PrimaryLayout />
</BrowserRouter>;
render(<App />, document.getElementById("root"));
注意,我們現(xiàn)在
import
的是BrowserRouter
,而且是從react-router-dom
引入,而不是react-router
接下來,我們用肉眼就能看出很多的變化,首先,V3
中的 router
不在了,在 V3
中,我們是將整個龐大的 router
直接丟給 DOM
,而在 V4
中,除了 BrowserRouter
, 我們丟給 DOM
的是我們的應用程序本身
另外,V4
中,我們不再使用 {props.children}
來嵌套組件了,替代的 <Route>
,當 route
匹配時,子組件會被渲染到 <Route>
書寫的地方
Inclusive Routing
在上面的 example
中,讀者可能注意到 V4
中有 exact
這么一個 props
,那么,這個 props
有什么用呢? V3
中的 routing
規(guī)則是 exclusive
,意思就是最終只獲取一個 route
,而 V4
中的 routes
默認是 inclusive
的,這就意味著多個 <Route>
可以同時匹配
和呈現(xiàn)
還是使用上面的 example
,如果我們調皮地刪除 exact
這個 props
,那么我們在訪問 /user
的時候,Home
和 User
兩個 Page
都會被渲染,是不是一下就明白了
為了更好地理解
V4
的匹配邏輯,可以查看 path-to-regexp,就是它決定routes
是否匹配URL
為了演示 inclusive routing
的作用,我們新增一個 UserMenu
組件如下
const PrimaryLayout = () =>
<div className="primary-layout">
<header>
Our React Router 4 App
<Route path="/user" component={UsersMenu} />
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/user" component={UsersPage} />
</main>
</div>;
現(xiàn)在,當訪問 /user
時,兩個組價都會被渲染,在 V3
中存在一些模式也可以實現(xiàn),但過程實在是復雜,在 V4
中,是不是感覺輕松了很多
Exclusive Routing
如果你只想匹配一個 route
,那么你也可以使用 <Switch>
來 exclusive routing
const PrimaryLayout = () =>
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/user/add" component={UserAddPage} />
<Route path="/user" component={UsersPage} />
<Redirect to="/" />
</Switch>
</main>
</div>;
在 <Switch>
中只有一個 <Route>
會被渲染,另外,我們還是要給 HomePage
所在 <Route>
添加 exact
,否則,在訪問 /user
或 /user/add
的時候還是會匹配到 /
,從而,只渲染 HomePage
。同理,不知有沒同學注意到,我們將 /user/add
放在 /user
前面是保證正確匹配
的很有策略性
的一步,因為,/user/add
會同時匹配 /user
和 /user/add
,如果不這么做,大家可以嘗試交換它們兩個的位置,看下會發(fā)生什么
當然,如果我們給每一個 <Route>
都添加一個 exact
,那就不用考慮上面的 策略
了,但不管怎樣,現(xiàn)在至少知道了我們還有其它選擇
<Redirect>
組件不用多說,執(zhí)行瀏覽器重定向,但它在 <Switch>
中時,<Redirect>
組件只會在 routes
匹配不成功的情況下渲染,另外,要想了解 <Redirect>
如何在 non-switch
環(huán)境下使用,可以參考下面的 Authorized Route
"Index Routes" 和 "Not Found"
V4
中也沒有 <IndexRoute>
,但 <Route exact>
可以實現(xiàn)相同的功能,或者 <Switch>
和 <Redirect>
重定向到默認的有效路徑,甚至一個找不到的頁面
嵌套布局
接下來,你可能很想知道 V4
中是如何實現(xiàn) 嵌套布局
的,V4
確實給我們了很多選擇,但這并不一定是好事,表面上,嵌套布局
微不足道,但選擇的空間越大,出現(xiàn)的問題也就可能越多
現(xiàn)在,我們假設我們要增加兩個 user
相關的頁面,一個 browse user
,一個 user profile
,對 product
我們也有相同的需求,實現(xiàn)的方法可能并不少,但有的仔細思考后可能并不想采納
第一種,如下修改 PrimaryLayout
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/user" exact component={BrowseUsersPage} />
<Route path="/user/:userId" component={UserProfilePage} />
<Route path="/products" exact component={BrowseProductsPage} />
<Route path="/products/:productId" component={ProductProfilePage} />
<Redirect to="/" />
</Switch>
</main>
</div>
);
};
雖然這種方法可以實現(xiàn),但仔細觀察下面的兩個 user
頁面,就會發(fā)現(xiàn)有點潛在的 問題
const BrowseUsersPage = () => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<BrowseUserTable />
</div>
</div>
)
const UserProfilePage = props => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<UserProfile userId={props.match.params.userId} />
</div>
</div>
)
userId
通過props.match.params
獲取,props.match
賦予給了<Route>
中的任何組件。除此之外,如果組件不通過<Route>
來渲染,要訪問props.match
,可以使用withRouter()
高階組件來實現(xiàn)
估計大家都發(fā)現(xiàn)了吧,兩個 user
頁面中都有一個<UserNav />
,這明顯會導致不必要的請求
,以上只是一個簡單實例,如果是在真實的項目中,不知道會重復消耗多少的流量,然而,這就是由我們以上方式使用路由引起的
接下來,我們再看看另一種實現(xiàn)方式
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/user" component={UserSubLayout} />
<Route path="/products" component={ProductSubLayout} />
<Redirect to="/" />
</Switch>
</main>
</div>
);
};
我們用 2 個 routes
替換之前的 4 個 routes
注意,這里我們沒有再使用
exact
,因為,我們希望/user
可以匹配任何以/user
開始的route
,products
同理
使用這種策略,子布局也開始承擔起了渲染 routes
的責任,現(xiàn)在,UserSubLayout
長這樣
const UserSubLayout = () =>
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path="/user" exact component={BrowseUsersPage} />
<Route path="/user/:userId" component={UserProfilePage} />
</Switch>
</div>
</div>;
現(xiàn)在是不是解決了第一種方式中的生命周期,重復渲染
的問題呢?
但有一點值得注意的是,routes
需要識別它的完整路徑才能匹配,為了減少我們的重復輸入,我們可以使用 props.match.path
來代替
const UserSubLayout = props =>
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path={props.match.path} exact component={BrowseUsersPage} />
<Route
path={`${props.match.path}/:userId`}
component={UserProfilePage}
/>
</Switch>
</div>
</div>;
Match
正如我們上面看到的那樣,props.match
可以幫我們獲取 userId
和 routes
match
對象為我們提供了 match.params
,match.path
,和 match.url
等屬性
match.path vs match.url
最開始,可能覺得這兩者的區(qū)別并不明顯,控制臺經常出現(xiàn)相同的輸出,比如,訪問 /user
const UserSubLayout = ({ match }) => {
console.log(match.url) // output: "/user"
console.log(match.path) // output: "/user"
return (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path={match.path} exact component={BrowseUsersPage} />
<Route path={`${match.path}/:userId`} component={UserProfilePage} />
</Switch>
</div>
</div>
)
}
match
在組件的參數(shù)中被解構,意思就是我們可以使用match.path
代替props.match.path
雖然我們看不到什么明顯的差異,但需要明白的是 match.url
是瀏覽器 URL
的一部分,match.path
是我們?yōu)?router
書寫的路徑
如何選擇
如果我們是構建 route
路徑,那么肯定使用 match.path
為了說明問題,我們創(chuàng)建兩個子組件,一個 route
路徑來自 match.url
,一個 route
路徑來自 match.path
const UserComments = ({ match }) =>
<div>
UserId: {match.params.userId}
</div>;
const UserSettings = ({ match }) =>
<div>
UserId: {match.params.userId}
</div>;
const UserProfilePage = ({ match }) =>
<div>
User Profile:
<Route path={`${match.url}/comments`} component={UserComments} />
<Route path={`${match.path}/settings`} component={UserSettings} />
</div>;
然后,我們按下面方式來訪問
/user/5/comments
/user/5/settings
實踐后,我們發(fā)現(xiàn),訪問 comments
返回 undefined
,訪問 settings
返回 5
正如 API
所述
match
:
path
- (string) The path pattern used to match. Useful for building nested <Route>s
url
- (string) The matched portion of the URL. Useful for building nested <Link>s
避免 Match Collisions
假設我們的 App
是一個儀表盤,我們希望訪問 /user/add
和 /user/5/edit
添加和編輯 user
。使用上面的實例,user/:userId
已經指向 UserProfilePage
,我們這是需要在 UserProfilePage
中再添加一層 routes
么?顯示不是這樣的
const UserSubLayou = ({ match }) =>
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route exact path={match.path} component={BrowseUsersPage} />
<Route path={`${match.path}/add`} component={AddUserPage} />
<Route path={`${match.path}/:userId/edit`} component={EditUserPage} />
<Route path={`${match.path}/:userId`} component={UserProfilePage} />
</Switch>
</div>
</div>;
現(xiàn)在,看清楚這個策略
了么
另外,我們使用 ${match.path}/:userId(\\d+)
作為 UserProfilePage
對應的 path
,保證 :userId
是一個數(shù)字,可以避免與 /users/add
的沖突,這樣,將其所在的 <Route>
丟到最前面去也能正常訪問 add
頁面,這一招,就是我在 path-to-regexp 學的
Authorized Route
在應用程序中限制未登錄的用戶訪問某些路由
是非常常見的,還有對于授權
和未授權
的用戶 UI
也可能大不一樣,為了解決這樣的需求,我們可以考慮為應用程序設置一個主入口
class App extends React.Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/auth" component={UnauthorizedLayout} />
<AuthorizedRoute path="/app" component={PrimaryLayout} />
</Switch>
</BrowserRouter>
</Provider>
)
}
}
現(xiàn)在,我們首先會去選擇應用程序在哪個頂級布局
中,比如,/auth/login
和 /auth/forgot-password
肯定在 UnauthorizedLayout
中,另外,當用戶登陸時,我們將判斷所有的路徑都有一個 /app
前綴以確保是否登錄。如果用戶訪問 /app
開頭的頁面但并沒有登錄,我們將會重定向
到登錄頁面
下面就是我寫的 AuthorizedRoute
組件,這也是 V4
中一個驚奇的特性,可以為了滿足某種需要而書寫自己的路由
class AuthorizedRoute extends React.Component {
componentWillMount() {
getLoggedUser();
}
render() {
const { component: Component, pending, logged, ...rest } = this.props;
return (
<Route
{...rest}
render={props => {
if (pending) return <div>Loading...</div>;
return logged
? <Component {...this.props} />
: <Redirect to="/auth/login" />;
}}
/>
);
}
}
const stateToProps = ({ loggedUserState }) => ({
pending: loggedUserState.pending,
logged: loggedUserState.logged
});
export default connect(stateToProps)(AuthorizedRoute);
點擊 這里 可以查看的我的整個 Authentication
總結
React Router 4
相比 V3
,變化很大,若是之前的項目使用的 V3
,不建議立即升級,但 V4
比 V3
確實存在較大的優(yōu)勢