React-Router v4简单入门教程

原文翻译自 a-simple-react-router-v4-tutorial,需要科学上网,译文有部分修改
原文作者在CodeSandbox上面上传了该教程的Demo,可直接访问,无需科学上网。


React-Router-v4是一个流行的纯React重写的包。 以前版本的React Router使用路由配置,并且难以理解。现在的第4版本中已经不需要路由配置,一切都是组件化。
本教程将为本地的足球队搭建一个网站,我们会学习React-Router-v4的基本知识来为我们的网站搭建一个路由,这将包括:选择路由,创建路由,使用Link来进行路由导航。

安装

React Router已被拆分成三个包:react-router,react-router-domreact-router-native。你不需要直接安装react-router,react-router包提供核心的路由组件与函数。其余两个提供运行环境(浏览器与react-native)所需的特定组件,但是他们都暴露出react-router中暴露的对象与方法。你应该为你的环境选择正确的包。我们进行的是网站(将会运行在浏览器)构建,所以应安装react-router-dom
npm install -save react-router-dom

Router

当进行新项目时,你需要选择使用哪种路由。对于浏览器项目(网站),react-router4提供了<BrowserRouter>和<HashRouter>两个组件。前者在你有服务器处理动态请求的时候使用,后者在静态网站的时候使用。
通常我们选择使用<BrowserRouter>,但是如果你的网站将运行在只有静态文件的服务器上,<HashRouter>是一个不错的方案。
对于我们的项目,我们假设我们的网站将架设在动态服务器上,所以我们选择的路由组件是<BrowserRouter>

History

每种路由都会创建history对象,用来追踪当前的location[1]
并在网站变化的时候重新渲染。React Router提供的其它的组件都依赖于上下文中的history对象,所以必须在组件内部渲染。
一个React Router组件如果没有父级组件将不会工作。

渲染一个<Router>

由于Router组件只能接收一个子元素,因为这个限制,我们创建一个<App>组件来渲染我们的应用(从路由中分离应用对于服务器端渲染也是很重要的,因为在服务器端渲染,切换到<MemoryRouter>的时候可以复用<App>)

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

接下来为我们可以渲染我们的应用了

<App>

我们的应用写在<App>组件中。为了简化,我们将分离我们的应用到两部分,<Header>组件将包含网站的导航链接,<Main>组件用于渲染剩下的内容

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

注:你可以根据自己来对应用进行分割,但是本教程分离路由和导航将更容易讲解react router怎么工作

我们将从<Main>组件开始

路由 Route

<Route>组件是React Router的主要构件模块,任何你想渲染的与pathname匹配的地方,你都应该创建一个<Route>元素。

路径 Path

一个<Route>需要一个path的字符串参数来描述路由匹配的路径名类型,例如,<Route path ='/roster' />应该匹配以/roster开头的路径名[2]。 当前位置的路径与path匹配时,路由将渲染一个React元素。 当路径不匹配时,路由将不会渲染任何东西[3]。

<Route path='/roster' />
// 当前路径是   '/', 不满足匹配
// 当前路径是   '/roster' 或 '/roster/2', 满足匹配
// 如果只想匹配 '/roster', 需要使用"exact"参数
// 下面的会匹配 '/roster', 但是不会匹配'/roster/2'.

<Route exact path='/roster'/>
// 你可能会发现你会为许多Route添加"exact"参数
// 在未来的版本里(i.e. v5), "exact"参数可能会成为默认值
// 更多信息请看 GitHub issue:
// https://github.com/ReactTraining/react-router/issues/4958

注意:在匹配路由时,React Router只关心位置的路径名(pathname)。 这意味着给定URL:
http://www.example.com/my-projects/one?extra=false
React Router尝试匹配的唯一部分是/my-projects/one。

匹配路径

path-to-regexp包用于确定路径元素的path参数是否与当前位置匹配。它将path字符串编译为一个正则表达式,该正则表达式将与该位置的路径名相匹配。path字符串具有比此处所述更高级的格式选项。你可以在正则表达式文档路径中阅读它们。
当路径的路径匹配时,将创建一个具有以下属性的匹配对象:

  • url - 当前位置路径名的匹配部分
  • path  - 路由的路径
  • isExact - path是否等于pathname
  • params - 一个包含path-to-regexp捕获的路径名值的对象

你可以使用此路由测试工具来查找与URL匹配的路由。
注意:目前,路由的路径必须是绝对路径[4]。

创建我们的路由

可以在路由内部的任何位置创建<Route>,但是通常在同一地方渲染它们。你可以使用<Switch>组件来包裹一组<Route>。 <Switch>将迭代其子元素(路由),并仅渲染与当前路径名匹配的第一个元素。
对于这个网站,我们想要匹配的路径是:

  • / - 主页
  • /roster - 球队的名单
  • /roster/:number - 球员的个人资料,使用球员的号码
  • /schedule - 球队的比赛时间表

为了在我们的应用程序中匹配一个路径,我们所要做的就是创建一个<Route>元素和我们想要匹配的路径。

