使用 React 升级:React 路由

Avatar of Brad Westfall
Brad Westfall

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

本教程是 Brad Westfall 的关于 React 的三部分系列文章的第一部分。当 Brad 向我推荐这篇文章时,他指出有很多关于 React 入门的教程,但关于如何从那里开始的教程却不多。如果您是 React 新手,我建议您 观看此介绍视频。本系列文章从基础知识的基础上继续。

文章系列

  1. React 路由 (您在此处!)
  2. 容器组件
  3. Redux

警告! 本文是在 React Router 4 之前编写的,React Router 4 已成为 React 中路由的更标准选择。这里有一篇 关于 React Router 4 的新文章,您一定要阅读。

当我刚开始学习的时候,我发现了很多针对初学者的指南(例如 1234),这些指南展示了如何创建单个组件并将它们渲染到 DOM 中。它们很好地讲解了 JSX 和 props 等基础知识,但我难以弄清楚 React 在更大的范围内是如何工作的——比如一个真实世界的单页面应用程序 (SPA)。由于本系列涵盖了大量内容,因此不会涵盖绝对的 初学者 概念。相反,它将假设您已经了解如何创建和渲染至少一个组件。

值得一提的是,这里还有一些其他针对初学者的优秀指南

系列代码

本系列文章还附带了一些可以在 GitHub 上使用的代码。在本系列文章中,我们将构建一个围绕用户和小部件的简单 SPA。

为了保持简单和简短,本系列文章中的示例将首先假设 React 和 React Router 来自 CDN。因此,您在下面的直接示例中不会看到 require()import。不过,在本教程快结束时,我们将为 GitHub 指南介绍 Webpack 和 Babel。到那时,它将全部使用 ES6!

React-路由

React 不是框架,而是一个库。因此,它并不能解决应用程序的所有需求。它在创建组件和提供管理状态的系统方面做得很好,但是创建更复杂的 SPA 将需要一个 支持演员阵容。我们将要看到的第一个是 React 路由.

如果您以前使用过任何前端路由器,那么许多这些概念您都会很熟悉。但是与我之前使用过的任何其他路由器不同,React Router 使用 JSX,这乍一看可能有点奇怪。

作为入门,这是渲染单个组件的样子

var Home = React.createClass({
  render: function() {
    return (<h1>Welcome to the Home Page</h1>);
  }
});

ReactDOM.render((
  <Home />
), document.getElementById('root'));

以下是使用 React Router 渲染 Home 组件的方式

...

ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
  </Router>
), document.getElementById('root'));

请注意,<Router><Route> 是两个不同的东西。它们在技术上是 React 组件,但它们本身并没有创建 DOM。虽然它看起来像是 <Router> 本身被渲染到 'root' 中,但实际上我们只是定义了有关应用程序如何工作的规则。今后,您将经常看到这个概念:组件有时存在的目的不是创建 DOM 本身,而是协调其他创建 DOM 的组件。

在示例中,<Route> 定义了一个规则,当访问主页 / 时,将渲染 Home 组件到 'root' 中。

多个路由

在前面的示例中,单个路由非常简单。它没有给我们带来太多价值,因为我们已经能够在没有路由器参与的情况下渲染 Home 组件。

当我们使用多个路由来定义哪个组件应该根据当前活动的路径进行渲染时,React Router 的力量就体现出来了

ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
    <Route path="/users" component={Users} />
    <Route path="/widgets" component={Widgets} />
  </Router>
), document.getElementById('root'));

当其路径与 URL 匹配时,每个 <Route> 将渲染其相应的组件。在任何给定时间,这三个组件中只有一个将被渲染到 'root' 中。使用这种策略,我们一次将路由器挂载到 DOM 的 'root' 中,然后路由器在路由更改时交换组件的进出。

还需要注意的是,路由器将在不向服务器发送请求的情况下切换路由,因此可以想象每个组件都可以是一个全新的页面。

可重用布局

我们开始看到单页面应用程序的雏形。但是,它仍然不能解决现实世界中的问题。当然,我们可以构建三个组件来作为完整的 HTML 页面,但是代码重用怎么办?这些三个组件很可能会共享一些公共资产,比如标题和侧边栏,那么我们如何防止在每个组件中重复 HTML 呢?

让我们假设我们正在构建一个类似于此模型的 Web 应用程序

一个简单的网站模型。

当您开始考虑如何将此模型分解成可重用部分时,您可能会得到以下想法

您如何将简单的 Web 模型分解成部分。

