w3ctech

React 初学者教程 12:在 React 中用 React Router 创建单页应用

概述:通过学习如何使用 React Router 来创建一个单页应用,来提升 React 技能。

现在我们已经熟悉了 React 的基础知识,该提升几个档次了。下面我们要用 React 创建一个单页应用程序(即 SPA)。如同我们在 React 介绍中所言,单页应用程序与传统的多页应用程序是不同的。最大的不同之处在于在单页应用程序中导航是不会进入到一个完全新的页面。单页应用程序中的页面(通常称为视图)通常是在相同页面本身内联加载。

当内联加载内容时,要稍微复杂一些。最难的部分不是加载内容本身,加载内容本身相对较为容易。最难的部分是确保单页应用的行为与用户习惯的方式一致。当用户在应用程序中导航时,他们期望:

  1. 显示在地址栏的 URL 总是反映用户正在浏览的事情。
  2. 用户可以成功地使用浏览器的后退和向前按钮。
  3. 用户可以使用合适的 URL 直接导航到特定的视图(即深度链接,deep link)。

在多页应用中,这三件事情是自然而然的,不需要为这些事情费神。而在单页应用中,因为不能导航到一个全新的页面,我们就必须为处理这三件用户期望的事情做一些实际工作。我们需要确保在应用程序内导航时恰当地调节 URL。需要确保浏览器的历史记录与每次导航同步,从而允许用户使用后退和前进按钮。如果用户把一个特定视图添加到书签,或者复制/粘贴一个 URL, 以便随后访问,我们需要确保我们的单页应用将用户带到正确的地方。

要处理所有这些事情,我们有一揽子称为路由(Routing)的技术。路由是映射 URL 到并非物理页面的目标,比如单页应用中的单个视图。这听起来很复杂,但是幸运的是有很多 JavaScript 库可以帮助我们实现。这其中一个 JavaScript 库, React Router, 是本教程的明星。React Router 为由 React 创建的单页应用提供了路由能力,它以在 React 中已经熟悉的方式扩展,给我们很棒的路由功能。

示例

我们来看如下示例:

这是一个简单的 React 应用,该应用用 React Router 来提供所有的导航和视图加载。点击不同的链接,可以加载相关的内容,随便在浏览器窗口中打开这个页面,用后退和前进按钮来看看。

在下面的小节中,我们会逐步创建这个应用。

创建应用

我们要做的第一件事情是让应用可以运行的模板标记和代码。创建一个新 HTML 文档,添加如下内容:

<!DOCTYPE html>
<html>

<head>
  <title>React! React! React!</title>
  <script src="https://fb.me/react-15.0.0-rc.2.js"></script>
  <script src="https://fb.me/react-dom-15.0.0-rc.2.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>

  <style>

  </style>
</head>

<body>

  <div id="container">

  </div>

  <script type="text/babel">
    var destination = document.querySelector("#container");

    ReactDOM.render(
      <div>
        Hello!
      </div>,
      destination
    );
  </script>
</body>

</html>

初始代码跟我们所看到的所有其它示例基本相同。这只是一个空的应用,用来加载 React 和 React-DOM 库。如果在浏览器中预览,会看到浏览器中显示一个孤单的 Hello!。

依然让一切保持简单 目前,我们还是继续让浏览器做所有繁重的工作。后面我们会用一个“时髦的”构建过程来处理,所以现在就好好享受简化的乐趣吧。

下一步,因为 React Router 不是 React 本身的一部分,我们需要添加对它的引用。在标记中,找到已有的 script 引用,添加最后一行:

<script src="https://fb.me/react-15.0.0-rc.2.js"></script>
<script src="https://fb.me/react-dom-15.0.0-rc.2.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://npmcdn.com/react-router@2.4.0/umd/ReactRouter.min.js"></script>

通过添加最后一行,就能让 React Router 库与核心 React、ReactDOM 以及 Babel 库一起被加载。此时,我们就准备好爱是创建应用,并利用 React Router 带来的甜蜜功能了。

显示初始帧

当创建单页应用时,总会有一部分页面是保持静态的。这个静态的部分,也称为应用帧(app frame),可以只是一个不可见的 HTML 元素,用来充当所有内容的容器;也可以包含其它视觉元素,比如标题、脚注、导航等等。在我们的案例中,应用帧将包含导航标题、以及一个要加载内容的空白区域。要显示它们显示出来,需要创建一个组件来负责此事。

