四、将 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 代码来做好准备。要执行此操作,请执行以下步骤:
- 现在,让我们打开一个终端并输入以下命令来创建一个新的 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
- 创建项目后,让我们将 TSLint 作为开发依赖项添加到我们的项目中,以及一些与 React 和 Prettier 配合良好的规则:
cd reactshop
npm install tslint tslint-react tslint-config-prettier --save-dev
- 现在我们添加一个包含一些规则的
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"
]
}
}
- 现在,让我们输入以下命令将 React Router 安装到我们的项目中:
npm install react-router-dom
- 我们还将为 React Router 安装 TypeScript 类型,并将其另存为开发依赖项:
npm install @types/react-router-dom --save-dev
在继续下一节之前,我们将删除一些不需要的文件create-react-app
:
- 首先,让我们移除
App
组件。那么,让我们删除App.css
、App.test.tsx
和App.tsx
文件。我们还要删除index.tsx
中的导入引用"./App"
。 - 我们还可以通过删除
serviceWorker.ts
文件并删除index.tsx
中对它的引用来删除服务工作者 - 在
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
);
申报路线
我们使用BrowserRouter
和Route
组件在我们的应用程序中声明页面。BrowserRouter
是顶级组件,它在下面寻找Route
组件,以确定所有不同的页面路径。
在本节后面,我们将使用BrowserRouter
和Route
在应用程序中声明一些页面,但在此之前,我们需要创建前两个页面。第一个页面将包含我们将在商店中销售的 React 工具列表。我们使用以下步骤创建页面:
- 那么,让我们先创建一个包含以下内容的
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
}
];
- 让我们创建另一个名为
ProductsPage.tsx
的文件,其中包含要导入的以下内容以及我们的数据:
import * as React from "react";
import { IProduct, products } from "./ProductsData";
- 我们将引用组件状态中的数据,因此让我们为此创建一个接口:
interface IState {
products: IProduct[];
}
- 让我们继续创建名为
ProductsPage
的类组件,将状态初始化为空数组:
class ProductsPage extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
products: []
};
}
}
export default ProductsPage;
- 现在让我们实现
componentDidMount
生命周期方法,并将数据从ProductData.ts
设置到products
数组:
public componentDidMount() {
this.setState({ products });
}
- 继续实施
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
产品。
- 我们已经引用了一些 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;
}
- 现在让我们实现我们的第二个页面,它将是一个管理面板。那么,让我们创建一个名为
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;
- 现在我们有两个页面在我们的商店,我们可以宣布我们的两条路线给他们。让我们创建一个名为
Routes.tsx
的文件,其中包含以下内容,以便从 React Router 导入React
、BrowserRouter
和Route
组件,以及我们的两个页面:
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
,以节省一些按键操作。
- 让我们继续实现一个包含两条路线的功能组件:
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"
.
- 下面是将我们的
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);
- 我们现在应该可以运行我们的应用程序了:
npm start
应用程序可能会在根页面上启动,该页面将为空,因为该路径不指向任何内容。
- 如果我们将路径更改为
"/products"
,我们的产品列表应显示以下内容:
- 如果我们将路径更改为
"/admin"
,我们的管理面板应呈现以下内容:
现在我们已经成功地创建了几个路由,我们真的需要一个导航组件来让我们的页面更容易被发现。我们将在下一节中这样做。
创建导航
React 路由带有一些不错的组件,用于提供导航。我们将使用这些来实现应用程序标题中的导航选项。
使用链接组件
我们将使用 React Router 的Link
组件通过执行以下步骤来创建导航选项:
- 让我们首先创建一个名为
Header.tsx
的新文件,其中包含以下导入:
import * as React from "react";
import { Link } from "react-router-dom";
import logo from "./logo.svg";
- 让我们使用
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.
- 我们已经引用了一些 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;
}
- 现在我们的
Header
组件已经就位,让我们import
进入Routes.tsx
:
import Header from "./Header";
- 然后,我们可以在 JSX 中使用它,如下所示:
<Router>
<div>
<Header />
<Route path="/products" component={ProductsPage} />
<Route path="/admin" component={AdminPage} />
</div>
</Router>
- 如果我们检查 running 应用程序,它应该看起来像下面的屏幕截图,带有一个漂亮的标题和两个导航选项,可以转到我们的产品和管理页面:
- 尝试单击导航选项-它们可以工作!如果我们使用浏览器开发人员工具检查产品和管理元素,我们会看到 React Router 将它们呈现为锚定标记:
如果我们在单击导航选项时查看开发人员工具中的“网络”选项卡,我们将看到没有提出网络请求来服务页面。这表明 React 路由正在 React 应用程序中为我们处理导航。
使用导航链接组件
React 路由提供另一个用于链接页面的组件,称为NavLink
。这实际上更符合我们的要求。以下步骤解释了如何重构Header
组件以使用NavLink
:
- 那么,让我们在
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;
此时,我们的应用程序的外观和行为完全相同。
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>
- 让我们将
header-link-active
的 CSS 添加到index.css
中:
.header-link-active {
border-bottom: #ebebeb solid 2px;
}
- 如果我们现在切换到 running 应用程序,活动链接将加下划线:
因此,NavLink
对于主应用程序导航非常有用,我们希望突出显示活动链接,Link
对于我们应用程序中的所有其他链接都非常有用
路线参数
路由参数是路径的可变部分,可在目标组件中用于有条件地呈现某些内容。
我们需要为我们的商店添加另一个页面,以显示每种产品的说明和价格,以及将其添加到购物篮的选项。我们希望能够使用"/products/{id}"
路径导航到此页面,其中id
是产品的 ID。例如,反应 Redux 的路径将是"products/2"
。因此,路径的id
部分是一个路由参数。我们可以通过以下步骤来实现这一切:
- 让我们将此路由添加到两条现有路由之间的
Routes.tsx
。路由的id
部分将是一个路由参数,我们在它前面定义了一个冒号:
<Route path="/products" component={ProductsPage} />
<Route path="/products/:id" component={ProductPage} />
<Route path="/admin" component={AdminPage} />
- 当然,
ProductPage
组件还不存在,因此,让我们先创建一个名为ProductPage.tsx
的新文件,并使用以下导入来创建它:
import * as React from "react";
import { RouteComponentProps } from "react-router-dom";
import { IProduct, products } from "./ProductsData";
- 这里的关键部分是我们将使用
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;
}
- 产品最初将是
undefined
,这就是为什么它被定义为可选的。让我们创建ProductPage
类并初始化状态,以便产品不在篮子中:
class ProductPage extends React.Component<Props, IState> {
public constructor(props: Props) {
super(props);
this.state = {
added: false
};
}
}
export default ProductPage;
- 当组件加载到 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
将其转换为一个数字
- 现在,我们的产品处于组件状态,让我们转到
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 });
};
- 现在我们已经实现了
ProductPage
组件,让我们回到Routes.tsx
并导入它:
import ProductPage from "./ProductPage";
- 让我们转到我们的跑步应用程序,输入
"/products/2"
作为路径:
Not quite what we want! Both ProductsPage
and ProductPage
have rendered because "/products/2"
matches both "/products"
and "/products/:id"
.
- 为了解决这个问题,我们可以告诉
"/products"
路径仅在存在精确匹配时渲染:
<Route exact={true} path="/products" component={ProductsPage} />
- 在我们进行此更改并保存
Routes.tsx
后,我们的产品页面看起来好多了:
- 我们不会让我们的用户输入访问产品的特定路径!因此,对于使用
Link
组件的每个产品,我们将ProductsPage
更改为链接到ProductPage
。首先,我们将Link
从 React 路由导入ProductsPage
:
import { Link } from "react-router-dom";
- 现在,我们不在每个列表项中呈现产品名称,而是呈现一个
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>
);
}
- 在我们查看 running 应用程序之前,让我们在
index.css
中添加以下 CSS 类:
.product-list-item a {
text-decoration: none;
}
现在,如果我们转到应用程序中的产品列表并单击列表项,它会将我们带到相关的产品页面。
处理未找到的路由
如果用户输入的路径在我们的应用程序中不存在怎么办?例如,如果我们尝试导航到"/tools"
,标题下方将不会显示任何内容。这是有道理的,因为 React Router 没有找到任何匹配的路由,所以不会渲染任何内容。但是,如果用户确实导航到无效路径,我们希望通知他们该路径不存在。以下步骤可实现此目的:
- 那么,让我们用以下组件创建一个名为
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;
- 让我们将其导入
Routes.tsx
中的路线:
import NotFoundPage from "./NotFoundPage";
- 然后,让我们将一个
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
组件中。
- 首先将
Switch
导入Routes.tsx
:
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
- 现在让我们将
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"
。
- 首先,我们需要将
Redirect
组件从 React Router 导入Routes.tsx
:
import { BrowserRouter as Router, Redirect,Route, Switch } from "react-router-dom";
- 当路径为
/
时,我们现在可以使用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>
- 我们在
Redirect
上使用了exact
属性,因此它只匹配/
,而不匹配"/products/1"
和"/admin"
。如果我们尝试一下,并在我们的跑步应用程序中输入/
作为路径,它将立即重定向到"/products"
。
条件重定向
我们可以使用Redirect
组件保护页面免受未经授权用户的攻击。在我们的商店,我们可以使用它来确保只有登录的用户才能访问我们的管理页面。我们通过以下步骤来实现这一点:
- 让我们先在
Routes.tsx
中的LoginPage
中添加一个路由,该路由位于进入管理页面的路由之后:
<Route path="/login" component={LoginPage} />
- 当然,
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;
- 然后我们可以返回到
Routes.tsx
并导入LoginPage
:
import LoginPage from "./LoginPage";
- 如果我们转到 running 应用程序并导航到
"/login"
,我们将看到我们的登录页面:
我们不会完全实现我们的登录页面;我们实现的页面足以演示条件重定向。
- 在
"admin"
路径上实现条件重定向之前,需要在Routes.tsx
中添加一个用户是否登录的状态:
const Routes: React.SFC = () => {
const [loggedIn, setLoggedIn] = React.useState(false);
return (
<Router>
...
</Router>
);
};
因此,我们使用了一个useState
钩子来添加一个名为loggedIn
的状态变量和一个名为setLoggedIn
的函数来设置它。
- 最后一步是在具有
"/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.
- 如果我们在初始化时将
loggedIn
状态更改为 true,我们可以再次访问我们的管理页面:
const [loggedIn, setLoggedIn] = React.useState(true);
查询参数
查询参数是允许将其他参数传递到路径的 URL 的一部分。例如,"/products?search=redux"
有一个名为search
的查询参数,其值为redux
。
让我们实现此示例,并允许商店用户搜索产品:
- 首先,我们在
ProductsPage.tsx
中添加一个状态为search
的变量,该变量将保存搜索条件:
interface IState {
products: IProduct[];
search: string;
}
- 考虑到我们需要访问 URL,我们需要使用
RouteComponentProps
作为ProductsPage
中的道具类型。让我们首先导入以下内容:
import { RouteComponentProps } from "react-router-dom";
- 然后我们可以将其用作
props
类型:
class ProductsPage extends React.Component<RouteComponentProps, IState> {
- 我们可以在
constructor
中将search
状态初始化为空字符串:
public constructor(props: RouteComponentProps) {
super(props);
this.state = {
products: [],
search: ""
};
}
- 然后我们需要将
componentDidMount
中的search
状态设置为搜索查询参数。React Router 允许我们访问传入组件的props
参数中location.search
中的所有查询参数。然后,我们需要解析该字符串以获得搜索查询字符串参数。我们可以使用URLSearchParams
JavaScript 函数来实现这一点。我们将使用静态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
};
}
- 不幸的是,
URLSearchParams
尚未在所有浏览器中实现,因此我们可以使用名为url-search-params-polyfill
的 polyfill。让我们安装这个:
npm install url-search-params-polyfill
- 我们将其导入
ProductPages.tsx
:
import "url-search-params-polyfill";
- 然后我们可以使用
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>
- 如果我们在 running app 中输入
"/products?search=redux"
作为路径,我们将看到我们的产品列表,其中只包含 React Redux:
- 我们将通过在应用程序标题中添加设置搜索查询参数的搜索输入来完成此功能的实现。首先,我们在
Header
组件中为Header.tsx
中的搜索值创建一些状态:
const [search, setSearch] = React.useState("");
- 我们还需要通过 React Router 和
URLSearchParams
访问查询字符串,所以我们将RouteComponentProps
、withRouter
、和导入URLSearchParams
polyfill:****
import { NavLink, RouteComponentProps, withRouter} from "react-router-dom";
import "url-search-params-polyfill";
- 让我们在
Header
组件中添加一个props
参数:
const Header: React.SFC<RouteComponentProps> = props => { ... }
- 我们现在可以从路径查询字符串中获取搜索值,并在组件首次呈现时将
search
状态设置为:
const [search, setSearch] = React.useState("");
React.useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
setSearch(searchParams.get("search") || "");
}, []);
- 现在让我们在
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>
);
}
- 让我们添加刚才引用到
index.css
的search-container
CSS 类:
.search-container {
text-align: right;
margin-bottom: -25px;
}
- 回到
Header.tsx
中,让我们添加handleSearchChange
方法,该方法在render
方法中被引用,并将通过输入的值使我们的search
状态保持最新:
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.currentTarget.value);
};
- 我们现在可以实现
handleSearchKeydown
方法,该方法在render
方法中引用。这需要在按下Enter
键时将search
状态值添加到路径查询字符串中。我们可以利用RouteComponentProps
提供给我们的history
道具中的push
方法:
const handleSearchKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
props.history.push(`/products?search=${search}`);
}
};
- 我们需要导出用
withRouter
高阶组件包装的Header
组件,以便参考this.props.history
工作。那么,让我们这样做并调整我们的export
表达式:
export default withRouter(Header);
- 让我们在 running 应用程序中尝试一下。如果我们在搜索输入中输入
redux
并按下输入键,应用程序应导航到产品页面并过滤产品以反应 Redux:
路线提示
有时,我们可能会要求用户确认他们想要离开某个页面。这是有用的,如果用户在页面上的数据输入的中间,并按下导航链接去一个不同的页面之前,他们已经保存了数据。React Router 中的Prompt
组件允许我们这样做,如下步骤所述:
- 在我们的应用程序中,如果用户没有将产品添加到他们的购物篮中,我们将提示用户确认他们想要离开产品页面。首先,在
ProductPage.tsx
中,我们从 React 路由导入Prompt
组件:
import { Prompt, RouteComponentProps } from "react-router-dom";
- 当满足某个条件时,
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?";
- 让我们尝试一下,导航到 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
组件导航到两个选项的嵌套路径。
- 让我们添加刚才在
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;
}
- 回到
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>
- 我们刚刚引用了尚不存在的
AdminUsers
和AdminProducts
组件。让我们首先通过在AdminPage.tsx
中的AdminPage
组件下面输入以下内容来实现AdminProducts
组件:
const AdminProducts: React.SFC = () => {
return <div>Some options to administer products</div>;
};
所以,这个组件只是在屏幕上呈现一些文本。
- 现在让我们转到更复杂的
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 个用户。
- 让我们开始实现
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>
);
};
该组件呈现包含每个用户名的链接。该链接指向一个嵌套路径,该路径最终将显示有关用户的详细信息。
- 因此,我们需要定义另一条路由,该路由将调用组件来呈现有关用户的详细信息。我们可以使用另一个
Route
组件:
<div>
<ul className="admin-sections">
...
</ul>
<Route path="/admin/users/:id" component={AdminUser} />
</div>
- 我们刚刚定义的路径指向我们尚未定义的
AdminUser
组件。那么,让我们从AdminUsers
组件下面开始:
const AdminUser: React.SFC<RouteComponentProps<{ id: string }>> = props => {
return null;
};
我们使用RouteComponentProps
从 URL 路径获取id
并在道具中提供。
- 我们现在可以使用路径中的
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;
};
- 现在我们有了
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>
);
};
- 如果我们转到 running 应用程序,转到 Admin 页面并单击 Products 菜单项,它将如下所示:
- 如果我们点击用户菜单项,我们将看到 3 个用户,我们可以点击这些用户来获取有关用户的更多信息。这将像本节中的第一个屏幕截图。
因此,为了实现嵌套路由,我们使用NavLink
或Link
组件创建必要的链接,并将这些链接路由到该组件,以使用Route
组件呈现内容。在本节之前,我们已经了解了这些组件,因此,我们只需要学习如何在嵌套路由的上下文中使用这些组件。
动画过渡
在本节中,我们将在用户导航到不同页面时添加一些动画。我们使用react-transition-group npm
包中的TransitionGroup
和CSSTransition
组件进行此操作,如下步骤所示:
- 因此,让我们首先使用 TypeScript 类型安装此软件包:
npm install react-transition-group
npm install @types/react-transition-group --save-dev
TransitionGroup
跟踪其本地状态中的所有子项,并计算子项何时进入或退出。CSSTransition
获取子项是否离开TransitionGroup
并根据该状态将 CSS 类应用于子项。因此,TransitionGroup
和CSSTransition
可以包装我们的路由并调用 CSS 类,我们可以创建这些类来设置页面进出的动画。
- 那么,让我们将这些组件导入到
Routes.tsx
中:
import { CSSTransition, TransitionGroup } from "react-transition-group";
- 我们还需要从 React 路由导入
RouteComponentProps
:
import { Redirect, Route, RouteComponentProps, Switch} from "react-router-dom";
- 我们使用
RouteComponentProps
作为Route
组件道具类型:
const Routes: React.SFC<RouteComponentProps> = props => {
...
}
- 让我们将
CSSTransition
和TransitionGroup
组件添加到围绕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 类最初隐藏正在转换的元素,并逐渐降低元素的不透明度,以便显示。
- 如果我们转到
index.tsx
,我们会得到一个编译错误,我们引用Routes
组件,因为它希望我们从路由传递history
之类的道具:
不幸的是,我们不能使用withRouter
高阶组件,因为这将在Router
组件之外。为了解决这个问题,我们可以添加一个名为RoutesWrap
的新组件,它不接受任何道具,并包装我们现有的Routes
组件。Router
将向上移动到RoutesWrap
,并包含一个Route
组件,该组件始终呈现我们的Routes
组件。
- 那么,让我们将这个
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;
编译错误消失了,这很好。
- 现在让我们从
Routes
组件中删除Router
,将div
标记保留为其根:
public render() {
return (
<div>
<Header />
<TransitionGroup>
...
</TransitionGroup>
</div>
);
}
如果我们转到 running 应用程序并导航到不同的页面,当页面进入视图时,您将看到一个漂亮的淡入淡出动画。
延迟加载路径
目前,我们应用程序的所有 JavaScript 都是在应用程序首次加载时加载的。这包括用户不经常使用的管理页面。如果应用程序加载时没有加载AdminPage
组件,而是按需加载,那就太好了。这正是我们在本节要做的。这称为“延迟加载”组件。以下步骤允许我们按需加载内容:
- 首先,我们将从 React 导入
Suspense
组件,稍后我们将使用该组件:
import { Suspense } from "react";
- 现在,我们将以不同的方式导入
AdminPage
组件:
const AdminPage = React.lazy(() => import("./AdminPage"));
我们使用一个名为lazy
的 React 函数,它接收一个返回动态导入的函数,该函数反过来被分配给我们的AdminPage
组件变量。
- 完成此操作后,可能会出现一个 linting 错误:ES5/ES3 中的动态导入调用需要“Promise”构造函数。确保您有一个“Promise”构造函数的声明,或者在您的“--lib”选项中包含“ES2015”。因此,在
tsconfig.json
中,我们添加lib
编译器选项:
"compilerOptions": {
"lib": ["es6", "dom"],
...
}
- 下一部分是将
Suspense
组件包裹在AdminPage
组件上:
<Route path="/admin">
{loggedIn ? (
<Suspense fallback={<div className="page-container">Loading...</div>}>
<AdminPage />
</Suspense>
) : (
<Redirect to="/login" />
)}
</Route>
Suspense
组件显示一个div
标签,其中包含加载……同时AdminPage
正在加载。
- 让我们在 running 应用程序中试试这个。让我们打开浏览器开发人员工具并转到“网络”选项卡。在我们的应用程序中,让我们转到产品页面并刷新浏览器。然后,让我们清除开发人员工具中“网络”选项卡中的内容。如果我们进入应用程序的管理页面,查看网络选项卡中的内容,我们将看到动态加载的
AdminPage
组件的JavaScript 块:
AdminPage
组件加载速度非常快,因此我们从未真正看到加载…div
标记。因此,让我们在浏览器开发人员工具中降低连接速度:
- 如果我们刷新浏览器,然后再次转到管理页面,我们将看到加载…:
在本例中,AdminPage
组件并没有那么大,因此这种方法实际上不会对性能产生积极影响。但是,按需加载较大的组件确实可以提高性能,特别是在连接速度较慢的情况下。
总结
React Router 为我们提供了一套全面的组件,用于管理应用程序中页面之间的导航。我们了解到顶级组件是Router
,它在下面寻找Route
组件,我们在其中定义对于某些路径应该呈现哪些组件。
Link
组件允许我们通过应用程序链接到不同的页面。我们了解到,NavLink
组件类似于Link
,但它包括根据是否为活动路径对其进行样式设置的能力。因此,NavLink
非常适合应用程序中的主要导航元素,Link
非常适合页面上出现的其他链接。
RouteComponentProps
是一种允许我们访问路由参数和查询参数的类型。我们发现 React Router 不为我们解析查询参数,但可以使用本机 JavaScriptURLSearchParams
接口为我们解析查询参数。
Redirect
组件在特定条件下重定向到路径。我们发现,这非常适合保护只有特权用户才能访问的页面。
Prompt
组件允许我们要求用户确认他们想要在特定条件下离开页面。我们在产品页面上使用它来再次检查用户是否想要购买该产品。该组件的另一个常见用例是,当输入的数据尚未保存时,确认导航离开数据输入页面。
我们了解了嵌套路由如何为用户提供深入到我们应用程序特定部分的链接。我们只是使用Link
或NavLink
和Route
组件来定义相关链接,以处理这些链接。
我们使用react-transition-group npm
软件包中的TransitionGroup
和CSSTransition
组件改进了页面转换的应用体验。我们将这些组件包装在我们的Route
组件周围,这些组件定义了应用程序路径,并添加了 CSS 类,以在页面退出并进入视图时制作我们想要的动画。
我们了解到,Reactlazy
功能及其Suspense
组件可用于用户很少使用的大型组件上,以根据需要加载它们。这有助于提高应用程序启动时的性能。
问题
让我们通过以下问题来测试我们对 React 路由的了解:
- 我们有以下
Route
组件,显示客户列表:
<Route path="/customers" component={CustomersPage} />
当页面为"/customers"
时,CustomersPage
组件会呈现吗?
- 当页面为
"/customers/24322"
时,CustomersPage
组件会呈现吗? - 我们只希望在路径为
"/customers"
时渲染CustomersPage
组件。我们如何更改Route
上的属性来实现这一点? -
能够处理
"/customers/24322"
路径的Route
组件是什么?它应该将"24322"
放在一个名为customerId
的路由参数中。 -
我们如何捕捉不存在的路径以便通知用户?
- 我们如何在
CustomersPage
中实现search
查询参数,所以"/customers/?search=Cool Company"
会向客户显示名称"Cool Company"
。 - 过了一会儿,我们决定将
"customer"
路径更改为"clients"
。我们如何实现这一点,以便用户仍然可以使用现有的"customer"
路径,但自动将路径重定向到新的"client"
路径?
进一步阅读
-
以下链接的 React 路由文档值得一读:https://reacttraining.com/react-router
-
react-transition-group
文档也值得一看,以进一步了解过渡组件:https://reactcommunity.org/react-transition-group/****
版权属于:月萌API www.moonapi.com,转载请注明出处