Skip to content

Latest commit

 

History

History
1250 lines (841 loc) · 61.9 KB

File metadata and controls

1250 lines (841 loc) · 61.9 KB

九、React 路由

既然我们已经组织了项目并添加了开发工具以提高生产力,那么让我们回到添加更多特性到问题跟踪器上来。

在这一章中,我们将探索路由的概念,或者处理我们可能需要显示的多个页面。即使在单页面应用(SPA)中,实际上应用中也有多个逻辑页面(或视图)。只是页面加载只在第一次从服务器进行。之后,通过操作或更改 DOM 而不是从服务器获取整个页面来显示其他视图。

要在应用的不同视图之间导航,需要 routing 。路由将页面的状态链接到浏览器中的 URL。这不仅是一种根据 URL 推断页面中显示内容的简单方法,它还具有以下非常有用的属性:

  • 用户可以使用浏览器的前进/后退按钮在应用的已访问页面(实际上是视图)之间导航。

  • 个人网页可以加入书签,以后再访问。

  • 视图链接可以与其他人共享。假设您想请某人帮助您解决某个问题,并且您想向他们发送显示该问题的链接。对于收件人来说,通过电子邮件向他们发送链接比让他们浏览用户界面要容易和方便得多。

在水疗真正成熟之前,这是相当困难的,有时甚至是不可能的。SPAs 只有一个页面,也就是说只有一个 URL。所有的导航都必须是交互式的:用户必须通过预定义的步骤浏览应用。例如,无法将特定问题的链接发送给某人。相反,他们必须被告知按照 SPA 上的一系列步骤来解决问题。但是现代水疗会优雅地处理这个问题。

在本章中,我们将探索如何使用 React Router 来简化在视图之间设置导航的任务。我们将从应用的另一个视图开始,在这个视图中,用户可以查看和编辑单个问题。然后,我们将在视图之间创建链接,以便用户可以在它们之间导航。在我们创建的超链接上,我们将添加可以传递到不同视图的参数,例如,需要显示的问题的 ID,到显示单个问题的视图。最后,我们将看到如何嵌套组件和路由。

