几个月前,
React Router 4
发布,我能清晰地感受到来自大量的修改
的不一样声音。诚然,我在学习React Router 4
的第一天,也是很是痛苦
的,可是,这并非由于看它的API
,而是反复思考使用它的模式
和策略
,由于V4
的变化确实有点大,V3
的功能它都有,除此以外,还增长了一些特性
,我不能直接将使用V3
的心得直接迁移过来,如今,我必须从新审视router
和layout components
之间的关系react
本篇文章不是把 React Router 4
的 API
再次呈现给读者看,而是简单介绍其中最经常使用的几个概念,和重点讲解我在实践的过程当中发现的比较好的 模式
和 策略
浏览器
不过,在阅读下文以前,你得首先保证如下的 概念
对你来讲 并不陌生
react-router
React stateless(Functional) 组件
app
ES6 的 箭头函数
和它的 隐式返回
less
ES6 的 解构
dom
ES6 的 模板字符串
ide
若是你就是那 万中无一
的绝世高手,那么你也能够选择直接 view demo函数
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>
嵌套,实现 Layout
和 page 嵌套
Layout
和 page 组件
是做为 router
的一部分
咱们使用 V4
来实现相同的应用程序对比一下
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"));
注意,咱们如今
import
的是BrowserRouter
,并且是从react-router-dom
引入,而不是react-router
接下来,咱们用肉眼就能看出不少的变化,首先,V3
中的 router
不在了,在 V3
中,咱们是将整个庞大的 router
直接丢给 DOM
,而在 V4
中,除了 BrowserRouter
, 咱们丢给 DOM
的是咱们的应用程序自己
另外,V4
中,咱们再也不使用 {props.children}
来嵌套组件了,替代的 <Route>
,当 route
匹配时,子组件会被渲染到 <Route>
书写的地方
在上面的 example
中,读者可能注意到 V4
中有 exact
这么一个 props
,那么,这个 props
有什么用呢? V3
中的 routing
规则是 exclusive
,意思就是最终只获取一个 route
,而 V4
中的 routes
默认是 inclusive
的,这就意味着多个 <Route>
能够同时匹配
和呈现
仍是使用上面的 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>;
如今,当访问 /user
时,两个组价都会被渲染,在 V3
中存在一些模式也能够实现,但过程实在是复杂,在 V4
中,是否是感受轻松了不少
若是你只想匹配一个 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
,若是不这么作,你们能够尝试交换它们两个的位置,看下会发生什么
固然,若是咱们给每个 <Route>
都添加一个 exact
,那就不用考虑上面的 策略
了,但无论怎样,如今至少知道了咱们还有其它选择
<Redirect>
组件不用多说,执行浏览器重定向,但它在 <Switch>
中时,<Redirect>
组件只会在 routes
匹配不成功的状况下渲染,另外,要想了解 <Redirect>
如何在 non-switch
环境下使用,能够参考下面的 Authorized Route
V4
中也没有 <IndexRoute>
,但 <Route exact>
能够实现相同的功能,或者 <Switch>
和 <Redirect>
重定向到默认的有效路径,甚至一个找不到的页面
接下来,你可能很想知道 V4
中是如何实现 嵌套布局
的,V4
确实给咱们了不少选择,但这并不必定是好事,表面上,嵌套布局
微不足道,但选择的空间越大,出现的问题也就可能越多
如今,咱们假设咱们要增长两个 user
相关的页面,一个 browse user
,一个 user profile
,对 product
咱们也有相同的需求,实现的方法可能并很多,但有的仔细思考后可能并不想采纳
第一种,以下修改 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> ); };
虽然这种方法能够实现,但仔细观察下面的两个 user
页面,就会发现有点潜在的 问题
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()
高阶组件来实现
估计你们都发现了吧,两个 user
页面中都有一个<UserNav />
,这明显会致使没必要要的请求
,以上只是一个简单实例,若是是在真实的项目中,不知道会重复消耗多少的流量,然而,这就是由咱们以上方式使用路由引发的
接下来,咱们再看看另外一种实现方式
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
的责任,如今,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>;
如今是否是解决了第一种方式中的生命周期,重复渲染
的问题呢?
但有一点值得注意的是,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>;
正如咱们上面看到的那样,props.match
能够帮咱们获取 userId
和 routes
match
对象为咱们提供了 match.params
,match.path
,和 match.url
等属性
最开始,可能以为这二者的区别并不明显,控制台常常出现相同的输出,好比,访问 /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
在组件的参数中被解构,意思就是咱们可使用match.path
代替props.match.path
虽然咱们看不到什么明显的差别,但须要明白的是 match.url
是浏览器 URL
的一部分,match.path
是咱们为 router
书写的路径
若是咱们是构建 route
路径,那么确定使用 match.path
为了说明问题,咱们建立两个子组件,一个 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
实践后,咱们发现,访问 comments
返回 undefined
,访问 settings
返回 5
正如 API
所述
match
:path
- (string) The path pattern used to match. Useful for building nested <Route>surl
- (string) The matched portion of the URL. Useful for building nested <Link>s
假设咱们的 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>;
如今,看清楚这个策略
了么
另外,咱们使用 ${match.path}/:userId(\\d+)
做为 UserProfilePage
对应的 path
,保证 :userId
是一个数字,能够避免与 /users/add
的冲突,这样,将其所在的 <Route>
丢到最前面去也能正常访问 add
页面,这一招,就是我在 path-to-regexp 学的
在应用程序中限制未登陆的用户访问某些路由
是很是常见的,还有对于受权
和未受权
的用户 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> ) } }
如今,咱们首先会去选择应用程序在哪一个顶级布局
中,好比,/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
确实存在较大的优点