script 标记内,ReactDOM.render 调用的正上面,添加如下 JSX 代码块:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li>Home</li>
          <li>Stuff</li>
          <li>Contact</li>
        </ul>
        <div className="content">

        </div>
      </div>
    )
  }
});

代码粘贴完后,看看我们有了什么。我们有了一个叫做 app 的组件,该组件返回一些 HTML。为看到 这个 HTML 是啥样子的,我们修改一下 ReactDOM.render 调用,让它引用这个组件,而不是显示单词 Hello!。OK,继续,把代码修改为如下:

ReactDOM.render(
  <div>
    <App/>
  </div>, 
  destination
);

改完之后,在浏览器中预览我们的应用,会看到一个没有样式版本的 app,并且会出现一些列表条目:

我知道这看起来不咋好看,但是现在还是够用了。随后我们会进一步处理。再深入一点讲,我们刚做完的只是创建了一个称为 App 的组件,并且通过 ReactDOM.render 调用显示它。必须注意的是,这里并没有用到 React Router。下面我们可以使用 React Router,修改 ReactDOM.render 调用的内容如下:

ReactDOM.render(
  <ReactRouter.Router>
    <ReactRouter.Route path="/" component={App}>

    </ReactRouter.Route>
  </ReactRouter.Router>,
  destination
);

我们先忽略看起来陌生的东西一会儿,在浏览器中预览一下我们的应用。如果一切运行正常,那么我们会看到 App 组件就像前面看到的一样显示出来了。现在,我们来搞清楚到底发生了什么。这里我们需要从核心 React 概念偏离一会儿,学习 React Router 本身特定的东西。

首先,我们所做的是指定 Router 组件:

ReactDOM.render(
  <ReactRouter.Router>
    <ReactRouter.Route path="/" component={App}>

    </ReactRouter.Route>
  </ReactRouter.Router>,
  destination
);

Router 组件是 React Router API 的一部分,它的主要工作是处理应用程序所需的所有路由相关的逻辑。在这个组件内,我们指定路由配置。路由配置用于描述 URL 和视图之间的映射关系。这个配置由另一个组件 Route 来处理:

ReactDOM.render(
  <ReactRouter.Router>
    <ReactRouter.Route path="/" component={App}>

    </ReactRouter.Route>
  </ReactRouter.Router>,
  destination
);

Route 组件有几个属性,用来定义在某个 URL 上显示什么。path 属性指定要匹配的 URL。在本例中是根 URL,即 /。component 属性用来指定要显示的组件的名称。在本例中,就是我们的 App 组件。把所有这些东西放在一起,这个 Route 就是说,如果 URL 包含根 URL,就显示 App 组件。当预览应用时,因为这个条件为真,所以我们看到的是 App 组件渲染时的结果。

显示主页

如我们所见,React Router 提供路由功能的方式,实际上是模仿我们已经熟悉的 React 中的概念 :组件、props 和 JSX。我们现在所有的显示应用程序的帧是一个很好的例子。现在,是时候更进一步了。下一步我们要做的是把要显示的内容定义为主页视图一部分。

为此,我们要创建一个 Home 组件,让该组件包含所有要显示的标记。在 App 组件的上面,添加如下代码:

var Home = React.createClass({
  render: function() {
      return (
        <div>
          <h2>HELLO</h2>
          <p>Cras facilisis urna ornare ex volutpat, et
          convallis erat elementum. Ut aliquam, ipsum vitae
          gravida suscipit, metus dui bibendum est, eget rhoncus nibh
          metus nec massa. Maecenas hendrerit laoreet augue
          nec molestie. Cum sociis natoque penatibus et magnis
          dis parturient montes, nascetur ridiculus mus.</p>
          <p>Duis a turpis sed lacus dapibus elementum sed eu lectus.</p>
        </div>
      );
    }
});

如我们所见,Home 组件并没有做什么特殊的事情,它只是返回一堆 HTML。现在,我们要做的是在页面加载时显示 Home 组件的内容。这个组件等于应用程序的主页。实现的方式很简单,在 App 组件内,我们有一个 class 值为 contentdiv。我们打算在这个元素内加载我们的 Home 组件。

