第四章 React Router

  • 安装React Router
  • 声明路由
  • 创建向导
  • 路由参数
  • 处理not found路由
  • 实现页面重定向
  • 查询参数
  • 路由提示符
  • 内嵌路由
  • 动画转换
  • lazy loading 路由

安装路由

将React Router添加进项目中,

1
yarn add react-router-dom

以及将TypeScript版本的React Router添加到devDependency的开发依赖中,

1
yarn add -D @types/react-router-dom

声明路由

在页面我们需要使用BrowserRouterRoute组件。BrowserRouter是top-level组件,会寻找下层的Route组件以决定不同的页面路径。

在引入BrowserRouterRoute之前,首先创建两个页面,

  1. 创建一个ProductsData.ts文件,内容如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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文件导入这些数据,
1
2
import * as React from "react";
import { IProduct, products } from "./ProductsData";
  1. 因为需要在组件引用数据,创建一个接口,
1
2
3
interface IState {
products: IProduct[];
}
  1. 创建类组件,初始化状态,
1
2
3
4
5
6
7
8
9
10
class ProductsPage extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
products: []
};
}
}

export default ProductsPage;
  1. 实现componentDidMount生命周期方法,更新组件的State,
1
2
3
public componentDidMount() {
this.setState({ products });
}
  1. 实现对应的render方法进行渲染,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
);
}
  1. 对应CSS样式为,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.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,它是个无状态组件,
1
2
3
4
5
6
7
8
9
10
11
12
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的文件,包含下面的内容,
1
2
3
4
import * as React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import AdminPage from "./AdminPage";
import ProductsPage from "./ProductsPage";
  1. 渲染路由页面,
1
2
3
4
5
6
7
8
9
10
11
12
const Routes: React.SFC = () => {
return (
<Router>
<div>
<Route path="/products" component={ProductsPage} />
<Route path="/admin" component={AdminPage} />
</div>
</Router>
);
};

export default Routes;
  1. 最后一步,把Routes添加到根组件index.tsx中,
1
2
3
4
5
6
7
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Routes from "./Routes";

ReactDOM.render(<Routes />,
document.getElementById('root') as HTMLElement);
  1. 目前页面上是什么也没有看到的,需要我们在浏览器直接输入地址,“/products”,或者访问另一个页面"/admin",

创建路由向导

非常幸运的是,React Router有一些组件提供了向导的功能。

使用Link组件实现向导功能,

  1. 创建一个Header.tsx文件,包含以下内容,
1
2
3
4
import * as React from "react";
import { Link } from "react-router-dom";

import logo from "./logo.svg";
  1. 接着创建两个Link链接,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
  1. 添加对应的CSS样式,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.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添加到我们的Routes.tsx
1
import Header from "./Header";
  1. 加入导航,
1
2
3
4
5
6
7
<Router>
<div>
<Header />
<Route path="/products" component={ProductsPage} />
<Route path="/admin" component={AdminPage} />
</div>
</Router>

使用NavLink组件

React Router还提供了另外一个组件用于页面链接,称为NavLink。下面我们用NavLink重构一下原来的Header组件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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属性
1
2
<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. 添加对应CSS样式,
1
2
3
4

.header-link-active {
border-bottom: #ebebeb solid 2px;
}

因此,NavLink主要让我们导航带上样式功能。

路由参数

路由参数是路径变量的部分,用于决定目标组件的渲染逻辑。

我们需要添加另外一个页面,来展示商品的描述内容和价格,我们想要让页面导向使用"/products/{id}"路径,其中id对应商品ID。

  1. 首先在原来的Routes.tsx添加路由路径和参数,
1
2
3
<Route path="/products" component={ProductsPage} />
<Route path="/products/:id" component={ProductPage} />
<Route path="/admin" component={AdminPage} />
  1. 创建对应的ProductPage组件,
1
2
3
import * as React from "react";
import { RouteComponentProps } from "react-router-dom";
import { IProduct, products } from "./ProductsData";
  1. 这里的关键点是,我们需要用到RouteComponentProps来访问路径上的参数id。另外需要定义类型别名
1
type Props = RouteComponentProps<{id: string}>;
  1. 另外还需要有个状态记录商品被添加到购物篮中,
1
2
3
4
interface IState {
product?: IProduct;
added: boolean;
}
  1. 初始化该状态,
1
2
3
4
5
6
7
8
class ProductPage extends React.Component<Props, IState> {
public constructor(props: Props) {
super(props);
this.setState({ added: false });
}
}

export default ProductPage;
  1. 当组件被加载进DOM,会通过路径参数上的id查找商品。RouteComponentProps给我们提供了一个match对象,以访问路径上的参数,
1
2
3
4
5
6
7
8
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是个字符串,需要使用parseInt进行转换,

  1. 初始化好商品和组件状态后,我们进入到render函数,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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>
);
}

