fi3ework / blog

📝
861 stars 51 forks source link

[译]简明React Router v4教程 #10

Open fi3ework opened 6 years ago

fi3ework commented 6 years ago

原文地址:A Simple React Router v4 Tutorial

React Router v4 是一个完全使用 React 重写的流行的 React 包,之前版本的 React Router 版本配置是使用伪组件也很晦涩难懂。现在 v4 版本的 React Router,所有的东西都 “仅仅是组件”。

在这个教程中,我们将建立一个本地的 "运动队" 页面,我们将完成所有的基本需求来建立我们的网站和路由,这包括:

  1. 选择 router
  2. 创建 routes
  3. 在路由之间通过链接进行导航。

代码

Edit A Simple React Router v4 Tutorial

安装

React Router 现在已经被划分成了三个包:react-routerreact-router-domreact-router-native

你不应该直接安装 react-router,这个包为 React Router 应用提供了核心的路由组件和函数,另外两个包提供了特定环境的组件(浏览器和 react-native 对应的平台),不过他们也是将 react-router 导出的模块再次导出。

你应该选择这两个中适应你开发环境的包,我们需要构建一个网站(在浏览器中运行),所以我们要安装 react-router-dom

npm install --save react-router-dom

Router

当开始一个新项目时,你应该决定要使用哪种 router。对于在浏览器中运行的项目,我们可以选择 <BrowserRouter<HashRouter> 组件,<BrowserRouter> 应该用在服务器处理动态请求的项目中(知道如何处理任意的URI),<HashRouter> 用来处理静态页面(只能响应请求已知文件的请求)。

通常来说更推荐使用 <BrowserRouter>,可是如果服务器只处理静态页面的请求,那么使用 <HashRouter> 也是一个足够的解决方案。

对于我们的项目,我们假设所有的页面都是由服务器动态生成的,所以我们的 router 组件选择 <BrowserRouter>

History

每个 router 都会创建一个 history 对象,用来保持对当前位置[1]的追踪还有在页面发生变化的时候重新渲染页面。React Router 提供的其他组件依赖在 context 上储存的 history 对象,所以他们必须在 router 对象的内部渲染。一个没有 router 祖先元素的 React Router 对象将无法正常工作,如果你想学习更多的关于 history 对象的知识,可以参照 这篇文章

渲染一个 <Router>