显而易见的解决方案看起来是这样子:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li>Home</li>
          <li>Stuff</li>
          <li>Contact</li>
        </ul>
        <div className="content">
          <Home/>
        </div>
      </div>
    )
  }
});

注意,我们是在 content div 内定义 Home 组件。如果预览一下,我们期待显示的是这样的:

如果点击导航标题,我们会看到 Home 组件的内容。这种方法虽然是可以运行了,但是实际上我们做了错事。之所以是错误的,是因为用户在应用程序中导航时,应用程序应该加载不同的内容块。而我们这里实际上是硬编码让应用程序只显示 Home 组件。这是个问题,但是我们很快会回来解决这个问题。

临时清理时间

取得 app 上的进展之前,我们休息片刻,先给我们的应用添加一点格式上的提升。

添加 CSS

现在,我们的应用没有设置任何样式。这里我们还是先依赖于 CSS 这个我们的老朋友好了。在 style 标记内,添加如下样式规则:

body {
  background-color: #FFCC00;
  padding: 20px;
  margin: 0;
}
h1, h2, p, ul, li {
  font-family: Helvetica, Arial, sans-serif;
}
ul.header li {
  display: inline;
  list-style-type: none;
  margin: 0;
}
ul.header {
  background-color: #111;
  padding: 0;
}
ul.header li a {
  color: #FFF;
  font-weight: bold;
  text-decoration: none;
  padding: 20px;
  display: inline-block;
}
.content {
  background-color: #FFF;
  padding: 20px;
}
.content h2 {
  padding: 0;
  margin: 0;
}
.content li {
  margin-bottom: 10px;
}

是的,我们用的是 CSS,没有用过去所用的 inline style 对象。这里主要是为了方便。我们的组件不打算在这个应用之外重用,并且我们实际上是利用 CSS 继承来最小化重复的标记。如果不用 CSS,就必须为 HTML 标记中几乎每个元素都定义一个样式对象。这会让我们在读代码时,即使最有耐心的人也难以容忍。

无论如何,一旦添加了这段 CSS,应用程序就会开始好看一些:

当然,这里还有一些工作要做(比如,导航链接消息在黑色 banner 背后),但是我们很快就会解决这些问题。

避免 ReactRouter 前缀

在返回到正常编程进度之前,我们还有一件善后任务。你是否注意到,每一次我们调用由 React Router API 定义的事情时,都要用单词 ReactRouter 作为这件事情的前缀?

<ReactRouter.Router>
  <ReactRouter.Route path="/" component={App}>

  </ReactRouter.Route>
</ReactRouter.Router>

每次调用每个API 都要重复带上 ReactRouter 前缀,显然有点冗长。解决方法是用 ES6 新技巧手动指定哪些值可以自动获得前缀。在 <script> 标记的顶部,添加如下代码:

var { Router,
      Route,
      IndexRoute,
      IndexLink,
      Link } = ReactRouter;

添加了这段代码后,每次我们使用在大括号中定义的值时,在应用运行时前缀 ReactRouter 会自动添加。这意味着我们现在可以回到 ReactDOM.render 方法中,删掉 RouterRoute 对象前的 ReactRouter 前缀:

ReactDOM.render(
  <Router>
    <Route path="/" component={App}>

    </Route>
  </Router>,
  destination
);

如果在浏览器中预览,最终结果前面的一样。唯一的不同之处是标记更为简洁点。

现在,在继续前,你可能想知道,为什么这些会被自动被加上 ReactRouter 前缀的值列表,会包含除了我们在代码中用到的 RouterRoute 值之外的一大堆东西。把这些额外的值,当作是不久我们将要用到 React Router API 的其它部分的预览。剧透警告!(或者太晚提到这个,嗯?)

正确显示主页

之前我们说当前主页显示的方式是不正确的。当页面加载时,虽然你得到了想要的结果,但是在用户浏览时,这种方法并不会加载除了主页之外的其它页面。因为对 Home 组件的调用是硬编码进 App 的。

正确的解决方案是让 React Router 根据当前 URL 结构来决定调用哪个组件,用在 Route 组件内的嵌套 Route 组件来进一步更好地定义 URL 到 视图的映射。回到 ReactDOM.render 方法,做出如下修改:

ReactDOM.render(
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Home}/>
    </Route>
  </Router>,
  destination
);

