React-路由快速启动指南-全-

React 路由快速启动指南(全)

原文:zh.annas-archive.org/md5/64054E4C94EED50A4AF17DC3BC635620

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Facebook 的 React 框架重新定义了前端应用程序的构建方式。React Router 已成为使用 React 构建的应用程序的事实标准路由框架。通过其最新的 4 版本发布,该库已经在 React 中重写,并且它允许您以声明方式处理路由。在本书中,您将学习 react-router 库如何在任何 React 应用程序中使用,包括使用 React Native 开发的 Web 和原生移动应用程序。该书还涵盖了诸如服务器端路由和 Redux 与 React Router 集成等主题。

这本书适合谁

本书适用于考虑使用 React 和 React Router 构建应用程序的 Web 和原生移动应用程序开发人员。了解 React 框架和 JavaScript 的一些知识将有助于理解本书中讨论的概念。

要充分利用这本书

React Router 用于使用 React 开发的 Web 和原生应用程序。本书假定您对 JavaScript 有很好的理解,并且了解 ECMAScript 6 中引入的一些新语言特性,例如类和扩展运算符。

本书简要介绍了 React 和基于组件的架构。React 的一些其他核心概念在reactjs.org有文档记录。

本书假定读者已经使用 Node.js 和 NPM 从 NPM 存储库安装了库和软件包。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册

  2. 选择“支持”选项卡

  3. 单击“代码下载和勘误”

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保您使用最新版本的解压缩或提取文件夹:

  • WinRAR/Windows 的 7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/React-Router-Quick-Start-Guide。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789532555_ColorImages.pdf.

使用的约定

本书中使用了许多文本约定。

CodeInText:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

In GitHubComponent
GitHub ID - mjackson

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

<Route
 to='/github/**:githubID**'
    component={GitHubComponent}  />

任何命令行输入或输出都以以下方式编写:

 Root:
 path: /category, isExact: true

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“从管理面板中选择系统信息。”

警告或重要说明会出现在这样的地方。提示和技巧会出现在这样的地方。

第一章:React Router 4 简介和创建您的第一个路由

单页应用程序(SPA)已成为开发 Web 应用程序的事实标准。许多 JavaScript 库和框架已经出现,帮助前端工程师开发 SPA。其中包括 React、Angular、Ember 和 Backbone 等。这些库或框架抽象了原生 API,并提供了可以用于更快地构建应用程序的服务和组件。SPA 是提供流畅用户体验的绝佳选择;当用户在网站中浏览时,会触发 HTTP 请求,只会更新页面的某些部分,而不是请求整个页面的服务器。

React 是一个开源的 JavaScript 库,帮助您构建 Web 和移动应用程序中的用户界面和视图层。它鼓励开发人员将视图层视为可以在整个应用程序中重用的组件集合。大多数前端框架都包含一个路由包,它使您能够在用户点击网站上提供的各种链接时更新页面的各个部分。前端框架中的路由器会监听 URL 的变化,并通过渲染相应的视图组件来保持应用程序同步。例如,当用户访问'/dashboard'时,页面将呈现各种仪表板组件,如图表和表格;当用户访问'/user'时,页面将列出各种用户属性。在基于 React 的应用程序中,需要一个路由器库,因为 React 本身不带有路由器。React-Router 是一个完全基于 React 构建的流行路由库。该库包括各种组件,可用于在用户浏览应用程序时呈现视图。除了匹配 URL 和呈现视图组件外,React-Router 还具有一些功能,可帮助您轻松配置路由。

本章讨论以下主题:

  • React 简介:本节介绍了 React 中的一些核心概念,如基于组件的架构、在 React 中创建组件以及如何向应用程序树中的子组件提供数据

  • React-Router 简介:在这里,我们首先使用create-react-app CLI 创建一个 React 应用程序,然后将 React-Router 库('react-router-dom'包)添加为依赖项

  • 创建您的第一个路由:在添加 React-Router 作为依赖项后,使用 <BrowserRouter><Route> 组件创建应用程序的第一个路由。

简要了解 React

React 是一个提供一组组件和服务的 JavaScript 库,使您能够构建用户界面。

以下是来自 reactjs.org 的引用:

"React 是一个声明式、高效、灵活的 JavaScript 库,用于构建用户界面。"

该库由 Facebook 开发和维护,根据 MIT 许可。它被广泛用于构建 Facebook 的各种应用程序,包括 Facebook 网站和 Instagram 网站。

React 使您能够构建视图组件,在应用程序状态更改时进行更新。这里的状态可能指的是底层领域数据,也可能反映用户在应用程序旅程中的位置。React 确保视图组件反映应用程序状态。

React 的一些重要特性:

  • JSX:React 应用程序中的组件使用类似 XML/HTML 的语法,称为 JSX,来渲染视图元素。JSX 允许您在 JavaScript/React 代码中包含 HTML;在 React 组件的渲染函数中使用熟悉的带有属性的 HTML 语法,无需学习新的模板语言。预处理器(如 Babel)将使用 JSX 将 HTML 文本转译为 JavaScript 对象,以便 JavaScript 引擎能够理解。

  • 单向数据绑定:React 应用程序组织为一系列嵌套组件;一组不可变值作为属性传递给组件的渲染器,以 HTML 标签的形式。组件不会修改其从父组件接收的属性(或 props);相反,子组件将用户操作传达给其父组件,父组件通过更新组件的状态来修改这些属性。

  • 虚拟 DOM:在 React 中,为每个 DOM 对象创建一个相应的虚拟 DOM 对象,其具有与真实 DOM 对象相同的一组属性。但是,虚拟 DOM 对象缺乏在用户与页面交互时更新视图的能力。React 中的组件在检测到状态变化时重新渲染视图元素,这种重新渲染会更新虚拟 DOM 树。然后,React 将此虚拟 DOM 树与更新前创建的快照进行比较,以确定更改的 DOM 对象。最后,React 通过仅更新更改的 DOM 对象来修改真实 DOM。

React 中的基于组件的架构

自 2013 年发布以来,React 已经重新定义了前端应用程序的构建方式。它引入了基于组件的架构的概念,本质上允许您将应用程序视为由小型、自包含的视图组件组成。这些视图组件是可重用的;也就是说,诸如CommentBoxFooter之类的组件封装了必要的功能,并可以在站点的各个页面中使用。

在这种情况下,页面本身是一个视图组件,由其他小的视图组件组成,如下所示:

<Dashboard>
    <Header>
        <Brand />
    </Header>
    <SideNav>
        <NavLink key=”1”>
        <NavLink key=”2”>
    </SideNav>
    <ContentArea>
        <Chart>
        <Grid data="stockPriceList">
    </ContentArea>
    <Footer />
</Dashboard>

在这里,<Dashboard>是一个视图组件,包含了几个其他视图组件(HeaderSideNavContentAreaFooter),这些又由小组件(BrandNavLinkChartGrid)组成。基于组件的架构鼓励您构建提供特定功能并且不与任何父级或同级组件紧密耦合的组件。这些组件实现了某些功能,并提供了一个接口,通过这个接口它们可以被包含在页面中。

在前面的例子中,<Grid>组件将包括渲染数据的行和列、提供搜索功能,以及按升序或降序对列进行排序的功能。<Grid>组件将实现所有上述功能,并提供一个接口,通过这个接口它可以被包含在页面中。这里的接口将包括标签名(Grid)和一组属性(props),接受来自其父组件的值。在这里,<Grid>组件可以与后端系统进行接口,并检索数据;然而,这将使组件与给定的后端接口紧密耦合,因此无法重用。理想情况下,视图组件将从其父组件接收数据并相应地进行操作。

<Grid data="stockPriceList" />

在这里,<Grid>组件通过其data属性接收包含股票价格信息的列表,并以表格格式呈现这些信息。包含这个<Grid>组件的组件可以被称为Container组件,Grid作为子组件。

Container组件也是View组件;然而,它的责任包括为其子组件提供必要的数据来渲染。Container组件可以发起 HTTP 调用到后端服务并接收渲染其子组件所需的数据。此外,Container组件还负责将单个视图组件定位在其视图区域内。

创建一个 React 组件

通过扩展 React 提供的Component类来创建 React 组件如下:

import React, { Component } from 'react';
import './button.css';

export class Button extends Component {
    render() {
        return (
            <button className={this.props.type}>
                {this.props.children}
            </button>
        );
    }
}

在这里,Button类扩展了 React 的Component类并重写了render方法。render方法返回将在页面加载时呈现在 DOM 上的 JSX。typechildren属性在this.props中可用。React 允许您通过 props 将数据传递给其组件,并通过以下语法来实现:

import React, { Component } from 'react';
import { Button } from './components/Button/button';
import './App.css';

export default class App extends Component {
    render() {
        return (
            <div className="App">
                <Button type="secondary">CANCEL</Button>
                <Button type="primary">OK</Button>
            </div>
        );
    }
}

在这里,我们将Button组件包裹在父组件App中,以渲染两个按钮元素。type属性被Button组件使用来设置CANCELOK按钮的类名(className)和Button标签内提到的文本。这可以通过children属性来引用。children属性可以是纯文本或其他视图组件。子组件使用this.props来引用其父组件提供的数据。'this.props'中的children属性提供了父组件在标签之间包含的所有子元素的引用。如果您以前使用过 Angular,请将前面的片段视为类似于在 AngularJS 中使用ng-transclude或在 Angular 中使用ng-content来包含元素。

在这里,<App>组件包含<Button>组件,可以被称为容器组件,负责在页面上渲染按钮。

下一步是在 DOM 上呈现<App>组件。<App>组件充当根组件,即树中的根节点。应用程序中的每个组件都将<App>组件作为其最顶层的父组件:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'; 
import './index.css'; 

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

这段代码包含在index.js中,它导入了ReactReactDOM库。ReactDOM库有一个render方法,它接受要渲染的组件作为其第一个参数,并且根组件要渲染到的 DOM 节点的引用作为第二个参数。

运行应用程序时,将呈现在<App>组件内的内容:

React-Router 简介

React-Router 是用 React 构建的 SPA 的路由库。React-Router 版本 4 是一个完全的重写,并采用了基于组件的架构的 React 哲学。