这里有一些新东西,

  • 第一行创建了常量product
  • 三元运算符
  • 使用了React.Fragment,类似实现单独一个parent的功能
  • 使用了Intl.NumberFormat装换货币符号
  1. 另外还需要添加handleAddClick方法处理按钮事件,
1
2
3
private handleAddClick = () => {
this.setState({ added: true });
};
  1. 现在,将我们实现好的ProductPage组件,导入到Routes.tsx中,
1
import ProductPage from "./ProductPage";
  1. 直接浏览器键入"/products/2"查看新路由页面,但有个问题是"/products"和"/products/:id"都被渲染了

  2. 为了解决这个问题,修改为,

1
<Route exact={true} path="/products" component={ProductsPage} />
  1. 进一步,我们需要为每个条目添加链接,回到原来的ProductsPage.tsx组件,修改相应部分内容,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Link } from "react-router-dom";

...

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>
{product.name}
</li>
))}
</ul>
</div>
);
}
...

  1. 补充CSS样式,
1
2
3
.product-list-item a {
text-decoration: none;
}

Handling not found routes

如果用户输入的路径不存在怎么处理?例如,我们尝试键入"/tools",但路由中没有找到任何匹配的路由,我们希望提示该路径不存在。

  1. 首先创建一个新的文件NotFoundPage.tsx
1
2
3
4
5
6
7
8
9
10
11
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. 在路由组件中,导入,
1
import NotFoundPage from "./NotFoundPage";
  1. 添加到路由,
1
2
3
4
5
6
7
8
9
<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,这是需要用到Switch组件。

  1. 首先导入Swith组件到Routes.tsx中,
1
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
  1. Route包一层Switch组件即可,
1
2
3
4
5
6
<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组件。在我们案例中,当找不到页面是,第一个匹配到的就是NotFoundPage,这样就解决了我们的问题。

实现页面重定向

页面重定向使用了Redirect组件实现

Simple redirect

如果我们访问/路径,我们注意到它是个not found页面。我们希望当路径是/时,重定向到/products

  1. 首先,需要在Routes.tsx导入Redirect组件,
1
import { BrowserRouter as Router, Redirect, Route, Switch } from "react-router-dom";
  1. 然后使用Redirect组件进行重定向,
1
2
3
4
5
6
7
<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>

Conditional redirect

另外我们可以用Redirect组件对为授权用户进行保护访问。例如,我们的shop例子中,我们仅确保登录用户可以访问我们的Admin页面。

  1. 创建一个LoginPage路由,
1
<Route path="/login" component={LoginPage} />
  1. 添加一个登录页LoginPage.tsx组件,
1
2
3
4
5
6
7
8
9
10
11
12
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:
1
import LoginPage from "./LoginPage";
  1. 在重定向到"admin"之前,我们需要在Routes.tsx中添加一些状态值,
1
2
3
4
5
6
7
8
const Routes: React.SFC = () => {
const [loggedIn, setLoggedIn] = React.useState(false);
return (
<Router>
...
</Router>
);
};
  1. 最后一步是添加条件判断,
1
2
3
<Route path="/admin">
{loggedIn ? <AdminPage /> : <Redirect to="/login" />}
</Route>
  1. 如果我们将loggedIn状态修改为true,我们就可以再次访问我们的Admin页面了,
1
const [loggedIn, setLoggedIn] = React.useState(true);

Query parameters

查询参数是URL地址的一部分,例如"/products?search=redux"。让我们实现一个商品查询功能。

  1. 在原来的ProductsPage.tsx中,添加一个状态变量search
1
2
3
4
interface IState {
products: IProduct[];
search: string;
}
  1. 我们需要用到RouteComponentProps作为属性,
1
import { RouteComponentProps } from "react-router-dom";
  1. 初始化
1
2
3
4
5
6
7
8
9
class ProductsPage extends React.Component<RouteComponentProps, IState> {
public constructor(props: RouteComponentProps) {
super(props);
this.state = {
products: [],
search: ""
};
}
...
  1. 我们需要在componentDidMount确定好search的值,因此,实现getDerivedStateFromProps方法获取URL参数,并更新state,
1
2
3
4
5
6
7
8
9
10
11
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
1
yarn add -D url-search-params-polyfill
  1. 导入到ProductPages.tsx中,
1
import "url-search-params-polyfill";
  1. 在渲染部分使用search状态,并包装一个if语句,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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. Header.tsx文件中创建state,
1
const [search, setSearch] = React.useState("");
  1. 另外需要获取搜索参数,需要导入,
1
2
3
import { NavLink, RouteComponentProps, withRouter} from "reactrouter-
dom";
import "url-search-params-polyfill";
  1. 添加props
1
const Header: React.SFC<RouteComponentProps> = props => { ... }
  1. 组件首次渲染时,从路径参数获取值并设置search的状态,
1
2
3
4
5
6
const [search, setSearch] = React.useState("");
React.useEffect(() => {
const searchParams = new
URLSearchParams(props.location.search);
setSearch(searchParams.get("search") || "");
}, []);
  1. search添加到redner方法中,让用户进行输入,
1
2
3
4
5
6
7
8
9
10
11
12
... 
<header className="header">
<div className="search-container">
<input
type="search"
placeholder="search"
value={search}
onChange={handleSearchChange}
onKeyDown={handleSearchKeydown}
/>
</div>
...
  1. 添加search-containerCSS样式,
1
2
3
4
.search-container {
text-align: right;
margin-bottom: -25px;
}
  1. 回到原来的Header.tsx,添加对应的事件处理方法,
1
2
3
4
5
6
7
8
9
10
11
const handleSearchChange = (e:
React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.currentTarget.value);
};