在 根 Route 元素内,我们定义了另一个 IndexRoute 类型的 route 元素,并设置它的视图为 Home 组件。还有一个地方我们需要修改一下。在 App 组件内,删除掉对 Home 组件的调用,用如下高亮度代码行替换它:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li>Home</li>
          <li>Stuff</li>
          <li>Contact</li>
        </ul>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
});

如果现在预览页面,你依然会看到 home 内容显示出来了。但是不同的是,我们这次是正确显示 home 内容,不会阻止其它内容被显示。这是因为两件事情:

  1. App 内要显示的内容是被 this.props.childen 的结果所控制,而不是硬编码的组件。
  2. ReactDOM.render 内的 Route 元素包含了一个 IndexRoute 元素,这个元素存在的唯一用途是声明哪个组件在应用开始加载的时候将要被显示出来。

所有这些看起来可能会比几分钟前你期待的更奇怪,但是在后面小节当我们使用更多不同的 API 时候,就会更有意义。

创建导航链接

现在,我们只设置有 frame 和 home 视图。除了只是看到已经设置为 home 的页面之外,这里用户并没有别的事情要做。我们通过创建一些导航链接来修正它。更确切来说,我们要将已有的导航元素转为链接:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li>Home</li>
          <li>Stuff</li>
          <li>Contact</li>
        </ul>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
});

如果你不明白为什么这些元素在预览应用时候是不可见的,那我告诉你,是因为在加了 CSS 后,这些元素与黑色背景混合在一起了。这不是啥大问题,后面我们会解决它,但是首先我们来看看怎么把这些元素转换成链接。

在 React Router 中我们指定导航链接的方式,不是直接用久经考验的 a 标记 以及通过 href 属性设置路径,而是用 React Router 的 Link 组件来指定。这个 Link 组件与 a 标记类似,但是提供更多的功能。我们用 Link 组件来修改上述代码如下:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li><Link to="/">Home</Link></li>
          <li><Link to="/stuff">Stuff</Link></li>
          <li><Link to="/contact">Contact</Link></li>
        </ul>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
});

在上面的代码中, Link 组件指定了一个 to 属性。该属性指定要在地址栏显示的 URL 的值,它实际上是间接告诉 React Router 我们要导航到的位置。Home 链接将用户带到根(/),Stuff 链接将用户带到 stuff 位置,Contact 链接将用户带到 Contact 的位置。

如果在预览一下应用,并点击链接(现在是可见的,因为它们的 CSS 开始生效了),你不会看到任何新的显示。你只会看到 Home 的内容,是因为我们前面就是这么指定的。你还可以在地址栏上看到 URL 的更新。你会看到当前页面后面跟了一个 #/contact#/stuff、 或者 #/ 。这是进步!

添加 Stuff 和 Contact 视图

我们的应用慢慢有点像最终的样子了。下一步我们要做的是定义 Stuff 和 Contact 视图相关的组件。在 Home 组件下,添加如下代码:

var Contact = React.createClass({
  render: function() {
      return (
        <div>
          <h2>GOT QUESTIONS?</h2>
          <p>The easiest thing to do is post on
          our <a href="http://forum.kirupa.com">forums</a>.
          </p>
        </div>
      );
    }
});

var Stuff = React.createClass({
  render: function() {
      return (
        <div>
          <h2>STUFF</h2>
          <p>Mauris sem velit, vehicula eget sodales vitae,
          rhoncus eget sapien:</p>
          <ol>
            <li>Nulla pulvinar diam</li>
            <li>Facilisis bibendum</li>
            <li>Vestibulum vulputate</li>
            <li>Eget erat</li>
            <li>Id porttitor</li>
          </ol>
        </div>
      );
    }
});

上面代码我们只添加了 StuffContact 组件,这两个组件目前只渲染出 HTML。剩下我们要做的是更新路由配置,来包含这两个组件,并在恰当的 URL 上显示它们。

ReactDOM.render 方法中,添加如下高亮度代码:

ReactDOM.render(
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Home}/>
      <Route path="stuff" component={Stuff} />
      <Route path="contact" component={Contact} />
    </Route>
  </Router>,
  destination
);

这里我们所做的就是更新路由逻辑,如果 URL 中包含单词 stuff,就显示 Stuff 组件,如果 URL 中包含单词 Contact,就显示Contact 组件。如果现在在浏览器中预览页面,点击 Stuff 和 Contact 链接,如果一切顺利,那么你会看到相对应的视图被加载。