这是来自 React-Router 文档(reacttraining.com/react-router/

“React Router 是一组与您的应用程序声明性地组合的导航组件。无论您是想为您的 Web 应用程序拥有可书签的 URL还是想以React Native中的可组合方式导航,React Router都可以在 React 渲染的任何地方使用--所以随你选择!”

React-Router 可以在 React 可以应用的任何地方使用;也就是说,React-Router 在浏览器和使用 React Native 的本地环境中都可以工作。

该库分为三个包:

  • react-router:DOM 和本地版本的常见核心组件

  • react-router-dom:用于浏览器和 Web 应用程序的组件

  • react-router-native:用于使用 React Native 构建的本地应用程序的组件

该库提供了各种组件,可用于动态添加路由到您的应用程序。React-Router v4 中的动态路由允许您在用户通过应用程序旅程时指定应用程序路由。诸如 AngularJS 和 Express 之类的框架要求您预先指定路由,并且在应用程序引导时需要此路由信息。实际上,React-Router 的早期版本遵循了相同的范例,并且需要提前提供路由配置。

除了在 React 应用程序中进行动态路由和提供流畅导航之外,该库还包括传统网站中可用的各种功能。这些包括以下内容:

  • 通过应用程序向后和向前导航,维护历史记录,并恢复应用程序的状态

  • 在提供 URL(深度链接)时呈现适当的页面组件

  • 将用户从一个路由重定向到另一个路由

  • 在没有任何路由匹配 URL 时支持呈现 404 页面

  • 支持基于哈希的路由和使用 HTML5 模式的漂亮 URLs

React-Router 是 Facebook 提供的官方路由解决方案是一个常见的误解。实际上,它是一个第三方库,根据 MIT 许可证授权。

使用 React-Router 入门

让我们创建一个 React 应用程序,然后将 React-Router 作为依赖项添加进去。

为了创建一个 React 应用程序,我们将使用create-react-appCLI。create-react-appCLI 使创建一个已经工作的应用程序变得更容易。CLI 创建了一个项目脚手架,以便您可以开始使用最新的 JavaScript 功能,并提供了用于为生产环境构建应用程序的脚本。有各种 React 和 React-Router 入门套件可用;然而,使用create-react-app有助于演示如何将 React-Router 添加到现有的基本 React 应用程序中。

第一步是使用 NPM 全局安装create-react-app,如下所示:

npm install -g create-react-app

CLI 要求node版本大于或等于 6,并且npm版本大于 5.2.0。

安装完 CLI 后,我们将使用create-react-app命令创建一个新的应用程序,如下所示:

create-react-app react-router-demo-app

create-react-app完成安装包时,将显示以下输出:

Inside that directory, you can run several commands:
 npm start
 Starts the development server.

 npm run build
 Bundles the app into static files for production.

 npm test
 Starts the test runner.

 npm run eject
 Removes this tool and copies build dependencies, configuration 
 files
 and scripts into the app directory. If you do this, you can't 
 go back!
 We suggest that you begin by typing:
 cd react-router-demo-app
 npm start

如果您使用yarn包管理器(yarnpkg.com/en/),则前面片段中的npm命令将被替换为yarn

在安装过程中创建了react-router-demo-app目录(如果尚不存在)。在该目录内,创建了以下项目结构:

/react-router-demo-app
    |--node_modules
    |--public
    |   |--favicon.ico 
    |   |--index.html
    |   |--manifest.json
    |--src
    |   |--App.css
    |    |--App.js
    |    |--App.test.js
    |    |--index.css
    |    |--index.js
    |    |--logo.svg
    |    |--registerServiceWorker.js
    |--package-lock.json
    |--package.json
    |--README.md

CLI 安装了所有必要的依赖项,如 Babel,用于将 ES6 代码转译为 ES5,从而使您能够利用最新的 JavaScript 功能。它还使用 webpack 创建了一个构建管道配置。安装后,无需额外配置即可启动或构建应用程序。如前面的输出所示,您可以使用npm start命令启动应用程序,并使用npm build构建一个生产就绪的应用程序。

运行npm start后,应用程序将被编译,并将打开一个浏览器窗口,显示“欢迎来到 React”的消息,如下所示:

index.js文件中,使用ReactDOM引用来呈现应用程序的根组件,如下所示:

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

<App>组件标记了应用程序启动时将呈现的树的开始。

添加 React-Router 库

现在我们的示例应用程序已经运行起来了,让我们使用npm添加 React-Router 库作为一个依赖项:

npm install --save react-router-dom

此命令将下载并将react-router-dom添加到/node_modules目录中。package.json文件现在将其包含为一个依赖项:

"dependencies": {
 "react": "¹⁶.4.0",
 "react-dom": "¹⁶.4.0",
 "react-router-dom": "⁴.3.0",
 "react-scripts": "1.1.4"
}

在撰写本书时,react-router-dom的版本为 4.3.0。您可以通过在使用npm包含库时提到react-router-dom@next来尝试 alpha 和 beta 版本。

定义应用程序路由

react-router-dom包括一个<BrowserRouter>组件,它用作在应用程序中添加路由之前的包装器。要在 React Native 应用程序中使用 React-Router,需要使用react-router-native包。这将在后面的章节中详细讨论。<BrowserRouter>组件是路由器接口的一种实现,它利用 HTML5 的历史 API 来使 UI 与 URL 路径保持同步。

第一步是使用<BrowserRouter>将应用程序的根组件包装起来,如下所示:

import { BrowserRouter } from 'react-router-dom';

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

将您的应用程序包装在<BrowserRouter>中将为我们的<App>组件创建一个 history 实例,使其所有子组件都可以访问来自原生浏览器历史 API 的 props。这允许组件匹配 URL 路径并呈现适当的页面组件。

History 是一个 JavaScript 库,它允许您管理历史堆栈导航,并有助于在会话之间保持状态。

在 React-Router 中的路由实际上并不是路由-它是基于与当前 URL 路径匹配的模式的组件的条件渲染。要定义路由,我们需要两个信息:要匹配的 URL 路径和要呈现的组件。让我们创建两个组件,HomeComponentDashboardComponent,分别在/home/dashboard上呈现。

src/components/home/home.component.js中:

import  React  from  'react'; export  const  HomeComponent  = () => ( <div> Inside Home route </div> );

src/components/dashboard/dashboard.component.js中:

import  React  from  'react'; export  const  DashboardComponent  = () => ( <div  className="dashboard"> Inside Dashboard route </div> );

由于我们从前面的组件返回 JSX,所以需要import语句。

下一步是使用Route组件(来自'react-router-dom')定义路由。Route组件接受几个 props,但在本例中,我们将使用pathcomponent

App.js中:

class  App  extends  Component { render() { return ( <div  className="container"> <Route path="/home" component={HomeComponent} /> <Route path="/dashboard" component={DashboardComponent} /> </div> ); } } export  default  App;

在这里,我们在<App>组件的'render'方法中定义路由。每个<Route>组件都有一个path属性,它提到要匹配的 URL 路径,以及一个component属性,提到一旦路径匹配 URL 就要呈现的组件。

在前面的示例中,该组件是在不扩展 React 组件类的情况下创建的。如果通过将扩展 React 组件类创建的组件作为component属性的值提供,则每次<Route>呈现组件时都会调用组件的生命周期方法componentWillMountcomponentWillUnmount

当您运行应用程序(npm start)并访问localhost:3000/home时,将呈现HomeComponent并显示消息“Inside Home Component”。类似地,当您访问localhost:3000/dashboard时,将呈现DashboardComponent

<BrowserRouter>创建一个History对象,它用于跟踪当前位置并在位置更改时重新渲染站点。<BrowserRouter>通过 React 的上下文将History对象提供给其后代子组件。如果一个Route组件没有<BrowserRouter>作为其父级,它将无法工作。

另外,<BrowserRouter>必须只有一个子元素的要求。在下面的片段中,<BrowserRouter>给出了两个子元素:

<BrowserRouter>
    <Route
        path="/home"
        component={HomeComponent} />
    <Route
        path="/dashboard"
        component={DashboardComponent} />
</BrowserRouter>

上述代码将导致错误,例如“只能有一个子元素”。要解决此问题,您可以将这些路由移入一个组件并提供组件引用,或者将前面片段中的<Route>组件包装在另一个元素中,例如divReact Fragment

React fragment用于将一组子元素分组,而不向 DOM 添加额外的节点。当组件返回多个元素时,使用片段。

除了BrowserRouter之外,React-Router 库中还有其他类型的路由器:HashRouterMemoryRouterStaticRouter。这些将在后面的章节中讨论。

总结

React 是一个用于构建用户界面的 JavaScript 库。与 Angular 和 Ember 等库不同,它们包括路由包,React 库不包括任何帮助进行路由的组件或服务。React-Router 是一个路由库,可以在任何 React 应用程序中使用,包括 Web 或原生应用。React-Router 版本 4 是对早期版本的完全重写,所有组件都是用 React 编写的。该库包括用于 Web 应用程序的react-router-dom包;用于使用 React-Native 构建的原生应用程序的react-router-native;以及react-router,这是react-router-domreact-router-native都依赖的核心包。

create-react-app CLI 用于快速搭建 React 应用程序。它包括可以用于生成开发和生产环境构建的构建配置脚本。然后将react-router-dom包添加为应用程序的依赖项。该包包括<BrowserRouter>组件,它实现了History接口。应用程序的根组件<App />被包裹在 React-Router 的<BrowserRouter>组件中,以使History对象对应用程序树中的所有组件都可用。

创建我们的第一个路由,包括<Route>组件。它接受pathcomponent作为 props,并在浏览器的 URL 匹配<Route>路径时渲染组件。

在第二章中,配置路由-在 Route 组件中使用各种选项,详细讨论了<Route>组件的 props。此外,我们将看看渲染组件接收的各种 props,并考虑如何使用这些 props 来创建嵌套路由。

第二章:配置路由-使用路由组件中的各种选项

React-Router 允许您使用<Route>组件声明性地定义路由。它是 React-Router 的主要构建块,并在path prop 中提到的路径值与浏览器的 URL 位置匹配时呈现component prop 中提到的组件。<Route>组件像任何其他 React 组件一样,接受一组 props。这些 props 可以更精细地控制浏览器的 URL 路径应该如何匹配<Route>组件的路径,以及一些其他呈现选项。

在上一章中,我们简要地看到了如何使用<Route>组件来匹配 URL 路径并呈现组件。在本章中,我们将看一下以下内容:

  • 深入研究可以添加到<Route>组件的各种 props,例如exactstrictrenderchildrensensitive

  • 路由组件 props:作为<Route>路径匹配的结果呈现的组件接收数据作为 props,然后可以用于创建嵌套路由。

  • 路由参数<Route>组件的路径可以配置为从 URL 段接受附加参数,并且可以在呈现的组件中读取这些参数。

  • 嵌套或动态路由:可以在呈现的组件中添加<Route>组件,而不是在应用程序级别定义路由。因此,呈现的组件为应用程序旅程提供了下一步。

  • 从 JSON 配置生成路由:JSON 对象中可用的路由信息可用于向应用程序添加路由。

路由 props

当您查看 React-Router 的源代码时,<Route>组件接受以下 props:

Route.propTypes  = { computedMatch:  PropTypes.object, // private, from <Switch> path:  PropTypes.string, exact:  PropTypes.bool, strict:  PropTypes.bool, sensitive:  PropTypes.bool, component:  PropTypes.func, render:  PropTypes.func, children:  PropTypes.oneOfType([PropTypes.func, PropTypes.node]), location:  PropTypes.object };

让我们在下一节中看看这些 props 的每一个。

exact prop

在我们之前的<Route>示例中,让我们将'/home'路由路径更改为'/',如下所示:

<div className="container">
     <Route
         path="/"
         component={HomeComponent} 
     />
     <Route
         path="/dashboard"
         component={DashboardComponent} 
     />
 </div>

有了这些路由,当浏览器的 URL 设置为/dashboard时,您会注意到两个组件的内容如下显示:

Inside Home route
Inside Dashboard route

在这里,'/dashboard'中的'/'匹配<Route>的两个路径'/''/dashboard';因此它从两个组件中呈现内容。要使浏览器的location.pathname<Route>组件的路径完全匹配,请向<Route>添加 exact prop,如下所示:

..
 <Route
     path="/"
     component={HomeComponent}
     exact
 />
 ..

类似地,当您尝试访问'/dashboard''/dashboard/portfolio'路径时,您会注意到在两种情况下都会呈现DashboardComponent。为了防止'/dashboard/portfolio'与具有'/dashboard'路径的<Route>组件匹配,添加exact属性。

React-Router 在内部使用path-to-regexp库来确定路由元素的路径属性是否与当前位置匹配。

严格属性

<Route>路径有尾随斜杠,并且您希望将此路径与浏览器的 URL 匹配,包括尾随斜杠时,请包括strict属性。例如,在将<Route>路径从'/dashboard'更改为'/dashboard/'后,<Route>组件仍将匹配不带尾随斜杠的 URL 路径。换句话说,'/dashboard'将匹配具有'/dashboard/'路径的<Route>组件。

但是,在添加strict属性之后,React-Router 确保<Route>仅在 URL 有尾随斜杠时匹配:

<Route
    path="/dashboard/"
    component={DashboardComponent}
    strict
/>

有了这个<Route>配置,'/dashboard'路径将不匹配。但是,当您在 URL 中添加尾随斜杠时,例如'/dashboard/',具有strict属性的<Route>组件将匹配,并且将呈现DashboardComponent

请注意,如果您提到额外的 URL 段,那么它仍将匹配<Route>组件中提到的path属性。例如,如果 URL 路径是'/dashboard/123',它将与具有strict属性的<Route>组件匹配'/dashboard/'路径。要匹配包括额外 URL 段的路径,可以在strict属性旁边指定exact属性。

敏感属性

<Route>组件的路径不区分大小写,也就是说,<Route>组件的路径属性值设置为'/Dashboard'将匹配'/dashboard''/DASHBOARD'的 URL 路径。要使<Route>组件的路径区分大小写,添加sensitive属性:

<Route
 path="/Dashboard" component={DashboardComponent} **sensitive** />

sensitive属性确保在将其与浏览器的 URL 路径匹配时,考虑路径属性的大小写。通过添加sensitive属性,可以使用不同的大小写定义具有相同路径名的路由。

<Route
 path=**"/Dashboard"** component={DashboardComponent} **sensitive** /> <Route path=**"/dashboard"** component={StockListComponent} **sensitive** />

这段代码将创建两个不同的路由,并且当<Route>组件的区分大小写路径与浏览器的 URL 路径匹配时,将呈现相应的组件。

使用 render prop 进行内联渲染

我们已经看过component属性如何在<Route>路径匹配浏览器的location.pathname时用于渲染视图。还有两个其他可用于渲染视图的属性:renderchildren

render属性用于内联渲染。作为render属性值的函数应返回一个类似于以下的 React 元素:

<Route
    path="/user"
    render={() => (
 <div> Inside User Route </div>
 )}
/>

从前面的代码片段中,当'/user'路径匹配浏览器的 URL 时,作为render属性值指定的函数被执行,并且从该函数返回的 React 元素被渲染。

当在同一个<Route>组件中同时指定componentrender属性时,component属性将优先。

使用 children 属性进行内联渲染

children属性应该在您想要渲染视图的情况下使用,无论是否有路径匹配。children属性的语法与render属性类似,如下所示:

<Route
    path="/sidenav"
    children={() => (
 <div> Inside Sidenav route </div>
 )}
/>

具有children属性的<Route>组件即使未指定path属性也会被渲染。此外,exactstrict属性对具有children属性的<Route>组件没有任何影响。

componentrender属性都优先于children属性。此外,当componentrender属性被提及时,只有当路径匹配请求的 URL 时才会渲染视图。

基于路由列表中的位置,具有children属性的<Route>组件被渲染。例如,如果前一个<Route>组件被指定为路由列表中的最后一个条目,则在渲染所有先前匹配的路由之后被渲染。此外,如果前一个<Route>组件在匹配路由之前列出,则路由的内容在渲染匹配路由的内容之前被渲染,如下所示:

<Route
 path="/sidenav"
 children={() => ( <div> Inside Sidenav route </div>
 )} /> <Route path="/user" render={() => ( <div> Inside User route </div> )} />

在这里,当您尝试访问'/user'路径时,具有children属性的<Route>组件在渲染'/user'路径之前被渲染。

路由组件属性

<Route>路径匹配浏览器的 URL 路径时,被渲染的组件接收特定的props,例如historylocationmatchstaticContext。这些 props 提供的数据包括与路由相关的信息。这些 props 可用于使用<Route>组件的componentrenderchildren属性渲染的组件。

当您在服务器端渲染应用程序时设置staticContext属性,并且在客户端路由器中(即使用<BrowserRouter>接口时)不可用(即设置为undefined)时。

历史

React-Router 依赖于history包。history是一个 JavaScript 库,用于在任何 JavaScript 应用程序中维护会话。请考虑来自history文档的以下引用(github.com/ReactTraining/history):

history是一个 JavaScript 库,让您可以轻松地在 JavaScript 运行的任何地方管理会话历史。history抽象了各种环境的差异,并提供了一个最小的 API,让您可以管理历史堆栈、导航、确认导航和在会话之间保持状态。”

history对象有几个属性和方法:

  • 动作:当前动作,PUSHPOPREPLACE

  • 长度:历史堆栈中条目的计数

  • 位置:包括hashpathnamesearchstate属性的当前位置

  • hash:哈希片段

  • pathname:URL 路径

  • search:URL 查询字符串

  • 状态:使用location.pushState从一个路由导航到另一个路由时提供的状态信息

  • block(): 注册一个提示消息的函数,当用户尝试离开当前页面时将显示该消息。

  • createHref(): 构造 URL 段的函数;它接受一个带有pathnamesearchhash属性的对象。

  • go(n): 导航历史堆栈。history.go(-1)将指针向后移动一个位置,history.go(1)将指针向前移动一个位置。

  • goBack(): 将指针向后移动一个位置在history堆栈中;与history.go(-1)相同。

  • goForward(): 将指针向前移动一个位置在history堆栈中;与history.go(1)相同。

  • listen(listenerFn): 注册一个监听器函数,每当history.location发生变化时就会调用该函数。

  • push(path, state?): 导航到给定的路径名,向history堆栈添加一个条目。它可以选择接受一个state参数,用于传递应用程序状态数据。

  • replace(path, state?): 一个函数,用于导航到给定的路径名,替换history堆栈中的当前条目。它还接受一个可选的state参数。

history对象由 React-Router 在内部使用,用于在用户尝试在页面之间导航时更新历史堆栈中的条目。它作为 prop 提供给渲染的组件,以便用户可以使用history对象中的上述方法导航到不同的页面。在下一章中,我们将看看 React-Router 提供的各种 API,帮助您导航到应用程序中定义的不同路由。

位置对象

location对象提供了表示应用程序当前状态的数据快照。它包括以下属性:pathnamehashsearchstate。导航组件可以为这些 prop 提供值,然后由匹配浏览器 URL 的渲染组件读取。如前所述,我们将在第三章中看看各种导航组件,使用 Link 和 NavLink 组件导航到路由

位置信息也可以在history对象中找到;但是,history对象是可变的,因此应避免在history对象中访问位置。

匹配对象

match对象包含有关<Route>路径如何匹配当前 URL 的信息。它包括urlpathisExactparams属性。

让我们参考之前使用render prop 的路由之一:

<Route
 path="/user" render={({ match }) => { console.log(match);
        return ( <div> Inside User route </div> ); }} />

当您尝试访问/user路径时,match对象的属性将具有以下值:

url - '/user'
path - '/user'
params - {}
isExact - true
  • url: 返回 URL 的匹配部分的字符串

  • path: 返回路由路径字符串的字符串,即在<Route>组件的路径 prop 中提到的路径模式

  • params: 包含传递给路由的路径参数列表的对象(在接下来的部分中将更多地介绍参数)

  • isExact: 一个布尔值;如果 URL 完全匹配提供的path prop,则为true

如果 URL 段的部分仅匹配<Route>组件的路径,则isExact属性为false。例如,具有/user路径的<Route>组件与/user/123的 URL 不完全匹配,在这种情况下,isExact为 false。

如前所述,带有 children 属性的 <Route> 组件会被渲染,无论 path 属性是否匹配浏览器的 URL 路径。在这种情况下,如果路径不匹配 URL 段,match 对象将被设置为 null:

<Route
 path="/sidenav" children={({ match }) => { console.log(match) return ( <div> Inside Sidenav route </div> ); }} />

使用这个 <Route> 配置时,当您尝试访问 /user 路径时,将匹配带有 /sidenav 路径的 <Route> 组件,因为它有一个 children 属性。然而,在这里,match 对象被设置为 null。这有助于确定带有 children 属性的 <Route> 组件的路径是否匹配了 URL 段。

路由参数

在 React-Router 中,可以配置 <Route> 组件来接受给定对象的 URL 参数。例如,要显示给定 userID 的用户信息,URL 路径可能看起来像 '/user/1'userID'1' 的用户)和 '/user/123'userID'123' 的用户)。URL 的最后部分是动态的;然而,在每种情况下,渲染的组件都会对给定的 userID 执行相同的操作。

这样的用例示例是 Twitter 的个人资料页面。该页面接受 twitterID 并显示给定用户的动态。

to 属性中附加一个以冒号 (😃 为前缀的额外路径,可以配置 React-Router 中的 <Route> 组件来接受 URL 中的动态部分,如下所示:

<Route
 to='/github/**:githubID**'
    component={GitHubComponent}  />

在这里,'/:githubID' 路径是动态的,可以匹配诸如 '/github/ryanflorence''/github/mjackson' 这样的路径(React-Router 的创建者的 GitHub ID)。

然后,可以在渲染的组件中使用 match.params 来使用这些匹配的 URL 参数:

export  class  GitHubComponent  extends  Component { render() { const { match: { params } } =  this.props; return ( <div> In GitHubComponent <br  /> GitHub ID - {params.githubID} </div> ) } }

当您尝试访问 '/github/mjackson' URL 路径时,您将看到这条消息:

In GitHubComponent
GitHub ID - mjackson

match.params 对象包含路由中匹配参数的键值对。<Route> 组件也可以接受 URL 中的多个参数,如下所示:

<Route
 path="/github/**:githubID**/**:twitterID**" component={GitHubComponent} />

在这里,githubIDtwitterID 参数是动态的,可以匹配 URL 路径,比如 '/github/ryanflorence/mjackson'。第二个参数 twitterID 可以在组件中使用 match.params.twitterID 进行读取。

在之前的 <Route> 配置中,githubIDtwitterID 参数是必需的参数,也就是说,如果 URL 路径中没有这两个参数,路由就不会匹配。要将参数标记为可选的,可以在参数后面加上问号 (?),如下面的代码片段所示:

<Route
 path="/github/:githubID/**:twitterID?**" component={GitHubComponent} />

在前面的<Route>配置中,twitterID参数被标记为可选。这意味着当您尝试访问'/github/ryanflorence'路径,即在 URL 中不提供twitterID参数的值时,路径将匹配 URL 并渲染组件。然而,当组件尝试使用match.params.twitterID访问参数时,它将返回undefined

<Route>路径也可以配置为接受与正则表达式匹配的参数,如下所示:

...
<Route
 path="/github/**:githubID(\w+)**" component={GitHubComponent} /> <Route path="/user/**:userID(\d+)**" component={UserComponent} />
...

在这里,githubID参数限制为字母数字字符串,userID参数限制为数字值。参数后缀有一个正则表达式模式,用于定义<Route>参数将接受的值的类型,即限制可以提供给参数的值的模式。

嵌套路由和动态路由

React-Router 的早期版本要求预先定义路由,并将子路由嵌套在另一个路由内,如下所示:

<Router>
    <Route path='/' component={Container}>
        <IndexRoute component={Home} />
        <Route path='user' component={User}>
            <IndexRoute component={Twitter} />
            <Route path='instagram' component={Instagram} />
        </Route>
    </Route>
</Router>

这段代码可以被认为是静态路由,即在应用程序初始化时,库需要路由配置。在这里,具有'/'路径的路由作为所有路由的父路由,具有'user'路径的路由是'/'的子路由,也是具有'instagram'路径的路由的父路由。

在 React-Router v4 中,可以在渲染的组件内定义嵌套路由,也就是说,随着用户在应用程序中导航,路由会被注册。通过 v4 的重写,<Route>是一个 React 组件,因此可以包含在任何组件的render方法中。

考虑在App.js<App />根组件)中定义的父路由:

<Route
 path="/category" component={CategoryComponent} />

在这里,'/category'路径映射到CategoryComponent组件。

CategoryComponent可以反过来使用相同的<Route>组件渲染其他路由。然而,在渲染组件(CategoryComponent)内部定义路由时,需要在<Route>组件的to属性中指定对当前匹配 URL 的引用。例如,可以使用<Route>组件创建一个带有'/pictures'路径的子路由;然而,在to属性中需要指定绝对路径,即'/category/pictures'或更一般地,'/<current_matching_url>/pictures'

如前所述,传递给呈现组件的match属性包含有关路径如何匹配当前 URL 的信息。match属性的 URL 属性可用于引用父 URL:

export  const  CategoryComponent  = ({ match }) => { return ( <div  className="nested-route-container"> <div  className="root-info"> <h4> Root: </h4> <h5> path: {match.path}, isExact: {match.isExact.toString()}</h5> </div> <Route path={`${match.url}/pictures`} render={({ match }) => { return ( <div> <h4> Viewing pictures: </h4> <h5> path: {match.path}, 
                                 isExact: {match.isExact.toString**()}** </h5> </div>  ) }} /> <Route path={`${match.url}/books`} render={({ match }) => { return ( <div> <h4> Viewing books: </h4> <h5> path: {match.path},
                                 isExact: {match.isExact.toString()**}** </h5> <Route path={`${match.url}/popular`} render={({ match }) => ( <div> Inside popular, 
                                          path: {match.path} </div> )}  /> </div>  ) }} /> </div> ) }

在前面片段中定义的CategoryComponent接受match属性,并且组件中定义的路由具有'${match.url}/<child_route_path>'格式的路径值。match.url模板变量包含父路由的 URL 值,在本例中为/category。使用相同的原则,还定义了路径为'/category/pictures''/category/books'的路由。

让我们测试这些路由:

  • 场景 1location.pathname'/category'

在这里,将呈现父路由,并且页面将呈现如下路由信息:

         Root:
         path: /category, isExact: true

在这里,match.isExact为 true,因为在/category路径之后没有其他 URL 段。

  • 场景 2location.pathname'/category/pictures''/category/books'