const handleSearchKeydown = (e:
React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
props.history.push(`/products?search=${search}`);
}
};
  1. 另外我们需要用withRouter包装Header暴露接口,以使this.props.history可以生效,
1
export default withRouter(Hader);

Route prompts

react-router-dom组件中还有一个Prompt组件,顾名思义就是弹出框…

  1. 我们希望用户离开商品页面时,如果没有购物,则弹出提示,首先,在ProductPage.tsx中导入Prompt组件,
1
import { Prompt, RouteComponentProps } from "react-router-dom";
  1. 添加对应触发条件即可,
1
2
3
4
5
<div className="page-container">
<Prompt when={!this.state.added} message={this.navAwayMessage} />
...
private navAwayMessage = () => "Are you sure you leave without buying this product?";

Nested routes

内嵌路由就是在某一个一级URL下渲染多个组件。

譬如我们想要创建一个3层渲染,

  • 第一层包含UsersProducts的链接
  • Users层又包含所有用户
  • 点击每个用户可以看到具体信息
  1. 首先修改AdminPage.tsx,导入向导组件,
1
import { NavLink, Route, RouteComponentProps } from "react-router-dom";
  • 我们将会使用NavLink组件渲染菜单
  • Route用于内嵌路由
  • RouteComponentProps类型将会获取URL的参数id
  1. p标签内容替换为下面…
1
2
3
4
5
6
7
8
9
10
11
<div className="page-container">
<h1>Admin Panel</h1>
<ul className="admin-sections">
<li key="users" >
<NavLink to={`/admin/user`} activeClassName="admin-link-active">Users</NavLink>
</li>
<li key="products">
<NavLink to={`/admin/products`} activeClassName="admin-link-active">Products</NavLink>
</li>
</ul>
</div>
  1. 添加对应样式…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.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,添加两个路由组件,
1
2
3
4
5
6
7
8
<div className="page-container">
<h1>Admin Panel</h1>
<ul className="admin-sections">
...
</ul>
<Route path="/admin/users/:id" component={AdminUsers} />
<Route path="/admin/products" component={AdminProducts} />
</div>
  1. 创建这两个路由组件,在AdminPage.tsx内添加,
1
2
3
const AdminProducts: React.SFC = () => {
return <div>Some options to administer products</div>;
};
  1. 接下来的AdminUsers组件可能会复杂一些。首先定义一种数据结构,在AdminPage.tsx的组件AdminProducts下,
1
2
3
4
5
6
7
8
9
10
11
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组件的内容,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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. 另外,还需要定义另外一个路由展示用户详细信息。添加一个路由,
1
2
3
4
5
6
<div>
<ul className="admin-sections">
...
</ul>
<Route path="/admin/users/:id" component={AdminUser} />
</div>
  1. 这个AdminUser也还没有实现,因此,在AdminUsers组件下面添加上,
1
2
3
const AdminUser: React.SFC<RouteComponentProps<{ id: string }>> = props => {
return null;
};

我们用到了RouteComponentProps通过id来获取可用的属性。

  1. 然后通过id获取定义的adminUsersData中的记录,
1
2
3
4
5
6
7
8
9
10
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对象后,渲染其内容,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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. 再次运行,进入到Admin页面,点击Products… 点击Users

因此,如果需要实现内嵌路由,需要用到NavLinkLink组件,以及使用Route组件渲染这些内容。

Animated transitions

本小节将给不同的导航添加动画。我们会使用react-transition-group中的TransitionGroupCSSTransition组件,步骤如下,

  1. 首先安装对应这些包,
1
2
npm install react-transition-group
npm install @types/react-transition-group --save-dev

TransitionGroup 会持续跟踪它内部本地state的子组,并计算子组进入和退出。CSSTransition则在TransitionGroup子组离开或退出时,提供CSS类来表示一种状态。

因此,TransitionGroupCSSTransition可以包装我们的路由,并调用CSS类,

  1. 首先我们需要在我们的Routes.tsx中导入这些组件,