从可嵌套组件和布局的角度思考将使我们能够创建可重用的部分。

突然,艺术部门通知您,应用程序需要一个用于搜索小部件的页面,它类似于用于搜索用户的页面。由于 用户列表小部件列表 都需要相同的搜索页面“外观”,因此将 搜索布局 作为单独组件的想法现在更有意义了

现在搜索小部件,而不是用户,但父部分保持不变。

搜索布局 现在可以作为所有类型搜索页面的父模板。虽然有些页面可能需要 搜索布局,但其他页面可以直接使用 主布局,而无需它

一个分离的布局。

这是一种常见的策略,如果您使用过任何模板系统,您可能已经做过类似的事情。现在让我们处理 HTML。首先,我们将创建静态 HTML,而不考虑 JavaScript

<div id="root">

  <!-- Main Layout -->
  <div class="app">
    <header class="primary-header"><header>
    <aside class="primary-aside"></aside>
    <main>

      <!-- Search Layout -->
      <div class="search">
        <header class="search-header"></header>
        <div class="results">

          <!-- User List -->
          <ul class="user-list">
            <li>Dan</li>
            <li>Ryan</li>
            <li>Michael</li>
          </ul>

        </div>
        <div class="search-footer pagination"></div>
      </div>

    </main>
  </div>

</div>

请记住,'root' 元素将始终存在,因为它是 JavaScript 启动之前初始 HTML Body 中的唯一元素。“root”这个词很恰当,因为我们的整个 React 应用程序将挂载到它。但是,没有“正确名称”或约定来定义您对它的称呼。我选择“root”,因此我们将在整个示例中继续使用它。只是要注意,直接挂载到 <body> 元素上是 强烈建议避免的.

在创建静态 HTML 后,将其转换为 React 组件

var MainLayout = React.createClass({
  render: function() {
    // Note the `className` rather than `class`
    // `class` is a reserved word in JavaScript, so JSX uses `className`
    // Ultimately, it will render with a `class` in the DOM
    return (
      <div className="app">
        <header className="primary-header"><header>
        <aside className="primary-aside"></aside>
        <main>
          {this.props.children}
        </main>
      </div>
    );
  }
});

var SearchLayout = React.createClass({
  render: function() {
    return (
      <div className="search">
        <header className="search-header"></header>
        <div className="results">
          {this.props.children}
        </div>
        <div className="search-footer pagination"></div>
      </div>
    );
  }
});

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        <li>Dan</li>
        <li>Ryan</li>
        <li>Michael</li>
      </ul>
    );
  }
});

不要过于区分我所说的“布局”和“组件”。这三个都是 React 组件。我只是选择将其中两个称为“布局”,因为它们执行的角色就是这样。

我们最终将使用“嵌套路由”将 UserList 放置在 SearchLayout 中,然后放置在 MainLayout 中。但是首先,请注意,当 UserList 被放置在它的父 SearchLayout 中时,父级将使用 this.props.children 来确定其位置。所有组件都有 this.props.children 作为 prop,但只有当组件嵌套时,父组件才会自动获得 React 填充的此 prop。对于不是父组件的组件,this.props.children 将为 null

嵌套路由

那么我们如何让这些组件嵌套呢?当我们嵌套路由时,路由器会为我们完成这项工作

ReactDOM.render((
  <Router>
    <Route component={MainLayout}>
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));

组件将根据路由器嵌套路由的方式进行嵌套。当用户访问 /users 路由时,React Router 将把 UserList 组件放在 SearchLayout 中,然后将两者都放在 MainLayout 中。访问 /users 的最终结果是将三个嵌套组件放在 'root' 中。

请注意,我们没有为用户访问主页路径 (/) 或要搜索小部件的情况制定规则。为了简单起见,这些规则被省略了,但让我们用新的路由器来添加它们