呈现'/category'父路由后,库会查找具有'/category/pictures''/category/books'路径的<Route>组件。它找到一个并呈现相应的组件:

            Root:
            path: /category, isExact: false
            Viewing pictures:
           path: /category/pictures, isExact: true

现在,在父路由(具有'/category'路径的<Route>组件)中,match.isExact为 false;但是在子路由中为 true。

  • 场景 3location.pathname'/category/books/popular'

您可以嵌套任意多个路由。在这里,'/books' 是一个嵌套路由,并且还有另一个嵌套路由,'/popular',它匹配了'/category/books/popular'路径:

              Root:path: /category, 
              isExact: false
              Viewing books:
             path: /category/books, isExact: false
             Inside popular, 
             path: /category/books/popular

match属性在创建嵌套路由时非常有用。这些嵌套路由只有在呈现其父路由时才可访问,从而允许您动态添加路由。

来自 JSON 的动态路由

还可以通过查找包含路由配置选项集合的数组来生成一组<Route>组件。每个路由选项应包含必要的详细信息,如'path''component'

一组路由可能如下所示:

const  STOCK_ROUTES  = [ { path:  'stats', component:  StatsComponent, }, { path:  'news', component:  NewsComponent }, { path:  'trending', component:  TrendingComponent  } ];

前面数组中的每个对象都包含一个指定路由路径的'path'键,以及包含用户访问路由时要呈现的组件的引用的'component'键。然后可以在组件的render方法中使用前面的集合来生成一组<Route>组件,如下所示:

...
render() {
 const { match } =  this.props; return ( <div> Inside Stocks, try /stocks/stats or /stocks/news or /stocks/trending { STOCK_ROUTES.map((route, index) => { return ( <Route key={index} path={`${match.url}/${route.path}`} component={route.component} /> ) **})** } </div> ); }
...

STOCK_ROUTES中定义的路由配置用于在StockComponent渲染时添加一系列<Route>组件。父级<Route>组件在'/stocks'路径处渲染,因此在生成'/stocks'路径下的<Route>组件时使用了match.url

总结

在本章中,我们了解到<Route>组件可以使用各种 props 进行配置。这包括使用exact prop 仅在浏览器的 URL 路径与<Route>组件中的路径值匹配时才渲染组件;在<Route>组件中使用strict prop 确保 URL 路径与path prop 中指定的尾部斜杠匹配;包括sensitive prop 使path prop 的值区分大小写;以及使用renderchildren props 进行内联渲染。带有children prop 的<Route>组件会渲染,而不管path prop 中指定的值是什么。这在页面布局中有多个视图组件并且这些组件应该渲染时非常有用,而不管path prop 中指定的值是什么。

由于<Route>路径匹配的结果组件可以接收数据作为 props。这包括 props,如historylocationmatchstaticContextmatch prop 可用于创建嵌套路由,即match prop 中的url属性包含的信息可以用于渲染组件中包含的<Route>组件的path prop 中。<Route>组件也可以通过查找对象中指定的配置来添加。然后可以使用包含pathcomponent信息的数组来在应用程序中添加多个路由。

<Route>组件的path prop 可以配置为接受 URL 段作为路径参数。然后渲染的组件可以使用match.params来读取这些参数。可以通过在path参数的后缀中指定正则表达式来配置参数以接受特定值。

第三章:使用 Link 和 NavLink 组件导航到路由

React-Router 提供了 <Link><NavLink> 组件,允许您导航到应用程序中定义的不同路由。这些导航组件可以被视为页面上的锚链接,允许您导航到站点中的其他页面。在传统网站中,使用锚链接导航应用程序会导致页面刷新,并且页面中的所有组件都会重新渲染。使用 <Link><NavLink> 创建的导航链接不会导致页面刷新,只有使用 <Route> 定义的页面特定部分并匹配 URL 路径的部分会更新。

<Route> 组件类似,导航组件 <Link><NavLink> 是 React 组件,允许您声明性地定义导航链接。

在本章中,我们将看看导航到应用程序中定义的路由的各种选项。这包括以下内容:

  • <Link> 组件及其属性

  • <NavLink> 组件及其属性

  • 使用 match 属性导航到嵌套路由

  • 使用 history 程序化地导航到路由

  • 使用高阶组件 withRouter

  • 使用 <Prompt> 组件阻止路由转换

组件

使用 <Link> 组件导航到使用 <Route> 组件定义的现有路由。要导航到一个路由,将路由中使用的路径名指定为 to 属性的值:

import { Link } from 'react-router-dom';

class  App  extends  Component {
    render() {
        return (
            <div class="container">
                <nav>
                    **<Link to="/">Home</Link>**
                    **<Link to="/dashboard">Dashboard</Link>**
                </nav>
                <Route
                    path="/"
                    component={HomeComponent}
                    exact 
                />
                <Route
                    path="/dashboard"
                    component={DashboardComponent} 
                />
            </div>
        );
    }
} 

注意 to 属性的值与 <Route> 中分配给 path 属性的值相同。页面现在呈现两个链接:

当您点击主页时,您将看到显示的文本“Inside Home route”,当您点击仪表板时,您将被导航到其 path 属性设置为 /dashboard 的路由。

当您使用 <Link> 导航到一个路由时,会调用 history.push(),这会向历史堆栈添加一个条目。因此,当您点击浏览器的返回按钮时,您将被导航到之前访问的上一个路由(主页路由)。如前一章所述,React-Router 使用 history 库来在用户在应用程序旅程中穿越各种路由时维护应用程序的状态。

<Link> 组件还有两个其他属性——replaceinnerRef

replace 属性

replace属性在<Link>中调用history.replace(),用to属性中提到的新路径名替换历史堆栈中的当前条目:

<Link  to="/dashboard" replace>Dashboard</Link>

例如,如果您访问路径为/home的页面,则访问上述链接将用/dashboard替换历史堆栈中的当前条目,这基本上将条目/home替换为/dashboard

innerRef 属性

React 提供ref来获取对渲染的 DOM 元素的引用。然后可以使用此引用(ref)来执行常规流程之外的某些操作,例如聚焦输入元素,媒体播放等。<Link>是一个复合组件,在 DOM 上呈现一个锚元素。

在前面的代码片段中提到的<Link>组件翻译为以下锚元素:

..
<nav>
    <a href="/">Home</a>
    <a href="/dashboard">Dashboard</a>
</nav>
..

要获取对此渲染的锚元素的引用,需要将innerRef属性添加到<Link>中:

<nav> <Link to="/" innerRef={this.refCallback}> Home </Link> <Link to="/dashboard" innerRef={this.refCallback}> Dashboard </Link> </nav>

innerRef属性接受回调函数作为其值;在这里,函数refCallback被指定为innerRef属性的值。refCallback获取对<Link>组件的内部元素的引用:

refCallback(node) { node.onmouseover  = () => { node.focus(); } } 

回调函数refCallback<Link>组件挂载时被调用。从上述代码片段中,我们可以看到为两个<Link>组件渲染的锚元素都添加了mouseover处理程序。当用户悬停在链接上时,相应的锚点获得焦点。

带有对象的 to 属性

to属性可以是字符串,也可以是对象。该对象可以包含以下属性:

  • pathname:要导航到的路径

  • search:路径的查询参数,表示为字符串值

  • hash:要添加到 URL 的哈希字符串

  • state:包含渲染组件可以使用的状态信息的对象

使用这些参数,让我们添加一个<Link>组件:

<Link to={{ pathname:  '/user', search:  '?id=1', hash:  '#hash',
 state: { isAdmin:  true } }}>
 User </Link>

前面的代码翻译为以下内容:

<a href="/user?id=1#hash">User</a>

state信息不包含在 URL 路径中;但是,它可用于作为<Route>匹配的结果呈现的组件:

<Route path="/user" render={({ location }) => { const { pathname, search, hash, state } = location; return ( <div> Inside User route <h5>Pathname: {pathname}</h5> <h5>Search: {search}</h5> <h5>Hash: {hash}</h5> <h5>State: {'{'}  {Object.keys(state).map((element, index) => { return ( <span  key={index}> {element}: {state[element].toString()} </span> ) })}  {'}'} </h5> </div> ); }} />

location对象包含所有先前定义的参数,包括state对象。

state对象可用于在用户浏览应用程序时存储数据,并将此数据提供给由于<Route>匹配而呈现的下一个组件。

组件

<NavLink>组件类似于<Link>组件,不同之处在于可以指定多个属性,这些属性可以帮助您有条件地向呈现的元素添加样式属性。它接受与<Link>组件相同的一组属性(toreplaceinnerRef)用于导航到一个路由,并包括用于样式化选定路由的属性。

让我们来看看这些属性,它们可以帮助您为<NavLink>组件设置样式。

activeClassName 属性

默认情况下,类名active将应用于活动的<NavLink>组件。例如,当点击<NavLink>并呈现相应的路由时,所选的<NavLink>的类名将设置为active。要更改此类名,请在<NavLink>组件上指定activeClassName属性,并将其值设置为要应用的 CSS 类名:

<nav>
    <NavLink to="/">Home</NavLink> <NavLink to="/dashboard" activeClassName="selectedLink"> Dashboard
    </NavLink> </nav>

下一步是在应用程序的 CSS 文件中指定 CSS 类selectedLink的样式。请注意,第一个<NavLink>没有指定activeClassName属性。在这种情况下,当点击<NavLink>时,将添加active类:

<nav>
    <a class="active" aria-current="page" href="/">Home</a>
    <a aria-current="page" href="/dashboard">Dashboard</a>
</nav>

然而,当点击第二个<NavLink>时,将应用selectedLink类:

<nav>
    <a aria-current="page" href="/">Home</a>
    <a class="selectedLink" aria-current="page" href="/dashboard">Dashboard</a>
</nav>

activeStyle 属性

activeStyle属性也用于为选定的<NavLink>设置样式。但是,与其在<NavLink>被选中时提供一个类不同,可以在内联中提供 CSS 样式属性:

<NavLink
 to="/user" activeStyle={{ background:  'red', color:  'white' }}> User </NavLink>

exact 属性

当您点击具有to属性/dashboard<NavLink>时,active类(或在activeStyle属性中指定的内联样式)将应用于页面中的两个<NavLink>组件。与<Route>组件类似,/dashboard中的/to属性中指定的路径匹配,因此将active类应用于两个<NavLink>组件。

在这种情况下,exact属性可用于仅在路径与浏览器的 URL 匹配时应用active类或activeStyle

<NavLink
 to="/" exact> Home </NavLink> <NavLink to="/dashboard" activeClassName="selectedLink"> Dashboard </NavLink>

strict 属性

<NavLink>组件还支持strict属性,可用于匹配to属性中指定的尾随斜杠。

<NavLink
 to="/dashboard/"
 activeClassName="selectedLink"
 strict>
 Dashboard </NavLink>

在这里,当浏览器的 URL 路径匹配路径/dashboard/时,类selectedLink仅应用于<NavLink>组件,例如,当 URL 中存在尾随斜杠时。

isActive 属性

isActive 属性用于确定 <NavLink> 组件是否应用 active 类(或在 activeStyle 属性中指定的内联样式)。作为 isActive 属性值指定的函数应返回一个布尔值:

<NavLink
 to={{ pathname:  '/user', search:  '?id=1', hash:  '#hash', state: { isAdmin:  true } }} activeStyle={{ background:  'red', color:  'white' }} isActive={(match, location) => { if (!match) { return  false; } const  searchParams = new  URLSearchParams(location.search); return  match.isExact && searchParams.has('id'**)**; }}> User </NavLink>

从上面的例子中,该函数接受两个参数——matchlocation。仅当条件 match.isExact && searchParams.has('id') 评估为 true 时,才会应用在 activeStyle 属性中定义的样式,因此,只有当 matchexact 并且 URL 具有查询参数 id 时。

当浏览器的 URL 是 /user 时,与 <Route> 定义的相应路由将显示。然而,<NavLink> 组件将具有默认样式,而不是 activeStyle 属性中提到的样式,因为缺少查询参数 id

位置属性

<NavLink> 中的 isActive 函数接收浏览器的历史 location,并确定浏览器的 location.pathname 是否与给定条件匹配。要提供不同的位置,包括 location 属性:

<NavLink
 to="/user" activeStyle={{ background:  'red', color:  'white' }} location={{ search:  '?id=2', }**}** isActive={(match, location) => { if (!match) { return  false; } const  searchParams = new  URLSearchParams(location.search); return  match.isExact && searchParams.has('id'**)**; }}> User </NavLink>

请注意,to 属性没有指定 search 参数;然而,location 属性包括它,因此当浏览器的位置是 /user 时,isActive 函数返回 true,因为搜索参数包括 id 属性。

导航到嵌套路由

在上一章中,我们看到如何使用渲染组件接收的 match 属性创建嵌套路由。match.url 属性包含与 <Route> 组件的路径匹配的浏览器 URL 路径。同样,<Link><NavLink> 组件可用于创建导航链接以访问这些嵌套路由:

<nav>
 <Link to={`${match.url}/pictures`}> Pictures </Link> <NavLink to={`${match.url}/books`**}** activeStyle={{ background:  'orange' }}>
     Books
    </NavLink> </nav>

在前面的代码片段中,<Link><NavLink> 组件利用 match.url 来获取对当前渲染路由的引用,并添加所需的附加路径值以导航到嵌套路由。

使用历史对象以编程方式导航到路由

<Link><NavLink> 组件在页面上呈现锚链接,允许您从当前路由导航到新路由。然而,在许多情况下,当事件发生时,用户应该以编程方式导航到新的路由。例如,在登录表单中点击提交按钮时,用户应该被导航到新的路由。在这种情况下,渲染组件可用的 history 对象可以被使用:

export  const  DashboardComponent  = (props) => (    <div  className="dashboard"> Inside Dashboard route <button  onClick={() =>  props.history.push('/user')}> User </button> </div> );

在这里,DashboardComponentprops作为其参数,其中包含history对象。onClick处理程序调用props.history.push,路径名为/user。此调用将向历史堆栈添加一个条目,并将用户导航到路径为/user<Route>history对象还可以用于使用history.replace替换历史堆栈中的当前条目,而不是使用history.push

使用 withRouter 高阶组件

history对象可用于使用<Route>匹配渲染的组件。在前面的示例中,DashboardComponent作为导航到路径/dashboard的结果进行了渲染。渲染的组件接收了包含history对象(以及matchlocationstaticContext)的props。在页面上渲染的组件不是路由导航的结果时,history对象将不可用于该组件。

考虑在App.js中包含的FooterComponent

class  FooterComponent  extends  Component { render() { return ( <footer> In Footer <div> <button  onClick={() =>                         this.props.history.push('/user')}> User </button> <button  onClick={() =>                          this.props.history.push('/stocks')}> Stocks </button> </div> </footer> ) } }

FooterComponent有两个按钮,调用history.push导航到应用程序中的一个页面。单击按钮时,会抛出错误TypeError: Cannot read property 'push' of undefined。错误是因为history对象在props属性中不可用,因为该组件不是作为导航的结果进行渲染的。为了避免这种情况,使用高阶组件withRouter

export  const  Footer  =  withRouter(FooterComponent**)**;

在这里,react-router包中定义的withRouter函数接受一个 React 组件作为其参数,并增强它以在props属性上提供必要的对象—historymatchlocationstaticContext

有关 HOC 的 React 文档:高阶组件是一个接受组件并返回新组件的函数。尽管组件将 props 转换为 UI,但高阶组件将组件转换为另一个组件。

包装在withRouter HOC 中的组件可以使用<Route><Link><NavLink>定义路由和导航链接:

import { withRouter } from 'react-router';

class  FooterComponent  extends  Component { render() { return ( <footer> In Footer <div> <button  onClick={() =>                  this.props.history.push('/user')}>User</button> <button  onClick={() =>                   this.props.history.push('/stocks')}>Stocks</button> <Link  to='subroute'>User</Link> <Route path='/subroute' render={() => { return  <span>Inside Footer Subroute</span> }}  /> </div> </footer  > ) } } export const Footer = withRouter(FooterComponent);

在前面的代码片段中,withRouter HOC 使组件能够获取路由器的上下文,因此使诸如LinkNavLinkRoute之类的组件可用。

使用阻止转换

当您在应用程序中的页面之间导航时,转换到新路由会立即发生。然而,有些情况下,您希望根据应用程序的状态来阻止这种转换。一个常见的例子是,当用户在表单字段中输入数据并花费了几分钟(或几个小时)填写表单数据时。如果用户意外点击导航链接,所有在表单中输入的数据将丢失。用户应该被通知这种路由导航,以便用户有机会保存输入到表单中的数据。

传统网站会跟踪表单的状态,并在用户尝试离开包含尚未提交到服务器的表单的页面时显示确认消息。在这些情况下,将显示一个带有两个选项(OK 和 CANCEL)的确认对话框;前者允许用户转换到下一步,后者取消转换:

React-Router 提供了<Prompt>组件,可以用来显示确认对话框,以防止用户意外离开当前的<Route>

import { **Prompt** } from 'react-router-dom'

**<**Prompt
 when={this.state.isFormSubmitted} message='Are you sure?'  />

<Prompt>组件在这里接受两个属性——whenmessage。从前面的代码片段可以看出,如果state属性isFormSubmitted的值为true,并且用户尝试离开当前路由时,将向用户显示带有消息“您确定吗?”的确认对话框。

请注意,只有当用户尝试离开当前路由时,才会显示<Prompt>消息。当state属性设置为true时,不会显示任何消息。

分配给when属性的值可以是任何布尔变量或布尔值。在 React 中,组件的state被用作视图模型来维护呈现组件的状态。在这种情况下,state属性非常理想,可以确定当用户尝试离开当前路由时是否应该显示<Prompt>

message属性的值可以是字符串或函数:

<Prompt
 when={this.state.isFormSubmitted} message={(location) => 'Are you sure you want to navigate to ${location.pathname}?'}  />

该函数接收location参数,其中包括用户试图导航到的路由的位置信息。

'react-router-dom'包中的其他组件类似,<Prompt>组件应该在渲染的<Route>内使用。当您尝试在没有当前路由上下文的情况下使用<Prompt>时,会显示消息。您不应该在<Router>之外使用<Prompt>

还可以通过不包括when属性来在用户尝试离开当前路由时(不考虑应用程序的state)显示消息。

<Prompt  message=**'Are you sure?**'  />

往往在<Prompt>中包含when属性,并且分配给when属性的值用于确定是否应该显示确认对话框。

在尝试这些示例时,请确保给定的<Route>只有一个<Prompt>,否则库将报告警告历史记录一次只支持一个提示