1
import { CSSTransition, TransitionGroup } from "react-transition-group";
  1. 我们还需要路由属性,
1
import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom";
  1. 作用我们的Route组件,
1
2
const Routes: React.SFC<RouteComponentProps> = props => {
...
  1. CSSTransitionTransitionGroup来包装我们的路由,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<TransitionGroup>
<CSSTransition
key={props.location.key}
timeout={500}
classNames="animate"
>
<Switch>
<Redirect exact={true} from="/" to="/products" />
<Route exact={true} path="/products" component={ProductsPage} />
<Route path="/products/:id" component={ProductPage} />
<Route path="/admin">
{loggedIn ? <AdminPage /> : <Redirect to="/login" />}
</Route>
<Route path="/login" component={LoginPage} />
<Route component={NotFoundPage} />
</Switch>
</CSSTransition>
</TransitionGroup>

TransitionGroup要求子组要有一个唯一的key决定退出和进入的动作。因此我们在CSSTransition上指定key属性为RouteComponentProps上的location.key。我们设置了transtion的timeout属性为半秒,以及它的样式类。

  1. 接下来,我们需要添加这些CSS类到index.css中,
1
2
3
4
5
6
7
8
9
10
11
12
13
.animate-enter {
opacity: 0;
z-index: 1;
}

.animate-enter-active {
opacity: 1;
transition: opacity 450ms ease-in;
}

.animate-exit {
display: none;
}

当它的key改变时,CSSTransition会调用这些CSS样式。

  1. 因为我们不能在Router组件外部使用高阶组件withRouter。要解决这个问题,可以添加一个新的组件RoutesWrap进行包装,在原来的Routes.tsx中添加,
1
2
3
4
5
6
7
8
9
10
11
12
13
const RoutesWrap: React.SFC = () => {
return (
<Router>
<Route component={Routes} />
</Router>
);
};

class Routes extends React.Component<RouteComponentProps,
IState> {
...
}
export default RoutesWrap;
  1. Routes组件中移除Router,让div标签作为它的根。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public render() {
return (
<div>
<Header />
<TransitionGroup>
...
</TransitionGroup>
</div>
);
}```


再次执行程序,可以看到进入不同页面是有一种渐退效果。

## Lazy loading routes

截至目前为止,所有JavaScript会在app第一次加载时全部加载。包括`Admin`这些我们不需要的页面。这一点会造成一些资源浪费。所以需要用到一种称为“lazy loading”的组件。

1. 首先我们需要导入`Suspense`这个组件,

```tsx
import { Suspense } from "react";
  1. 接着以另一种方式导入这个AdminPage组件,
1
const ADminPage = React.lazy(() => import("./AdminPage"));

我们使用了React的一个函数lazy,它接收一个函数并返回一个动态导入,换句话说就是指派了一个AdminPage组件变量。

  1. 上面步骤会获得一个lint错误,A dynamic import call in ES5/ES3 requires the ‘Promise’ constructor. Make sure you have a declaration for the ‘Promise’ constructor or include ‘ES2015’ in your --lib option. 需要在tsconfig.json中添加lib选项。
1
2
3
4
"compilerOptions": {
"lib": ["es6", "dom"],
...
}
  1. 接下来的内容是,使用Suspense组件包装这个AdminPage组件,
1
2
3
4
5
6
7
8
9
10
<Route path="/admin">
{loggedIn ? (
<Suspense fallback={<div className="pagecontainer">
Loading...</div>}>
<AdminPage />
</Suspense>
) : (
<Redirect to="/login" />
)}
</Route>

Suspense组件会在AdminPage被加载时,展示这个div部分内容。有点说明的是,组件加载非常快,可能实际浏览器看了没效果,需要进入Network选择Slow 3G网络环境模式。刷新页面便可看到出现Loading…

总结

React Router给我们一套全面的组件管理导航页。最先学习的是底层组件Router,它会查找下游的Route组件并根据路径进行渲染。

Link组件让我们在应用中链接到不同的页面。我们学习了NavLinkeLink相似,但提供了对不同激活路径的样式功能。因此NavLink最大好处用在侧边栏或标头横幅,Link用在内嵌页面跳转。

RouteComponentProps是一种泛型类型,它提供了访问路径参数的能力。我们还发现了React Router并不能解析路径参数,但可以使用本地的JavaScript的URLSearchParams实现。

Redirect组件提供了路径重定向的功能。这非常适用于对权限页面的控制。

Prompt组件提供了弹窗的能力。

我们还学习了通过使用TransitionGroupCSSTransition组件,通过包装我们的Route组件实现一种渐退的效果。

另外我们还学习了使用Suspense组件配合React的lazy函数,对不同的页面实现延迟加载,帮助我们提升应用的性能效果。