ReactDOM.render((
  <Router>
    <Route component={MainLayout}>
      <Route path="/" component={Home} />
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
        <Route path="widgets" component={WidgetList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));

你可能已经注意到,JSX 遵循 XML 规则,这意味着 Route 组件可以写成一个标签:<Route /> 或两个标签:<Route>...</Route>。这适用于所有 JSX,包括你的自定义组件和普通 DOM 节点。例如,<div /> 是有效的 JSX,渲染时将转换为 <div></div>

为了简洁,只需想象 WidgetList 类似于 UserList

由于 <Route component={SearchLayout}> 现在有两个子路由,用户可以访问 /users/widgets,相应的 <Route> 将在 SearchLayout 组件中加载其各自的组件。

另外,请注意 Home 组件将直接放置在 MainLayout 内部,而不会涉及 SearchLayout,这是由于 <Route> 的嵌套方式。你可能可以想象,通过重新排列路由,可以轻松地重新排列布局和组件的嵌套方式。

IndexRoutes

React Router 表达能力很强,通常有多种方法可以完成同一件事。例如,我们也可以这样编写上面的路由器

ReactDOM.render((
  <Router>
    <Route path="/" component={MainLayout}>
      <IndexRoute component={Home} />
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
        <Route path="widgets" component={WidgetList} />
      </Route> 
    </Route>
  </Router>
), document.getElementById('root'));

尽管外观不同,但它们的工作方式完全相同。

可选路由属性

有时,<Route> 将具有一个 component 属性,但没有 path,如上面的 SearchLayout 路由。其他时候,可能需要一个具有 path 但没有 component<Route>。为了了解原因,让我们从这个例子开始

<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />

path 中的 /product 部分是重复的。我们可以通过将所有三个路由包装在一个新的 <Route> 中来删除重复部分

<Route path="product">
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

同样,React Router 展示了它的表达能力。测验:你注意到这两个解决方案的问题了吗?目前我们没有规则来规定用户访问 /product 路径时的行为。

为了解决这个问题,我们可以添加一个 IndexRoute

<Route path="product">
  <IndexRoute component={ProductProfile} />
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

使用 <Link> 而不是 <a>

在为你的路由创建锚点时,你需要使用 <Link to=""> 而不是 <a href="">。不过不用担心,在使用 <Link> 组件时,React Router 最终会为你提供 DOM 中的普通锚点。但是,使用 <Link> 对 React Router 完成其部分路由魔法是必要的。

让我们在 MainLayout 中添加一些链接(锚点)

var MainLayout = React.createClass({
  render: function() {
    return (
      <div className="app">
        <header className="primary-header"></header>
        <aside className="primary-aside">
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/users">Users</Link></li>
            <li><Link to="/widgets">Widgets</Link></li>
          </ul>
        </aside>
        <main>
          {this.props.children}
        </main>
      </div>
    );
  }
});

<Link> 组件上的属性将传递到它们创建的锚点。所以这个 JSX

<Link to="/users" className="users">

将成为 DOM 中的这个

<a href="/users" class="users">

如果你需要为非路由路径创建锚点,例如外部网站,则照常使用普通锚点标签。有关更多信息,请参阅 IndexRoute 和 Link 的文档

活动链接

<Link> 组件的一个很酷的功能是它能够知道何时处于活动状态

<Link to="/users" activeClassName="active">Users</Link>

如果用户位于 /users 路径上,路由器将查找使用 <Link> 创建的匹配锚点,并将切换其 active 类。有关更多信息,请参阅 此功能

浏览器历史记录

为了避免混淆,我到现在才省略了一个重要的细节。<Router> 需要知道要使用哪种 历史记录 跟踪策略。React Router 文档 推荐使用 browserHistory,其实现如下

var browserHistory = ReactRouter.browserHistory;

ReactDOM.render((
  <Router history={browserHistory}>
    ...
  </Router>
), document.getElementById('root'));

在 React Router 的早期版本中,history 属性不是必需的,默认使用的是 hashHistory。顾名思义,它使用 URL 中的 # 哈希符号来管理前端 SPA 风格的路由,类似于你可能从 Backbone.js 路由器中期望的那样。

使用 hashHistory,URL 将类似于

  • example.com
  • example.com/#/users?_k=ckuvup
  • example.com/#/widgets?_k=ckuvup

那些 难看的查询字符串是怎么回事呢?

当实现 browserHistory 时,路径看起来更自然

  • example.com
  • example.com/users
  • example.com/widgets

但是,当在前端使用 browserHistory 时,服务器上有一个注意事项。如果用户从 example.com 开始访问,然后导航到 /users/widgets,React Router 将按预期处理这种情况。但是,如果用户直接在浏览器中输入 example.com/widgets 开始访问,或者在 example.com/widgets 上刷新,那么浏览器必须至少向服务器发送一个对 /widgets 的请求。但是,如果没有服务器端路由器,这将返回 404

小心处理 URL。你需要一个服务器端路由器。

为了解决服务器端的 404 问题,React Router 推荐在服务器端使用通配符路由器。使用这种策略,无论调用哪个服务器端路由,服务器都应该始终提供相同的 HTML 文件。然后,如果用户从 example.com/widgets 开始直接访问,即使返回了相同的 HTML 文件,React Router 也足够聪明,能够加载正确的组件。

用户不会注意到任何奇怪的事情,但你可能对始终提供相同的 HTML 文件有所顾虑。在本系列的代码示例中,将继续使用“通配符路由器”策略,但由你自行决定以你认为合适的方式处理服务器端路由。

React Router 可以以 同构 方式 在服务器端和客户端使用吗?当然可以,但这超出了本教程的范围。

使用 browserHistory 重定向

browserHistory 对象是单例,因此你可以在任何文件中包含它。如果你需要在任何代码中手动重定向用户,可以使用它的 push 方法来完成。

browserHistory.push('/some/path');

路由匹配

React 路由器处理 路由匹配,方式类似于其他路由器

<Route path="users/:userId" component={UserProfile} />

当用户访问以 users/ 开头且之后具有任何值的任何路径时,此路由将匹配。它将匹配 /users/1/users/143,甚至 /users/abc(你将需要自行验证)。

React Router 将 :userId 的值作为道具传递给 UserProfile。此道具在 UserProfile 内部以 this.props.params.userId 的形式访问。

路由器演示

在这一点上,我们有足够的代码来展示一个演示。

查看 Brad Westfall 在 CodePen 上的 Pen React-Router 演示 (@bradwestfall)。

如果你在示例中点击了几个路由,你可能会注意到浏览器的后退和前进按钮与路由器一起工作。这是这些 history 策略存在的主要原因之一。此外,请记住,对于你访问的每个路由,除了获取初始 HTML 的第一个请求之外,都不会向服务器发出任何请求。这多么酷啊!

ES6

在我们的 CodePen 示例中,ReactReactDOMReactRouter 是来自 CDN 的全局变量。在 ReactRouter 对象中,包含了我们需要的所有内容,例如 RouterRoute 组件。所以我们可以这样使用 ReactRouter

ReactDOM.render((
  <ReactRouter.Router>
    <ReactRouter.Route ... />
  </ReactRouter.Router>
), document.getElementById('root'));

在这里,我们必须在所有路由组件前面加上它们的父对象 ReactRouter。或者,我们可以使用 ES6 的新 解构 语法,如下所示

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

这将“提取” ReactRouter 的部分内容到普通的变量中,这样我们就可以直接访问它们。

从现在开始,本系列中的示例将使用各种 ES6 语法,包括解构、扩展运算符导入/导出,以及其他一些语法。在每个新语法出现时,都会对其进行简要说明,本系列附带的 GitHub 仓库中也包含大量有关 ES6 的说明。

使用 webpack 和 Babel 打包

如前所述,本系列附带一个 GitHub 仓库,您可以在其中进行代码实验。由于它将类似于真实世界中的 SPA,因此将使用 webpackBabel 等工具。

  • webpack 将多个 JavaScript 文件打包成一个文件,供浏览器使用。
  • Babel 将 ES6 (ES2015) 代码转换为 ES5,因为大多数浏览器还不支持所有 ES6 功能。随着这篇文章的更新,浏览器将支持 ES6,Babel 可能不再需要。

如果您不太熟悉这些工具,请不要担心,示例代码 已经将所有内容都设置好了,因此您可以专注于 React。但请务必查看示例代码的 README.md 文件,以获取其他工作流程文档。

注意弃用的语法

在 Google 上搜索有关 React Router 的信息可能会带您找到许多文章或 StackOverflow 页面,这些页面是在 React Router 1.0 版本之前发布时编写的。现在,1.0 版本之前的许多功能都已弃用。以下列出了简短清单

  • <Route name="" /> 已弃用。请使用 <Route path="" /> 代替。
  • <Route handler="" /> 已弃用。请使用 <Route component="" /> 代替。
  • <NotFoundRoute /> 已弃用。请 查看替代方案
  • <RouteHandler /> 已弃用。
  • willTransitionTo 已弃用。请 查看 onEnter
  • willTransitionFrom 已弃用。请 查看 onLeave
  • “Locations” 现在称为“histories”。

请查看 1.0.02.0.0 中的完整列表。

总结

React Router 还有更多未显示的功能,请务必查看 API 文档。React Router 的创建者还创建了 React Router 的分步教程,还可以查看 React.js Conf 视频,了解 React Router 的创建过程。

特别感谢 Lynn Fisher 提供的插图 @lynnandtonic


文章系列

  1. React 路由 (您在此处!)
  2. 容器组件
  3. Redux