总结

在本章中,我们看了如何使用<Link><NavLink>导航组件导航到应用程序中定义的各种路由。这些组件在页面中呈现anchor链接,当用户点击这些链接时,页面的部分会更新,而不是进行完整的页面重新加载,从而提供清晰的用户体验。<Link>组件接受toreplaceinnerRef等 props。

<NavLink>组件类似于<Link>组件,并接受<Link>组件使用的所有 props。除了向页面添加链接外,<NavLink>组件还接受几个 props——activeClassNameactiveStyleexactstrictisActive

要创建到嵌套路由的链接,<Link><NavLink>组件可以在to属性中使用前缀match.url。此外,您还可以在事件处理程序函数中使用history.pushhistory.replace进行程序化导航。通过withRouter高阶组件,可以使historymatchlocationstaticContext等 props 在 Route 上下文之外呈现的组件中可用。'react-router-dom'包包括一个<Prompt>组件,可用于在用户尝试通过意外点击导航链接导航到路由时显示确认对话框。<Prompt>组件接受whenmessage属性,并根据分配给when属性的布尔值,将显示在message属性中指定的消息给用户。

在第四章中,使用重定向和切换组件,我们将看看<Redirect><Switch>组件。此外,我们将看到这些组件如何用于保护路由,并在页面中没有任何路由匹配请求的 URL 时显示一个未找到页面。

第四章:使用重定向和切换组件

使用 React-Router 的<Redirect>组件可以将用户从一个路由重定向到另一个路由。在传统网站中,页面是在服务器端呈现的,托管应用程序的 Web 服务器配置了重写规则,将用户重定向到不同的 URL。当内容已经移动到新页面或者网站的某些页面仍在建设中时,可以使用此重定向。HTTP 重定向是一项昂贵的操作,因此也会影响应用程序的性能。

在单页应用程序(SPA)中,重定向发生在浏览器上,根据特定条件将用户重定向到不同的路由。这种重定向更快,因为没有涉及 HTTP 往返,并且转换类似于使用<Link><NavLink>组件从一个路由导航到另一个路由。

本章讨论以下主题:

  • <Redirect>组件:将用户从一个路由重定向到另一个路由

  • 保护路由和授权:一种情况是当用户尝试访问受保护的路由时,将用户重定向到登录页面

  • <Switch>组件:渲染第一个匹配的<Route>

  • 添加 404 页面未找到页面:一种情况是当没有任何<Route>组件匹配浏览器的 URL 路径时,使用<Switch><Route><Switch><Redirect>组件来渲染 404 页面

<Redirect>组件

<Redirect>组件包含在react-router-dom包中。它帮助将用户从包含它的组件重定向到'to'属性中指定的路由:

import { Redirect } from 'react-router-dom';

export class HomeComponent extends Component {
    render() {
        return (
            <Redirect to='/dashboard' />
        )
    }
}

在上述情况下,当HomeComponent被渲染时(基于<Route>匹配),用户将被重定向到'/dashboard'路由。例如,当用户访问主页(路径为'/')时,具有路径'/'<Route>会渲染先前的组件,然后用户立即被重定向到具有路径值'/dashboard'<Route>。这类似于使用带有'to'属性的<Link><NavLink>组件将用户导航到不同的路由。在这里,重定向发生在组件被渲染时,而不是作为用户操作的结果触发导航。

先前提到的重定向示例在应用程序中的某些页面已经移动到不同的目录的情况下是理想的。

<Redirect>组件类似于 React-Router 中的其他组件,如<Route><Link>。正如之前观察到的那样,它是一个可以包含在渲染函数中的 React 组件。此外,<Redirect>组件接受与<Link>组件相似的一组 props。

to 属性

to 属性用于指定用户应该被重定向到的路由。如果找到匹配的<Route>,用户将被重定向到指定的路径,并渲染相应的组件。

to 属性还可以接受一个对象,该对象指定了pathnamesearchhashstate属性的值:

<Redirect 
    to={{
        pathname: '/dashboard',
        search: '?q=1',
        hash: '#hash',
        state: { from: match.url }
      }} 
/>

<Link>组件类似,前面提到的属性被指定在<Redirect>组件的 to 属性中。请注意,状态属性的值为{ from: match.url }。在这里,match.url提供了浏览器 URL 路径的当前值,然后在重定向发生时将该值提供给渲染的组件。

然后,渲染的组件可以使用this.props.location.state来读取状态信息:

export class DashboardComponent extends Component {
    render() {
        const { location } = this.props;
        return (
            <div>
                In DashboardComponent <br />
                From : {location.state.from}
            </div>
        )
    }
}

在前面的示例中,DashboardComponent作为从HomeComponent重定向的结果进行渲染。location.state.from的值与被重定向的组件共享有关重定向发生的页面的路径信息。当您希望被重定向到一个通用页面,并且被重定向的页面必须显示有关重定向发生的路径的信息时,这将非常有用。例如,当应用程序发生错误时,用户应该被重定向到一个呈现错误消息的页面,提供有关发生错误的页面的信息。在这种情况下,状态信息可以包括属性errorMessagefrom;后者的值为match.url,即发生错误的页面。

如果未找到被重定向的<Route>,浏览器的 URL 将被更新,不会抛出错误。这是有意设计的;理想情况下,如果没有匹配的路由,用户应该被重定向到一个404Page Not Found页面。在下一节中将讨论当没有匹配时渲染的<Route>

在组件内部,当您尝试重定向到相同的路由时,React-Router 会抛出警告消息 Warning: You tried to redirect to the same route you're currently on: "/home"。这个检查确保重定向不会导致无限循环。

还可能遇到这样的情况,被重定向的组件在其渲染方法中包含一个<Redirect>,将用户重定向回相同的路由,也就是说,按照这个路由重定向path: /home => /dashboard => /home。这样会一直循环,直到 React 停止渲染组件;然后 React 会抛出一个错误,最大更新深度超过。当组件在componentWillUpdatecomponentDidUpdate中重复调用setState时,就会发生这种情况。React 限制了嵌套更新的次数,以防止无限循环。React-Router 使用状态来跟踪用户在应用程序旅程中的位置,因此在重定向时,由于重定向的原因,React 尝试多次更新状态而导致前面的错误发生。在处理重定向时,您需要确保它不会导致无限循环的重定向。

推送属性

<Redirect>组件通过调用history.replace(<path>)将用户重定向到给定的路径,即用新路径替换历史堆栈中的当前条目。通过在<Redirect>组件中指定推送属性,将调用history.push而不是history.replace

<Redirect to="/dashboard" push />

保护路由和授权

使用<Route>组件定义的路由可以通过浏览器的 URL 访问,通过使用<Link><NavLink>导航到路由,或者通过使用<Redirect>组件将用户重定向。但是,在大多数应用程序中,一些路由应该只对授权或已登录的用户可用。例如,假设/user路径显示已登录用户的数据;这个路径应该受到保护,只有已登录用户才能访问该路径。在这些情况下,当您尝试访问路径/user时,<Redirect>组件非常有用,它会将用户重定向到登录页面(在路径/login)。

为了证明这一点,让我们创建一个名为UserComponent的组件,当您尝试访问路径/user时,它将被渲染出来:

export class UserComponent extends Component {
    render() {
        const { location } = this.props;
        return (
            <div>
                Username: {location && location.state ? location.state.userName 
                 : ''} <br />
                From: {location && location.state ? location.state.from : ''} 
                 <br />
                <button onClick={this.logout}>LOGOUT</button>
            </div>
        )
    }
}

从前面的代码片段中,我们可以看到UserComponent显示了在this.props.location中可用的状态信息和 LOGOUT 按钮。

要检查用户是否已登录,应该向服务器发出请求,以检查用户的会话是否存在。但是,在我们的情况下,通过引用浏览器的localStorage中的变量来检查用户是否已登录:

export class UserComponent extends Component {
    state = {
       isUserLoggedIn: false
    }
    componentWillMount() {
        const isUserLoggedIn = localStorage.getItem('isUserLoggedIn');
        this.setState({isUserLoggedIn});
    }
    render() {
    ...
    }
}

在这里,组件的状态属性isUserLoggedIn将使用存储在同名 localStorage 变量中的值进行更新。

下一步是在UserComponent类的渲染函数中使用此状态信息,并使用<Redirect>组件重定向用户:

export class UserComponent extends Component {
    ...
    render() {
        const { location } = this.props;
        if (!this.state.isUserLoggedIn) {
            return (
                <Redirect to="/login" />
            );
        }
        ...
    }
}

在这里,将检查状态属性isUserLoggedIn的值,如果评估为 false,或者未找到,则将用户重定向到路径'/login'

最后一步是实现logout函数,当用户点击 LOGOUT 按钮时调用:

export class UserComponent extends Component {
    logout = (event) => {
        localStorage.removeItem('isUserLoggedIn');
        this.setState({ isUserLoggedIn: false });
    }
    ...
}

登出用户涉及删除localStorage变量并将状态属性isUserLoggedIn更新为'false'

有了这些更改,当状态属性isUserLoggedIn设置为 false 时,UserComponent会重新渲染,并将用户重定向到路径/login,要求用户提供凭据以访问页面。此外,现在当您尝试通过在浏览器地址栏中输入路径/user来访问时,具有路径属性/user<Route>将匹配。然而,当UserComponent被渲染时,状态属性isUserLoggedIn将评估为 false,将用户重定向到/login页面。

使用回调路由进行重定向

当您尝试访问受保护的<Route>时,将被重定向到登录页面以提供凭据。提供凭据后,您应该被重定向到之前尝试访问的页面。例如,当您尝试访问路径/stocks的受保护路由时,您将被重定向到路径/login,然后,在提供正确的凭据后,您应该被重定向到之前尝试访问的相同路径/stocks。然而,根据先前的示例,您将被重定向到路径/user,并显示用户的个人资料信息。期望的行为是重定向到受保护的路径/stocks,而不是路径/user

这可以通过在重定向用户时提供状态信息来实现。

StocksComponent(作为<Route>匹配结果呈现的组件,/stocks),当您将用户重定向到登录页面时,在 to 属性中提供状态信息:

export class StocksComponent extends Component {
    ...
    render() {
        const {match } = this.props;
        if (!this.state.isUserLoggedIn) {
            return (
                <Redirect 
                    to={{
                        pathname: "/login",
                        state: { callbackURL: match.url }
                    }}
                />
            )
        }

        return (
            <div>
                In StocksComponent
            </div>
        )
    }
}

在组件的渲染函数中,用户使用<Redirect>组件被重定向到登录页面。这里的<Redirect>组件包括一个 to 属性,指定用户应该被重定向到的pathname,它还包括一个状态对象,提到了callbackURL属性。callbackURL属性的值是match.url,即当前浏览器的 URL 路径/stocks

然后可以在LoginComponent中使用这些状态信息将用户重定向到路径/stocks

export class LoginComponent extends Component {
    ...
    render() {
        const { location: { state } } = this.props;
        if (this.state.isUserLoggedIn) {
            return (
                <Redirect 
                    to={{
                        pathname: state && 
                        state.callbackURL || "/user",
                        state: {
                            from: this.props.match.url,
                            userName: this.state.userName
                        }
                    }} 
                />
            )
        }
        ...
    }
}

在这里,当用户提供凭据访问受保护的路由时,<Redirect>组件将用户重定向到state.callbackURL中提到的路径。如果callbackURL不可用,用户将被重定向到默认路由,该路由将重定向到路径/user

Route 组件的 props、match.url和 location.state 的组合可以用来将用户重定向到之前请求的受保护路由。

使用组件进行独占路由

当 URL 被提供给<BrowserRouter>时,它将寻找使用<Route>组件创建的路由,并渲染所有与浏览器 URL 路径匹配的路由。例如,考虑以下路由:

<Route
    path="/login"
    component={LoginComponent}
/>
<Route
    path="/:id"
    render={({ match }) => 
        <div> Route with path {match.url}</div>
    }
/>

在这里,具有路径/login/:id的两个路由都匹配/login的 URL 路径。React-Router 渲染所有与 URL 路径匹配的<Route>组件。然而,为了只渲染第一个匹配的路由,该库提供了<Switch>组件。<Switch>组件接受一组<Route>组件作为其子组件,并且只渲染与浏览器 URL 匹配的第一个<Route>

<Switch>
    <Route
        path="/login"
        component={LoginComponent}
    />
    <Route
        path="/:id"
        render={({ match }) =>
            <div> Route with path {match.url}</div>
        }
    />
</Switch>

通过将一组<Route>组件包装在<Switch>组件内,React-Router 会顺序搜索与浏览器 URL 路径匹配的<Route>。一旦找到匹配的<Route><Switch>就会停止搜索并渲染匹配的<Route>

在上面的例子中,如果浏览器的 URL 路径是/login,那么<Switch>中的第一个<Route>将被渲染,而除/login 之外的路径(如/123、/products、/stocks 等)将匹配第二个路由并渲染相应的组件。

如果交换前两个<Route>组件的顺序(即,将具有路径/:id 的<Route>列在具有路径/login 的<Route>之上),那么具有路径/login 的<Route>将永远不会被渲染,因为<Switch>只允许渲染一个第一个匹配的路由。

组件的顺序

<Switch><Route>组件的顺序很重要,因为<Switch>组件会顺序查找匹配的<Route>,一旦找到与浏览器 URL 匹配的<Route>,就会停止搜索。这种行为可能不是期望的,您可能希望渲染<Switch>中列出的另一个路由。但是,可以通过更改在<Switch>中列出<Route>的顺序来纠正这一点:

在以下示例中,提到了在<Switch>中列出<Route>组件时的一些常见错误:

带有路径'/'的<Route>作为<Switch>的第一个子级

考虑以下代码片段:

<Switch>
    <Route
        path="/"
        component={LoginComponent}
    />
    <Route
        path="/dashboard"
        component={DashboardComponent}
    />
</Switch>

如果浏览器的 URL 路径是/dashboard,它将匹配第一个路径为/<Route>,而路径为/dashboard<Route>将永远不会匹配和渲染。要解决这个问题,要么包括exact属性,要么将路径为/<Route>列为<Switch>中的最后一个条目。

带有路径参数的<Route>

在以下代码片段中,将带有路径参数的<Route>列为第二个条目:

<Switch>
    <Route
        path="/github"
        component={LoginComponent}
    />
    <Route
        path="/github/:userId"
        component={DashboardComponent}
    />
</Switch>

在上一个示例中,路径为/github<Route>将匹配 URL 路径/github以及路径/github/mjackson;因此,即使有特定路径的<Route>可用,第一个<Route>也会被渲染。要解决这个问题,要么提供exact属性,要么将路径为/github<Route>列在路径为/github/:userId<Route>下面。

从前一段提到的两种情况中,将具体路径的<Route>组件列在通用路径的<Route>组件上面,可以避免不良结果。

添加 404 - 未找到页面

如前所述,<Switch>组件会顺序查找所有<Route>组件,一旦找到与浏览器 URL 匹配的<Route>,就会停止搜索。这与在页面中列出<Route>的列表不同,页面中的每个匹配的<Route>都会被渲染。因此,<Switch>非常适合渲染Page Not Found页面,即在<Switch>的子级中没有任何匹配浏览器 URL 的<Route>时渲染一个组件。

让我们在<Switch>中包含一个没有路径属性的<Route>作为最后一个条目:

<Switch>
    <Route
        path="/login"
        component={LoginComponent}
    />
    <Route
        path="/user"
        render={({ match }) =>
            <div> Route with path {match.url}</div>
        }
    />
    <Route
        render={({ location }) =>
            <div> 404 - {location.pathname} not 
            found</div>
        }
    />
</Switch>

从前面的代码片段中,我们可以看到当没有任何带有路径属性的<Route>与浏览器的 URL 匹配时,最后一个没有路径属性的<Route>将匹配并渲染。

包括Page Not Found <Route>作为最后一个条目是很重要的,因为<Switch>组件一旦找到匹配的<Route>就会停止搜索。在前面的情况下,如果没有属性的<Route>被包括在其他<Route>上面,那么即使列表中存在与浏览器 URL 匹配的<Route>Page Not Found路由也会被渲染。

您还可以指定一个<Route>,其路径属性值为*,而不是没有路径属性的<Route>,以渲染Page Not Found页面:

<Switch>
    ...
    <Route
        path="*"
        render={({ location }) =>
            <div> 404 - {location.pathname} not 
            found</div>
        }
    />
</Switch>

在这两种情况下,路径将匹配浏览器的 URL 并渲染Page Not Found页面。

<Switch>中使用<Redirect>重定向到一个 Page Not Found 页面

<Switch>组件的子元素也可以包括一系列<Route><Redirect>组件。当包括为<Switch>中的最后一个条目时,<Redirect>组件将在没有任何在<Redirect>组件上面提到的<Route>匹配浏览器 URL 时将用户重定向到给定路径:

<Switch>
    <Route
        path="/login"
        component={LoginComponent}
    />
    <Route
        path="/user"
        render={({ match }) =>
            <div> Route with path {match.url}</div>
        }
    />
    <Redirect to="/home" />
</Switch>

前面提到的<Redirect>组件将用户重定向到路径为/home<Route>。这类似于显示404:Page Not Found页面;而不是在行内显示组件,用户被重定向到不同的路径。

例如,如果浏览器的 URL 路径是/dashboard,前两个路径(路径为/login/user)不会匹配,因此用户将使用在<Switch>中作为最后一个条目提到的<Redirect>组件进行重定向。

从旧路径重定向到新路径

<Redirect>组件也可以用于将用户从给定路径重定向到新路径。<Redirect>组件接受一个from属性,该属性可用于指定应该匹配用户应该被重定向的浏览器 URL 的路径。此外,应该在to属性中指定用户应该被重定向到的路径。

<Switch>
    <Route
        path="/login"
        component={LoginComponent}
    />
    <Route
        path="/user"
        render={({ match }) =>
            <div> Route with path {match.url}</div>
        }
    />
    <Redirect
        from="/home"
        to="/login"
    />
    <Redirect to="/home" />
</Switch>

从前面的例子中,我们可以看到当浏览器的 URL 路径是/home时,具有from属性的<Redirect>组件将匹配给定路径并将用户重定向到路径为/login<Route>

<Redirect>组件的from属性在网站上的一些页面已经移动到新目录时非常有用。例如,如果用户页面已经移动到新的目录路径settings/user,那么<Redirect from="/user" to="/settings/user" />将把用户重定向到新路径。

