我在 React Rally 2016 上第一次遇到了 Michael Jackson,当时我刚写完 一篇关于 React Router 3 的文章。Michael 是 React Router 的主要作者之一,与 Ryan Florence 共同编写。能遇见这样一位构建了我非常喜欢的工具的人,我感到非常兴奋,但当他说“让我向你展示我们对 React Router 4 的想法,它完全不同!”时,我感到震惊。说实话,我不理解新的方向以及为什么需要如此大的改变。鉴于路由器是应用程序架构中如此重要的组成部分,这可能会改变一些我喜欢的模式。这些变化的想法让我感到焦虑。考虑到社区的凝聚力和 React Router 在许多 React 应用程序中发挥的巨大作用,我不知道社区会如何接受这些变化。
几个月后,React Router 4 发布了,我从 Twitter 上的热议中就能看出,人们对这次彻底的重写有不同的看法。这让我回想起 React Router 第一个版本为了其渐进概念而受到的抵制。在某些方面,早期版本的 React Router 与我们对应用程序路由器“应该是什么”的传统思维模式类似,将所有路由规则放在一个地方。然而,嵌套 JSX 路由的使用并没有被所有人接受。但正如 JSX 本身克服了它的批评者(至少大部分)一样,许多人开始相信嵌套 JSX 路由是一个非常酷的想法。
因此,我学习了 React Router 4。不可否认,第一天很艰难。困难不在于 API,而是在于使用它的模式和策略。我对 React Router 3 的使用思维模式并没有很好地迁移到 v4。如果我想取得成功,我将不得不改变我对路由器和布局组件之间关系的思考方式。最终,新的模式出现了,对我来说很有意义,我对路由器的新的方向感到非常满意。React Router 4 让我能够做所有 v3 可以做到的事情,甚至更多。此外,起初我对 v4 的使用过于复杂。当我获得了新的思维模式后,我意识到这个新的方向太棒了!
我写这篇文章的用意不是要重新讲述 React Router 4 已经写得很好的文档。我将涵盖最常见的 API 概念,但重点是我已经发现有效的模式和策略。
React Router 5 现在已发布,它与 React Router 4 向后兼容。它主要有 错误修复和内部增强,使其更兼容 React 16。
以下是一些您需要熟悉才能阅读这篇文章的 JavaScript 概念
- React (无状态)函数式组件
- ES2015 箭头函数 及其“隐式返回”
- ES2015 解构
- ES2015 模板字面量
如果您喜欢直接跳到一个可工作的演示,那就看这里吧
新的 API 和新的思维模式
早期版本的 React Router 将路由规则集中到一个地方,将它们与布局组件分开。当然,路由器可以被分割并组织成多个文件,但在概念上,路由器是一个单元,基本上是一个功能强大的配置文件。
也许最好的方法是分别在每个版本中编写一个简单的两页应用程序并进行比较,以便了解 v4 与 v3 的不同之处。示例应用程序只有两个路由,用于首页和用户的页面。
这是 v3 中的代码
import { Router, Route, IndexRoute } from 'react-router'
const PrimaryLayout = props => (
<div className="primary-layout">
<header>
Our React Router 3 App
</header>
<main>
{props.children}
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<Router history={browserHistory}>
<Route path="/" component={PrimaryLayout}>
<IndexRoute component={HomePage} />
<Route path="/users" component={UsersPage} />
</Route>
</Router>
)
render(<App />, document.getElementById('root'))
以下是一些 v3 中的关键概念,在 v4 中不再适用
- 路由器被集中到一个地方。
- 布局和页面嵌套是由
<Route>
组件的嵌套推导出来的。 - 布局和页面组件完全不知道它们是路由器的一部分。
React Router 4 不再提倡使用集中的路由器。相反,路由规则存在于布局和 UI 本身中。例如,这是 v4 中同一个应用程序的代码
import { BrowserRouter, Route } from 'react-router-dom'
const PrimaryLayout = () => (
<div className="primary-layout">
<header>
Our React Router 4 App
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UsersPage} />
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<BrowserRouter>
<PrimaryLayout />
</BrowserRouter>
)
render(<App />, document.getElementById('root'))
新的 API 概念:由于我们的应用程序是为浏览器设计的,我们需要用 <BrowserRouter>
包裹它,它来自 v4。还要注意我们现在从 react-router-dom
中导入(这意味着我们使用 npm install react-router-dom
而不是 react-router
)。提示!它现在被称为 react-router-dom
,因为还有一个 原生版本。
使用 React Router v4 构建的应用程序,首先映入眼帘的是“路由器”似乎不见了。在 v3 中,路由器是一个巨大的东西,我们直接渲染到 DOM 中,它负责协调我们的应用程序。现在,除了 <BrowserRouter>
之外,我们首先扔到 DOM 中的是我们的应用程序本身。
v3 中的另一个主要部分,即 v4 示例中没有使用的是 {props.children}
来嵌套组件。这是因为在 v4 中,无论 <Route>
组件被编写在哪里,如果路由匹配,子组件都将在那里渲染。
包容性路由
在前面的示例中,您可能已经注意到 exact
属性。那么它到底是什么呢?v3 中的路由规则是“排他的”,这意味着只有一条路由会匹配。v4 中的路由默认是“包容性的”,这意味着多个 <Route>
可以同时匹配并渲染。
在前面的示例中,我们试图根据路径渲染 HomePage
或 UsersPage
。如果从示例中删除了 exact
属性,那么在浏览器中访问 `/users` 时,HomePage
和 UsersPage
组件都会同时渲染。
要更好地理解匹配逻辑,请查看 path-to-regexp,它现在被 v4 用来确定路由是否匹配 URL。
为了演示包容性路由如何有用,让我们在标题中包含一个 UserMenu
,但仅当我们位于应用程序的用户部分时才包含它
const PrimaryLayout = () => (
<div className="primary-layout">
<header>
Our React Router 4 App
<Route path="/users" component={UsersMenu} />
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UsersPage} />
</main>
</div>
)
现在,当用户访问 `/users` 时,两个组件都会渲染。v3 中也有一些模式可以实现类似的功能,但这更困难。由于 v4 的包容性路由,现在变得轻而易举。
排他性路由
如果您需要在一个组中只有一条路由匹配,请使用 <Switch>
来启用排他性路由
const PrimaryLayout = () => (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users/add" component={UserAddPage} />
<Route path="/users" component={UsersPage} />
<Redirect to="/" />
</Switch>
</main>
</div>
)
给定 <Switch>
中只有一条路由会渲染。但是,如果我们要将 HomePage
路由放在第一个,我们仍然需要在 HomePage
路由上使用 exact
。否则,当访问像 `/users` 或 `/users/add` 这样的路径时,首页路由也会匹配。事实上,在使用排他性路由策略时(就像使用传统路由器一直以来的情况一样),策略性放置是关键。请注意,我们将 /users/add
的路由策略性地放在 /users
之前,以确保正确的匹配。由于路径 /users/add
会匹配 `/users` 和 `/users/add`,因此将 /users/add
放在第一个是最好的。
当然,如果我们在某些情况下使用 exact
,我们可以将它们放在任何顺序,但至少我们有选择。
<Redirect>
组件在遇到时总是会执行浏览器重定向,但当它位于 <Switch>
语句中时,重定向组件只有在没有其他路由先匹配的情况下才会渲染。要了解 <Redirect>
如何在非 switch 环境中使用,请参阅下面的授权路由。
“索引路由”和“未找到”
虽然 v4 中不再有 <IndexRoute>
,但使用 <Route exact>
可以实现相同的效果。或者,如果没有任何路由解析,那么可以使用 <Switch>
和 <Redirect>
将其重定向到具有有效路径的默认页面(就像我在示例中使用 HomePage
一样),甚至重定向到一个未找到页面。
嵌套布局
您可能开始预测嵌套的子布局以及如何实现它们。我认为自己不会遇到这个概念的困难,但我确实遇到了。React Router v4 为我们提供了很多选择,这使得它非常强大。但是,选择意味着我们可以自由选择并非理想的策略。从表面上看,嵌套布局很简单,但根据您的选择,您可能会遇到摩擦,因为您的路由组织方式。
为了演示,让我们想象一下,我们想要扩展用户部分,以便我们有一个“浏览用户”页面和一个“用户资料”页面。我们也想要类似的产品页面。用户和产品都需要特殊的子布局,这些子布局对每个部分都是独特的。例如,每个部分可能具有不同的导航选项卡。有几种方法可以解决这个问题,一些方法很好,一些方法很差。第一种方法不是很好,但我希望向您展示,以免您陷入这种陷阱。第二种方法要好得多。
对于第一个,让我们修改我们的PrimaryLayout
,以适应用户和产品的浏览和资料页面
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users" exact component={BrowseUsersPage} />
<Route path="/users/:userId" component={UserProfilePage} />
<Route path="/products" exact component={BrowseProductsPage} />
<Route path="/products/:productId" component={ProductProfilePage} />
<Redirect to="/" />
</Switch>
</main>
</div>
)
}
虽然这在技术上是可行的,但仔细看看这两个用户页面,就会发现问题所在
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>
)
新的 API 概念:props.match
被提供给由<Route>
渲染的任何组件。正如您所看到的,userId
由props.match.params
提供。在v4 文档中查看更多信息。或者,如果任何组件需要访问props.match
,但该组件不是由<Route>
直接渲染的,我们可以使用withRouter() 高阶组件。
每个用户页面不仅渲染其各自的内容,而且还必须关心子布局本身(并且子布局对每个页面都重复)。虽然这个例子很小,可能看起来微不足道,但在实际应用中,重复的代码会成为问题。更不用说,每次BrowseUsersPage
或UserProfilePage
被渲染时,它都会创建一个新的UserNav
实例,这意味着它的所有生命周期方法都将重新开始。如果导航选项卡需要初始网络流量,这会导致不必要的请求——这一切都是因为我们如何决定使用路由器。
以下是一种更好的方法
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UserSubLayout} />
<Route path="/products" component={ProductSubLayout} />
<Redirect to="/" />
</Switch>
</main>
</div>
)
}
我们没有为用户和产品的每个页面设置四个路由,而是为每个部分的布局设置了两个路由。
请注意,上面的路由不再使用exact
属性,因为我们希望/users
匹配以/users
开头的任何路由,对于产品也是如此。
通过这种策略,子布局的任务就变成了渲染额外的路由。以下是UserSubLayout
可能的样子
const UserSubLayout = () => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path="/users" exact component={BrowseUsersPage} />
<Route path="/users/:userId" component={UserProfilePage} />
</Switch>
</div>
</div>
)
新策略中最明显的优势是布局不会在所有用户页面之间重复。这也是双赢的,因为它不会像第一个例子那样遇到相同生命周期问题。
需要注意的是,即使我们在布局结构中嵌套很深,路由仍然需要识别其完整路径才能匹配。为了节省您的重复输入(如果您决定将“users”更改为其他内容),请使用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
以及编写路由非常有用。match
对象为我们提供了几个属性,包括match.params
、match.path
、match.url
和更多属性。
match.path vs match.url
这两个之间的区别乍一看可能不清楚。在控制台中记录它们有时会显示相同的输出,使它们的差异更加不清楚。例如,当浏览器路径为/users
时,这两个控制台日志将输出相同的值
const UserSubLayout = ({ match }) => {
console.log(match.url) // output: "/users"
console.log(match.path) // output: "/users"
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>
)
}
ES2015 概念:match
在组件函数的参数级别被解构。这意味着我们可以键入match.path
而不是props.match.path
。
虽然我们还看不到区别,但match.url
是浏览器 URL 中的实际路径,而match.path
是为路由器编写的路径。这就是它们相同的原因,至少到目前为止是这样。但是,如果我们在UserProfilePage
中更深一层执行相同的控制台日志并访问浏览器中的/users/5
,match.url
将为"/users/5"
,而match.path
将为"/users/:userId"
。
选择哪个?
如果您要使用其中一个来帮助构建路由路径,我建议您选择match.path
。使用match.url
来构建路由路径最终会导致您不希望出现的场景。以下是我遇到的一个场景。在一个像UserProfilePage
这样的组件(当用户访问/users/5
时渲染)中,我渲染了如下子组件
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>
)
为了说明问题,我渲染了两个子组件,一个路由路径来自match.url
,另一个来自match.path
。以下是访问浏览器中的这些页面时会发生的事情
- 访问
/users/5/comments
将渲染“UserId: undefined”。 - 访问
/users/5/settings
将渲染“UserId: 5”。
那么为什么match.path
可以帮助我们构建路径,而match.url
不行呢?答案在于{${match.url}/comments}
基本上与我硬编码的{'/users/5/comments'}
相同。这样做意味着后续组件将无法正确填充match.params
,因为路径中没有参数,只有硬编码的5
。
直到后来我才看到文档的这一部分,才意识到它有多重要
匹配
- path – (string) 用于匹配的路径模式。对于构建嵌套的
<Route>
很有用- url – (string) URL 中匹配的部分。对于构建嵌套的
<Link>
很有用
避免匹配冲突
让我们假设我们正在制作的应用程序是一个仪表盘,因此我们希望能够通过访问/users/add
和/users/5/edit
来添加和编辑用户。但是,在前面的示例中,users/:userId
已经指向一个UserProfilePage
。那么,这意味着具有users/:userId
的路由现在需要指向另一个子子布局来容纳编辑和资料吗?我不这么认为。由于编辑页面和资料页面共享相同的用户子布局,因此这种策略效果很好
const UserSubLayout = ({ match }) => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route exact path={props.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>
)
请注意,添加和编辑路由策略性地位于资料路由之前,以确保正确匹配。如果资料路径位于第一个,访问/users/add
将匹配资料(因为“add”将匹配:userId
)。
或者,如果我们将资料路由放在第一个,并将路径设置为${match.path}/:userId(\\d+)
,这将确保:userId
必须是一个数字。那么访问/users/add
就不会产生冲突。我在path-to-regexp的文档中了解到这个技巧。
授权路由
在应用程序中,根据用户的登录状态限制用户访问某些路由的能力非常普遍。另一个常见的做法是为未经授权的页面(如“登录”和“忘记密码”)提供与已授权页面(应用程序的主要部分)不同的“外观和感觉”。为了解决这些需求,请考虑此应用程序的主要入口点
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>
)
}
}
使用react-redux与 React Router v4 的工作方式与之前非常相似,只需将<BrowserRouter>
包装在<Provider>
中,一切就都设置好了。
这种方法有一些要点。首先,我根据应用程序的哪个部分来选择两个顶级布局。访问像/auth/login
或/auth/forgot-password
这样的路径将使用UnauthorizedLayout
——一个适合这些上下文的外观。当用户登录后,我们将确保所有路径都有一个/app
前缀,该前缀使用AuthorizedRoute
来确定用户是否登录。如果用户尝试访问以/app
开头的页面,但他们没有登录,他们将被重定向到登录页面。
AuthorizedRoute
不是 v4 的一部分。我在v4 文档的帮助下自己创建了它。v4 中一个很棒的新功能是能够创建自己的路由以用于特殊目的。不是将component
属性传递给<Route>
,而是传递render
回调
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)
虽然您的登录策略可能与我的不同,但我使用网络请求来getLoggedUser()
并将pending
和logged
插入 Redux 状态。pending
只是意味着请求仍在路由中。
单击此处查看CodePen 上的完整工作身份验证示例。