关于路由匹配 路由配置就是一套规则,用来判断当一个 URL 匹配我们安排的条件时要做什么。用华丽点的术语来说就是路由匹配。React Router 中关于路由匹配的完整解释在 React Router 文档中可以看到。但是对于我们的示例,我们有一套简单的嵌套规则,在这里你可以有可以同时匹配的多件事情。如果 URL 包含 /,就匹配外层的路由。然后,如果 URL 包含 stuff 或者 contact,那么就匹配内层路由。

意思很简单。对于匹配的每个路由,你指定显示的组件会出现。当导航到像 /stuff 这样的页面时,App 组件会显示,因为 URL 中包含了 /。然后显示 Stuff 组件,因为 URL 中也包含了 stuff 的路径。这就是为什么导航到 Stuff 或者 Contact 页面时,我们会看到它们是在 frame 中。你也可以有深层嵌套的路由。

看看如下的配置:

ReactDOM.render(
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Home} />
      <Route path="stuff" component={Stuff}>
        <Route path="blah" component={MyBlah}/>
      </Route>
      <Route path="contact" component={Contact} />
    </Route>
  </Router>,
  destination);

在本例中,注意到 pathstuffRoute 元素现在包含了一个 path 为 blah 的嵌套路由。这意味着,如果你刚好有一个 URL 是 /stuff/blah,那么除了父路由匹配的 Stuff 组件和 App 组件要被显示外, MyBlash 也会被显示。

通过嵌套路由,以及遵从路由匹配规则,你可以根据在应用中暴露的让用户导航的不同的 URL 安排,来显示定制的视图。

创建活动链接

我们要处理的最后一件事情是增加应用程序的可用性。根据当前显示的页面,用蓝色背景让活动链接高亮度显示。例如,如下是 stuff 内容被显示时应用程序的样子:

在 React Router 中完成这件事情的方式是,在 Link 实例上设置一个 activeClassName 的属性,该属性的值是在该链接当前被激活时被设置的 CSS 类。要让这发生,回到 App 组件,做出如下修改:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li><Link to="/" activeClassName="active">Home</Link></li>
          <li><Link to="/stuff" activeClassName="active">Stuff</Link></li>
          <li><Link to="/contact" activeClassName="active">Contact</Link></li>
        </ul>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
});

我们指定 activeClassName 属性,并设置它为 active。这确保在链接被点击后(以及它的路径开始成为活动的),link 元素的 class 属性值在运行时被设置为 active。结果是如下样式规则被应用:

.active {
  background-color: #0099FF;
}

如果现在预览应用,点击任意链接。会发现活动链接显示的是蓝色背景。不过,我们还没完成呢。我们的 Home 链接一直是高亮度显示的,它应该是只有在我们加载主页或者显式导航到 Home 链接本身时才高亮度显示。要修正这个问题,需要修改链接到 Home 内容的方式。我们打算用 IndexLInk 元素替换 Link 元素,来指定 Home 的内容。

继续,作出修改:

var App = React.createClass({
  render: function() {
    return (
      <div>
        <h1>Simple SPA</h1>
        <ul className="header">
          <li><IndexLink to="/" activeClassName="active">Home</IndexLink></li>
          <li><Link to="/stuff" activeClassName="active">Stuff</Link></li>
          <li><Link to="/contact" activeClassName="active">Contact</Link></li>
        </ul>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
});

一旦 Home 导航元素被 IndexLink 表示,而不是 Link,就再预览一下我们的应用。这次,当应用加载时,你会注意到你的 Home 链接默认是很酷的蓝色背景。当导航到 Stuff 或者 Contact 页面时,Home 链接不再是高亮度。有了这个,你的应用就算准备好了! Home link no longer has the highlight applied. And with this, your app is mostly good to go!

总结

至此,我们已经讲解了 React Router 具备的一些很酷的帮助我们创建单页应用的功能。这并不是说就没有更多有趣的东西可以用。我们的示例应用相当简单,需要实现的路由功能需要也很适度。但是 React Router 可以提供更多的功能,所以如果我们要创建更为复杂的单页应用,就应该多花点时间看看完整的 React Router 文档 以及示例。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复