<Switch>
  <Route exact path='/' component={Home}/>
  {/* /roster 和 /roster/:number 都是以 /roster 开头 */}
  <Route path='/roster' component={Roster}/>
  <Route path='/schedule' component={Schedule}/>
</Switch>

<Route>怎么渲染?

当path匹配时,<Route>有三个参数可用于定义什么该被渲染。但是只能提供一个给<Route>元素。

  • component  —  一个React组件。当带有component 参数的路由匹配时,路由将返回一个新元素,其类型是一个React component (使用React.createElement创建)。
  • render  —  一个返回React元素的函数[5]。它将在path匹配时被调用。这与component类似,但对于 内联渲染 和 更多参数传递 很有用。
  • children  — 一个返回React元素的函数。与之前的两个参数不同,无论路由的路径是否与当前位置相匹配,都将始终被渲染。
//component
<Route path='/page' component={Page} /> 

const extraProps = { color: 'red' }

//render
<Route path='/page' render={(props) => (  
  <Page {...props} data={extraProps}/>
)}/>

//children
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

通常,我们会选择conmponent或render。children参数偶尔会用上,但通常最好在路径不匹配时不渲染任何东西。在这里我们没有任何额外的参数传递给组件,因此我们的每个<Route>将使用component参数。
由<Route>渲染的元素将传递多个参数。这些将是match对象,location对象[6]和history对象(由我们的路由创建的对象)[7]。

<Main>

现在我们已经确定了我们的根路由结构,我们只需要实际渲染我们的路由。对于这个应用程序,我们将在<Main>组件内渲染我们的<Switch>和<Route> ,把它俩放置在<main> DOM节点内。

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>
)

注意:主页的Route包含"exact"参数。这用于说明只有当路径名与路径路径完全匹配时,该路由才应匹配。

嵌套路由

球员个人资料路由 /roster/:number 不包含在上面的<Switch>中,它将由<Roster>组件渲染,每当路径名以/roster开头时就会渲染该组件。
在<Roster>组件中,我们将渲染两种路由:

  1. /roster - 只有当路径名称刚好是/roster时,才应该匹配,所以我们也应该给那个路由元素添加exact参数。
  2. /roster/:number - 此路径使用路径参数来捕获/roster之后的部分路径名。
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>
)

路径参数

有时,我们想要获得路径中的变量。例如,球员资料的路径,我们想要获得球员的号码。我们可以通过将路径参数添加到<Route>的 path参数 中来完成。

路径/roster/:number中的:number部分意味着/roster/后面的路径名部分将被捕获并存储为match.params.number。

例如,路径 /roster/6 将生成一个params对象:{ number: '6'}
<Player>组件可以使用props.match.params对象来确定应该渲染哪个Player的数据。

// 假设PlayerAPI是一个返回player对象的API
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
  const player = PlayerAPI.get(
    parseInt(props.match.params.number, 10)
  )
  //未找到球员信息
  if (!player) {
    return <div>球员信息没有找到</div>
  }
  return (
    <div>
      <h1>{player.name} (#{player.number})</h1>
      <h2>{player.position}</h2>
    </div>
)

你可以在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

最后,我们的应用程序需要在页面间导航。如果我们要使用锚点元素创建链接,点击它们会导致整个页面重新加载。 React Router提供了一个<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>使用to参数来描述他们应该导航到的位置。这可以是字符串或位置对象(包含路径名,search,hash和state的组合)。当它是一个字符串时,它将被转换为location对象。
<Link to={{ pathname: '/roster/7' }}>Player #7</Link>

结束

希望在这一点上,您已准备好开始构建自己的网站。

我们已经介绍了构建网站所需的最基本组件(<BrowserRouter>,<Route>和<Link>)。尽管如此,还有其他一些组件没有涵盖(以及组件的参数)。幸运的是,React Router还有文档网站,您可以使用它来查找有关其组件的更深入的信息。该网站还提供了许多有关源代码的工作示例。


注:

[1] locations是一个描述URL不同部分的对象:{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }
[2] 你可以渲染一个无路径的<Route>,它将匹配所有路径,这对于获取存储在上下文中的方法和变量很有用。
[3] 如果你使用children参数,不管当前路径是否匹配Route的path该Route都会渲染。
[4] 当需要支持相对路径的<Route>和<Link>时,你需要做更多事,相对路径的<Link>比看起来的要复杂,因为他们使用父级的match对象,而不是当前的URL。
[5] 这实际上是一个无状态的函数组件。从内部看,传递给组件和渲染的组件之间的巨大差异在于该组件将使用React.createElement创建该元素,而渲染将作为函数调用该组件。 如果你要定义一个内联函数并将其传递给组件prop,它将比使用渲染参数慢得多。

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

[6] <Route> 和 <Switch> 组件都可以带一个location参数,这将允许他们匹配上不同于实际地址的地址。
[7] 它们还传递一个staticContext参数,但它只在服务端渲染才有用。

推荐阅读更多精彩内容