其他提及
React Router v4 还有很多其他很酷的功能。不过,为了总结,让我们确保提及一些小细节,以免它们让您措手不及。
<Link> vs <NavLink>
在 v4 中,有两种方法可以将锚点标签与路由器集成:<Link>
和 <NavLink>
<NavLink>
的工作方式与 <Link>
相同,但根据 <NavLink>
是否与浏览器的 URL 匹配,它提供了一些额外的样式功能。例如,在 示例应用程序 中,有一个名为 <PrimaryHeader>
的组件,看起来像这样
const PrimaryHeader = () => (
<header className="primary-header">
<h1>Welcome to our app!</h1>
<nav>
<NavLink to="/app" exact activeClassName="active">Home</NavLink>
<NavLink to="/app/users" activeClassName="active">Users</NavLink>
<NavLink to="/app/products" activeClassName="active">Products</NavLink>
</nav>
</header>
)
使用 <NavLink>
使我能够将 active
类设置为任何活动的链接。但是,还要注意,我也可以在这些链接上使用 exact
。如果不用 exact
,当访问 `/app/users` 时,主页链接将会处于活动状态,因为 v4 的匹配策略是包含式的。根据我的个人经验,<NavLink>
与 exact
选项组合起来,比 v3 中的 <Link>
等效项更稳定。
URL 查询字符串
在 React Router v4 中,不再有从 URL 获取查询字符串的方法。在我看来,做出这个决定 的原因是,没有关于如何处理复杂查询字符串的标准。因此,v4 没有在模块中加入自己的看法,而是选择让开发人员自己决定如何处理查询字符串。这是一个好事情。
我个人使用的是 query-string,它是由一直很出色的 sindresorhus 开发的。
动态路由
v4 最棒的特性之一是,几乎所有内容(包括 <Route>
)都只是一个 React 组件。路由不再是神奇的东西。我们可以在任何时候有条件地渲染它们。想象一下,当满足某些条件时,你的应用程序的整个部分都可供路由。当这些条件不满足时,我们可以移除路由。我们甚至可以做一些非常酷的 递归路由 东西。
React Router 4 更加简单,因为它 Just Components™
这篇文章很棒,我一直难以理解 v4 的新方法。我非常喜欢这些更改,它们基本上将消除像这样的操作的必要性
迫不及待地想尝试一下。感谢 Brad。
有一件事我仍然不太确定如何在 React Router v4 中做,那就是如何在非 React 组件中以编程方式重定向到新的路由。也许我只是没有用 v4 的正确方式来思考。
举个例子,如何根据 redux 状态的变化重定向到经过身份验证的路由。或者如何确保用户在未经身份验证的情况下被重定向到登录页面,但在身份验证后返回到之前的页面。
你可以直接使用 history api 来做吗?
我最近开始使用 React Router 4,一开始这就是我最困惑的地方。据我所知,有两种方法。最正常,也可能是最简单的方法,就像 Brad 说的那样
还有一种新的 HOC 叫做 。要以编程方式使用它,你需要根据状态条件显示它。类似于
当然,你也可以将重定向状态存储在 Redux 中,如果你愿意的话……
在我看来,第二种方法设置起来有点麻烦,但它允许你添加一个状态对象,你可以像查询字符串一样使用它(RR4 不会保留查询字符串)。
我遇到的主要问题是,组件必须是 Route 的子级,或者你必须使用 withRouter HOC 来包装它。对我来说,我无法从我的头部或底部重定向……它们没有嵌套在任何路由中……直到我弄明白了这一点。
看起来有点像
喜欢这篇文章,尤其是它专注于将 aside 放在这些小“嘿”部分内。
嗨,
我正在使用 react-router v4,但我有一个用例,我无法以优雅的方式解决。
想象一下,一个路由渲染 app.js 组件。
在这个组件中,我有一个头部、一个底部和一个主要组件。主要组件渲染一个带有 switch 的路由列表。到目前为止,一切都工作正常,但我需要在头部和头部信息中获取传递给我的子 switch 路由的匹配参数。因为那里有项目 ID。
现在我使用 matchpath 来检查所有路由并获取匹配项。
在 v3 中,父路由知道关于后代的参数,但在 v4 中则不知道。
你知道如何解决这个用例吗?
谢谢
我试图理解它,但不太确定自己是否完全理解。我知道这可能是一个复杂的情况,所以用 codepen 来演示并不容易。但我认为我可能找到了解决方案。听起来头部没有被路由渲染,所以,如你所知,只有被路由渲染的组件才具有
match
之类的道具。需要从 v4 中访问match
、history
等道具的组件,如果它们没有被路由渲染,可以使用withRouter
高阶组件,如下所示很棒的文章,老兄。很喜欢。谢谢。:)
非常感谢,太棒了
另一篇很棒的文章。感谢 Brad。
到目前为止我见过的最好的路由教程!
很棒的文章,但你没有提到
onEnter
、onChange
和onLeave
钩子的移除。它基本上让开发者在使用函数式组件而不是带有componentWillMount
等的经典状态化组件时,无法在进入前显示免责声明或其他东西。<Prompt />
组件的加入有助于解决这个问题,但它无法真正进行样式化,只能显示警报。很多负面评价都来自于这样一个事实,即这个弃用导致很多人的网站崩溃,而且没有简单的修复方法。
是的,所以某些东西不见了。我不知道所有用例,但你提到的那个(在进入前同意)可能可以通过创建你自己的“路由包装器”组件来解决。很抱歉今天时间不多,但这里有一个我创建身份验证包装器的例子。
此路由现在可以用作
<AuthorizedRoute path="..." component="..."/>
,它充当一个条件,用于确定你是否被允许进入该组件。看起来你可能可以修改这种概念,以满足你的需求?并且,是的,v4 会破坏试图以 v3 方式执行操作的代码。我不知道你提到的确切的负面评价,但它是一个主要的版本变更,这意味着“破坏性变更”。所以我认为开发者需要意识到这一点。
这是来自 Gilly Ames,在比赛结束的哨声响起之后