四、将 React 路由用于路由

如果我们的应用程序有多个页面,我们需要管理不同页面之间的导航。React 路由是一个很好的库,可以帮助我们做到这一点!

在本章中,我们将建立一个网络商店,在那里我们可以为 React 购买一些工具。我们的简单商店将有多个页面,我们将使用 React 路由进行管理。当我们完成后,商店将如以下屏幕截图所示:

在本章中,我们将学习以下主题:

  • 使用路由类型安装 React 路由
  • 申报路线
  • 创建导航
  • 路线参数
  • 处理未找到的路由
  • 实现页面重定向
  • 查询参数

  • 路线提示

  • 嵌套路由
  • 动画过渡
  • 延迟加载路径

技术要求

在本章中,我们将使用以下技术:

  • Node.js 和npm:TypeScript 和 React 依赖于这些。我们可以从安装这些 https://nodejs.org/en/download/ 。如果我们已经安装了这些,请确保npm至少是 5.2 版

  • Visual Studio 代码:我们需要一个编辑器来编写 React 和 TypeScript 代码,可以从安装 https://code.visualstudio.com/ 。我们还需要在 VisualStudio 代码中安装 TSLint(由 egamma 编写)和 Prettier(由 Estben Petersen 编写)扩展。

All the code snippets in this chapter can be found online at https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/04-ReactRouter.

使用路由类型安装 React 路由

React Router及其类型在npm中,我们可以从那里安装。

在安装 React 路由之前,我们需要创建 React 商店项目。让我们通过选择一个空文件夹并打开 VisualStudio 代码来做好准备。要执行此操作,请执行以下步骤:

  1. 现在,让我们打开一个终端并输入以下命令来创建一个新的 React 和 TypeScript 项目:
npx create-react-app reactshop --typescript

请注意,我们使用的 React 版本至少需要为版本16.7.0-alpha.0。我们可以在package.json文件中查看。如果package.json中 React 的版本小于16.7.0-alpha.0,那么我们可以使用以下命令安装此版本:

npm install react@16.7.0-alpha.0
npm install react-dom@16.7.0-alpha.0
  1. 创建项目后,让我们将 TSLint 作为开发依赖项添加到我们的项目中,以及一些与 React 和 Prettier 配合良好的规则:
cd reactshop
npm install tslint tslint-react tslint-config-prettier --save-dev
  1. 现在我们添加一个包含一些规则的tslint.json文件:
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "rules": {
    "ordered-imports": false,
    "object-literal-sort-keys": false,
    "no-debugger": false,
    "no-console": false,
  },
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  }
}
  1. 现在,让我们输入以下命令将 React Router 安装到我们的项目中:
npm install react-router-dom
  1. 我们还将为 React Router 安装 TypeScript 类型,并将其另存为开发依赖项:
npm install @types/react-router-dom --save-dev

在继续下一节之前,我们将删除一些不需要的文件create-react-app

  1. 首先,让我们移除App组件。那么,让我们删除App.cssApp.test.tsxApp.tsx文件。我们还要删除index.tsx中的导入引用"./App"
  2. 我们还可以通过删除serviceWorker.ts文件并删除index.tsx中对它的引用来删除服务工作者
  3. index.tsx中,让我们将根组件从<App/>更改为<div/>。我们的index.tsx文件现在应该包含以下内容:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import './index.css';

ReactDOM.render(
  <div />,
  document.getElementById('root') as HTMLElement
);

申报路线

我们使用BrowserRouterRoute组件在我们的应用程序中声明页面。BrowserRouter是顶级组件,它在下面寻找Route组件,以确定所有不同的页面路径。

在本节后面,我们将使用BrowserRouterRoute在应用程序中声明一些页面,但在此之前,我们需要创建前两个页面。第一个页面将包含我们将在商店中销售的 React 工具列表。我们使用以下步骤创建页面:

  1. 那么,让我们先创建一个包含以下内容的ProductsData.ts文件来为我们的工具列表创建数据:****
export interface IProduct {
  id: number;
  name: string;
  description: string;
  price: number;
}

export const products: IProduct[] = [
  {
    description:
      "A collection of navigational components that compose  
       declaratively with your app",
    id: 1,
    name: "React Router",
    price: 8
  },
  {
    description: "A library that helps manage state across your app",
    id: 2,
    name: "React Redux",
    price: 12
  },
  {
    description: "A library that helps you interact with a GraphQL backend",
    id: 3,
    name: "React Apollo",
    price: 12
  }
];
  1. 让我们创建另一个名为ProductsPage.tsx的文件,其中包含要导入的以下内容以及我们的数据:
import * as React from "react";
import { IProduct, products } from "./ProductsData";
  1. 我们将引用组件状态中的数据,因此让我们为此创建一个接口:
interface IState {
  products: IProduct[];
}
  1. 让我们继续创建名为ProductsPage的类组件,将状态初始化为空数组:
class ProductsPage extends React.Component<{}, IState> {
  public constructor(props: {}) {
    super(props);
    this.state = {
      products: []
    };
  }
}

export default ProductsPage;
  1. 现在让我们实现componentDidMount生命周期方法,并将数据从ProductData.ts设置到products数组:
public componentDidMount() {
  this.setState({ products });
}
  1. 继续实施render方法,让我们欢迎我们的用户,并在列表中列出产品:
public render() {
  return (
    <div className="page-container">
      <p>
        Welcome to React Shop where you can get all your tools for ReactJS!
      </p>
      <ul className="product-list">
        {this.state.products.map(product => (
          <li key={product.id} className="product-list-item">
           {product.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

我们使用了products数组中的map函数来迭代元素,并为每个产品生成一个列表项标记li。我们需要给每个li一个唯一的key属性,以帮助管理列表项的任何更改,在我们的例子中,就是id产品。

  1. 我们已经引用了一些 CSS 类,所以让我们将它们添加到index.css中:
.page-container {
 text-align: center;
 padding: 20px;
 font-size: large;
}

.product-list {
 list-style: none;
 margin: 0;
 padding: 0;
}

.product-list-item {
 padding: 5px;
}
  1. 现在让我们实现我们的第二个页面,它将是一个管理面板。那么,让我们创建一个名为AdminPage.tsx的文件,其中包含以下函数组件:
import * as React from "react";

const AdminPage: React.SFC = () => {
  return (
    <div className="page-container">
      <h1>Admin Panel</h1>
      <p>You should only be here if you have logged in</p>
    </div>
  );
};

export default AdminPage;
  1. 现在我们有两个页面在我们的商店,我们可以宣布我们的两条路线给他们。让我们创建一个名为Routes.tsx的文件,其中包含以下内容,以便从 React Router 导入ReactBrowserRouterRoute组件,以及我们的两个页面:
import * as React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";

import AdminPage from "./AdminPage";
import ProductsPage from "./ProductsPage";

我们在 import 语句中将BrowserRouter重命名为Router,以节省一些按键操作。

  1. 让我们继续实现一个包含两条路线的功能组件:
const Routes: React.SFC = () => {
  return (
    <Router>
      <div>
        <Route path="/products" component={ProductsPage} />
        <Route path="/admin" component={AdminPage} />
      </div>
    </Router>
  );
};

export default Routes;

During rendering, if the path in a Route component matches the current path, the component will be rendered, and if not, null will be rendered. In our example, ProductPage will be rendered if the path is "/products" and AdminPage will be rendered if the path is "/admin".

  1. 下面是将我们的Routes呈现为index.tsx中的根组件的最后一步:
import * as React from "react";
import * as ReactDOM from "react-dom";
import "./index.css";
import Routes from "./Routes";

ReactDOM.render(<Routes />, document.getElementById("root") as HTMLElement);
  1. 我们现在应该可以运行我们的应用程序了:
npm start

应用程序可能会在根页面上启动,该页面将为空,因为该路径不指向任何内容。

  1. 如果我们将路径更改为"/products",我们的产品列表应显示以下内容:

  1. 如果我们将路径更改为"/admin",我们的管理面板应呈现以下内容:

现在我们已经成功地创建了几个路由,我们真的需要一个导航组件来让我们的页面更容易被发现。我们将在下一节中这样做。

创建导航

React 路由带有一些不错的组件,用于提供导航。我们将使用这些来实现应用程序标题中的导航选项。

使用链接组件

我们将使用 React Router 的Link组件通过执行以下步骤来创建导航选项:

  1. 让我们首先创建一个名为Header.tsx的新文件,其中包含以下导入:
import * as React from "react";
import { Link } from "react-router-dom";

import logo from "./logo.svg";
  1. 让我们使用Header功能组件中的Link组件创建两个链接:
const Header: React.SFC = () => {
  return (
    <header className="header">
      <img src={logo} className="header-logo" alt="logo" />
      <h1 className="header-title">React Shop</h1>
      <nav>
        <Link to="/products" className="header-
         link">Products</Link>
        <Link to="/admin" className="header-link">Admin</Link>
      </nav>
    </header>
  );
};

export default Header;

The Link component allows us to define the path where the link navigates to as well as the text to display.

  1. 我们已经引用了一些 CSS 类,所以,让我们将它们添加到index.css中:
.header {
  text-align: center;
  background-color: #222;
  height: 160px;
  padding: 20px;
  color: white;
}

.header-logo {
  animation: header-logo-spin infinite 20s linear;
  height: 80px;
}

@keyframes header-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.header-title {
  font-size: 1.5em;
}

.header-link {
  color: #fff;
  text-decoration: none;
  padding: 5px;
}
  1. 现在我们的Header组件已经就位,让我们import进入Routes.tsx
import Header from "./Header";
  1. 然后,我们可以在 JSX 中使用它,如下所示:
<Router>
  <div>
    <Header />
    <Route path="/products" component={ProductsPage} />
    <Route path="/admin" component={AdminPage} />
  </div>
</Router>
  1. 如果我们检查 running 应用程序,它应该看起来像下面的屏幕截图,带有一个漂亮的标题和两个导航选项,可以转到我们的产品和管理页面:

  1. 尝试单击导航选项-它们可以工作!如果我们使用浏览器开发人员工具检查产品和管理元素,我们会看到 React Router 将它们呈现为锚定标记:

如果我们在单击导航选项时查看开发人员工具中的“网络”选项卡,我们将看到没有提出网络请求来服务页面。这表明 React 路由正在 React 应用程序中为我们处理导航。

使用导航链接组件

React 路由提供另一个用于链接页面的组件,称为NavLink。这实际上更符合我们的要求。以下步骤解释了如何重构Header组件以使用NavLink

  1. 那么,让我们在Header组件中将Link替换为NavLink并进行一些改进:
import * as React from "react";
import { NavLink } from "react-router-dom";

import logo from "./logo.svg";

const Header: React.SFC = () => {
  return (
    <header className="header">
      <img src={logo} className="header-logo" alt="logo" />
      <h1 className="header-title">React Shop</h1>
      <nav>
        <NavLink to="/products" className="header-
         link">Products</NavLink>
        <NavLink to="/admin" className="header-
         link">Admin</NavLink>
      </nav>
    </header>
  );
};

export default Header;

此时,我们的应用程序的外观和行为完全相同。

  1. NavLink公开了一个activeClassName属性,我们可以使用该属性设置活动链接的样式。那么,让我们用这个:
<NavLink to="/products" className="header-link" activeClassName="header-link-active">
  Products
</NavLink>
<NavLink to="/admin" className="header-link" activeClassName="header-link-active">
  Admin
</NavLink>
  1. 让我们将header-link-active的 CSS 添加到index.css中:
.header-link-active {
  border-bottom: #ebebeb solid 2px;
}
  1. 如果我们现在切换到 running 应用程序,活动链接将加下划线:

因此,NavLink对于主应用程序导航非常有用,我们希望突出显示活动链接,Link对于我们应用程序中的所有其他链接都非常有用

路线参数

路由参数是路径的可变部分,可在目标组件中用于有条件地呈现某些内容。

我们需要为我们的商店添加另一个页面,以显示每种产品的说明和价格,以及将其添加到购物篮的选项。我们希望能够使用"/products/{id}"路径导航到此页面,其中id是产品的 ID。例如,反应 Redux 的路径将是"products/2"。因此,路径的id部分是一个路由参数。我们可以通过以下步骤来实现这一切:

  1. 让我们将此路由添加到两条现有路由之间的Routes.tsx。路由的id部分将是一个路由参数,我们在它前面定义了一个冒号:
<Route path="/products" component={ProductsPage} />
<Route path="/products/:id" component={ProductPage} />
<Route path="/admin" component={AdminPage} />
  1. 当然,ProductPage组件还不存在,因此,让我们先创建一个名为ProductPage.tsx的新文件,并使用以下导入来创建它:
import * as React from "react";
import { RouteComponentProps } from "react-router-dom";
import { IProduct, products } from "./ProductsData";
  1. 这里的关键部分是我们将使用RouteComponentProps类型来访问路径中的id参数。让我们使用RouteComponentProps泛型类型并传入具有id属性的类型,为ProductPage组件定义 props 类型别名:
type Props = RouteComponentProps<{id: string}>;

Don't worry if you don't understand the angle brackets in the type expression. This denotes a generic type, which we will explore in Chapter 5, Advanced Types.

理想情况下,我们应该将id属性指定为一个数字,以匹配产品数据中的类型。但是,RouteComponentProps只允许我们有字符串类型或未定义类型的路由参数。

**4. ProductPage组件将具有保存正在渲染的产品的状态,以及是否已将其添加到篮子中,因此让我们为状态定义一个接口:

interface IState {
  product?: IProduct;
  added: boolean;
}
  1. 产品最初将是undefined,这就是为什么它被定义为可选的。让我们创建ProductPage类并初始化状态,以便产品不在篮子中:
class ProductPage extends React.Component<Props, IState> {
  public constructor(props: Props) {
    super(props);
    this.state = {
      added: false
    };
  }
}

export default ProductPage;
  1. 当组件加载到 DOM 中时,我们需要从Route参数中具有id属性的产品数据中找到我们的产品。RouteComponentProps给我们一个match对象,包含一个params对象,包含我们的id路由参数。那么,让我们来实现这一点:
public componentDidMount() {
  if (this.props.match.params.id) {
    const id: number = parseInt(this.props.match.params.id, 10);
    const product = products.filter(p => p.id === id)[0];

    this.setState({ product });
  }
}

请记住,id路由参数是一个字符串,这就是为什么我们在将其与filter数组中的产品数据进行比较之前,使用parseInt将其转换为一个数字

  1. 现在,我们的产品处于组件状态,让我们转到render功能:
public render() {
  const product = this.state.product;
  return (
    <div className="page-container">
      {product ? (
        <React.Fragment>
          <h1>{product.name}</h1>
          <p>{product.description}</p>
          <p className="product-price">
            {new Intl.NumberFormat("en-US", {
              currency: "USD",
              style: "currency"
            }).format(product.price)}
          </p>
          {!this.state.added && (
            <button onClick={this.handleAddClick}>Add to 
              basket</button>
          )}
        </React.Fragment>
      ) : (
        <p>Product not found!</p>
      )}
    </div>
  );
}

在这个 JSX 中有一些有趣的地方:

  • 在函数的第一行,我们将一个product变量设置为产品状态,以节省一些击键,因为我们在 JSX 中大量引用了产品。
  • div中的三元表示产品(如果有)。否则,它会通知用户找不到产品。
  • 我们在三元体的真实部分使用React.Fragment,因为三元体的每个部分只能有一个单亲,并且React.Fragment是实现这一点的机制,而不会呈现像div标记这样的不需要的东西。
  • 我们使用Intl.NumberFormat将产品价格格式化为带有货币符号的货币。

  • 当点击 Addtobasket 按钮时,我们也在调用handleAddClick方法。我们还没有实现这一点,所以现在让我们这样做,并将added状态设置为true

private handleAddClick = () => {
  this.setState({ added: true });
};
  1. 现在我们已经实现了ProductPage组件,让我们回到Routes.tsx并导入它:
import ProductPage from "./ProductPage";
  1. 让我们转到我们的跑步应用程序,输入"/products/2"作为路径:

Not quite what we want! Both ProductsPage and ProductPage have rendered because "/products/2" matches both "/products" and "/products/:id".

  1. 为了解决这个问题,我们可以告诉"/products"路径仅在存在精确匹配时渲染:
<Route exact={true} path="/products" component={ProductsPage} />
  1. 在我们进行此更改并保存Routes.tsx后,我们的产品页面看起来好多了:

  1. 我们不会让我们的用户输入访问产品的特定路径!因此,对于使用Link组件的每个产品,我们将ProductsPage更改为链接到ProductPage。首先,我们将Link从 React 路由导入ProductsPage
import { Link } from "react-router-dom";
  1. 现在,我们不在每个列表项中呈现产品名称,而是呈现一个Link组件,该组件进入我们的产品页面:
public render() {
  return (
    <div className="page-container">
      <p>
        Welcome to React Shop where you can get all your tools 
         for ReactJS!
      </p>
      <ul className="product-list">
        {this.state.products.map(product => (
          <li key={product.id} className="product-list-item">
            <Link to={`/products/${product.id}`}>{product.name}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
  1. 在我们查看 running 应用程序之前,让我们在index.css中添加以下 CSS 类:
.product-list-item a {
  text-decoration: none;
}

现在,如果我们转到应用程序中的产品列表并单击列表项,它会将我们带到相关的产品页面。

处理未找到的路由

如果用户输入的路径在我们的应用程序中不存在怎么办?例如,如果我们尝试导航到"/tools",标题下方将不会显示任何内容。这是有道理的,因为 React Router 没有找到任何匹配的路由,所以不会渲染任何内容。但是,如果用户确实导航到无效路径,我们希望通知他们该路径不存在。以下步骤可实现此目的:

  1. 那么,让我们用以下组件创建一个名为NotFoundPage.tsx的新文件:
import * as React from "react";

const NotFoundPage: React.SFC = () => {
  return (
    <div className="page-container">
      <h1>Sorry, this page cannot be found</h1>
    </div>
  );
};

export default NotFoundPage;
  1. 让我们将其导入Routes.tsx中的路线:
import NotFoundPage from "./NotFoundPage";
  1. 然后,让我们将一个Route组件添加到其他路由中:
<Router>
  <div>
    <Header />
    <Route exact={true} path="/products" component={ProductsPage} 
       />
    <Route path="/products/:id" component={ProductPage} />
    <Route path="/admin" component={AdminPage} />
    <Route component={NotFoundPage} />
  </div>
</Router>

但是,这将为每个路径渲染:

NotFoundPage没有找到另一条路线时,我们怎么能只渲染它呢?答案是将路由封装在 React Router 的Switch组件中。

  1. 首先将Switch导入Routes.tsx
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
  1. 现在让我们将Route组件包装成Switch组件:
<Switch>
  <Route exact={true} path="/products" component={ProductsPage} />
  <Route path="/products/:id" component={ProductPage} />
  <Route path="/admin" component={AdminPage} />
  <Route component={NotFoundPage} />
</Switch>

Switch组件仅呈现第一个匹配的Route组件。如果我们查看 running 应用程序,就会发现问题已经解决。如果我们输入一个不存在的路径,我们会收到一条“未找到”的好消息:

实现页面重定向

React 路由有一个名为Redirect的组件,我们可以使用它重定向到页面。在以下几节中,我们在几个案例中使用此组件来改进我们的车间

简单重定向

如果我们访问/路由路径,我们会注意到收到“对不起,找不到此页面”消息。当路径为/时,我们将其更改为重定向到"/products"

  1. 首先,我们需要将Redirect组件从 React Router 导入Routes.tsx
import { BrowserRouter as Router, Redirect,Route, Switch } from "react-router-dom";
  1. 当路径为/时,我们现在可以使用Redirect组件重定向到"/products"
<Switch>
 <Redirect exact={true} from="/" to="/products" />
  <Route exact={true} path="/products" component={ProductsPage} 
   />
  <Route path="/products/:id" component={ProductPage} />
  <Route path="/admin" component={AdminPage} />
  <Route component={NotFoundPage} />
</Switch>
  1. 我们在Redirect上使用了exact属性,因此它只匹配/,而不匹配"/products/1""/admin"。如果我们尝试一下,并在我们的跑步应用程序中输入/作为路径,它将立即重定向到"/products"

条件重定向

我们可以使用Redirect组件保护页面免受未经授权用户的攻击。在我们的商店,我们可以使用它来确保只有登录的用户才能访问我们的管理页面。我们通过以下步骤来实现这一点:

  1. 让我们先在Routes.tsx中的LoginPage中添加一个路由,该路由位于进入管理页面的路由之后:
<Route path="/login" component={LoginPage} />
  1. 当然,LoginPage目前不存在,所以我们创建一个名为LoginPage.tsx的文件并输入以下内容:
import * as React from "react";

const LoginPage: React.SFC = () => {
  return (
    <div className="page-container">
      <h1>Login</h1>
      <p>You need to login ...</p>
    </div>
  );
};

export default LoginPage;
  1. 然后我们可以返回到Routes.tsx并导入LoginPage
import LoginPage from "./LoginPage";
  1. 如果我们转到 running 应用程序并导航到"/login",我们将看到我们的登录页面:

我们不会完全实现我们的登录页面;我们实现的页面足以演示条件重定向。

  1. "admin"路径上实现条件重定向之前,需要在Routes.tsx中添加一个用户是否登录的状态:
const Routes: React.SFC = () => {
  const [loggedIn, setLoggedIn] = React.useState(false);
  return (
    <Router>
      ...
    </Router>
   );
};

因此,我们使用了一个useState钩子来添加一个名为loggedIn的状态变量和一个名为setLoggedIn的函数来设置它。

  1. 最后一步是在具有"/admin"路径的Route组件中添加以下内容:
<Route path="/admin">
 {loggedIn ? <AdminPage /> : <Redirect to="/login" 
 />}
</Route>

We conditionally render AdminPage if the user is logged in, otherwise, we redirect to the "/login" path. If we now click the admin link in our running app, we get redirected to the Login page.

  1. 如果我们在初始化时将loggedIn状态更改为 true,我们可以再次访问我们的管理页面:
const [loggedIn, setLoggedIn] = React.useState(true);

查询参数

查询参数是允许将其他参数传递到路径的 URL 的一部分。例如,"/products?search=redux"有一个名为search的查询参数,其值为redux

让我们实现此示例,并允许商店用户搜索产品:

  1. 首先,我们在ProductsPage.tsx中添加一个状态为search的变量,该变量将保存搜索条件:
interface IState {
  products: IProduct[];
  search: string;
}
  1. 考虑到我们需要访问 URL,我们需要使用RouteComponentProps作为ProductsPage中的道具类型。让我们首先导入以下内容:
import { RouteComponentProps } from "react-router-dom";
  1. 然后我们可以将其用作props类型:
class ProductsPage extends React.Component<RouteComponentProps, IState> {
  1. 我们可以在constructor中将search状态初始化为空字符串:
public constructor(props: RouteComponentProps) {
  super(props);
  this.state = {
    products: [],
    search: ""
  };
}
  1. 然后我们需要将componentDidMount中的search状态设置为搜索查询参数。React Router 允许我们访问传入组件的props参数中location.search中的所有查询参数。然后,我们需要解析该字符串以获得搜索查询字符串参数。我们可以使用URLSearchParamsJavaScript 函数来实现这一点。我们将使用静态getDerivedStateFromProps生命周期方法来实现这一点,当组件加载时,当其props参数发生变化时,调用此方法:
public static getDerivedStateFromProps(
  props: RouteComponentProps,
  state: IState
) {
  const searchParams = new URLSearchParams(props.location.search);
  const search = searchParams.get("search") || "";
  return {
    products: state.products,
    search
  };
}
  1. 不幸的是,URLSearchParams尚未在所有浏览器中实现,因此我们可以使用名为url-search-params-polyfill的 polyfill。让我们安装这个:
npm install url-search-params-polyfill
  1. 我们将其导入ProductPages.tsx
import "url-search-params-polyfill";
  1. 然后我们可以使用render方法中的search状态,在返回的列表项周围包装一条if语句,仅当search的值包含在产品名称中时才返回:
<ul className="product-list">
  {this.state.products.map(product => {
    if (
 !this.state.search ||
 (this.state.search &&
 product.name
 .toLowerCase()
 .indexOf(this.state.search.toLowerCase()) > -1)
 ) {
      return (
        <li key={product.id} className="product-list-item">
          <Link to={`/products/${product.id}`}>{product.name}
           </Link>
        </li>
      );
    } else {
 return null;
 }
  })}
</ul>
  1. 如果我们在 running app 中输入"/products?search=redux"作为路径,我们将看到我们的产品列表,其中只包含 React Redux:

  1. 我们将通过在应用程序标题中添加设置搜索查询参数的搜索输入来完成此功能的实现。首先,我们在Header组件中为Header.tsx中的搜索值创建一些状态:
 const [search, setSearch] = React.useState("");
  1. 我们还需要通过 React Router 和URLSearchParams访问查询字符串,所以我们将RouteComponentPropswithRouter导入URLSearchParamspolyfill:****
import { NavLink, RouteComponentProps, withRouter} from "react-router-dom";
import "url-search-params-polyfill";
  1. 让我们在Header组件中添加一个props参数:
const Header: React.SFC<RouteComponentProps> = props => { ... }
  1. 我们现在可以从路径查询字符串中获取搜索值,并在组件首次呈现时将search状态设置为:
const [search, setSearch] = React.useState("");
React.useEffect(() => {
  const searchParams = new URLSearchParams(props.location.search);
 setSearch(searchParams.get("search") || "");
}, []);
  1. 现在让我们在render方法中添加一个search输入,供用户输入他们的搜索:
public render() {
  return (
    <header className="header">
      <div className="search-container">
 <input
 type="search"
 placeholder="search"
 value={search}
 onChange={handleSearchChange}
 onKeyDown={handleSearchKeydown}
 />
 </div>
      <img src={logo} className="header-logo" alt="logo" />
      <h1 className="header-title">React Shop</h1>
      <nav>
        ...
      </nav>
    </header>
  );
}
  1. 让我们添加刚才引用到index.csssearch-containerCSS 类:
.search-container {
  text-align: right;
  margin-bottom: -25px;
}
  1. 回到Header.tsx中,让我们添加handleSearchChange方法,该方法在render方法中被引用,并将通过输入的值使我们的search状态保持最新:
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setSearch(e.currentTarget.value);
};
  1. 我们现在可以实现handleSearchKeydown方法,该方法在render方法中引用。这需要在按下Enter键时将search状态值添加到路径查询字符串中。我们可以利用RouteComponentProps提供给我们的history道具中的push方法:
const handleSearchKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === "Enter") {
    props.history.push(`/products?search=${search}`);
  }
};
  1. 我们需要导出用withRouter高阶组件包装的Header组件,以便参考this.props.history工作。那么,让我们这样做并调整我们的export表达式:
export default withRouter(Header);
  1. 让我们在 running 应用程序中尝试一下。如果我们在搜索输入中输入redux并按下输入键,应用程序应导航到产品页面并过滤产品以反应 Redux:

路线提示

有时,我们可能会要求用户确认他们想要离开某个页面。这是有用的,如果用户在页面上的数据输入的中间,并按下导航链接去一个不同的页面之前,他们已经保存了数据。React Router 中的Prompt组件允许我们这样做,如下步骤所述:

  1. 在我们的应用程序中,如果用户没有将产品添加到他们的购物篮中,我们将提示用户确认他们想要离开产品页面。首先,在ProductPage.tsx中,我们从 React 路由导入Prompt组件:
import { Prompt, RouteComponentProps } from "react-router-dom";
  1. 当满足某个条件时,Prompt组件在导航过程中调用确认对话框。我们可以在 JSX 中使用Prompt组件,如下所示:
<div className="page-container">
  <Prompt when={!this.state.added} message={this.navAwayMessage}
  />
  ...
</div>

when属性允许我们指定对话框何时出现的表达式。在我们的例子中,这是指产品尚未添加到篮子中。

message属性允许我们指定一个函数,返回要在对话框中显示的消息

**3. 在我们的例子中,我们调用一个navAwayMessage方法,接下来我们将实现它:

private navAwayMessage = () =>
    "Are you sure you leave without buying this product?";
  1. 让我们尝试一下,导航到 React 路由产品,然后导航到别处,而不单击“添加到篮子”按钮:

我们被要求确认是否要离开。

嵌套路由

嵌套路由是指 URL 的深度超过一级并且呈现多个组件的情况。我们将在管理页面的本节中实现一些嵌套路由。我们完成的管理页面将如以下屏幕截图所示:

前面屏幕截图中的 URL 有 3 层,呈现以下内容:

  • 包含用户和产品链接的顶级菜单。
  • 包含所有用户的菜单。这只是弗雷德,鲍勃,简是我们的例子
  • 有关所选用户的信息。

  • 我们先打开AdminPage.tsx并添加react-router-dom中的import语句:

import { NavLink, Route, RouteComponentProps } from "react-router-dom";
  • 我们将使用NavLink组件呈现菜单
  • Route组件将用于呈现嵌套路由
  • RouteComponentProps类型将用于从 URL 获取用户的id

  • 我们将用包含菜单选项、用户和产品的无序列表替换p标签:

<div className="page-container">
  <h1>Admin Panel</h1>
  <ul className="admin-sections>
 <li key="users">
 <NavLink to={`/admin/users`} activeClassName="admin-link-
        active">
 Users
 </NavLink>
 </li>
 <li key="products">
 <NavLink to={`/admin/products`} activeClassName="admin-link-
       active">
 Products
 </NavLink>
 </li>
 </ul>
</div>

我们使用NavLink组件导航到两个选项的嵌套路径。

  1. 让我们添加刚才在index.css中引用的 CSS 类:
.admin-sections {
  list-style: none;
  margin: 0px 0px 20px 0px;
  padding: 0;
}

.admin-sections li {
  display: inline-block;
  margin-right: 10px;
}

.admin-sections li a {
  color: #222;
  text-decoration: none;
}

.admin-link-active {
  border-bottom: #6f6e6e solid 2px;
}
  1. 回到AdminPage.tsx,让我们在刚才添加的菜单下面添加两个Route组件。这些将处理我们在菜单中引用的/admin/users/admin/products路径:
<div className="page-container">
  <h1>Admin Panel</h1>
  <ul className="admin-sections">
    ...
  </ul>
 <Route path="/admin/users" component={AdminUsers} />
 <Route path="/admin/products" component={AdminProducts} />
</div>
  1. 我们刚刚引用了尚不存在的AdminUsersAdminProducts组件。让我们首先通过在AdminPage.tsx中的AdminPage组件下面输入以下内容来实现AdminProducts组件:
const AdminProducts: React.SFC = () => {
  return <div>Some options to administer products</div>;
};

所以,这个组件只是在屏幕上呈现一些文本。

  1. 现在让我们转到更复杂的AdminUsers组件。我们将首先为用户定义一个界面,并在AdminPage.tsx中的AdminProducts组件下定义一些用户数据:
interface IUser {
  id: number;
  name: string;
  isAdmin: boolean;
}
const adminUsersData: IUser[] = [
  { id: 1, name: "Fred", isAdmin: true },
  { id: 2, name: "Bob", isAdmin: false },
  { id: 3, name: "Jane", isAdmin: true }
];

因此,在我们的示例中有 3 个用户。

  1. 让我们开始实现AdminUsers组件,然后在AdminPage.tsx中:
const AdminUsers: React.SFC = () => {
  return (
    <div>
      <ul className="admin-sections">
        {adminUsersData.map(user => (
          <li>
            <NavLink
              to={`/admin/users/${user.id}`}
              activeClassName="admin-link-active"
            >
              {user.name}
            </NavLink>
          </li>
        ))}
      </ul>
    </div>
  );
};

该组件呈现包含每个用户名的链接。该链接指向一个嵌套路径,该路径最终将显示有关用户的详细信息。

  1. 因此,我们需要定义另一条路由,该路由将调用组件来呈现有关用户的详细信息。我们可以使用另一个Route组件:
<div>
  <ul className="admin-sections">
    ...
  </ul>
 <Route path="/admin/users/:id" component={AdminUser} />
</div>
  1. 我们刚刚定义的路径指向我们尚未定义的AdminUser组件。那么,让我们从AdminUsers组件下面开始:
const AdminUser: React.SFC<RouteComponentProps<{ id: string }>> = props => {
  return null;
};

我们使用RouteComponentProps从 URL 路径获取id并在道具中提供。

  1. 我们现在可以使用路径中的id从我们的adminUsersData数组中获取用户:
const AdminUser: React.SFC<RouteComponentProps<{ id: string }>> = props => {
  let user: IUser;
 if (props.match.params.id) {
 const id: number = parseInt(props.match.params.id, 10);
 user = adminUsersData.filter(u => u.id === id)[0];
 } else {
 return null;
 }
  return null;
};
  1. 现在我们有了user对象,我们可以呈现其中的信息。
const AdminUser: React.SFC<RouteComponentProps<{ id: string }>> = props => {
  let user: IUser;
  if (props.match.params.id) {
    const id: number = parseInt(props.match.params.id, 10);
    user = adminUsersData.filter(u => u.id === id)[0];
  } else {
    return null;
  }
  return (
 <div>
 <div>
 <b>Id: </b>
 <span>{user.id.toString()}</span>
 </div>
 <div>
 <b>Is Admin: </b>
 <span>{user.isAdmin.toString()}</span>
 </div>
 </div>
 );
};
  1. 如果我们转到 running 应用程序,转到 Admin 页面并单击 Products 菜单项,它将如下所示:

  1. 如果我们点击用户菜单项,我们将看到 3 个用户,我们可以点击这些用户来获取有关用户的更多信息。这将像本节中的第一个屏幕截图。

因此,为了实现嵌套路由,我们使用NavLinkLink组件创建必要的链接,并将这些链接路由到该组件,以使用Route组件呈现内容。在本节之前,我们已经了解了这些组件,因此,我们只需要学习如何在嵌套路由的上下文中使用这些组件。

动画过渡

在本节中,我们将在用户导航到不同页面时添加一些动画。我们使用react-transition-group npm包中的TransitionGroupCSSTransition组件进行此操作,如下步骤所示:

  1. 因此,让我们首先使用 TypeScript 类型安装此软件包:
npm install react-transition-group
npm install @types/react-transition-group --save-dev

TransitionGroup跟踪其本地状态中的所有子项,并计算子项何时进入或退出。CSSTransition获取子项是否离开TransitionGroup并根据该状态将 CSS 类应用于子项。因此,TransitionGroupCSSTransition可以包装我们的路由并调用 CSS 类,我们可以创建这些类来设置页面进出的动画。

  1. 那么,让我们将这些组件导入到Routes.tsx中:
import { CSSTransition, TransitionGroup } from "react-transition-group";
  1. 我们还需要从 React 路由导入RouteComponentProps
import { Redirect, Route, RouteComponentProps, Switch} from "react-router-dom";
  1. 我们使用RouteComponentProps作为Route组件道具类型:
const Routes: React.SFC<RouteComponentProps> = props => {
  ...
}
  1. 让我们将CSSTransitionTransitionGroup组件添加到围绕Switch组件的 JSX 中:
<TransitionGroup>
  <CSSTransition
    key={props.location.key}
    timeout={500}
    classNames="animate"
    >
    <Switch>
      ...
    </Switch>
  </CSSTransition>
</TransitionGroup>

TransitionGroup要求孩子们有一个唯一的钥匙,以确定什么是退出和进入。因此,我们在CSSTransition上指定了一个key属性作为RouteComponentProps中的location.key属性。我们已经通过timeout属性指定转换将运行半秒。我们还指定了将通过classNames属性以animate前缀调用的 CSS 类。

**6. 那么,让我们在index.css中添加这些 CSS 类:

.animate-enter {
  opacity: 0;
  z-index: 1;
}
.animate-enter-active {
  opacity: 1;
  transition: opacity 450ms ease-in;
}
.animate-exit {
  display: none;
}

CSSTransition将在其键更改时调用这些 CSS 类。CSS 类最初隐藏正在转换的元素,并逐渐降低元素的不透明度,以便显示。

  1. 如果我们转到index.tsx,我们会得到一个编译错误,我们引用Routes组件,因为它希望我们从路由传递history之类的道具:

不幸的是,我们不能使用withRouter高阶组件,因为这将在Router组件之外。为了解决这个问题,我们可以添加一个名为RoutesWrap的新组件,它不接受任何道具,并包装我们现有的Routes组件。Router将向上移动到RoutesWrap,并包含一个Route组件,该组件始终呈现我们的Routes组件。

  1. 那么,让我们将这个RoutesWrap组件添加到Routes.tsx中,并导出RoutesWrap而不是Routes
const RoutesWrap: React.SFC = () => {
 return (
 <Router>
 <Route component={Routes} />
 </Router>
 );
};

class Routes extends React.Component<RouteComponentProps, IState> { 
  ...
}

export default RoutesWrap;

编译错误消失了,这很好。

  1. 现在让我们从Routes组件中删除Router,将div标记保留为其根:
public render() {
  return (
    <div>
      <Header />
      <TransitionGroup>
        ...
      </TransitionGroup>
    </div>
  );
}

如果我们转到 running 应用程序并导航到不同的页面,当页面进入视图时,您将看到一个漂亮的淡入淡出动画。

延迟加载路径

目前,我们应用程序的所有 JavaScript 都是在应用程序首次加载时加载的。这包括用户不经常使用的管理页面。如果应用程序加载时没有加载AdminPage组件,而是按需加载,那就太好了。这正是我们在本节要做的。这称为“延迟加载”组件。以下步骤允许我们按需加载内容:

  1. 首先,我们将从 React 导入Suspense组件,稍后我们将使用该组件:
import { Suspense } from "react";
  1. 现在,我们将以不同的方式导入AdminPage组件:
const AdminPage = React.lazy(() => import("./AdminPage"));

我们使用一个名为lazy的 React 函数,它接收一个返回动态导入的函数,该函数反过来被分配给我们的AdminPage组件变量。

  1. 完成此操作后,可能会出现一个 linting 错误:ES5/ES3 中的动态导入调用需要“Promise”构造函数。确保您有一个“Promise”构造函数的声明,或者在您的“--lib”选项中包含“ES2015”。因此,在tsconfig.json中,我们添加lib编译器选项:
"compilerOptions": { 
  "lib": ["es6", "dom"],
  ...
}
  1. 下一部分是将Suspense组件包裹在AdminPage组件上:
<Route path="/admin">
  {loggedIn ? (
    <Suspense fallback={<div className="page-container">Loading...</div>}>
      <AdminPage />
    </Suspense>
  ) : (
    <Redirect to="/login" />
  )}
</Route>

Suspense组件显示一个div标签,其中包含加载……同时AdminPage正在加载。

  1. 让我们在 running 应用程序中试试这个。让我们打开浏览器开发人员工具并转到“网络”选项卡。在我们的应用程序中,让我们转到产品页面并刷新浏览器。然后,让我们清除开发人员工具中“网络”选项卡中的内容。如果我们进入应用程序的管理页面,查看网络选项卡中的内容,我们将看到动态加载的AdminPage组件的JavaScript 块

  1. AdminPage组件加载速度非常快,因此我们从未真正看到加载…div标记。因此,让我们在浏览器开发人员工具中降低连接速度:

  1. 如果我们刷新浏览器,然后再次转到管理页面,我们将看到加载…:

在本例中,AdminPage组件并没有那么大,因此这种方法实际上不会对性能产生积极影响。但是,按需加载较大的组件确实可以提高性能,特别是在连接速度较慢的情况下。

总结

React Router 为我们提供了一套全面的组件,用于管理应用程序中页面之间的导航。我们了解到顶级组件是Router,它在下面寻找Route组件,我们在其中定义对于某些路径应该呈现哪些组件。

Link组件允许我们通过应用程序链接到不同的页面。我们了解到,NavLink组件类似于Link,但它包括根据是否为活动路径对其进行样式设置的能力。因此,NavLink非常适合应用程序中的主要导航元素,Link非常适合页面上出现的其他链接。

RouteComponentProps是一种允许我们访问路由参数和查询参数的类型。我们发现 React Router 不为我们解析查询参数,但可以使用本机 JavaScriptURLSearchParams接口为我们解析查询参数。

Redirect组件在特定条件下重定向到路径。我们发现,这非常适合保护只有特权用户才能访问的页面。

Prompt组件允许我们要求用户确认他们想要在特定条件下离开页面。我们在产品页面上使用它来再次检查用户是否想要购买该产品。该组件的另一个常见用例是,当输入的数据尚未保存时,确认导航离开数据输入页面。

我们了解了嵌套路由如何为用户提供深入到我们应用程序特定部分的链接。我们只是使用LinkNavLinkRoute组件来定义相关链接,以处理这些链接。

我们使用react-transition-group npm软件包中的TransitionGroupCSSTransition组件改进了页面转换的应用体验。我们将这些组件包装在我们的Route组件周围,这些组件定义了应用程序路径,并添加了 CSS 类,以在页面退出并进入视图时制作我们想要的动画。

我们了解到,Reactlazy功能及其Suspense组件可用于用户很少使用的大型组件上,以根据需要加载它们。这有助于提高应用程序启动时的性能。

问题

让我们通过以下问题来测试我们对 React 路由的了解:

  1. 我们有以下Route组件,显示客户列表:
<Route path="/customers" component={CustomersPage} />

当页面为"/customers"时,CustomersPage组件会呈现吗?

  1. 当页面为"/customers/24322"时,CustomersPage组件会呈现吗?
  2. 我们只希望在路径为"/customers"时渲染CustomersPage组件。我们如何更改Route上的属性来实现这一点?
  3. 能够处理"/customers/24322"路径的Route组件是什么?它应该将"24322"放在一个名为customerId的路由参数中。

  4. 我们如何捕捉不存在的路径以便通知用户?

  5. 我们如何在CustomersPage中实现search查询参数,所以"/customers/?search=Cool Company"会向客户显示名称"Cool Company"
  6. 过了一会儿,我们决定将"customer"路径更改为"clients"。我们如何实现这一点,以便用户仍然可以使用现有的"customer"路径,但自动将路径重定向到新的"client"路径?

进一步阅读