总结

<Redirect>组件可用于将用户从当前渲染的路由重定向到新的路由。该组件接受 props:to 和 push。当应用程序中的组件已经移动到不同的目录,或者用户未被授权访问页面时,可以使用此重定向。<Redirect>组件在用户访问受保护的路由并且只有授权用户被允许查看页面时非常有用。

<Switch>组件用于在一组<Route>中只渲染一个<Route><Switch>组件接受<Route><Redirect>组件的列表作为其子组件,并依次搜索匹配的<Route><Redirect>组件。当找到匹配时,<Switch>渲染该组件并停止寻找匹配的路径。

<Switch>的这种行为可以用来构建一个404:页面未找到,当<Switch>中列出的<Route>组件都不匹配浏览器的 URL 路径时,将会渲染该页面。通过在<Switch>的最后一个条目中列出一个没有任何路径属性的<Route>,如果上面列出的<Route>组件都不匹配浏览器的 URL 路径,那么将会渲染该<Route>。另外,也可以将<Redirect>组件列为最后一个条目,以在<Switch>中没有匹配的<Route>组件时将用户重定向到另一个页面。

第五章:理解核心路由器,并配置BrowserRouterHashRouter组件

React-Router 库提供了几个组件,用于解决各种用例,例如使用<Link><NavLink>添加导航链接,使用<Redirect>组件重定向用户等。<BrowserRouter>组件包装了应用程序的根组件(<App />),并使这些组件能够与history对象交互。当应用程序初始化时,<BrowserRouter>组件初始化history对象,并使用 React 的context使其可用于所有子组件。

单页应用程序中的路由实际上并不是真正的路由;相反,它是组件的条件渲染。<BrowserRouter>组件创建了history对象,history对象具有诸如pushreplacepop等方法,这些方法在导航发生时被使用。history对象使应用程序能够在用户在页面之间导航时保持历史记录。除了<BrowserRouter>,React-Router 还提供了各种 Router 实现——<HashRouter><StaticRouter><MemoryRouter><NativeRouter>。这些路由器利用了包含在react-router核心包中的低级Router接口。

在本章中,我们将看一下低级<Router>组件和各种路由器实现:

  • <Router>react-router

  • <BrowserRouter>属性

  • HashRouter——用于在旧版浏览器中使用的 Router 实现

其他<Router>实现,如<StaticRouter><MemoryRouter><NativeRouter>,将在接下来的章节中讨论。

<Router>组件

如前所述,React-Router 提供了各种 Router 实现:

  • <BrowserRouter>

  • <HashRouter>

  • <MemoryRouter>

  • <StaticRouter>

  • <NativeRouter>

这些路由器利用了低级接口<Router><Router>组件是react-router包的一部分,<Router>接口提供的功能由这些 Router 实现扩展。

<Router>组件接受两个 props——historychildrenhistory对象可以是对浏览器历史记录的引用,也可以是应用程序中维护的内存中的历史记录(这在原生应用程序中很有用,因为浏览器历史记录的实例不可用)。<Router>组件接受一个子组件,通常是应用程序的根组件。此外,它创建一个context对象,context.router,通过它,所有后代子组件,如<Route><Link><Switch>等,都可以获得history对象的引用。

来自 reactjs.org:

上下文提供了一种通过组件树传递数据的方式,而无需在每个级别手动传递 props。

通常不使用<Router>接口来构建应用程序;而是使用适合给定环境的高级别 Router 组件之一。使用<Router>接口的常见用例之一是将自定义的history对象与诸如ReduxMobX之类的状态管理库同步。

包括来自 react-router 的

核心的react-router包可以通过npm安装:

npm install --save react-router

Router类然后可以包含在应用程序文件中:

import { Router } from 'react-router'

下一步是创建一个history对象,然后将其作为值提供给<Router>history prop:

import  createBrowserHistory  from  'history/createBrowserHistory'; const customHistory = createBrowserHistory()

在这里,使用history包中的createBrowserHistory类来为浏览器环境创建history对象。history包包括适用于各种环境的类。

最后一步是用<Router>组件包装应用程序的根组件并渲染应用程序:

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

注意,<Router>组件接受一个history prop,其值是使用createBrowserHistory创建的history对象。与<BrowserRouter>组件类似,<Router>组件只接受一个子组件,在有多个子组件时会抛出错误。

React 允许其 prop 值发生变化,并在检测到变化时重新渲染组件。在这种情况下,如果我们尝试更改分配给 history prop 的值,React-Router 会抛出警告消息。考虑以下代码片段:

class  App  extends  Component { state  = { customHistory:  createBrowserHistory() } componentDidMount() { this.setState({ customHistory:  createBrowserHistory() **});** } render() { return ( <Router  history={**this**.state.customHistory}> <Route path="/" render={() =>  <div> In Home </div>}  /> </Router> ); } }

在前面的例子中,state 属性customHistory包含了提供给<Router>组件的history对象。然而,当customHistory的值在componentDidMount生命周期函数中改变时,React-Router 会抛出警告消息 Warning: You cannot change history。

react-router 包

react-router包括一些核心组件,比如之前提到的<Router>组件。该包还包括其他一些组件,然后被react-router-domreact-router-native包中的组件使用。react-router包导出这些组件:

export MemoryRouter from "./MemoryRouter";
export Prompt from "./Prompt";
export Redirect from "./Redirect";
export Route from "./Route";
export Router from "./Router";
export StaticRouter from "./StaticRouter";
export Switch from "./Switch";
export generatePath from "./generatePath";
export matchPath from "./matchPath";
export withRouter from "./withRouter";

这里提到的一些组件在之前的章节中已经讨论过。该包还提供了一些辅助函数,比如generatePathmatchPath,以及 Router 实现,比如<MemoryRouter><StaticRouter>react-router-domreact-router-native中定义的组件和服务导入了这些组件和服务,并包含在各自的包中。

react-router-dom 包

react-router-dom包提供了可以在基于浏览器的应用程序中使用的组件。它声明了对react-router包的依赖,并导出以下组件:

export BrowserRouter from "./BrowserRouter";
export HashRouter from "./HashRouter";
export Link from "./Link";
export MemoryRouter from "./MemoryRouter";
export NavLink from "./NavLink";
export Prompt from "./Prompt";
export Redirect from "./Redirect";
export Route from "./Route";
export Router from "./Router";
export StaticRouter from "./StaticRouter";
export Switch from "./Switch";
export generatePath from "./generatePath";
export matchPath from "./matchPath";
export withRouter from "./withRouter";

请注意,这里提到的一些组件也包含在react-router包中。react-router-dom中的组件导入了react-router中定义的组件,然后导出它们。例如,看一下<Route>组件:

import { Route } from "react-router";
export default Route;

BrowserRouter<HashRouter><MemoryRouter>的 Router 实现会创建一个特定于给定环境的history对象,并渲染<Router>组件。我们很快将会看一下这些 Router 实现。

react-router-native包使用了react-router中的<MemoryRouter>实现,并提供了一个<NativeRouter>接口。NativeRouter的实现和其打包细节将在接下来的章节中讨论。

组件

<BrowserRouter>组件在第一章中简要讨论过。正如其名称所示,<BrowserRouter>组件用于基于浏览器的应用程序,并使用 HTML5 的 history API 来保持 UI 与浏览器的 URL 同步。在这里,我们将看一下该组件如何为浏览器环境创建history对象并将其提供给<Router>

<BrowserRouter>组件接受以下属性:

static propTypes = {
    basename: PropTypes.string,
    forceRefresh: PropTypes.bool,
    getUserConfirmation: PropTypes.func,
    keyLength: PropTypes.number,
    children: PropTypes.node
};

<Router>接口类似,<BrowserRouter>只接受一个子组件(通常是应用程序的根组件)。前面代码片段中提到的children属性指的是这个子节点。使用history包中的createBrowserHistory方法来创建一个用于初始化<Router>history对象:

import { createBrowserHistory as createHistory } from "history";
import Router from "./Router";

class  BrowserRouter  extends  React.Component {    ...
    history = createHistory(this.props);
    ...
    render() {
        return <Router 
                   history={this.history}
                   children={this.props.children}
               />;
    }
}

在前面的代码片段中,<BrowserRouter>使用提供的属性使用history/createBrowserHistory类创建一个history对象。然后渲染<Router>组件,并从属性中提供创建的history对象和children对象。

basename 属性

basename属性用于为应用程序中的所有位置提供基本的 URL 路径。例如,如果您希望在/admin路径上呈现应用程序,而不是在根路径/上呈现,则在<BrowserRouter>中指定basename属性:

<BrowserRouter basename="/admin">
    <App />
</BrowerRouter>

basename属性现在将基本 URL 路径/admin添加到应用程序中。当您使用<Link><NavLink>进行导航时,basename路径将添加到 URL 中。例如,考虑以下带有两个<Link>组件的代码:

<BrowserRouter  basename="/admin">
 <div  className="component">
 <nav> <Link  to="/">Home</Link**>** <Link  to="/dashboard">Dashboard</Link**>** </nav>
    </div> </BrowserRouter>

当您点击Home链接(路径/)时,您会注意到 URL 路径更新为/admin而不是/。当您点击Dashboard链接时,更新后的 URL 路径为/admin/dashboard。使用<BrowserRouter>中的basename属性,前面的<Link>组件转换为以下内容:

<a href='/admin'>Home</a>
<a href='/admin/dashboard'>Dashboard</a>

锚链接的href属性前缀为/admin路径。

forceRefresh 属性

forceRefresh属性是一个布尔属性,当设置为true时,导航到任何路由都会导致页面刷新 - 而不是更新页面的特定部分,整个页面都会重新加载:

<BrowserRouter forceRefresh={true}>
    <Link to="/dashboard">Dashboard</Link>
</BrowserRouter>

当您点击导航链接Dashboard时,您会注意到在请求 URL 路径/dashboard时页面重新加载。

keyLength 属性

keyLength属性用于指定location.key的长度。locaction.key属性表示提供给位置的唯一键。看一下以下代码片段:

<BrowserRouter keyLength={10}>
    <div  className="container"> <nav> <Link  to="/dashboard">Dashboard</Link> <Link  to="/user">User</Link> </nav> <Route path="/dashboard" render={({ location }) => <div> In Dashboard, Location Key: {location.key}  </div> }
        />
        <Route path="/user" render={({ location }) => <div> In User, Location Key: {location.key}  </div> }
        />
    </div>
</BrowserRouter>

当您导航到/dashboard/user路径中的任何一个时,location.key的值将是一个长度为 10 的随机字母数字字符串。默认情况下,用于生成密钥的keyLength属性的值为 6。

当您使用导航链接在/dashboard/user路径之间来回导航时,您会注意到每次导航都会生成一个新的键。这是因为当您使用导航链接导航时,会调用history.push并生成一个新的键,而该键对于历史堆栈中的每个条目都是唯一的。因此,当您通过单击浏览器的后退按钮导航时,将调用history.pop,您会注意到为位置生成的键,并且不会生成新的键。

getUserConfirmation 属性

getUserConfirmation属性接受一个函数作为其值,并且当用户发起的导航被<Prompt>组件阻止时执行。<Prompt>组件使用window.confirm方法显示一个确认对话框,并且仅当用户单击确定按钮时才将用户导航到所选路径。然而,当<BrowserRouter>组件指定了getUserConfirmation属性时,将执行作为该属性值的函数。这提供了显示自定义对话框的机会。

让我们看一下以下配置:

<BrowserRouter getUserConfirmation={this.userConfirmationFunc**}**>
    <div className="container">
        <nav>  <Link  to="/dashboard">Dashboard</Link> <Link  to="/user">User</Link> </nav>
        <Route path="/dashboard" render={({ location }) => <div> In Dashboard, Location Key: {location.key}  </div> }
        />
        <Route path="/user" render={({ location }) => <div> In User, Location Key: {location.key} <Prompt  message="This is shown in a confirmation 
                     window" **/>** </div> }
        />
    </div>
</BrowserRouter>

假设当前的 URL 路径是/user,您尝试通过单击nav菜单中提供的导航链接来导航到不同的路由,比如/dashboard。如果未指定getUserConfirmation属性,则会显示<Prompt>消息。在这种情况下,将执行在组件类中定义的userConfirmationFunc函数。

您可以调用window.confirm来显示一个确认对话框,询问用户是否导航:

userConfirmationFunc  = (message, callback) => { const  status  =  window.confirm(message); callback(status**);** }

该函数接受两个参数——messagecallbackmessage参数指定需要显示的消息,而<Prompt>组件中包含的message属性提供了该值。该函数预计执行作为第二个参数提供的回调函数。

在这里,<BrowserRouter>的第二个参数提供了一个回调函数。使用提供的message调用window.confirm函数,用户将看到两个按钮——确定和取消;单击确定时,status设置为 true,单击取消时,status设置为false。将使用作为第二个参数提供的callback函数调用此status值;这是一个允许用户导航到所选路由的真值。

这是默认行为;在允许用户导航到所选页面之前,会显示一个原生浏览器确认对话框。然而,这种行为可以在前面提到的userConfirmationFunc中进行更改;你可以显示一个自定义对话框,而不是显示浏览器的原生确认对话框。

使用 getUserConfirmation prop 显示自定义对话框

为了这个例子,让我们添加material-UI,其中包括一个自定义对话框组件:

npm install --save @material-ui/core

让我们创建一个自定义对话框,将Dialog组件包装在@material-ui/core中:

import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from  '@material-ui/core'; export  class **ConfirmationDialog** extends  Component { render() { const { message, handleClose, isOpen } =  this.props; return ( <Dialog open={isOpen**}**> <DialogTitle>Custom Prompt</DialogTitle> <DialogContent>{message}</DialogContent> <DialogActions> <Button onClick={handleClose.bind(this, true)}> OK
                    </Button> <Button  onClick={handleClose.bind(this, false)}> CANCEL
                    </Button> </DialogActions> </Dialog> )
    }
}

这个组件接受三个 props——messagehandleCloseisOpenmessage prop 是你想在自定义对话框中显示的消息,handleClose prop 是一个函数引用,当用户点击 OK 或 CANCEL 按钮时调用该函数引用,分别允许或取消转换到所选路径。

让我们在根组件文件(在App.js中)中使用这个,并在用户尝试导航到不同的路由时显示ConfirmationDialog

class  App  extends  Component {    state  = { showConfirmationDialog:  false, message:  '', callback:  null }
    ...

我们首先在 React 组件中将state属性设置为它们的初始值。当用户尝试导航到不同的路由时,前面提到的state属性会发生变化:

... userConfirmationFunc  = (message, callback) => { this.setState({ showConfirmationDialog:  true, message:  message, callback:  callback });
    }

前面的userConfirmationFunc函数设置state属性,以便在用户尝试离开当前路由时显示自定义确认对话框(ConfirmationDialog)。

App组件中定义的以下handleClose函数将提供给我们之前创建的ConfirmationDialog组件:

    ...
 handleClose(status) { this.state.callback(status**)**; this.setState({ showConfirmationDialog:  false, message:  '', callback:  null })
    }

这为我们提供了一种隐藏自定义确认对话框和将组件的state属性重置为它们的初始值的方法。this.state.callback(status)语句将关闭确认对话框,并且根据状态为真还是假,将用户导航到所选路由(如果状态为真)或取消导航(如果状态为假)。

这是组件类的更新渲染方法:

    ...
    render() { return ( <BrowserRouter getUserConfirmation={this.userConfirmationFunc**}**> ...
                <Route path="/user" render={({ location }) => {
                        return ( <div> In User, Location Key: {location.key} <Prompt  message="This is shown in a 
                             confirmation modal" **/>** </div>
                        ); }}
                />
                <ConfirmationDialog isOpen={this.state.showConfirmationDialog} message={this.state.message} handleClose={this.handleClose.bind(this)} />
                ...
            </BrowserRouter>
        )
    }
}

在前面的渲染方法中,包括了自定义的ConfirmationDialog对话框,并且只有当state属性showConrfirmationDialog设置为true时才会渲染。userConfirmationFunc设置state属性,自定义对话框显示如下:

在前面的代码片段中,handleClose 函数是由 ConfirmDialog 框在用户单击 OK 或 CANCEL 按钮时调用的。OK 按钮将发送值 true,而 CANCEL 按钮将发送值 false 到先前定义的 handleClose 函数。

组件

<HashRouter> 组件是 react-router-dom 包的一部分,与 <BrowserRouter> 类似,也用于构建浏览器环境的应用程序。<BrowserRouter><HashRouter> 之间的主要区别是组件创建的 URL:

<BrowserRouter> 创建的 URL 如下:

www.packtpub.com/react-router

<HashRouter> 在 URL 中添加了一个哈希:

www.packtpub.com/#/react-router

<BrowserRouter> 组件利用 HTML5 History API 来跟踪路由历史记录,而 <HashRouter> 组件使用 window.location.hash(URL 的哈希部分)来记住浏览器历史堆栈中的更改。应该在支持 HTML5 History API 的现代浏览器上构建应用程序时使用 <BrowserRouter>,而在需要支持旧版浏览器的应用程序中使用 <HashRouter>

<HashRouter> 使用 createHashHistory 类来创建 history 对象。然后将此 history 对象提供给核心 <Router> 组件:

import { createHashHistory  as  createHistory } from  "history";  class  HashRouter  extends  React.Component {    ...
    history =  createHistory(this.props**)**; ...
    render() {
        return **<Router 
                  history={this.history}
                  children={this.props.children} 
               />**;
    } }

<HashRouter> 接受以下 props:

static propTypes = {
    basename: PropTypes.string,
    getUserConfirmation: PropTypes.func,
    hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"]),
    children: PropTypes.node
};

<BrowserRouter>类似,props basenamegetUserConfirmation 用于分别指定基本 URL 路径和确认导航到所选 URL 的函数。然而,<HashRouter> 不支持 location.keylocation.state,因此不支持 prop keyLength。此外,也不支持 prop forceRefresh

让我们来看看 hashType prop。

hashType prop

hashType prop 用于指定用于 window.location.hash 的编码方法。可能的值包括 slashnoslashhashbang

让我们来看看在包含 hashType prop 时如何形成 URL:

<HashRouter hashType="slash">
    <App />
</HashRouter>

当您将 slash 指定为 hashType prop 的值时,会在哈希 (#) 后添加斜杠 (/)。因此,URL 将采用以下形式 — #/#/dashboard#/user 等。

请注意,slash 是 prop hashType 的默认值,如果要在 # 后添加斜杠,则不需要包括 hashType prop。

类似地,当hashType属性的值为noslash时,URL 的形式为—##dashboard#user等:

<HashRouter hashType="noslash">

hashType属性分配值hashbang时,它创建的 URL 形式为—#!/#!/dashboard#!/user等:

<HashRouter  hashType="hashbang">

hashbang是为了让搜索引擎爬虫可以爬取和索引单页面应用程序而添加的。然而,谷歌已经弃用了这种爬取策略。在这里阅读更多信息:webmasters.googleblog.com/2015/10/deprecating-our-ajax-crawling-scheme.html

摘要

react-router包中的<Router>组件提供了路由器接口的低级实现。react-router-domreact-router-native中的各种路由器使用这个低级的<Router>接口为特定环境提供路由功能。<Router>中的history属性用于指定给定环境的history对象。例如,<BrowserRouter>组件在浏览器环境中使用history/createBrowserHistory来创建history对象。所有的 Router 组件只接受一个子组件,通常是应用程序的根组件。

react-router-dom中的BrowserRouter组件利用 HTML5 历史 API 与浏览器历史记录同步以保持应用程序的 URL。它接受basenamekeyLengthforceRefreshgetUserConfirmation等 props。另一方面,<HashRouter>在浏览器的 URL 中添加一个哈希(#)并使用window.location.hash来跟踪历史记录。它接受basenamegetUserConfirmationhashType等 props。hashType属性用于指定用于window.location.hash的编码方法;可能的值有slashnoslashhashbang

在第六章中,在服务器端渲染的 React 应用程序中使用 StaticRouter,我们将介绍使用<StaticRouter>组件进行服务器端渲染。

第六章:在服务器端呈现的 React 应用程序中使用 StaticRouter

服务器端渲染SSR)是一种在服务器上呈现仅客户端的单页面应用程序SPAs)的技术,并将完全呈现的页面作为响应发送给用户的请求。在客户端端 SPAs 中,JavaScript 捆绑包被包含为脚本标签,并且最初页面中没有呈现任何内容。捆绑包首先被下载,然后 DOM 节点通过执行捆绑包中的代码进行填充。这有两个缺点——在较差的连接上,可能需要更长时间来下载捆绑包,并且不执行 JavaScript 的爬虫将无法看到任何内容,从而影响页面的 SEO。

SSR 通过在用户请求时加载 HTML、CSS 和 JavaScript 来解决这些问题;内容在服务器上呈现,并且最终的 HTML 交给爬虫。可以使用 Node.js 在服务器上呈现 React 应用程序,并且 React-Router 中可用的组件可以用来定义应用程序中的路由。

在本章中,我们将看看如何在服务器端呈现的 React 应用程序中使用 React-Router 组件:

  • 使用 Node.js 和 Express.js 执行 React 应用程序的 SSR

  • 添加<StaticRouter>组件并创建路由

  • 理解<StaticRouter>属性

  • 通过在服务器上呈现第一页,然后允许客户端代码接管后续页面的呈现来创建同构 React 应用程序

使用 Node.js 和 Express.js 执行 React 应用程序的 SSR

在这个例子中,我们将使用 Node.js 和 Express.js 创建一个服务器端应用程序,该应用程序将在服务器上呈现 React 应用程序。Node.js 是一个用于服务器和应用程序的跨平台 JavaScript 运行时环境。它构建在 Google 的 V8 JavaScript 引擎上,并且使用事件驱动的非阻塞 I/O 模型,使其高效且轻量级。Express.js 是 Node.js 环境中使用的最流行的路由和中间件 Web 框架模块之一。它允许您创建中间件,以帮助处理来自客户端的 HTTP 请求。

安装依赖项

让我们首先使用npm init命令创建一个服务器端应用程序:

npm init -y

这将创建一个名为package.json的文件,并为各种字段添加默认值。下一步是添加依赖项:

npm install --save react react-dom react-router react-router-dom express

上述命令将把所有必要的库添加到package.json文件中的dependencies列表中。请注意,我们不是使用create-react-app CLI 创建 React 应用程序;相反,我们将添加所需的依赖项并编写构建应用程序的配置文件。

为了构建应用程序,以下开发依赖项被添加到devDependencies列表中:

npm install --save-dev webpack webpack-cli nodemon-webpack-plugin webpack-node-externals babel-core babel-loader babel-preset-env babel-preset-react 

上述命令将把构建应用程序所需的库添加到package.json文件中的devDependencies列表中。

下一步是编写构建配置,以便构建服务器端应用程序。

Webpack 构建配置

这是来自 Webpack 文档的:

Webpack的核心是现代 JavaScript 应用程序的静态模块打包程序。当 webpack 处理您的应用程序时,它在内部构建一个依赖图,该图将映射项目所需的每个模块,并生成一个或多个捆绑包

Webpack 已成为为 JavaScript 应用程序创建捆绑包的事实标准。create-react-app CLI 包含内部使用webpack为开发和生产环境创建捆绑包的脚本。

创建一个名为webpack-server.config.babel.js的文件,并包含以下配置:

import path from 'path'; import  webpack  from  'webpack'; import  nodemonPlugin  from  'nodemon-webpack-plugin'; import  nodeExternals  from  'webpack-node-externals'; export  default  { entry:  './src/server/index.js', target:  'node', externals:  [nodeExternals()], output:  { path:  path.resolve(__dirname,  'dist'), filename:  'server.js', publicPath:  '/' },
    module:  { rules:  [ {
                test:  /\.js$/, use:  'babel-loader' }
        ]
    },
    plugins:  [ new  webpack.DefinePlugin({ __isBrowser__:  false }),
        new  nodemonPlugin()
    ]
}

根据上述配置,文件index.js(位于./src/server路径)被指定为入口点,并且生成的输出文件server.js被复制到dist目录。使用Webpackbabel-loader插件来使用BabelWebpack转译应用程序中的 JavaScript 文件。使用nodemon-webpack-plugin来运行nodemon实用程序,它将监视应用程序中 JavaScript 文件的更改,并在webpack以观察模式运行时重新加载和构建应用程序。

下一步是创建一个.babelrc文件,其中将列出构建应用程序所需的预设:

{
 "presets": ["env","react"] }

babel-preset-envbabel-preset-react插件用于将 ES6 和 React 代码转译为 ES5。作为最后一步,在package.json文件中添加一个脚本命令,以使用webpack-server.config.babel.js文件中提到的配置启动应用程序:

"scripts": {
 "start": "webpack --config webpack-server.config.babel.js --watch --mode development" }

命令npm start将构建应用程序,并将监听应用程序中 JavaScript 文件的更改,并在检测到更改时重新构建应用程序。

服务器端应用程序

webpack配置中所述,应用程序的入口点位于/src/server/index.js。让我们在此路径下创建index.js文件,并包含以下代码,该代码在给定端口启动服务器应用程序:

import  express  from  'express'; const  PORT  =  process.env.PORT  ||  3001; const  app  =  express(); app.get('*', (req, res) => { res.send(` <!DOCTYPE HTML>
 <html>
 <head>
 <title>React SSR example</title>
 </head>
 <body>
 <main id='app'>Rendered on the server side</main>
 </body>
 </html>
 `); });

app.listen(PORT, () => { console.log(`SSR React Router app running at ${PORT}`); });

当您运行npm start命令并访问 URLhttp://localhost:3001时,将呈现前面的 HTML 内容。这确保了webpack配置构建了应用程序,并在端口3001上运行前面的服务器端代码,nodemon监视文件的更改。

使用 ReactDOMServer.renderToString 呈现 React 应用程序

要在服务器端呈现 React 应用程序,首先让我们创建一个 React 组件文件—shared/App.js

import  React, { Component } from  'react'; export  class  App  extends  Component { render() { return ( <div>Inside React App (rendered with SSR)</div> ); }
}

然后,在server/index.js文件中呈现前面的组件:

import  express  from  'express'; import  React  from  'react'; import  ReactDOMServer  from  'react-dom/server'; import { App } from  '../shared/App'; app.get('*', (req, res) => { const  reactMarkup  =  ReactDOMServer.renderToString(<App  />**)**; res.send(` <!DOCTYPE HTML>
        <html>
        ...
 **<main id='app'>**${reactMarkup}</main>   
        ...
        </html>
    `); });

ReactDOMServer类包括用于在服务器端 Node.js 应用程序中呈现 React 组件的各种方法。ReactDOMServer类中的renderToString方法在服务器端呈现 React 组件并返回生成的标记。然后,可以将此生成的标记字符串包含在发送给用户的响应中。

当您访问http://localhost:3001页面时,您会注意到显示了消息“Inside React App (rendered with SSR)”。

确认内容确实是在服务器端呈现的,您可以右键单击页面,然后从上下文菜单中选择“查看页面源代码”选项。页面源代码将显示在新标签页中,其中包括以下内容:

<main id='app'>
 <div data-reactroot=""> Inside React App (rendered with SSR) **</div>** </main>

当爬虫访问应用程序时,前面的内容很有帮助。通过在服务器端呈现 React 组件,标记被填充并作为来自服务器的响应包含。然后,此内容将被搜索引擎的爬虫索引,有助于应用程序的 SEO 方面。

添加并创建路由

<StaticRouter>组件是react-router-dom包的一部分(在react-router中使用<StaticRouter>定义),它用于在服务器端呈现 React-Router 组件。<StaticRouter>组件类似于其他路由器组件,因为它只接受一个子组件——React 应用程序的根组件(<App />)。此组件应该在无状态应用程序中使用,用户不会点击以导航到页面的不同部分。

让我们通过包装应用程序的根组件来包含<StaticRouter>组件:

import { StaticRouter } from  'react-router-dom'**;** app.get('*', (req, res) => { const  context  = {}; const  reactMarkup  =  ReactDOMServer.renderToString( <StaticRouter  context={context}  location={req.url}> <App  /> </StaticRouter**>**  );

    res.send(` ...
        <main id='app'>${reactMarkup}</main> ...
    `);
});

请注意,<StaticRouter>组件接受两个属性——contextlocationcontext对象是一个空对象,在<App />中的一个<Route>组件作为浏览器位置匹配的结果进行渲染时,它会被填充属性。

location对象通常是请求的 URL,这些信息对中间件函数是可用的。请求对象(req)包含指定请求的 URL 的url属性。

让我们在App.js中包含一对<Route>组件:

export  class  App  extends  Component {    render() { return ( <div> Inside React App (rendered with SSR) <Route exact
 path='/' render={() =>  <div>Inside Route at path '/'</div>} />
 <Route path='/home' render={() =>  <div>Inside Home Route at path '/home'</div> }
 />
            </div> ); }
}

<Route>组件匹配<StaticRouter>组件的location属性中指定的请求 URL 并进行渲染。

使用<Redirect>staticContext进行服务器端重定向

从前面的例子中,让我们使用<Redirect>组件将用户从/路径重定向到/home路径:

<Route
 path="/" render={() =>  <Redirect  to="/home"  />**}** exact />

当您尝试访问 URL http://localhost:3001/时,您会注意到重定向没有发生,浏览器的 URL 也没有更新。在客户端环境中,前面的重定向已经足够了。但是,在服务器端环境中,服务器负责处理重定向。在这种情况下,<StaticRouter>组件中提到的context对象被填充了必要的细节:

{
    "action": "REPLACE",
    "location": {
        "pathname": "/home",
        "search": "",
        "hash": "",
        "state": undefined
    },
    "url": "/home"
}

context对象包含组件渲染的结果。当组件仅渲染内容时,它通常是一个空对象。但是,当渲染的组件重定向到不同的路径时,它会填充前面的细节。请注意,url属性指定了应将用户重定向到的路径——到'/home'路径。

可以添加一个检查,看看context对象中是否存在url属性,然后可以使用response对象上的redirect方法来重定向用户:

...
const  reactMarkup  =  ReactDOMServer.renderToString(
 <StaticRouter  context={context}  location={req.url}> <App  /> </StaticRouter> ); if (context.url) { res.redirect(301, 'http://'  +  req.headers.host  +  context.url); } else { res.send(`
        <!DOCTYPE HTML>
        <html>
            ...
        </html>
    `);
}

response对象中的redirect方法用于执行服务器端重定向,并提到状态代码和要重定向到的 URL。

还可以使用渲染组件中的staticContext属性向context对象中填充更多属性:

<Route
 path="/" exact render={({ staticContext, }) => { if (staticContext) { staticContext.status = 301**;** } return ( <Redirect  to="/home"  /> ) }} />

在这里,staticContext属性在渲染的组件中可用,并且在使用<Redirect>组件重定向用户之前,status属性被添加到其中。然后status属性在context对象中可用:

res.redirect(context.status, 'http://'  +  req.headers.host  +  context.url);

在这里,context对象中的status属性用于在使用redirect方法重定向用户时设置 HTTP 状态。

使用 matchPath 进行请求 URL 匹配

在服务器端渲染 React 应用程序时,了解请求的 URL 是否与应用程序中现有路由中的任何一个匹配也是有帮助的。只有在路由可用时,才应在服务器端呈现相应的组件。但是,如果路由不可用,则应向用户呈现一个未找到页面(404)。react-router包中的matchPath函数允许您将请求的 URL 与包含路由匹配属性(如pathexactstrictsensitive)的对象进行匹配:

import { matchPath } from 'react-router'

app.use('*', (req, res) => {
    const isRouteAvailable = **matchPath(req.url, {** path: '/dashboard/',
 strict: true
 });
    ...

});

matchPath函数类似于库如何将<Route>组件与请求的 URL 路径进行匹配。传递给matchPath函数的第一个参数是请求的 URL,第二个参数是请求的 URL 应该匹配的对象。当路由匹配时,matchPath函数返回一个详细说明请求的 URL 如何与对象匹配的对象。

例如,如果请求的 URL 是/dashboard/matchPath函数将返回以下对象:

{
    path: '/dashboard/',
    url: '/dashboard/',
    isExact: true,
    params: {}
}

在这里,path属性提到了用于匹配请求的 URL 的路径模式,url属性提到了 URL 的匹配部分,isExact布尔属性如果请求的 URL 和路径完全匹配,则设置为trueparams属性列出了与提供的路径名匹配的参数。考虑以下示例,其中提到了路径中的参数:

const  matchedObject  =  matchPath(req.url, '/github/:githubID');

在这里,不是将对象指定为第二个参数,而是指定了一个路径字符串。如果要将路径与请求的 URL 进行匹配,并使用exactstrictsensitive属性的默认值,则这种简短的表示法非常有用。匹配的对象将返回以下内容:

{
    path: '/github/:githubID',
    url: '/github/sagar.ganatra',
    isExact: true,
    params: { githubID: 'sagar.ganatra' } 
}

请注意,params属性现在填充了在path中提到的参数列表,并提供了请求的 URL 中的值。

在服务器端,在初始化<StaticRouter>并渲染 React 应用程序之前,可以执行检查,以查看请求的 URL 是否与对象集合中定义的任何路由匹配。例如,考虑一个路由对象集合。

shared/routes.js中,我们有以下内容:

export  const  ROUTES  = [ { path:  '/', exact:  true  }, { path:  '/dashboard/', strict:  true }, { path:  '/github/:githubId' } ];

前面的数组包含路由对象,然后可以在matchPath中使用它们来检查请求的 URL 是否与前面列表中的任何路由匹配:

app.get('*', (req, res) => {
 const isRouteAvailable = ROUTES.find(route => { return matchPath(req.url, route**)**; })
    ...
});

如果找到请求的 URL,则isRouteAvailalbe将是ROUTES列表中的匹配对象,否则当没有路由对象匹配请求的 URL 时,它被设置为undefined。在后一种情况下,可以向用户发送页面未找到的标记:

if (!isRouteAvailable) {
 **res**.status(404**);** res.send(` <!DOCTYPE HTML> <html> <head><title>React SSR example</title></head> <body> <main id='app'> Requested page '${req.url}**' not found** </main> </body> </html>`); res.end(); }

当用户请求路径,比如/userROUTES中提到的对象都不匹配时,前面的响应被发送,提到404HTTP 状态,响应主体提到请求的路径/user未找到。

StaticRouter 上下文属性

<StaticRouter>组件接受basenamelocationcontext等 props。与其他路由器实现类似,<StaticRouter>中的basename属性用于指定baseURL位置,location属性用于指定位置属性——pathnamehashsearchstate

context属性仅在<StaticRouter>实现中使用,它包含组件渲染的结果。如前所述,context对象可以填充 HTTP 状态码和其他任意属性。

在初始化时,上下文对象可以包含属性,然后由渲染的组件消耗:

const  context  = { message:  'From StaticRouter\'s context object' **}** const  reactMarkup  =  ReactDOMServer.renderToString( <StaticRouter  context={context}  location={req.url}  > <App  /> </StaticRouter> );

在这里,上下文对象包含message属性,当找到匹配请求 URL 的<Route>组件时,包含此属性的staticContext对象可用于渲染组件:

<Route
 path='/home' render={({ staticContext }) => { return ( <div> Inside Home Route, Message - {staticContext.message**}** </div> ); }} />

当您尝试访问/home路径时,前面的<Route>匹配,并且在staticContext消息属性中提到的值被渲染。

staticContext属性仅在服务器端环境中可用,因此,在同构应用程序中尝试引用staticContext对象(在下一节中讨论),会抛出一个错误,指出您正在尝试访问未定义的属性消息。可以添加检查以查看staticContext是否可用,或者可以检查在 webpack 配置中定义的__isBrowser__属性的值:

<Route
 path='/home' render={({ staticContext }) => { if (!__isBrowser__) { return ( <div> Inside Home Route, Message - {staticContext.message} </div> ); } return ( <div>Inside Home Route, Message</div> ); }} />

在上面的例子中,如果页面在服务器端渲染,则__isBrowser__属性将为false,并且staticContext对象中指定的消息将被渲染。

创建同构 React 应用程序

一个应用程序,其中代码可以在服务器端和客户端环境中运行,几乎没有或没有变化,被称为同构应用程序。在同构应用程序中,用户的网络浏览器发出的第一个请求由服务器处理,任何后续请求由客户端处理。通过在服务器端处理和渲染第一个请求,并发送 HTML、CSS 和 JavaScript 代码,提供更好的用户体验,并帮助搜索引擎爬虫索引页面。然后,所有后续请求可以由客户端代码处理,该代码作为服务器的第一个响应的一部分发送。

以下是更新后的请求-响应流程:

为了在客户端渲染应用程序,可以使用<BrowserRouter><HashRouter>组件中的任何一个。在本例中,我们将使用<BrowserRouter>组件。

添加了用于客户端代码的目录后,应用程序结构如下:

/server-side-app
|--/src
|----/client
|------index.js
|----/server
|------index.js
|----/shared
|------App.js

在这里,shared目录将包含可以被服务器端和客户端代码使用的代码。使用<BrowserRouter>组件的客户端特定代码位于client目录中的index.js文件中:

import  React  from  "react"; import  ReactDOM  from  "react-dom"; import { BrowserRouter } from  "react-router-dom"; import { App } from  "../shared/App"; // using hydrate instead of render in SSR app ReactDOM.hydrate( <BrowserRouter> <App  /> </BrowserRouter>, document.getElementById("app") );

在这里,ReactDOM类中的hydrate方法被用来渲染应用程序,而不是调用render方法。hydrate方法专门设计用来处理初始渲染发生在服务器端(使用ReactDOMServer)的情况,以及所有后续的路由更改请求来更新页面的特定部分都由客户端代码处理。hydrate方法用于将事件监听器附加到在服务器端渲染的标记上。

下一步是构建应用程序,以便在构建时生成客户端包,并包含在服务器的第一个响应中。

Webpack 配置

现有的 webpack 配置构建了服务器端应用程序,并运行nodemon实用程序来监视更改。为了生成客户端包,我们需要包含另一个 webpack 配置文件—webpack-client.config.babel.js

import  path  from  'path'; import  webpack  from  'webpack'; export  default { entry:  './src/client/index.js', output: { path:  path.resolve(__dirname, './dist/public'), filename:  'bundle.js', publicPath:  '/' }, module: { rules: [ { test: /\.js$/, use:  'babel-loader' } ] }, plugins: [ new  webpack.DefinePlugin({ __isBrowser__:  "true" }) ] }

前面的配置解析了/src/client/index.js文件中的依赖关系,并在/dist/public/bundle.js处创建了一个包。这个包包含了运行应用程序所需的所有客户端代码;不仅是index.js文件中的代码,还包括shared目录中声明的组件。

当前的npm start脚本还需要修改,以便客户端应用程序代码与服务器端代码一起构建。让我们创建一个文件,导出服务器和客户端 webpack 配置——webpack.config.babel.js

import clientConfig from './webpack-client.config.babel'; import serverConfig from './webpack-server.config.babel'; export default [clientConfig, serverConfig];

最后,更新npm start脚本,以引用上述配置文件:

"start": "webpack --config webpack.config.babel.js --mode development --watch"

上述脚本将生成server.js,其中包含服务器端代码,以及bundle.js,其中包含客户端代码。

服务器端配置

最后一步是更新服务器端代码,将客户端 bundle(bundle.js)包含在第一个响应中。服务器端代码可以包含一个<script>标签,其中指定了bundle.js文件的源(src)属性:

res.send(`
 <!DOCTYPE HTML> <html> <head> <title>React SSR example</title> **<script src='/bundle.js' defer></script>** ...
    </html>
`);

另外,为了使我们的 express 服务器能够提供 JavaScript 文件,我们包括了用于提供静态内容的中间件函数:

app.use(express.static('dist/public'))

上述代码允许从dist/public目录提供静态文件,如 JavaScript 文件、CSS 文件和图像。在使用app.get()之前,应包含上述语句。

当您访问/home路径的应用程序时,第一个响应来自服务器,并且除了渲染与/home路径匹配的<Route>之外,客户端 bundle——bundle.js也包含在响应中。bundle.js文件由浏览器下载,然后路由路径的任何更改都由客户端代码处理。

摘要

在本章中,我们看了一下如何使用ReactDOMserver.renderToString方法在服务器端(使用 Node.js 和 Express.js)呈现 React 应用程序。React-Router 中的<StaticRouter>组件可用于包装应用程序的根组件,从而使您能够在服务器端添加与请求的 URL 路径匹配的<Route>组件。<StaticRouter>组件接受contextlocation属性。在渲染的组件中,staticContext属性(仅在服务器端可用)包含context属性中由<StaticRouter>提供的数据。它还可以用于在使用<Redirect>组件时添加属性以重定向用户。

matchPath 函数用于确定请求的 URL 是否与提供的对象 {path, exact, strict, sensitive} 匹配。这类似于库如何将请求的 URL 与页面中可用的 <Route> 组件进行匹配。matchPath 函数使我们能够确定请求的 URL 是否与集合中的任何路由对象匹配;这为我们提供了一个机会,可以提前发送 404:页面未找到的响应。

还可以创建一个同构的 React 应用程序,它在服务器端渲染第一个请求,然后在客户端渲染后续请求。这是通过在从服务器发送第一个响应时包含客户端捆绑文件来实现的。客户端代码在第一个请求之后接管,这使您能够更新与请求的路由匹配的页面的特定部分。

在第七章中,在 React Native 应用程序中使用 NativeRouter,我们将看看如何使用 NativeRouter 组件来定义 React-Native 创建的原生移动应用程序中的路由。

第七章:在 React Native 应用程序中使用 NativeRouter

React Router 库提供了react-router-native包,其中包括用于 React Native 应用程序的NativeRouter组件的实现。React Native 框架允许您使用 JavaScript 和 React 构建 iOS 和 Android 的本机移动应用程序。

来自 React Native 文档(facebook.github.io/react-native/):

“使用 React Native,您不会构建移动 Web 应用程序HTML5 应用程序混合应用程序。您构建的是一个与使用 Objective-C 或 Java 构建的应用程序无异的真实移动应用程序。React Native 使用与常规 iOS 和 Android 应用程序相同的基本 UI 构建块。您只需使用 JavaScript 和 React 将这些构建块组合在一起。”

在本章中,讨论了以下主题:

  • 在 React Native 应用程序中使用 NativeRouter

  • NativeRouter 组件及其属性

  • 使用<BackButton>组件与设备的返回按钮交互

  • 使用<DeepLinking>组件创建深链接

在 React Native 应用程序中使用 NativeRouter

create-react-appCLI 类似,create-react-native-appCLI 用于创建一个包含构建脚本的应用程序,可用于开发和生产环境。它还包括packager,允许您在 iOS 和 Android 模拟器以及真实设备上测试应用程序。

使用 create-react-native-app CLI 创建新项目

让我们首先安装 CLI:

npm install -g create-react-native-app

上一个命令将 CLI 安装在全局的node_modules目录中。下一步是使用 CLI 创建一个 React Native 项目:

create-react-native-app react-native-test-app

创建了react-native-test-app目录,并在node_modules目录中下载了所有必需的脚本。

现在,当您运行npm start脚本时,构建脚本会启动packager,并为您生成一个 QR 码和一个 URL,以便您在真实设备(iOS 或 Android)或模拟器上访问应用程序。此外,如果您已安装 Xcode 或 Android Studio,还可以启动 iOS 或 Android 模拟器。这是一个例子:

Your app is now running at URL: exp://192.168.1.100:19000
View your app with live reloading:
Android device:
-> Point the Expo app to the QR code above.
(You'll find the QR scanner on the Projects tab of the app.)
iOS device:
-> Press s to email/text the app URL to your phone.
Emulator:
-> Press a (Android) or i (iOS) to start an emulator.
Your phone will need to be on the same local network as this computer.
For links to install the Expo app, please visit https://expo.io.
Logs from serving your app will appear here. Press Ctrl+C at any time to stop.
› Press a to open Android device or emulator, or i to open iOS emulator.
› Press s to send the app URL to your phone number or email address
› Press q to display QR code.
› Press r to restart packager, or R to restart packager and clear cache.
› Press d to toggle development mode. (current mode: development)

在本例中,我们将使用 Xcode 模拟器;当您请求在 iOS 模拟器上查看应用程序时,这是应用程序的屏幕截图:

React Native 提供了几个组件,允许您为原生平台构建视图。让我们看一下代码,并了解用于构建前述视图的一些组件。

App.js中,包括以下代码:

export  default  class  App  extends  React.Component {    render() { return ( <View  style={styles.container}**>**  <Text>Open up App.js to start working on your app!</Text**>** <Text>Changes you make will automatically reload.</Text> <Text>Shake your phone to open the developer menu.</Text> </View> ); } }

在这里,React Native 的<View>组件被用来创建一个容器,类似于在 React 应用程序中使用<div><section>创建容器的方式。在 React Native 中,不是使用 HTML 元素,如<div><span>,而是使用 React Native 的组件,如<View><Text>

添加<NativeRouter>组件

让我们现在将react-router-native包添加到我们刚刚创建的应用程序中:

 npm install --save react-router-native

NativeRouter组件用于在 React Native 应用程序中提供路由和导航支持。它使得诸如<Route><Link>之类的组件可以在原生应用程序中使用。

让我们首先创建一个包含一对<Link>组件的侧边菜单:

import { Link } from 'react-router-native';

export  class  Menu  extends  Component { render() { return ( <ScrollView  scrollsToTop={false}  style={styles.menu}> <View> <Link  to="/"> <Text>Home</Text> </Link> <Link  to="/dashboard"> <Text>Dashboard</Text> </Link**>** </View> </ScrollView> ) } }

<ScrollView>组件被用作容器来承载我们的菜单项(<Link>组件)。正如其名称所示,<ScrollView>组件用于创建可滚动的容器。下一步是向应用程序添加<Route>组件:

export  class  ContentView  extends  Component { render() { return ( <View  style={styles.container}> <Route path="/" exact component={HomeComponent} /> <Route path="/dashboard" component={DashboardComponent} **/>** </View> ) } }

ContentView组件将<Route>组件包装在<View>组件中,从而定义了路径为//dashboard的两个应用程序路由。

作为最后一步,我们现在将使用react-native-side-menu中的<SideMenu>组件来创建一个抽屉菜单。然后在 App.js 中将此菜单包装在<NativeRouter>组件中:

export  default  class  App  extends  Component { render() { const  menu  =  <Menu  />**;** return ( <NativeRouter**>** <View  style={styles.container}> <SideMenu  menu={menu}> <ContentView  /> </SideMenu**>** </View> </NativeRouter**>** ); } }

类似于其他路由器实现,NativeRouter组件包装了应用程序根组件,并使得<Route><Link>组件可以在用户浏览应用程序时更新history

在模拟器上重新构建应用程序后:

当您选择任一链接时,ContentView将使用由<Route>匹配渲染的组件进行更新。

前述功能类似于BrowserRouter使您能够浏览应用程序中定义的各种路由。类似于<Route><Link>组件,其他组件,如<Switch><Redirect><NavLink>在 React Native 应用程序中的行为也是相同的。然而,当您尝试使用<Prompt>组件阻止导航时,应该使用 React Native 的Alert组件来显示确认消息。

从 NativeRouter 的实现:

import { Alert } from  "react-native";

NativeRouter.defaultProps = {
    getUserConfirmation: (message, callback) => {
        Alert.alert("Confirm", message, [
            { text: "Cancel", onPress: () => callback(false) },
            { text: "OK", onPress: () => callback(true) }
        ]);
    }
};

NativeRouter 提供了getUserConfirmation函数的默认实现,它使用react-native包中定义的Alert组件来向用户显示确认消息:

这种默认行为可以通过包括getUserConfirmation属性来覆盖:

<NativeRouter getUserConfirmation={customGetUserConfirmation}>
...
</NativeRouter>

组件

NativeRouter组件使用react-router包中定义的MemoryRouter组件在 React Native 应用程序中提供路由支持。当您希望在内存中保留浏览历史记录而不更新地址栏中的 URL 时,可以使用MemoryRouter。这在没有地址栏的非浏览器环境中特别有用。MemoryRouter组件使用history包中可用的createMemoryHistory类创建一个history对象。然后将此history对象提供给低级别的<Router>接口。

NativeRotuer.js中:

import  MemoryRouter  from  "react-router/MemoryRouter"; const  NativeRouter  =  props  =>  <MemoryRouter {...props} />;

然后,MemoryRouter组件使用createMemoryHistoryMemoryRouter.js中创建一个history对象:

import { createMemoryHistory  as  createHistory } from  "history"; class  MemoryRouter  extends  React.Component { **history** =  createHistory(this.props**)**;
    ...

    render() {
        return <Router 
                  history={this.history} children={this.props.children}
               />;
    }
}

NativeRouter组件接受 props:initialEntriesinitialIndexgetUserConfirmationkeyLengthchildren。如前所述,NativeRouter类中包含了getUserConfirmation的默认实现,而keyLengthchildren属性的行为与前几章中提到的其他路由器组件类似。

让我们来看看initialEntriesinitialIndex属性。

initialEntries 属性

initialEntries 属性用于使用位置列表填充历史堆栈:

export  default  class  App  extends  Component {    render() {
        const  initialEntries  = ['/', '/dashboard'**]**; return ( <NativeRouter  initialEntries={initialEntries**}**> ...
            </NativeRouter>
        );
    }
}

在初始化 NativeRouter 时,您可以通过提供位置路径数组来填充历史记录。位置路径可以是字符串,甚至是形状为{ pathname,search,hash,state }的对象:

const initialEntries = [
    '/' ,
    { 
 pathname: '/dashboard',
 search: '',
 hash: 'test', 
 state: { from: '/'}
 }
];

initialIndex 属性

initialIndex 属性用于指定在应用程序加载时渲染在initialEntries数组中的位置的索引值。例如,如果initialEntries数组列出了两个位置,那么initialIndex值为1会加载第二个条目;也就是说,匹配initialEntries数组中第二个条目作为路径名的<Route>实例会被渲染:

export  default  class  App  extends  Component {    render() { const  initialEntries  = ['/', '/dashboard']; const  initialIndex  =  1; return ( <NativeRouter  initialEntries={initialEntries}  initialIndex={initialIndex**}**> ...
            </NativeRouter>
        )
    }
}

在这个例子中,initialIndex的值设置为1,因此当应用程序加载时,匹配位置路径/dashboard<Route>被渲染。

组件

默认情况下,在 Android 设备上按下返回按钮时,应用程序会退出,而不是将用户导航到历史记录中的上一个状态。React Native 库包括一个BackHandler类,它允许您自定义设备的硬件返回按钮的行为。React Router 中的<BackButton>组件使用BackHandler类来自定义 Android 设备上返回按钮的行为:

import { NativeRouter, BackButton } from 'react-router-native';

export  default  class  App  extends  Component { render() { return (
            <NativeRouter>
                <View  style={styles.container}>
                    **<BackButton />** <SideMenu  menu={menu}> <ContentView  /> </SideMenu> </View> </NativeRouter> )
    }
}

<BackButton>组件可以包含在应用程序的任何位置。在前面的示例中,该组件包含在根组件中,不包含任何子组件。请注意,<BackButton>组件不会在视口上呈现任何内容;相反,它促进了与设备返回按钮的交互。

以下是工作流程:

在仪表板屏幕(路径为/dashboard)上,当您点击设备的返回按钮时,用户将被导航到主页(路径为/)。

使用创建深层链接

在 Web 应用程序中,HTTP URL 引用的位置可以通过在浏览器的地址栏中输入来访问。在单页应用程序中,此位置指的是用户可以导航到的特定路由。在移动应用程序的上下文中,DeepLink指的是您想要查看的特定页面或内容。例如,当您在移动设备上点击链接时,应用程序会启动,而不是在浏览器窗口中打开新标签,并显示所请求的页面。

与 Web 应用程序不同,移动设备上的应用程序需要为应用程序声明 URI 方案,而不是使用 HTTP 引用特定位置。例如,Twitter 应用程序使用 URI 方案twitter://,因此您可以通过引用 URI twitter://profile 查看他们的 Twitter 个人资料。当用户点击电子邮件中的链接或访问推送通知消息时,深层链接非常有用,这些链接将用户导航到应用程序以显示所请求的内容。

React Native 提供了接口,允许您在 iOS 和 Android 平台上为设备创建深层链接。在本节中,我们将看看如何在 Android 设备上创建深层链接,因此我们需要安装 Android Studio。Android Studio 允许我们创建虚拟设备(AVD),然后用于测试深层链接。

在 React Native 文档中详细介绍了在 iOS 和 Android 上安装必要组件的逐步指南:facebook.github.io/react-native/docs/getting-started.html

安装 Android Studio 并创建 AVD 后,需要为应用程序配置 URI 方案。要添加 URI 方案,需要更新一些本机文件,并且要访问这些本机文件,需要退出当前设置。

从 create-react-native-app 中退出

create-react-native-app CLI 是一个非常好的选项,可以为 React Native 应用程序提供脚手架和在模拟器上测试应用程序。然而,要测试DeepLinking,我们需要在清单文件中包含条目,因此需要使用以下命令退出配置:

npm run eject

上一个命令将为 iOS 和 Android 平台生成配置文件。这个最基本的配置允许你为 iOS 设备生成一个.ipa文件,为 Android 设备生成一个.apk文件。在本节中,我们将看到如何生成.apk文件,然后部署到 AVD 上。

退出后,你会看到为 iOS 和 Android 生成的各种目录和文件:

|--/android
|----/.gradle
|----/app
|----/build
|----/gradle
|----/keystores
|--/ios
|----/chapter7DeepLink
|----/chapter7DeepLink-tvOS |----/chapter7DeepLink-tvOSTests |----/chapter7DeepLink.Xcodeproj |----/chapter7DeepLinkTests

下一步是在 Android 设备上构建和运行应用程序:

npm run android

上一个命令将运行构建脚本并生成.apk文件,然后部署到 AVD 上。请确保在执行上一个命令之前虚拟设备正在运行。

要在 Android 设备上配置 URI 方案,需要更新位于/android/app/src/main路径的AndroidManifest.xml清单文件。在下一节中,我们将看到需要添加到清单文件中的配置。

向清单文件添加

AndroidManifest.xml文件包含有关应用程序的元信息,并用于声明应用程序中存在的各种组件。这些组件使用意图过滤器进行激活。清单文件中的<intent-filter>实例用于定义应用程序的功能,并定义其他应用程序与应用程序交互的策略。

当你退出配置时,AndroidManifest.xml文件将被生成:

<manifest  xmlns:android="http://schemas.android.com/apk/res/android"
 **package**="com.chapter7deeplink"> <uses-permission  android:name="android.permission.INTERNET"  /> <uses-permission  android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="false" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize"> <intent-filter> <action  android:name="android.intent.action.MAIN"  /> <category  android:name="android.intent.category.LAUNCHER"  /> </intent-filter**>** </activity> <activity  android:name="com.facebook.react.devsupport.DevSettingsActivity"  /> </application> </manifest>

在这里,<intent-filter>为应用程序定义了动作和类别,分别为android.intent.action.MAINandroid.intent.category.LAUNCHER。前一个intent-filter使应用程序能够在用户设备上看到,并且当用户点击应用程序时,应用程序中的MainActivity(请参阅 activity 标签)会被触发。

类似地,用于为应用程序定义 URI 方案的intent-filter可以添加到清单文件中:

<intent-filter  android:label="filter_react_native">
 <action  android:name="android.intent.action.VIEW"  /> <category  android:name="android.intent.category.DEFAULT"  /> <category  android:name="android.intent.category.BROWSABLE"  /> <data  android:scheme="deeplink"  android:host="app.chapter7.com" **/>** </intent-filter>

在这里,<data>标签用于指定应用程序的 URI 方案。<data>标签中的android:scheme属性用于指定方案名称,android:host属性用于指定应用程序使用的hostname类型。因此,deeplink://app.chapter7.com URI 用于访问应用程序中的主页。可以使用此 URI 访问具有/dashboard路径的路由:deeplink://app.chapter7.com/dashboard

下一步是使用 React Router 的<DeepLinking>组件,以便应用程序可以对传入的请求做出反应,并将用户导航到请求的路由。

包括组件

react-router-native包中的<DeepLinking>组件使用 React Native 的Linking接口来监听 URL 的更改。每当检测到更改时,用户就会通过在历史堆栈中添加条目来导航到请求的路径。

<DeepLinking>组件可以包含在应用程序的任何位置:

export  class  RootComponent  extends  Component { render() { return ( <View  style={styles.container}> <DeepLinking **/>** <View  style={styles.nav}> <Link  to="/app.chapter7.com">
                        <Text>Home</Text>
                    </Link> <Link  to="/app.chapter7.com/dashboard">
                        <Text>Dashboard</Text>
                    </Link> </View> <View  style={styles.routeContainer}> <Route  path="/app.chapter7.com"  exact  component={HomeComponent}  /> <Route  path="/app.chapter7.com/dashboard"  component={DashboardComponent}  /> </View> </View> ) } }

在这里,<DeepLinking>组件包含在应用程序的RootComponent中,并且<Route>路径使用前缀app.chapter7.com进行更新,以匹配AndroidManifest.xml文件中声明的主机名。

要测试深层链接,请尝试以下命令:

adb shell am start -W -a android.intent.action.VIEW -d deeplink://app.chapter7.com/dashboard

上一个命令应该在 AVD 上启动应用程序,并将您导航到具有/dashboard路径的路由。

摘要

在本章中,我们看了一下 React Router 的<NativeRouter>组件如何在 React Native 应用程序中使用。<NativeRouter>组件包含在react-router-native包中,并在内部使用react-router包中定义的<MemoryRouter>组件。<NativeRouter>组件接受 props:initialEntriesinitialIndexgetUserConfirmationkeyLengthchildren。此外,它为getUserConfirmation函数提供了默认实现,该函数使用 React Native 的Alert组件显示确认消息。当应用程序中包含<Prompt>组件并且用户尝试从当前路由导航时,将显示此确认消息。

<BackButton>组件在react-router-native中是 React Native 的BackHandler类的包装器,它监听设备的返回按钮,并通过历史堆栈中的一个条目将用户导航回去。<DeepLinking>组件用于处理应用程序中内容的深层链接。该组件使用 React Native 的Linking接口来监听 URL 的更改,并在使用深层链接 URI 方案访问应用程序时将用户导航到请求的路由。要为应用程序定义 URI 方案,需要更新AndroidManifest.xml清单文件,为主要活动(.MainActivity)添加<intent-filter>intent-filter声明要使用的 URI 方案和主机名以访问应用程序内的内容。

在下一章中,我们将看一下状态管理工具 Redux,并了解如何将 React Router 与 Redux 结合使用。

第八章:使用 connected-react-router 的 Redux 绑定

在之前的章节中,我们看到了如何使用组件的状态来存储模型数据,以及当模型由于用户操作而更新时,React 如何更新视图。在大型应用程序中,此状态信息不仅应该对当前组件及其子组件可用,还应该对应用程序树中的其他组件可用。有各种状态管理库可用,可帮助使用户界面组件与应用程序状态保持同步。Redux 是一个这样的库,它使用一个中央数据存储来管理应用程序的状态。存储作为真相的来源,应用程序中的组件可以依赖于存储中维护的状态。

在本章中,我们将看一下connected-react-router库,它为 React Router 提供了 Redux 绑定。本章讨论以下主题:

  • 使用 Redux 进行状态管理-介绍 Redux 概念

  • 开始使用connected-react-router

  • 从 Redux 存储中读取 react-router 状态

  • 通过分派操作导航到不同路由

使用 Redux 进行状态管理

如前所述,Redux 使用单个存储来管理应用程序的状态。除了Store,还有另外两个构建块:ActionsReducers

让我们看看这些构建块如何帮助维护state并在Store中的state更改时更新视图。

操作

操作让您定义用户可以执行的操作,以更新应用程序的状态。操作是一个 JavaScript 对象,具有{ type,payload }的形状,其中type是指用户操作的字符串,payload是应该更新状态的数据:

let todoId = 0;
export const addTodo = text => ({
    type: 'ADD_TODO'
    payload: {
        text,
        id: todoId++,
        isCompleted: false
    }
})

在这里,addTodo操作接受 TODO 文本,并指示该操作用于将 TODO 添加到 TODO 列表中。payload在这里是一个包含 TODO text,TODO ID和布尔标志isCompleted(设置为 false)的对象。也可以有不需要包含payload属性的操作。例如,考虑以下操作:

export const increment = () => ({
    type: 'INCREMENT'
})

在这里,action类型INCREMENT表示实体的值必须增加 1。前面的action不需要payload属性,并且根据操作类型,可以更新实体的状态。

减速器

Redux 中的 Reducer 根据分派到存储的操作改变实体的状态。Reducer 是一个纯函数,接受两个参数:stateaction。然后根据存储在action.type中的值返回更新后的状态。例如,考虑以下 reducer:

const todoReducer  = (state  = [], action) => { switch (action.type) { case  '**ADD_TODO**':
            return [
                ...state,
                {
                    id: action.payload.id,
                    text: action.payload.text,
                    isCompleted: action.payload.isCompleted
                }
            ];  default: return  state; } }

todoReducer的初始状态设置为空数组(状态参数的默认值),当操作类型为ADD_TODO时,TODO 被添加到列表中。Redux 的核心原则之一是不要改变状态树,而是返回一个新的状态树作为组件分派的操作的结果。这有助于保持 reducer 函数的纯净(即没有副作用),并有助于在 React 组件重新渲染视图元素时识别新的状态变化。

同样,可能会有多个更新 TODO 状态的操作(如MARK_COMPLETEDDELETE),并且根据分派到存储的操作类型,reducer 可以改变 TODO 列表的状态。

存储

存储是一个中心数据对象,应用程序的状态可以从中派生。应用程序中的组件订阅存储状态的变化并更新视图。

Redux 中数据的流动方式如下:

用户执行操作,比如提交表单或点击按钮,从而向存储分派一个操作。应用程序定义了用户可以执行的各种操作,reducer 被编码以便处理这些操作并更新实体的状态。应用程序中各种实体的状态都在一个中心位置维护:存储。例如,应用程序可能有各种实体,如 Todo 和用户配置文件,存储将维护这些实体的状态信息。每当 reducer 更新存储中特定实体的状态值时,用户界面组件从存储接收更新,更新组件的状态信息并重新渲染视图以显示更新后的状态。

React 中的 Redux

使用create-react-appCLI 创建项目后,包括依赖reduxreact-redux

npm install --save redux react-redux 

redux库包括createStorecombineReducersbindActionCreatorsapplyMiddlewarecompose辅助函数;而react-redux库包括 Redux 绑定,帮助你的 React 组件与 Redux 存储通信。

下一步是定义用户可以从用户界面发起的动作。在我们的示例中,我们将创建一个Counter组件,该组件可以增加减少计数器的值。

actions/counter.js中:

export  const  increment  = () => ({ **type:****'INCREMENT'** }); export  const  decrement  = () => ({ type: **'DECREMENT'** });

在为我们的计数器实体定义动作之后,需要定义更新counter状态的reducer

reducers/counter.js中:

const  counterReducer  = (state  =  0, action) => {    switch (action.type) { case  'INCREMENT': return  state  +  1; case  'DECREMENT': return  state  -  1; default: return  state; }
}

export  default **counterReducer**;

在这里定义的reducer根据用户触发的action类型更新state值。同样,应用程序中可以有各种 reducers 和 actions,它们在用户触发某个动作时更新实体的状态。

redux中的combineReducers实用程序允许您将所有 reducers 组合成一个单一的 reducer,然后可以在应用程序的存储中使用它来进行初始化。

reducers/index.js中:

import { combineReducers } from  'redux'; import  counterReducer  from  './counter'; const  rootReducer  =  combineReducers({ count:  counterReducer,
    todo: todoReducer }); export  default  rootReducer;

使用combineReducers函数创建了一个rootReducer,它接受一个包含实体和 reducer 键值映射的对象。这里counterReducer分配给了count实体,todoReducer分配给了一个带有todo键的实体。

然后在createStore函数中使用rootReducer来创建一个 store。

index.js中:

import { createStore } from 'redux';

const  store  =  createStore(
    rootReducer
);

使用react-redux库中定义的<Provider>组件,将 store 提供给应用程序中的组件:

ReactDOM.render(
 **<**Provider  store={store}**>**
 **<**Counter **/>**
 **</**Provider>,
 document.getElementById('root')
);

应用程序中的组件现在可以使用connect高阶函数订阅存储中实体(counttodo)的状态更改。创建了一个Counter组件,它将显示count的当前状态值,并将分发我们在actions/counter.js中定义的incrementdecrement动作。

components/counter.component.js中:

import { increment, decrement } from  '../actions/counter'; const  Counter  = ({ count, increment, decrement }) => ( <div> <h4>Counter</h4> <button  onClick={decrement}>-</button> <span>{count}</span> <button  onClick={increment}>+</button> </div> )

使用以下connect方法从store中提供countincrementdecrement属性:

import { connect } from  'react-redux'; import { increment, decrement } from  '../actions/counter';  ... const  mapStateToProps  =  state  => ({    count:  state.count });

const  mapDispatchToProps  =  dispatch  => ({    increment: () =>  dispatch(increment()),
    decrement: () =>  dispatch(decrement()) })

export  default  connect(mapStateToProps, mapDispatchToProps)(Counter**)**;

react-redux中的connect高阶函数帮助您将 Redux 状态注入到您的 React 组件中。connect HOC 接受两个参数:mapStateToPropsmapDispathToProps。如观察到的,Redux 状态count属性在mapStateToProps中分配给了组件的状态count属性,同样地,组件可以使用mapDispatchToProps中指定的incrementdecrement动作向存储分发动作。在这里,为了从 Redux 存储中读取状态值,使用了mapStateToPropsconnect提供了整个状态树给组件,以便组件可以从状态树中的各种对象中读取。为了改变状态树的状态,mapDispatchToProps帮助分发与存储注册的动作。connect HOC 提供了dispatch方法,以便组件可以在存储上调用动作。

开始使用 connected-react-router

connected-react-router库为 React Router 提供了 Redux 绑定;例如,可以从 Redux 存储中读取应用程序的历史记录,并且可以通过向存储分发动作来导航到应用程序中的不同路由。

让我们首先使用npm安装connected-react-router和其他库:

npm install --save connected-react-router  react-router  react-router-dom  history

接下来,我们将更新存储设置。

index.js中:

import { applyMiddleware, createStore, compose } from  'redux'; import { ConnectedRouter, connectRouter, routerMiddleware } from  'connected-react-router'; const  history  =  createBrowserHistory(); const  composeEnhancer  =  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__  ||  compose; const  store  =  createStore( connectRouter(history)(rootReducer), composeEnhancer(applyMiddleware(routerMiddleware(history))) );

createStore函数具有以下签名:

createStore(reducer, preloadedState, enhancer) 

它接受三个参数:第一个参数是reducer函数,它根据当前状态树和要处理的动作返回下一个状态树;第二个参数指定应用程序的初始state,应该是一个与combineReducers中使用的形状相同的对象;第三个参数指定存储enhancer,它为存储添加更多功能,如时间旅行、持久性等。

在我们的示例中,第一个参数如下:

connectRouter(history)(rootReducer)

connected-react-router中的connectRouter包装rootReducer并返回一个带有router状态的新根 reducer。connectRouter reducer 响应类型为@@router/LOCATION_CHANGE的动作以更新路由器状态。注意,connectRouter接受history对象作为其参数;然后使用history对象初始化路由器状态的locationaction属性。

createStore的第二个参数是增强器:

composeEnhancer  =  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__  ||  compose;
... composeEnhancer(applyMiddleware(routerMiddleware(history)))

请注意,我们将 enhancer 指定为第二个参数。如果 createStore 方法的第二个参数是函数,并且未指定 createStore 的第三个参数,则将第二个参数标记为 enhancerredux 中的 compose 实用程序返回通过从右到左组合给定函数获得的函数。在前面的情况下,我们正在检查浏览器中是否可用 Redux Devtools Extension,它使您能够查看应用程序中各种实体的状态。

routerMiddlewareconnected-react-router 中定义,是一个中间件函数,用于使用提供的 history 对象重定向用户。如果分发了一个 'CALL_HISTORY_METHOD' 类型的动作,中间件函数将通过调用 history 对象上的方法将用户导航到请求的路由。它还阻止了动作 (CALL_HISTORY_METHOD) 到达应用程序中定义的其他 reducer 和在 routerMiddleware 之后定义的中间件组件。

Redux 中的 applyMiddleware 实用程序用于创建存储增强器,它将中间件应用于 Redux 存储的分发方法。

下一步是使用 <Provider> 组件使存储(使用 createStore 创建)可用于应用程序中的组件:

ReactDOM.render(
 **<**Provider  store={store}> <ConnectedRouter  history={history}**>** <App  /> </ConnectedRouter> </Provider>, document.getElementById('root'));

在这里,我们将应用程序根组件包装在 <ConnectedRouter> 组件内部,而 <ConnectedRouter> 组件又包装在 <Provider> 组件内部。这是必需的,因为 ConnectedRouter 订阅了 router 状态的更改,以查看 location 属性是否已更改,然后调用 history.push 方法将用户导航到请求的路由。

通过这些更改,我们应用程序中的组件现在可以从存储中读取状态信息,并分发动作以导航到应用程序中定义的各种路由。

从 Redux 存储中读取状态信息

为了测试上述设置,让我们首先在我们的导航栏中创建一个 <Link> 组件和一个相应的具有相同路径名的 <Route>

<Link
 **to**={{ pathname: '/dashboard', search: 'q=1', hash: 'test',
        state: { key: 'value' } }**}** > Dashboard </Link> ...
<Route  path='/dashboard'  component={Dashboard}  />

请注意,<Link> 组件指定了带有 pathnamesearchhashstate 属性的 to 对象。我们将从 Redux 存储中读取此信息在我们的渲染组件中:

const  Dashboard  = ({ pathname, search, hash, state, count }) => { return ( <div> <h4>In Dashboard</h4> <div> Pathname   : {pathname}  </div> <div> Search     : {search}  </div> <div> Hash       : {hash}  </div> <div> State-Key  : {state? state.key : null} </div>  </div> ) } const  mapStateToProps  =  state  => ({ pathname:  state.router.location.pathname, search:  state.router.location.search, hash:  state.router.location.hash, state:  state.router.location.state  }); export  default  connect(mapStateToProps)(Dashboard);

从这段代码中,pathnamesearchlocationhash属性从state.router.location中读取。正如前面提到的,connectRouter函数创建了router状态,并在分发了LOCATION_CHANGE类型的动作时更新了值。<ConnectRouter>组件监听历史对象的变化,然后在你使用<Link>组件尝试导航时分发LOCATION_CHANGE动作。

如果你在 Chrome 中安装了 Redux Dev Tools(在 Chrome Web Store 中可用),你可以观察到当你尝试从一个路由导航到另一个路由时分发的动作。

在这个 Dev Tools 窗口中,当你尝试导航时,会分发@@router/LOCATION_CHANGE动作,下一节中的动作显示了分发动作时提供的有效载荷。

通过分发动作进行导航

connected-react-router库提供了可以从组件中分发的动作,以导航到应用程序中定义的路由。这些包括pushreplacegogoBackgoForward。这些方法调用历史对象上的相应方法,以导航到指定的路径。

前面例子中的DashboardComponent现在可以更新为使用mapDispatchToProps

import {push, replace} from 'connected-react-router'; const  Dashboard  = ({ pathname, search, hash, state, count, push, replace }) => {    return ( ...
<button  onClick={() => {push('/')}}>HOME</button> <button  onClick={() => {replace('/counter')}}>COUNTER</button>
        ...
 ) } 
const  mapStateToProps  =  state  => ({ ...
}); 
const  mapDispatchToProps  =  dispatch  => ({ push: (path) =>  dispatch(push(path**))**, replace: (path) =>  dispatch(replace(path**))** });

export  default  connect(mapStateToProps, mapDispatchToProps)(Dashboard**)**;

前面的组件现在在你点击 HOME 和 COUNTER 按钮时分发pushreplace动作。mapDispatchToProps函数使你能够向 store 分发动作,在我们的例子中,pushreplace函数接受一个pathname来分发动作。

总结

在本章中,我们看到了如何使用 Redux 库创建一个存储来管理应用程序中的各种状态实体。存储接收动作,当分发动作时,减少器改变应用程序的状态。connected-react-router库为 React Router 提供了 Redux 绑定,其中包括一个高阶函数connectRouter,它包装了rootReducer并创建了一个router状态。然后在createStore函数中使用connectRouter函数,使router状态可用于应用程序中的组件。

connected-react-router中的<ConnectedRouter>组件监听history位置的变化,并分发LOCATION_CHANGE动作来更新router状态属性。然后渲染的路由组件可以通过从存储中读取状态信息来读取这个router状态属性。

该库还包括pushreplacegogoBackgoForward动作,组件可以分发这些动作来导航到应用程序中定义的路由。

posted @ 2024-05-16 14:50  绝不原创的飞龙  阅读(18)  评论(0编辑  收藏  举报