Router 的组件只能接受一个子元素,为了遵照这种限制,创建一个 <App> 组件来渲染其他的应用将非常方便(将应用从 router 中分离对服务器端渲染也有重要意义,因为我们在服务器端转换到 <MemoryRouter> 时可以很快复用 <App>

import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'))

现在我们已经选择了 router,我们可以开始渲染我们真正的应用了。

<App>

我们的应用定义在 <App> 组件中,为了简化 <App>,我们将我们的应用分为两个部分,<Header> 组件包含链接到其他页面的导航,<Main> 组件包含其余的需要渲染的部分。

// this component will be rendered by our <___Router>
const App = () => (
  <div>
    <Header />
    <Main />
  </div>
)

Note: 你可以任意布局你的应用,分离 routes 和导航让你更加容易了解 React Router 是如何工作的。

我们先从渲染我们路由内容的 <Main> 组件开始。

Routes

<Route> 组件是 React Router 的主要组成部分,如果你想要在路径符合的时候在任何地方渲染什么东西,你就应该创造一个 <Route> 元素。

Path

一个 <Route> 组件需要一个 string 类型的 path prop 来指定路由需要匹配的路径。举例来说,<Route path='/roster/' 将匹配以 /roster [2] 开始的路径,当当前的路径和 path 匹配时,route 将会渲染对应的 React 元素。当路径不匹配的时候 ,路由不会渲染任何元素 [3]。

<Route path='/roster'/>
// when the pathname is '/', the path does not match
// when the pathname is '/roster' or '/roster/2', the path matches
// If you only want to match '/roster', then you need to use
// the "exact" prop. The following will match '/roster', but not
// '/roster/2'.
<Route exact path='/roster'/>
// You might find yourself adding the exact prop to most routes.
// In the future (i.e. v5), the exac t prop will likely be true by
// default. For more information on that, you can check out this 
// GitHub issue:
// https://github.com/ReactTraining/react-router/issues/4958

Note: 在匹配路由的时候,React Router 只会关心相对路径的部分,所以如下的 URL

http://www.example.com/my-projects/one?extra=false

React Router 只会尝试匹配 /my-projects/one

匹配路径

React Router使用 path-to-regexp 包来判断路径的 path prop 是否匹配当前路径,它将 path 字符串转换成正则表达式与当前的路径进行匹配,关于 path 字符串更多的可选格式,可以查阅 path-to-regexp 文档

当路由与路径匹配的时候,一个具有以下属性的 match 对象将会被作为 prop 传入

Note: 目前,路由的路径必须是绝对路径 [4]。

创建我们自己的路由

<Route>s 可以在router中的任意位置被创建,不过一般来说将他们放到同一个地方渲染更加合理,你可以使用 <Switch> 组件来组合 <Route>s,<Switch>将遍历它的 children 元素(路由),然后只匹配第一个符合的 pathname

对于我们的网站来说,我们想要匹配的路径为:

  1. / - 主页
  2. /roster - 队伍名单
  3. /roster/:number - 队员的资料,使用球员的球衣号码来区分
  4. /schedule - 队伍的赛程表

为了匹配路径,我们需要创建带 path prop的 <Route> 元素

<Switch>
  <Route exact path='/' component={Home}/>
  {/* both /roster and /roster/:number begin with /roster */}
  <Route path='/roster' component={Roster}/>
  <Route path='/schedule' component={Schedule}/>
</Switch>

<Route> 将会渲染什么

Routes 可以接受三种 prop 来决定路径匹配时渲染的元素,只能给 <Route> 元素提供一种来定义要渲染的内容。

  1. <component> - 一个 React 组件,当一个带有 component prop 的路由匹配的时候,路由将会返回 prop 提供的 component 类型的组件(通过 React.createElement 渲染)。
  2. render - 一个返回 React 元素 [5] 的方法,与 component 类似,也是当路径匹配的时候会被调用。写成内联形式渲染和传递参数的时候非常方便。
  3. children - 一个返回 React 元素的方法。与前两种不同的是,这种方法总是会被渲染,无论路由与当前的路径是否匹配。
<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
  <Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

一般来说,我们一般使用 component 或者 renderchildren 的使用场景不多,而且一般来说当路由不匹配的时候最好不要渲染任何东西。在我们的例子中,不需要向路由传递任何参数,所有我们使用 <component>

<Route> 渲染的元素将会带有一系列的 props,有 match 对象,当前的 location 对象 [6],还有 history 对象(由 router 创建)[7]。

<Main>

现在我们已经确定了 route 的结构,我们只需要将他们实现即可。在我们的应用中,我们将会在 <Main> 组件中渲染 <Switch><Route>,它们将会在 <main> 中渲染 HTML 元素。

import { Switch, Route } from 'react-router-dom'
const Main = () => (
  <main>
    <Switch>
      <Route exact path='/' component={Home}/>
      <Route path='/roster' component={Roster}/>
      <Route path='/schedule' component={Schedule}/>
    </Switch>
  </main>
)

Note: 主页的路由带有 exact prop,这表明只有路由的 path 完全匹配 pathname 的时候才会匹配主页。

路由的嵌套

队员资料页的路由 /roster/:number 是在 <Roster> 组件而没有包含在 <Switch> 中。但是,只要 pathname 由 /roster 开头,它就会被 <Roster> 组件渲染。

<Roster> 组件中我们将渲染两种路径:

  1. /roster - 只有当路径完全匹配 /roster 时会被渲染,我们要对该路径指定 exact 参数。
  2. /roster/:number - 这个路由使用一个路径参数来捕获 /roster 后面带的 pathname 的部分。
const Roster = () => (
  <Switch>
    <Route exact path='/roster' component={FullRoster}/>
    <Route path='/roster/:number' component={Player}/>
  </Switch>
)

将带有相同前缀的路由放在同一个组件中很方便,这样可以简化父组件并且让我们可以让我们在一个地方渲染所有带有相同前缀的组件。

举个例子,<Roster> 可以为所有以 /roster 开头的路由渲染一个标题

const Roster = () => (
  <div>
    <h2>This is a roster page!</h2>
    <Switch>
      <Route exact path='/roster' component={FullRoster}/>
      <Route path='/roster/:number' component={Player}/>
    </Switch>
  </div>
)

Path 参数

有的时候我们想捕捉 pathname 中的多个参数,举例来说,在我们的球员资料路由中,我们可以通过向路由的 path 添加路径参数来捕获球员的号码。

:number 部分代表在pathname中 /roster/ 后面的内容将会被储存在 match.params.number。举例来说,一个为 /roster/6 的 pathname 将会生成一个如下的params 对象。

{ number: '6' } // note that the captured value is a string

<Player>组件使用 props.match.params 对象来决定应该渲染哪个球员的资料。

// an API that returns a player object
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
  const player = PlayerAPI.get(
    parseInt(props.match.params.number, 10)
  )
  if (!player) {
    return <div>Sorry, but the player was not found</div>
  }
  return (
    <div>
      <h1>{player.name} (#{player.number})</h1>
      <h2>{player.position}</h2>
    </div>
)

关于 path 参数可以查阅 path-to-regexp 文档

紧挨着 <Player>,还有一个 <FullRoster><Schedule><Home> 组件。

const FullRoster = () => (
  <div>
    <ul>
      {
        PlayerAPI.all().map(p => (
          <li key={p.number}>
            <Link to={`/roster/${p.number}`}>{p.name}</Link>
          </li>
        ))
      }
    </ul>
  </div>
)
const Schedule = () => (
  <div>
    <ul>
      <li>6/5 @ Evergreens</li>
      <li>6/8 vs Kickers</li>
      <li>6/14 @ United</li>
    </ul>
  </div>
)
const Home = () => (
  <div>
    <h1>Welcome to the Tornadoes Website!</h1>
  </div>
)

Links

最后,我们的网站需要在页面之间导航,如果我们使用 <a> 标签导航的话,将会载入一整个新的页面。React Router 提供了一个 <Link> 组件来避免这种情况,当点击 <Link> 时,URL 将会更新,页面也会在不载入整个新页面的情况下渲染内容。

import { Link } from 'react-router-dom'
const Header = () => (
  <header>
    <nav>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/roster'>Roster</Link></li>
        <li><Link to='/schedule'>Schedule</Link></li>
      </ul>
    </nav>
  </header>
)

<Link>s 使用 to prop 来决定导航的目标,可以是一个字符串,或者是一个 location 对象(包含 pathname, search, hashstate 属性)。当只是一个字符串的时候,将会被转化为一个 location 对象

<Link to={{ pathname: '/roster/7' }}>Player #7</Link>

Note: 目前,链接的 pathname 必须是绝对路径。

例子

两个在线的例子:

  1. CodeSandbox
  2. CodePen.

Notes!

[1] locations 是包含描述 URL 不同部分的参数的对象

// a basic location object
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }

[2] 可以一个无路径的 <Route>,这个路由将会匹配所有路径,这样可以很方便的访问存储在 context 上的对象和方法。

[3] 当使用 children prop 时,即使在路径不匹配的时候也会渲染。

[4] 让 <Route>s 和 <Link>s 接受相对路径的工作还未完成,相对的 <Link>s 比看上去要复杂的多,因为它们需要父组件的 match 对象来工作,而不是当前的 URL。

[5] 这是个基本的无状态组件,componentrender 的区别是,component 会使用 React.createElement 来创建一个元素,render 使用将组件视作一个函数。如果你想创建一个内联函数并传递给 component,那么 render 会比 component 来的快得多。

<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()

[6] <Route><Switch> 组件都可以接受一个 location prop,这可以让他们被一个不同的 location 匹配到,而不仅仅是他们实际的 location(当前的 URL)。

[7] props 也可以传递 staticContext 这个 prop,但是只在使用服务端渲染的时候有效。