为了影响路由,任何页面都需要连接到浏览器能够识别并指示“这是用户正在查看的页面”的东西一般来说,对于水疗中心,有两种方式来建立这种联系:

  • 基于散列的 : 这使用 URL 的锚部分(跟随#的所有内容)。这个方法很自然,因为#部分可以被解释为页面中的一个位置,并且在一个 SPA 中只有一个页面。这个位置决定了显示页面的哪个部分。在#之前的部分永远不会从组成整个应用的唯一页面(index.html)改变。这很容易理解,并且对大多数应用都很有效。事实上,在不使用路由库的情况下,我们自己实现基于散列的路由是非常简单的。但是我们不会自己做,我们会用 React 路由来做。

  • *浏览器历史:*这使用了新的 HTML5 API,让 JavaScript 处理页面转换,同时防止浏览器在 URL 改变时重新加载页面。即使有 React Router 的帮助,实现起来也有点复杂(因为它迫使您考虑当服务器收到对不同 URL 的请求时会发生什么)。但是,当我们想从服务器本身呈现一个完整的页面时,这非常方便,尤其是让搜索引擎爬虫获取页面内容并对其进行索引。

我们将从基于散列的技术开始,因为它容易理解,然后切换到浏览器历史技术,因为我们将在后面的章节中实现服务器端呈现。

简单路由

在本节中,我们将创建两个视图,一个用于我们一直在处理的问题列表,另一个(占位符)用于报告节。我们还将确保主页,即/,重定向到问题列表。首先,让我们安装将帮助我们完成这一切的包:React Router。

$ cd ui
$ npm install react-router-dom@4

让我们也为报告视图创建一个占位符组件。我们将把它和其他组件一起保存在ui/src目录中。我们称这个组件的文件为IssueReport.jsx,其全部内容在清单 9-1 中列出。

import React from 'react';

export default function IssueReport() {
  return (
    <div>
      <h2>This is a placeholder for the Issue Report</h2>
    </div>
  );
}

Listing 9-1ui/src/IssueReport.jsx: New File for Report Placeholder

现在,让我们将应用的主页分成两个部分:一个标题部分包含一个导航栏,其中包含指向不同视图的超链接;一个内容部分,它将根据所选的超链接在两个视图之间切换。无论显示何种视图,导航栏都将保持不变。将来,我们可能会在内容部分看到其他视图。让我们为内容创建一个组件,并把它放在目录ui/src下名为Contents.jsx的文件中。该组件将负责视图之间的切换。

为了基于被点击的超链接实现不同组件之间的路由或切换,React Router 提供了一个名为Route的组件。它将路由需要匹配的路径和当路径与浏览器中的 URL 匹配时需要显示的组件作为属性。让我们使用路径/issues来显示问题列表,使用/report来显示报告视图。以下代码片段将实现这一点:

...
      <Route path="/issues" component={IssueList} />
      <Route path="/report" component={IssueReport} />
...

为了将主页重定向到/issues,我们可以进一步添加一个从/重定向到/issuesRedirect组件,如下所示:

...
      <Redirect from="/" to="/issues" />
...

最后,让我们添加一条当没有匹配的路由时显示的消息。注意,当属性path没有为Route组件指定时,这意味着它匹配任何路径。

...
const NotFound = () => <h1>Page Not Found</h1>;
...
      <Route component={NotFound} />
...

这四个路由需要封装在一个包装器组件中,它可以只是一个<div>。但是为了表明只需要显示这些组件中的一个,它们应该被包含在一个<Switch>组件中,以便只呈现第一个匹配的组件。在这种情况下,我们确实需要switch,因为最后一条路线将匹配任何路径。

还要注意,该匹配是一个前缀为的匹配。例如,路径/将不仅匹配/,还匹配/issues/report。所以路线的顺序也很重要。Redirect必须在/issues/report之后出现,并且总括路线必须在最后出现。或者,可以将exact属性添加到任何路由中,以表明它需要完全匹配。

请注意,匹配在两个方面不同于快速路由。首先,在 Express 中,默认情况下匹配是精确的,必须添加一个*来匹配后面的任何内容。其次,在 Express 中,路由匹配停止进一步的处理(除非是中间件,它可以为请求-响应过程增值并继续),而在 React Router 中,明确需要一个<Switch>来使它在第一次匹配时停止。否则,所有路径匹配的组件都会被渲染。

让我们对Redirect使用一个精确匹配,并让全包路线成为最后一个。在添加了必要的import语句和<Switch>包装器之后,Contents.jsx的最终内容如清单 9-2 所示。

import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';

import IssueList from './IssueList.jsx';
import IssueReport from './IssueReport.jsx';

const NotFound = () => <h1>Page Not Found</h1>;

export default function Contents() {
  return (
    <Switch>
      <Redirect exact from="/" to="/issues" />
      <Route path="/issues" component={IssueList} />
      <Route path="/report" component={IssueReport} />
      <Route component={NotFound} />
    </Switch>
  );
}

Listing 9-2ui/src/Contents.jsx: New File for the Contents Section

接下来,让我们创建显示导航栏和内容组件的页面。一个像这样一个接一个地显示NavBarContents组件的无状态组件将完成必要的工作。(需要一个<div>来包含这两个元素,因为组件的 render 方法只能返回一个元素) :

...
    <div>
      <NavBar />
      <Contents />
    </div>
...

至于导航栏,我们需要一系列超链接。因为我们将使用HashRouter,所有的页面都将有主 URL 作为/,后面是一个以#符号开始的页面内锚,并且有路线的实际路径。例如,为了匹配Route规范中指定的/issues路径,URL 将是/#/issues,其中第一个/是 SPA 的唯一页面,#是锚点的分隔符,/issues是路由的路径。

因此,到问题列表的链接将采用/#/issues的形式,如下所示:

...
      <a href="/#/issues">Issue List</a>
...

让我们有三个超链接,一个是主页,一个是问题列表,一个是报告。要用竖线(|)字符分隔它们,我们需要使用如下 JavaScript 表达式:

...
      {' | '}
...

这是因为空白被 JSX 变换去除了,否则我们不能自然地添加周围有空白的条。让我们将导航栏创建为一个带有三个超链接的无状态组件,并将它与同样无状态的Page组件放在一个名为Page.jsx的新文件中。这个新文件的内容如清单 9-3 所示。

import React from 'react';

import Contents from './Contents.jsx';

function NavBar() {
  return (
    <nav>
      <a href="/">Home</a>
      {' | '}
      <a href="/#/issues">Issue List</a>
      {' | '}
      <a href="/#/report">Report</a>
    </nav>
  );
}

export default function Page() {
  return (
    <div>
      <NavBar />
      <Contents />
    </div>
  );
}

Listing 9-3ui/src/Page.jsx: New File for Composite Page

最后,在App.jsx中,我们需要将这个页面而不是原始的IssueList组件呈现到 DOM 中。此外,页面本身需要包装在路由周围,因为所有路由功能都必须在路由中才能工作。我们将使用react-router-dom包中的HashRouter组件。清单 9-4 显示了对App.jsx的这些更改。

...
import ReactDOM from 'react-dom';

import { HashRouter as Router } from 'react-router-dom';

import IssueList from './IssueList.jsx';

import Page from './Page.jsx';

const element = <IssueList />;

const element = (
  <Router>
    <Page />
  </Router>
);
...

Listing 9-4ui/src/App.jsx: Changes to Mount Page Instead of IssueList

注意

尽管我们不遗余力地确保所有代码清单的准确性,但在本书付印之前,可能会有一些错别字甚至更正没有被收入书中。所以,总是依赖 GitHub 库( https://github.com/vasansr/pro-mern-stack-2 )作为所有代码清单的经过测试的和最新的源代码,尤其是当某些东西不能按预期工作时。

现在,如果您通过导航到localhost:8000在浏览器中测试应用,您会发现浏览器的 URL 会自动更改为http://localhost:8000/#/issues。这是因为Redirect路线。在屏幕上,您会看到一个导航栏和其下方的常见问题列表。截图如图 9-1 所示。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig1_HTML.jpg

图 9-1

导航栏和常见问题列表

现在,您应该能够通过单击导航栏中的超链接在三个视图之间切换。点击主页将重定向至问题列表,点击报告将显示类似报告视图的占位符,如图 9-2 所示。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig2_HTML.jpg

图 9-2

问题报告占位符

如果键入任何其他文本而不是报告或问题,您也应该看到“未找到页面”消息。重要的是,您应该能够使用前进和后退按钮在导航历史中导航。浏览器的刷新也应该显示当前页面。

练习:简单路由

  1. Redirect的属性列表中删除exact。会发生什么?你能解释这种行为吗?没有exact属性你能达到要求吗?(记住在此练习后恢复更改。)

  2. 用一个<div>替换<Switch>。现在发生了什么?你能解释一下这个吗?(在此练习之后,还原代码以恢复原始行为。)

  3. 查看问题列表时,如果您在 URL 后面追加一些额外的文本,例如/ #/issues000,您预计会发生什么情况?尝试一下来确认你的答案。现在,尝试同样的方法,但是在额外的文本前加一个/符号,例如/ #/issues/000。现在你期待什么,你看到了什么?也试着在路线中加入exact。关于匹配算法,它告诉了你什么?

本章末尾有答案。

路线参数

正如您刚才看到的(也就是说,如果您已经完成了上一节中的练习),URL 的路径和路由的路径不需要完全匹配。URL 中匹配部分后面的内容是路径的动态部分,它可以作为路由组件中的一个变量来访问。这是向组件提供参数的一种方式。另一种方法是使用 URL 的查询字符串,我们将在下一节探讨这一点。

让我们使用这个工具来显示一个允许我们编辑问题的页面。现在,我们将创建一个占位符,就像我们对报告所做的那样。我们将这个文件称为IssueEdit.jsx。稍后,我们将进行 Ajax 调用,获取问题的详细信息,并以表单的形式显示出来,供用户进行更改和保存。为了确保我们收到的是正确的问题 ID,让我们将其显示在占位符中。

在 route path 中指定参数类似于在 Express 中,使用字符:后跟将接收值的属性的名称。我们姑且称编辑一个问题的路径的基,/edit。然后,路径规范/edit/:id将匹配一个 URL 路径,如/edit/1/edit/2等。对路线的更改以及组件的导入如清单 9-5 所示,在Contents.jsx文件中。

...
import IssueReport from './IssueReport.jsx';

import IssueEdit from './IssueEdit.jsx';

...
      <Route path="/issues" component={IssueList} />
      <Route path="/edit/:id" component={IssueEdit} />
...

Listing 9-5ui/src/Contents.jsx: Changes for IssueEdit Route

通过props,所有路由组件都被提供一个名为match的对象,该对象包含匹配操作的结果。其中包含一个名为params的字段,用于保存路由参数变量。因此,要访问包含id的 URL 路径的尾部,可以使用match.params.id。让我们使用它并在IssueEdit.jsx中创建占位符编辑组件。该文件的内容如清单 9-6 所示。

import React from 'react';

export default function IssueEdit({ match }) {
  const { id } = match.params;
  return (
    <h2>{`This is a placeholder for editing issue ${id}`}</h2>
  );
}

Listing 9-6ui/src/IssueEdit.jsx: New File for Placeholder IssueEdit Component

现在,你可以输入/ #/edit/1等等。在浏览器的 URL 栏中进行测试,但是为了方便起见,我们在问题列表的每一行中创建一个超链接。为此,我们将创建一个名为 Action 的新列,并用一个指向/edit/<issue id>的超链接填充它。这些变化出现在IssueTable.jsx,如清单 9-7 所示。

...
function IssueRow({ issue }) {
...
      <td>{issue.title}</td>
      <td><a href={`/#/edit/${issue.id}`}>Edit</a></td>
    </tr>
...
}
...

export default function IssueTable({ issues }) {
...
          <th>Title</th>
          <th>Action</th>
        </tr>
...
}
...

Listing 9-7ui/src/IssueTable.jsx

现在,如果您测试应用并转到问题列表页面,您将在表格右侧看到一个额外的列,其中有一个名为 Edit 的链接。单击此链接应该会显示用于编辑问题的占位符页面,以及您单击的问题的 ID。要返回问题列表页面,您可以使用浏览器的后退按钮,或者单击导航栏中的问题列表超链接。占位符编辑页面的截图如图 9-3 所示。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig3_HTML.jpg

图 9-3

编辑页面占位符

查询参数

像我们在上一节中看到的那样,添加变量(如正在编辑的问题的 ID)作为路由参数是非常简单和自然的。但是会有这样的情况,变量很多,而且不一定有一定的顺序。

让我们以问题列表为例。到目前为止,我们一直在显示数据库中的所有问题。这显然不是一个好主意。理想情况下,我们将有许多方法来过滤要显示的问题。例如,我们希望根据状态、受托人等进行筛选。,能够在数据库中搜索包含特定文本的问题,并具有用于对列表进行排序和分页的附加参数。URL 的查询字符串部分是处理这些问题的理想方式。

我们现在不会实现所有可能的过滤器、排序和分页。但是为了理解查询参数是如何工作的,让我们基于 status 字段实现一个简单的过滤器,以便用户可以只列出具有特定状态的问题。让我们首先更改 List API 来接受这个过滤器。让我们从更改 GraphQL 模式开始。这是一个简单的变化;我们所需要做的就是将一个名为status的参数添加到issueList查询中,类型为StatusType。这一变化如清单 9-8 所示。

...
type Query {
  about: String!
  issueList(status: StatusType): [Issue!]!
}
...

Listing 9-8api/schema.graphql: Addition of a Filter to issueList API

让我们在文件issue.js的 API 实现中,在函数list()中接受这个新的参数。这个函数现在将接受一个名为status的参数,类似于add()函数。由于参数是可选的,我们将有条件地添加一个状态过滤器,并将其传递给集合的find()方法。这些变化如清单 9-9 所示,都是issue.js的一部分。

...
async function list(_, { status }) {
  const db = getDb();
  const filter = {};
  if (status) filter.status = status;
  const issues = await db.collection('issues').find(filter).toArray();
  return issues;
}
...

Listing 9-9api/issue.js: Handle Filtering on Issue Status

在这一点上,使用操场运行一个快速测试是很好的。您可以使用以下查询测试对状态为New的问题的问题列表的过滤:

{
  issueList(status: New) {
    id status title
  }
}

您应该得到一个只包含新问题的响应。此外,您可以确保原始查询(没有任何过滤器)也能正常工作,返回数据库中的所有问题。

现在,让我们用三个超链接替换筛选器的占位符:所有问题、新问题和已分配问题。让我们使用一个名为status的查询字符串变量,其值指示要过滤的状态,并添加超链接,就像我们在导航栏中所做的那样。带有超链接而不是占位符的新组件如清单 9-10 所示,作为IssueFilter.js文件的全部内容。

/* eslint "react/prefer-stateless-function": "off" */

import React from 'react';

export default class IssueFilter extends React.Component {
  render() {
    return (
      <div>This is a placeholder for the issue filter.</div>
      <div>
        <a href="/#/issues">All Issues</a>
        {' | '}
        <a href="/#/issues?status=New">New Issues</a>
        {' | '}
        <a href="/#/issues?status=Assigned">Assigned Issues</a>
      </div>
    );
  }
}

Listing 9-10ui/src/IssueFilter.js: New Component with Filter Links

查询字符串需要由作为loadData()函数一部分的IssueList组件来处理。就像match属性一样,React 路由也提供一个名为location的对象作为 props 的一部分,该对象包括路径(在字段pathname中)和查询字符串(在字段search中)。React 路由不解析查询字符串,而是让应用决定如何解析这个字段。让我们遵循查询字符串的常规解释和解析,这可以通过 JavaScript API URLSearchParams()轻松完成,就像这样,在loadData()方法中:

...
    const { location: { search } } = this.props;
    const params = new URLSearchParams(search);
...

API URLSearchParams()可能需要一个针对旧浏览器的 polyfill,尤其是 Internet Explorer,如 https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams 的 MDN 文档中所述。既然我们也承诺支持 IE,让我们安装 polyfill。

$ cd ui
$ npm install url-search-params@1

要包含 polyfill,我们必须在IssueList.jsx中将其导入。

...
import URLSearchParams from 'url-search-params';
...

解析完查询字符串后,我们将使用URLSearchParamsget()方法访问status参数,就像params.get('status')一样。让我们创建一个变量,作为查询变量提供给 GraphQL。如果参数不存在,params.get()方法返回null,但是在这种情况下我们想跳过设置变量。因此,在将状态添加到变量之前,我们将添加一个检查来查看状态是否已定义。

...
    const vars = {};
    if (params.get('status')) vars.status = params.get('status');
...

让我们将简单的 GraphQL 查询修改为一个带有变量的命名操作:

...
    const query = `query issueList($status: StatusType) {
      issueList (status: $status) {
        id title status owner
        created effort due
      }
    }`;
...

现在,我们可以修改对graphQLFetch()的调用,以包含具有状态过滤器参数的查询变量:

...
    const data = await graphQLFetch(query, vars);
...

此时,如果您尝试应用并通过单击每个过滤器超链接来应用过滤器,您会发现问题列表并没有改变。但是在 URL 中使用现有过滤器刷新浏览器时,它会显示正确的过滤问题列表。您还会发现,导航到另一个路径,例如报告页面或编辑页面,并使用 back 按钮会使过滤器生效。这表明在初始渲染时调用了loadData(),但是查询字符串的变化不会导致调用loadData()

我在前面简单地谈到了组件生命周期方法。这些是 React 对组件所做的各种更改的挂钩。我们使用生命周期方法componentDidMount()来挂钩组件的初始就绪状态。类似地,我们需要挂钩一个方法,告诉我们查询字符串已经更改,这样我们就可以重新加载列表。这个图表很好地描述了一整套生命周期方法: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

从图中可以清楚地看出,生命周期方法componentDidUpdate()有利于在某些属性发生变化时采取行动。发生这种情况时会自动调用一个render(),但这还不够。我们还需要通过调用loadData()来刷新状态。

让我们实现生命周期挂钩componentDidUpdate(),并在必要时通过比较IssueList中的新旧查询字符串来重新加载数据。这个方法是通过了前面的道具。当前道具可以使用this.props来获得。我们可以通过比较先前和当前属性的location.search属性来检测过滤器中的变化,并在发生变化时重新加载数据:

...
  componentDidUpdate(prevProps) {
    const { location: { search: prevSearch } } = prevProps;
    const { location: { search } } = this.props;
    if (prevSearch !== search) {
      this.loadData();
    }
  }
...

清单 9-11 显示了对IssueList组件的完整更改,包括最近的更改。

...
import React from 'react';

import URLSearchParams from 'url-search-params';

...

  componentDidUpdate(prevProps) {
    const { location: { search: prevSearch } } = prevProps;
    const { location: { search } } = this.props;
    if (prevSearch !== search) {
      this.loadData();
    }
  }
...

  async loadData() {
    const { location: { search } } = this.props;
    const params = new URLSearchParams(search);
    const vars = {};
    if (params.get('status')) vars.status = params.get('status');

    const query = `query {
      issueList {
    const query = `query issueList($status: StatusType) {
      issueList (status: $status) {
        id title status owner
        created effort due
      }
    }`;

    const data = await graphQLFetch(query, vars);
    if (data) {
    ...
  }
...

Listing 9-11ui/src/IssueList.jsx: Changes for Handling a Query String Based Filter

现在,如果您在不同的过滤器超链接之间导航,您会发现问题列表会根据所选的过滤器进行刷新。

练习:查询参数

  1. 通过在 URL 栏中更改问题 ID 或为其中一个问题创建书签,在两个不同问题的编辑页面之间导航。正确显示了两个不同的页面。与问题列表页面相比,这里需要一个componentDidUpdate()方法。为什么会这样?提示:思考属性的更改如何影响问题编辑页面中的问题列表。

  2. componentDidUpdate()中,检查新旧属性是否相同。这需要吗?一个原因当然是,它避免了不必要的loadData()。但是为什么不在属性改变时重新加载数据呢?自己试试看。(记得在练习后恢复更改。)

  3. IssueAddrender()方法中添加一个断点,在过滤器之间切换。你会发现当过滤器改变时,这个组件被再次渲染。对性能有什么影响?这怎么优化?提示:阅读关于在 https://reactjs.org/docs/react-component.html React 的文档中的“组件生命周期”部分。

本章末尾有答案。

链接

到目前为止,我们一直使用href来创建超链接。尽管这是可行的,但 React Router 提供了一种更好、更方便的方式来通过Link组件创建链接。这个组件很像一个href,但是它有以下不同之处:

  • Link中的路径总是绝对的;它不支持相对路径。

  • 查询字符串和散列可以作为单独的属性提供给Link

  • LinkNavLink的一个变体能够判断当前 URL 是否与链接匹配,并向链接添加一个类以将其显示为活动的。

  • 一个Link在不同种类的路由之间工作是相同的,也就是说,指定路由的不同方式(使用#字符,或使用路径原样)对程序员是隐藏的。

让我们利用链接组件的这些属性,将所有的href更改为Link,这个组件有一个属性to,它可以是一个字符串(对于简单的目标)或一个对象(对于带有查询字符串的目标,等等)。).让我们从IssueRow组件的变化开始,这里的目标是一个简单的字符串。我们需要将组件的名称从<a>更改为<Link>,并将属性href更改为to,同时保持与目标相同的字符串。清单 9-12 中显示了IssueTable.jsx的变化。

...
import React from 'react';

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

function IssueRow({ issue }) {
...
      <td>{issue.title}</td>
      <td><a href={`/#/edit/${issue.id}`}>Edit</a></td>
      <td><Link to={`/edit/${issue.id}`}>Edit</Link></td>
...
}
...

Listing 9-12ui/src/IssueTable.jsx: Changes to IssueRow to Use Link

接下来我们可以使用IssueFilter,这里有查询字符串。这一次,让我们提供一个分别包含路径和查询字符串的对象,而不是一个字符串。这些的对象属性分别是pathnamesearch。因此,对于新问题的链接,to属性将包含路径名为/issues,查询字符串为?status=New

请注意,React Router 不会对查询字符串做出假设,正如您在上一节中看到的那样。本着同样的精神,它也要求将前缀?作为查询字符串的一部分。清单 9-13 中显示了对IssueFilter的这些更改。

...
import React from 'react';

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

export default class IssueFilter extends React.Component {
...
        <a href="/#/issues">All Issues</a>
        <Link to="/issues">All Issues</Link>
        ...
        <a href="/#/issues?status=New">New Issues</a>
        <Link to={{ pathname: '/issues', search: '?status=New' }}>
          New Issues
        </Link>
        ...
        <a href="/#/issues?status=Assigned">Assigned Issues</a>
        <Link to={{ pathname: '/issues', search: '?status=Assigned' }}>
          Assigned Issues
        </Link>
...
}
...

Listing 9-13ui/src/IssueFilter.jsx: Change to Convert hrefs to Links

至于Page.jsx中的导航栏,让我们用一个NavLink来代替,这将允许我们突出显示当前活动的导航链接。注意,NavLink将高亮显示任何部分匹配 URL 路径的路径,这些路径基于由/分隔的段。当导航路径的整个层次结构都可以突出显示时,或者当导航链接在页面中有更多变化时,这很有用。对于我们目前在应用中的导航栏来说,Home链接,它的目标只是/,将匹配浏览器 URL 中的任何路径。为了避免总是高亮显示,NavLink有一个exact属性,就像Route组件的属性一样,强制进行精确匹配而不是前缀匹配。让我们只对Home链接使用该属性,并像对IssueRow组件那样简单地转换其他属性。这些变化如清单 9-14 所示。

...
import React from 'react';

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

...

function NavBar() {
...
      <a href="/">Home</a>
      <NavLink exact to="/">Home</NavLink>
      ...
      <a href="/#/issues">Issue List</a>
      <NavLink to="/issues">Issue List</NavLink>
      ...
      <a href="/#/report">Report</a>
      <NavLink to="/report">Report</NavLink>
...
}
...

Listing 9-14ui/src/Page.jsx: Change to Replace hrefs with NavLinks

NavLink只在链接匹配 URL 时添加一个名为active的类。为了改变活动链接的外观,我们需要为这个类定义一个样式。让我们为样式规范中的活动链接使用浅蓝色背景。清单 9-15 显示了index.html的这一变化。

...
  <style>
    ...
    a.active {background-color: #D8D8F5;}
  </style>
...

Listing 9-15ui/public/index.html: Style for Active NavLinks

现在,当您测试应用时,您不仅应该看到它像以前一样工作,还应该看到基于当前显示的页面突出显示的导航链接之一:问题列表页面或报告页面。然而,没有任何相应导航链接的编辑页面不会导致任何链接被突出显示。查看问题列表时的应用截图如图 9-4 所示。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig4_HTML.jpg

图 9-4

查看列表时突出显示的问题列表链接

如果您两次单击同一个链接,您可能还会在开发人员工具控制台中看到一条警告,称“哈希历史不能推送相同的路径...”此消息仅在开发模式下可见,并且仅在我们以编程方式推入与之前相同的路由路径时才会显示。您可以放心地忽略此警告。在任何情况下,我们将很快过渡到浏览器历史路由,在那里将不会看到这个警告。

练习:链接

  1. 你可能已经注意到我们没有使用NavLink s 来过滤链接。试着把这些也改成NavLinks。在过滤器之间导航时,您观察到了什么?你能解释这个吗?(记得在完成实验后恢复更改。)

  2. 假设您使用的是第三方 CSS 库,使用该库突出显示链接的方式是添加current类而不是active类。你会怎么做?提示:在 https://reacttraining.com/react-router/web/api/NavLink 查阅NavLink的文档。

本章末尾有答案。

程序导航

当变量值是动态的并且可能有许多无法预先确定的组合时,通常使用查询字符串。它们通常也是 HTML 表单的结果。一个表单需要动态地构造查询字符串,这与我们到目前为止在Link中使用的预先确定的字符串相反。

在后面的章节中,我们将创建一个更正式的表单,而不仅仅是将状态作为过滤器,但是在这一节中,我们将添加一个简单的下拉列表,并根据下拉列表的值设置查询字符串。我们可以通过传递一个来自IssueList的回调来直接重新加载列表,该回调接受新的过滤器作为参数。但是,URL 将不会反映页面的当前状态,这不是一个好主意,因为如果用户刷新浏览器,过滤器将被清除。建议保持数据流单向:当下拉列表值改变时,它会改变 URL 的查询字符串,进而应用过滤器。即使我们从中间开始也是一样的:直接改变 URL 的查询字符串,它将应用过滤器。

让我们首先创建这个简单的下拉列表,并用它替换IssueFilter中的链接。

...
      <div>
        Status:
        {' '}
        <select>
          <option value="">(All)</option
          <option value="New">New</option>
          ...
        </select>
      </div>
...

注意

编译器在元素边界处去除 JSX 中的所有空白,因此标签Status:后的空格将不起作用。在标签后添加空格的一种方法是使用 HTML 不间断空格。另一种插入元素的方法是将它作为 JavaScript 文本添加,这就是我们在本例中使用的方法。

接下来,让我们在 dropdown 值改变时捕获事件,并且可以预见的是,在onChange中捕获这个事件的属性。让我们添加这个属性,并将其设置为一个名为onChangeStatus的类方法。

...
        <select onChange={this.onChangeStatus}>
...

在方法onChangeStatus的实现中,我们可以通过value属性,使用事件的目标(它将是下拉列表本身的句柄)获取下拉列表中所选项目的值:

...
  onChangeStatus(e) {
    const status = e.target.value;
}
...

就像 React Router 给IssueList组件增加的location属性一样,它还增加了一些更多的属性,其中一个是history.使用 this,location,query string 等。可以设置浏览器的 URL。但是,与IssueList不同,由于IssueFilter不直接是任何路由的一部分,React 路由不能自动使这些可用。为此,我们必须将这些附加属性显式地注入到IssueFilter组件中。这可以使用 React Router 提供的名为withRouter()的包装函数来完成。这个函数接受一个组件类作为参数,并返回一个新的组件类,它的historylocationmatch作为props的一部分。因此,我们不导出组件,而是像这样导出包装好的组件:

...
export default class IssueFilter extends React.Component {
  ...
}
...

export default withRouter(IssueFilter);

...

现在,在onChangeStatus()中,我们将可以访问this.props.history,它可以用于根据更改后的过滤器推送新位置。但是要访问处理程序中的this,我们必须确保处理程序被绑定到构造函数中的this

...
  constructor() {
    super();
    this.onChangeStatus = this.onChangeStatus.bind(this);
  }
...

现在,在处理程序中,我们可以使用historypush()方法来推送新位置。这个方法接受一个对象,就像我们用于Link指定位置的对象一样,即一个pathname和一个search。让我们也处理一下空状态选项,我们将不会对其进行搜索。

...
  onChangeStatus(e) {
    ...
    const { history } = this.props;
    history.push({
      pathname: '/issues',
      search: status ? `?status=${status}` : '',
    });
  }
...

清单 9-16 中显示了IssueFilter.jsx的完整源代码。删除的代码没有显示出来,因为几乎所有以前的代码都被删除了。

import React from 'react';
import { withRouter } from 'react-router-dom';

class IssueFilter extends React.Component {
  constructor() {
    super();
    this.onChangeStatus = this.onChangeStatus.bind(this);
  }

  onChangeStatus(e) {
    const status = e.target.value;
    const { history } = this.props;
    history.push({
      pathname: '/issues',
      search: status ? `?status=${status}` : '',
    });
  }

  render() {
    return (
      <div>
        Status:
        {' '}
        <select onChange={this.onChangeStatus}>
          <option value="">(All)</option>
          <option value="New">New</option>
          <option value="Assigned">Assigned</option>
          <option value="Fixed">Fixed</option>
          <option value="Closed">Closed</option>
        </select>
      </div>
    );
  }
}

export default withRouter(IssueFilter);

Listing 9-16ui/src/IssueFilter.jsx: New Implementation of IssueFilter

如果您现在测试应用,您会发现当在下拉列表中选择不同的项目时,问题列表会发生变化。要查看它是否适用于除“新”和“已分配”之外的状态,您必须直接在 MongoDB 中或通过 Playground 添加更多关于其他状态的问题。图 9-5 中显示了应用的屏幕截图,其中问题列表已根据新问题进行了过滤。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig5_HTML.jpg

图 9-5

使用下拉列表过滤的问题列表

练习:程序化导航

  1. 我们用的是historypush()方法。还有什么方法可以使用,效果会有什么不同?提示:在 https://reacttraining.com/react-router/web/api/history 查阅history的文档。试试看。(记得在练习后恢复更改。)

  2. 过滤问题列表,例如,新建。现在,保持开发人员控制台打开,并在浏览器中单击刷新。下拉菜单是否反映了过滤器的状态?再次在下拉列表中选择新建。你看到了什么?这是什么意思?

  3. IssueList组件可以访问history对象。因此,不要在IssueFilter上使用withRouter,你可以将history对象从IssueList传递到IssueFilter,或者传递一个回调到IssueFilter,设置一个新的过滤器并从子组件调用它。比较这些选择。与使用withRouter的原始方法相比,有哪些优点和缺点?

本章末尾有答案。

嵌套路由

在显示对象列表的同时显示一个对象的细节的常见模式是使用 header-detail UI 模式。这与一些电子邮件客户端相同,特别是 Outlook 和 Gmail,它们可以纵向或横向拆分使用。对象列表显示了关于它们的简要信息(每封电子邮件的发件人和主题),选择其中一个对象后,所选对象(邮件本身)的更多详细信息将显示在详细信息区域。

问题跟踪器没有多大用处,除非它能够存储每个问题的详细描述和不同用户的评论。因此,与电子邮件客户端类似,让我们为问题添加一个描述字段,它可能是很长的文本,不适合显示在问题列表中。让我们也这样做,以便在选择一个问题时,页面的下半部分显示该问题的描述。

这需要嵌套路由,其中路径的开始部分描述了页面的一个部分,并且基于该页面内的交互,路径的后面部分描述了变化,或者对页面中额外显示的内容的进一步定义。在 Issue Tracker 应用的情况下,除了问题列表之外,我们将让/issues显示问题列表(没有详细信息),让/issues/1显示详细信息部分,其中包含对 ID 为 1 的问题的描述。

React Router 通过其动态路由理念使这一点变得容易。在组件层次结构中的任何一点,都可以添加一个Route组件,如果 URL 与 route 的路径匹配,就会呈现这个组件。在 Issue Tracker 应用中,我们可以定义这样一个Route,其实际组件是问题细节,在IssueList中,就在IssueAdd部分之后。路径可以是/issues/<id>的形式,类似于IssueEdit组件的路径匹配,如下所示:

...
        <IssueAdd createIssue={this.createIssue} />
        <hr />
        <Route path="/issues/:id" component={IssueDetail} />
...

因此,与快速路由不同,React 路由的路由不需要全部预先声明;它们可以放置在任何级别,并在渲染过程中进行评估。

但是在我们做这个改变之前,让我们修改模式来添加一个描述字段。我们将在类型Issue和类型IssueInputs中这样做。我们还需要一个新的 API,它可以检索给定 ID 的单个问题。这个 API 是组件IssueDetail用来获取描述的,而IssueTable不会获取描述。让我们简单地称这个 API 为issue,它接受一个整数作为参数来指定要获取的问题的 ID。清单 9-17 中列出了schema.graphql的变更。

...
type Issue {
  ...
  description: String
}
...

input IssueInputs {
  ...
  description: String
}
...

type Query {
  ...
  issue(id: Int!): Issue!
}
...

Listing 9-17api/schema.graphql: Changes for a New Field in Issue and a New Get API

接下来,让我们实现 API 来获得一个问题。这相当简单:我们需要做的就是使用id参数创建一个 MongoDB 过滤器,并使用这个过滤器在issues集合上调用findOne()。让我们调用这个函数get()并将它和其他从issue.js导出的函数一起导出。这组更改如清单 9-18 所示。

...

async function get(_, { id }) {

  const db = getDb();
  const issue = await db.collection('issues').findOne({ id });
  return issue;

}

async function list(_, { status }) {
  ...
}
...

module.exports = { list, add, get };
...

Listing 9-18api/issue.js: Implementation of New Function get() to Fetch a Single Issue

最后,我们需要在提供给 Apollo 服务器的解析器中绑定新函数。清单 9-19 中显示了对api_handler.js的更改。

const resolvers = {
  Query: {
    ...
    issue: issue.get,
  },
...

Listing 9-19api/api_handler.js

此时,您可以使用 Playground 测试新的 API。您可以创建一个带有描述字段的新问题,使用issue查询获取它,并查看描述是否被返回。为了方便起见,我们还可以修改模式初始化器脚本,为初始问题集添加一个描述字段。清单 9-20 中显示了对init.mongo.js的更改。

...
const issuesDB = [
  {
    ...
    description: 'Steps to recreate the problem:'
      + '\n1\. Refresh the browser.'
      + '\n2\. Select "New" in the filter'
      + '\n3\. Refresh the browser again. Note the warning in the console:'
      + '\n   Warning: Hash history cannot PUSH the same path; a new entry'
      + '\n   will not be added to the history stack'
      + '\n4\. Click on Add.'
      + '\n5\. There is an error in console, and add doesn\'t work.',
  },
  {
    ...
    description: 'There needs to be a border in the bottom in the panel'
      + ' that appears when clicking on Add',
  },
];
...

Listing 9-20api/scripts/init.mongo.js: Addition of Description to Sample Issues

您可以使用通常的命令运行这个脚本来初始化数据库,以便描述与您的测试和本章中的屏幕截图相匹配:

$ mongo issuetracker api/scripts/init.mongo.js

如果运行该脚本,您可能必须从主页链接开始,因为它可能已经删除了一些您手动创建的问题。否则,如果 UI 引用这些问题,您可能会得到一个 GraphQL 错误,大意是Query.issue不能为 null。

现在,我们可以实现IssueDetail组件了。作为该组件的一部分,我们将执行以下操作:

  1. 我们将维护状态,其中将包含一个问题对象。

  2. 像在IssueEdit组件中一样,将从match.params.id中检索发布对象的 ID。

  3. 问题对象将通过使用fetch() API 的issue GraphQL 查询以一种叫做loadData()的方法提取,并设置为状态。

  4. 方法loadData()将在组件安装后(第一次)或 ID 改变时(在componentDidUpdate()中)被调用。

  5. render()方法中,我们将使用<pre>标签显示描述,以便在显示中保持换行。

在一个名为IssueDetail.jsx的新文件中,组件的完整代码如清单 9-21 所示。

import React from 'react';

import graphQLFetch from './graphQLFetch.js';

export default class IssueDetail extends React.Component {
  constructor() {
    super();
    this.state = { issue: {} };
  }

  componentDidMount() {
    this.loadData();
  }

  componentDidUpdate(prevProps) {
    const { match: { params: { id: prevId } } } = prevProps;
    const { match: { params: { id } } } = this.props;
    if (prevId !== id) {
      this.loadData();
    }
  }

  async loadData() {
    const { match: { params: { id } } } = this.props;
    const query = `query issue($id: Int!) {
      issue (id: $id) {
        id description
      }
    }`;

    const data = await graphQLFetch(query, { id });
    if (data) {
      this.setState({ issue: data.issue });
    } else {
      this.setState({ issue: {} });

    }
  }

  render() {
    const { issue: { description } } = this.state;
    return (
      <div>
        <h3>Description</h3>
        <pre>{description}</pre>
      </div>
    );
  }
}

Listing 9-21ui/src/IssueDetail.jsx: New Component to Show the Description of an Issue

为了将IssueDetail组件集成到IssueList组件中,我们需要添加一条路由,如本节开头所讨论的。但是,不要硬编码/issues,让我们使用父组件中匹配的路径,使用this.props.match.path。这样,即使父路径由于任何原因发生更改,更改也会被隔离到一个位置。

这一变化以及必要的导入如清单 9-22 所示。

...
import URLSearchParams from 'url-search-params';

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

...

import IssueAdd from './IssueAdd.jsx';

import IssueDetail from './IssueDetail.jsx';

...

  render() {
    const { issues } = this.state;
    const { match } = this.props;
    ...
        <IssueAdd createIssue={this.createIssue} />
        <hr />
        <Route path={`${match.path}/:id`} component={IssueDetail} />
    ...
  }
...

Listing 9-22ui/src/IssueList.jsx: Changes for Including IssueDetail in a Route

要选择一个问题,让我们在问题列表中的“编辑”链接旁边创建另一个链接。这一次,让我们使用一个NavLink来突出显示所选的问题。理想情况下,我们应该能够通过单击行中的任何位置来进行选择,并且在选择时应该高亮显示整行。但是让我们留到后面的章节,在那里我们将有更好的工具来实现这个效果。NavLink将指向/issues/<id>,其中<id>是所选行中问题的 ID。

此外,为了不丢失 URL 的查询字符串部分,我们必须将当前查询字符串作为搜索属性添加到链接的目标中。但是,要访问当前的查询字符串,我们需要访问当前的位置,由于IssueRow没有显示为Route的一部分,我们必须通过用withRouter包装组件来注入位置。

IssueTable.jsx文件的修改如清单 9-23 所示。

...
import React from 'react';
import { Link, NavLink, withRouter } from 'react-router-dom';
...

function IssueRow({ issue }) {

const IssueRow = withRouter(({ issue, location: { search } }) => {

  const selectLocation = { pathname: `/issues/${issue.id}`, search };
  ...
      <td>{issue.title}</td>
      <td><Link to={`/edit/${issue.id}`}>Edit</Link></td>
      <td>
        <Link to={`/edit/${issue.id}`}>Edit</Link>
        {' | '}
        <NavLink to={selectLocation}>Select</NavLink>
      </td>
    </tr>
  ...

}

});

Listing 9-23ui/src/IssueTable.jsx: Addition of a Link to Select an Issue for Display in the Details Section

如果您现在测试这个应用,您会在每个问题的编辑链接旁边找到一个选择链接。单击此链接应该会更改 URL,以便将问题的 ID 附加到主路径,但在查询字符串(如果有)之前。您应该在有过滤器和没有过滤器的情况下进行尝试,以确保它在两种情况下都有效,并且刷新会继续显示所选问题的描述。

选中 ID 1 问题的页面截图如图 9-6 所示。

img/426054_2_En_9_Chapter/426054_2_En_9_Fig6_HTML.jpg

图 9-6

选定的问题和描述

练习:嵌套布线

  1. 在呈现IssueList时,我们可以不使用Route,而是将问题列表的路由路径定义为/issues/:id,然后将传递 ID 的IssueDetail显示为 props 的一部分。比较获得相同结果的两种方法。有哪些利弊?

本章末尾有答案。

浏览器历史路由

在本章的开始,我们讨论了两种路由——基于散列的和基于浏览器历史的。如果我们自己来做的话,基于散列的路由很容易理解和实现:只需在转换时改变 URL 的锚部分就足够了。此外,服务器必须只返回对/的请求的index.html,而不返回其他的。

但是使用基于散列的路由的缺点是当服务器需要响应不同的 URL 路径时。想象一下在浏览器上点击刷新。当使用基于散列的路由时,浏览器从服务器向/发出请求,而不管#或实际路由路径之后是什么。如果我们必须让服务器以不同的方式处理这种刷新,更好的策略是对不同的路由使用不同的 URL 基础(也就是说,没有#和它后面的内容)。

当我们需要支持对搜索引擎爬虫的响应时,这种需求(从服务器本身对不同的路由做出不同的响应)就出现了。这是因为,对于爬虫找到的每个链接,如果 URL 不同,就会产生一个新的请求。如果跟在#后面的是不同的,爬虫会认为它只是页面中的一个锚点,并且不管路径是什么,只对/发出请求。

为了使我们的应用搜索引擎友好,使用基于浏览器历史的路由是必要的。但这还不是全部,服务器还必须响应整个页面。相反,对于浏览器请求,页面将在浏览器上构建。我们还不会生成要显示的页面,因为实现它相当复杂,它应该有自己的一章。现在,我们将切换到基于浏览器历史的路由,但是假设页面是通过只操纵 DOM 来构造的。

切换到使用这种新路由就像改变import语句并使用BrowserRouter而不是HashRouter一样简单。该组件通过使用 HTML5 历史 API ( pushStatereplaceStatepopState)来保持 UI 与 URL 同步,从而实现路由。

这一变化显示在清单 9-24App.jsx中。

...
import ReactDOM from 'react-dom';
import { HashRouter BrowserRouter as Router } from 'react-router-dom';
...

Listing 9-24ui/src/App.jsx: Changes for Using Browser History Based Router

要测试这个变化,就得从原点位置开始,也就是http://localhost:8000。该应用将似乎工作,所有的链接将 Bootstrap 您到正确的页面和视图。此外,您会发现这些 URL 将没有一个#,而对于问题列表页面来说,它们只是简单的 URL,如http://localhost:8000/issues

但是任何视图的刷新都将失败。例如,在“问题列表”页面中,如果刷新浏览器,您将在屏幕上看到以下消息:

Cannot GET /issues

这是因为浏览器中的 URL 当前指向/issues并且浏览器向服务器请求/issues,这不是由 UI 服务器处理的。为了解决这个问题,我们需要对 UI 服务器进行更改,它会为任何未被处理的URL 返回index.html。这可以通过在路径*的所有其他路由之后安装一个快速路由来实现,该路由读取index.html的内容并将其返回。

response对象有一个方便的方法叫做sendFile()。但是出于安全原因,必须指定文件的完整绝对路径——它不接受相对路径。让我们使用内置 Node.js 模块path中的path.resolve()将相对路径转换为绝对路径。对uiserver.js的更改如清单 9-25 所示。

...
require('dotenv').config();

const path = require('path');

...

app.get('/env.js', (req, res) => {
  ...
});

app.get('*', (req, res) => {

  res.sendFile(path.resolve('public/index.html'));

});

...

Listing 9-25ui/uiserver.js: Respond with index.html for All Requests

如果您在做出这一更改后测试应用,您会发现任何页面上的刷新都可以像以前一样工作。测试公共目录中的其他文件是否得到了正确的服务也是一个好主意,特别是,app.bundle.jsvendor.bundle.js

但是在正常的开发模式下,HMR 会提供这些包,而不是让 UI 服务器从公共目录中获取它们。因此,您需要禁用 HMR(通过将环境变量ENABLE_HMR设置为false),使用npm run compile手动编译包,然后启动 UI 服务器。然后,在刷新应用时,您应该看到这些文件被正确地从服务器中检索出来。完成测试后,不要忘记将更改恢复到 HMR。

仍有一项影响 HMR 运作的变革有待完成。Webpack 在output下有一个名为publicPath的配置选项。当使用按需加载或加载图像、文件等外部资源时,这是一个重要的选项。但是到目前为止我们还没有使用它们,没有将它们设置为任何值也不会影响应用的功能。该值默认为空字符串,这意味着与当前页面的位置相同。

原来,当模块发生变化并被 HMR 重新编译时,Webpack 使用publicPath的值来获取模块的更新信息。因此,如果您在某个位置(如/edit/1/issues/1)更改源文件,您会发现 HMR 调用失败。如果你查看开发者工具的网络选项卡,你会发现这些请求返回的是index.html的内容,而不是模块更新信息。

当浏览器指向/issues/issues/1时,您可以通过查看源文件改变时发生的情况来比较这两个请求和响应。在第一种情况下,您将看到对像/f3f397176a7b9c3237cf.hot-update.json这样的资源的请求,它成功了。而在第二种情况下,就会像/edit/f3f397176a7b9c3237cf.hot-update.json一样,失败。这是因为 Webpack 正在向当前位置发出请求相对于。这个请求不能被热的中间件匹配,所以它失败到 catch-all Express route,它返回index.html的内容。

我们在使用基于散列的路由时没有遇到这个问题,因为页面的位置总是/,路由受到 URL 的锚部分的影响。正确的请求应该没有前缀/edit。为了实现这一点,我们必须改变webpack.config.js来设置publicPath配置。对此的更改如清单 9-26 所示。

...
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'public'),
    publicPath: '/',
  },
...

Listing 9-26ui/webpack.config.js: Changes to Add publicPath

经过这次修改后,你会发现 HMR 在应用的任何页面上都能正常工作。

练习:浏览器历史路由

  1. 如果我们在应用中使用href s 而不是Link s 作为超链接,那么过渡到使用BrowserRouter会不会同样简单?还需要做哪些改变?

  2. 现在让我们根据效果来比较使用hrefLink的情况。在Link之外增加一个href,用于在问题表中导航编辑。点击这两个链接,比较发生了什么。(提示:使用开发人员工具的“网络”选项卡来检查网络流量。)

    现在,用HashHistory做同样的对比(注意:你得在href里用/#/ edit/edit不行。)现在,有区别吗?试着解释你所看到的。(记得在练习后还原实验变化。)

本章末尾有答案

摘要

在本章中,您学习了如何实现客户端路由,即根据菜单或导航栏中的链接显示不同的页面。React 路由库对此有所帮助。

您还了解了如何将浏览器中的 URL 与页面中显示的内容连接起来,以及如何使用参数和查询字符串来调整页面内容。正确实现路由是让用户在点击超链接和使用浏览器中的后退/前进按钮时有一种自然感觉的关键。此外,将浏览器中的 URL 连接到页面上的内容不仅有助于我们以有组织的方式思考不同的页面或视图,还可以帮助用户将链接添加到书签中并使用浏览器的刷新按钮。

在下一章,我们将探索如何处理企业应用中一个非常常见的事件:表单,React 方式。当我们这样做时,我们还将通过实现对问题的更新和删除操作来完成对问题对象的 CRUD。

练习答案

练习:简单路由

  1. 如果从Redirect组件中删除了exact属性,您将看到内容部分是空白的,不管单击的是什么超链接。这是因为所有的URL 现在都匹配第一条路由。因为没有为路线定义组件,所以页面是空白的。此外,您还会在控制台中看到一个错误,提示您试图重定向到相同的路由。这是因为即使在导航中,相同的路线(T2)也是匹配的。

    通过对路由重新排序,您几乎可以实现所需的行为:重定向可以放在两条路由之后、全部捕获之前。现在,/issues/report路径将与前两条路径匹配,并在那里停止。如果两者都不匹配,那么任何其他路由将匹配/,并将重定向到/issues。这与之前的行为并不完全相同,因为它将总是重定向到/issues,而不是显示未找到的页面。

  2. 如果您将<Switch>替换为<div>,您会发现除了问题列表或报告占位符之外,始终会显示“未找到页面”消息。这是因为匹配不会在第一次匹配时停止,而是向显示所有匹配路线的组件。NotFound组件的路径(空)匹配任何路径,因此总是显示。

  3. URL / #/issues000显示未找到的页面,而/ #/issues/000显示没有exact属性的问题列表,否则显示未找到的页面。这表明非精确路由匹配路径的完整段,每段由/分隔。这不是简单的前缀匹配。

练习:查询参数

  1. 当一个组件的属性改变时,React 会自动调用一个render()。当属性的改变只影响渲染时,我们不需要做任何进一步的工作。

    问题列表中的不同之处在于属性的变化导致了状态的变化。这种变化必须在某个地方被触发,我们选择了生命周期方法componentDidUpdate()来做这件事。最终,即使在问题编辑页面中,当我们在对服务器的异步调用中加载问题细节时,我们也必须实现componentDidUpdate()方法。

  2. 如果不检查新旧属性是否相同,就会导致无限循环。这是因为一个新的状态也被认为是对组件的更新,因此再次调用componentDidUpdate(),这个循环将无休止地继续下去。

  3. 父组件中的任何更改都会触发子组件中的渲染,因为假设父组件的状态也会影响子组件。通常,这不是一个性能问题,因为重新计算的只是虚拟 DOM。由于新旧虚拟 DOM 将是相同的,所以实际的 DOM 将不会被更新。

    对虚拟 DOM 的更新并不昂贵,因为它们只不过是内存中的数据结构。但是,在极少数情况下,特别是当组件层次非常深并且受影响的组件数量非常大时,更新虚拟 DOM 的行为可能需要一些时间。这可以通过挂钩生命周期方法shouldComponentUpdate()并确定渲染是否有保证来优化。

练习:链接

  1. 如果你使用NavLinks,你会发现所有的链接总是高亮显示。那是因为Link只匹配 URL 和链接的路径,并不认为查询字符串是路径的一部分。因为所有链接的路径都是/issues,所以它们总是匹配的。

    与路径参数相比,查询参数不是一个有限集,因此,不鼓励用于导航链接。如果过滤器是导航链接,我们应该像对待主导航栏一样使用路由参数。

  2. NavLink组件的activeClassName属性决定了当链接活动时添加的类。您可以将该属性设置为current值,以获得想要的效果。

练习:程序化导航

  1. 可以使用history.replace()方法,它替换当前的 URL,这样历史记录就没有旧的位置。另一方面,router.push()确保用户可以使用 back 按钮返回到之前的视图。

    当两条路线没有真正不同时,可以使用替换。它类似于 HTTP 重定向,其中请求的内容是相同的,但是在不同的位置可用。在这种情况下,记住第一个位置作为浏览器历史的一部分是没有用的。

  2. 刷新时,下拉菜单重置为默认值All。但是列表是根据下拉列表的前一个选择进行过滤的,这反映在 URL 中作为查询字符串的一部分。我们将在下一章讨论表单时同步下拉列表值和查询字符串。

    如果下拉列表值更改为选择原始状态,开发人员控制台会显示一条警告:

Hash history cannot PUSH the same path; a new entry will not be added to the history stack.

由于路径相同,哈希历史拒绝推送路径,因此组件不会更新。

  1. 包装函数withRouter有点难以理解。其他选项很容易理解,甚至看起来更简单。但是想象一个更加嵌套的层次结构,其中IssueFilterIssueList中不止一层。在这种情况下,history对象必须通过所有中间组件,增加所有这些组件之间的耦合。

    IssueFilter直接操作 URL 减少了耦合,让每个组件处理一个单独的职责。对于IssueFilter,它是一个设置 URL 的任务,对于IssueList,它是一个使用来自 URL 的查询字符串的任务,不管它是如何设置的。

练习:嵌套布线

  1. 这两种方法之间的差别并不大,也可能是一致性的问题。无论如何,Route所做的就是匹配 URL,如果匹配就显示一个组件。因为匹配是作为IssueList的一部分发生的,所以嵌套路由并没有增加多少,至少在这种情况下是这样。因此,显示包装在if条件中的IssueDetail组件(在存在 ID 的情况下)就可以了。

    另一个考虑因素是子组件在层次结构中的嵌套深度。在IssueDetail的情况下,它只有一层深度,从IssueListIssueDetail的 ID 传递非常简单。如果嵌套路由的组件嵌套很深,那么 ID 必须通过多个其他组件传递,所以对于IssueDetail来说,通过路由本身从 URL 获取这个参数可能更容易。

练习:浏览器历史路由

  1. 如果我们没有使用Link s,我们将不得不改变所有的href s 来删除#/前缀。这是使用Link s 与普通href s 相比的一个优势

  2. 当使用BrowserHistory时,href使浏览器导航到另一个 URL,从而向服务器发起请求,获取页面,然后呈现它。相比较而言,Link不会对服务器产生新的请求;它只是在浏览器中更改 URL,并通过替换需要为新路由显示的组件,以编程方式处理这一更改。

    当使用HashHistory时,这两种方法没有明显的不同,因为基本 URL 总是相同的(/)。即使点击href,浏览器也不会向服务器发出新的请求,因为基本 URL 不会改变。*