react基础2

react基础2(分页分类排序+路由)

这篇文章的教程

Mosh的react课程

从第5章开始

上一篇:react基础1组件与组合

下一篇:react基础3表单与后端

下下篇: react基础4授权与部署

Pagination, Filtering, and Sorting 分页过滤和排序

Introduction

我们的目标:分类+排序+分页

Pagination- Component Interface

首先我们来做分页按钮。因为分页按钮也可以重复使用所以我们把他新建到common文件夹当中

imr导入import React, { Component } from “react”;

sfc导入stateless functional component

1
2
3
4
5
6
import React, { Component } from "react";
const Pagination = (props) => {
return null;
};

export default Pagination;

显然这个Pagination是Movies的一个受控组件,全部有props传递数据,所以我们在Movies中建一个\

1
2
3
4
5
6
7
8
9
10
11
12
13
//新建一个handlePageChange接口,完成翻页操作 
handlePageChange =() =>{
console.log("page");

}
<Pagination
// 计算电影信息条数
itemsCount={count}
// 这里state中放一个每页电影数目,我写的是25
pageSize={this.state.pageSize}
// 翻页操作
onPageChange={this.handlePageChange}
/>

Pagination- Displaying Pages

利用bootstrap的文档,利用快捷键nav>ul.pagination>li.page-item>a.page-link 生成翻页标签

然后我们要动态渲染页面。我们已经知道了props中传入了总信息条数,和每页包含的信息数量,那么我们就可以通过计算的方式得出有多少页码

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
import React, { Component } from "react";
import _ from "lodash";
const Pagination = (props) => {
const { itemsCount, pageSize } = props;
//计算得出有多少页码,这个计算结果是一个浮点数
const pagesCount = itemsCount / pageSize ;
//利用lodash包中的方法,可以生成一个页码的列表
const pages = _.range(1, pagesCount + 1);
//如果当前的总信息条数小于单页信息条数,我们就不用渲染翻页按钮了
if (pagesCount <= 1) return null;
return (
<nav>
<ul className="pagination">
{/* 否则我们就渲染,利用map函数对每一个页码都生成一个翻页标签 */}
{pages.map((page) => (
<li key={page} className="page-item">
<a className="page-link">{page}</a>
</li>
))}
</ul>
</nav>
);
};

export default Pagination;

Pagination- Handling Page Changes

接下来我们来实现当点击页码的时候,console会显示该页的页码,并且让其高亮。

那么我们就需要在点击页码的时候发起一个event,然后让父组件Movies去更新当前的页面,然后再通过props重新生成,传回给Pagination组件,实现页码更新+高亮

下面就是在pagination.jsx中修改的部分,我们首先解构props

1
2
3
4
5
6
7
8
9
10
11
12
 const { itemsCount, pageSize, onPageChange, currentPage } = props;
//...
// 让当前的页码标签高亮
<li
key={page}
className={page === currentPage ? "page-item active" : "page-item"}
>
{/*再点击的时候,借助props发起一个onPageChange(page)事件,交给Movies去处理*/}
<a className="page-link" onClick={() => onPageChange(page)}>
{page}
</a>
</li>

在movies.jsx中我们也有要改的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Movies extends Component {
state = {
movies: getMovies(),
pageSize: 25,
currentPage: 1,//在这里新建一个currentPage的本地数据
};
// 新建一个handlePageChange来处理改变页码的功能,这里就是把state中currentPage改成当前page
handlePageChange = (page) => {
this.setState({ currentPage: page });
};
render() {
const { length: count } = this.state.movies;
//解构state,简化代码
const { pageSize, currentPage } = this.state;
<Pagination
itemsCount={count}
currentPage={currentPage}
pageSize={pageSize}
// 接受Pagination发起的event,交给handlePageChange处理
onPageChange={this.handlePageChange}
/>
</React.Fragment>
}

这时候点击页码按钮,就会实现高亮

Pagination- Paginating Data

接下来我们正式处理翻页,因为翻页功能可能在其他的app中也会有用,所以我们新建一个文件夹utils存放这类函数。我们在utils文件夹中 新建一个 paginate.js文件

现在我们要理解这段话的意思

slice(items,startIndex)这个方法,将从起始位置开始切割这个数组

take(pageSize) 这个方法 传入截取的信息条数

value() 就是把这个lodash wrapper转化成一个 常规的数组

然后把他return掉

1
2
3
4
5
6
7
8
import _ from "lodash";

export function paginate(items, pageNumber, pageSize) {
//这就是计算每页起始位置的编号
const startIndex = (pageNumber - 1) * pageSize;
//进行切割
return _(items).slice(startIndex).take(pageSize).value();
}

然后在movies中引入import { paginate } from “../utils/paginate”;

接下来我们就需要在render渲染器中把原来的movies覆盖掉

1
2
3
4
5
6
7
//length:count 是解构中的一个语法,就是说给movies中的length属性一个叫做count的别名
const { length: count } = this.state.movies;
//在这里我们需要给this.state.movies 一个别名,否则这里的movies会和下面的const movies冲突
const { pageSize, currentPage, movies:allmovies } = this.state;
if (count === 0) return <h1>There are no movies in the database </h1>;
//对原有的movies进行覆盖
const movies = paginate(allmovies, currentPage, pageSize);

接下来就看看成效吧

Pagination- Type Checking with PropTypes

我们如果向props中传入了invalid 的参数,那么虽然不会报错,却会产生很严重的bug

这时候我们需要引入一个外部的库,来帮我们检查是否出现了参数类型错误

1
cnpm i prop-types@15.6.2

在pagination.jsx 中引入这个包,然后做检测

1
2
3
4
5
6
7
8
9
10
11
import PropTypes from "prop-types";
//...
//注意,下面的格式必须要对,Pagination是组件名称
Pagination.PropTypes = {
//后面的参数类型是可以多种多样的:number,string,bool,array,object,func,symbol
// isRequirement 代表了 这个参数必须传入,不传入为空也会报错
itemsCount: PropTypes.number.isRequired,
pageSize: PropTypes.number.isRequired,
onPageChange: PropTypes.number.isRequired,
currentPage: PropTypes.func.isRequired,
};

当我们给itemsCount传入一个字符串的时候,就会得到上面的Warning

Exercise- ListGroup Component

接下来我们要做一个旁边的类型筛选器来是先筛选操作。

Filtering- Component Interface 组件接口

我们首先在common文件夹中新建一个listGroup.jsx

imrc,cc,新建一个ListGroup组件

我们看到上面的效果,是左面一列,右边一列,所以说在一个div当中我们要做两列,第一列放ListGroup,第二列来放我们刚才的表

那么修改Movies 的render() ,通过快捷键 div.col-2+div.col快捷键新建

1
2
<div className="col-2">{/*存ListGroup*/}</div>
<div-col>{/*存放电影的表格*/}</div-col>

然后在ListGroup中传入props

1
2
3
4
5
6
7
8
<div className="col-2">
<ListGroup
//传入 items来传入电影的分类
items={this.state.genres}
//传入 onItemSelect来处理ListGroup 发起的一个events
onItemSelect={this.handleGenreSelect}
/>
</div>

同时新建一个handleGenreSelect来处理

1
2
3
handleGenreSelect = genre =>{
console.log(genre);
};

对了,我们这里只是在本地调用一些数据,以后我们将调用数据库中的数据,那么在state中直接声明就变得不可行了,所以我们修改一下state中的movies和genres,先声明一个空数组,然后通过componentDidMount这个hook来赋值

movies.jsx:

1
2
3
4
5
6
7
8
9
10
11
state = {
movies: [],
genres: [],
pageSize: 4,
currentPage: 1,
selectedGenre:null,
};

componentDidMount() {
this.setState({ movies: getMovies(), genres: getGenres() });
}

Filtering- Displaying Items

通过快捷键 ul.list-group>li.list-group-item来新建,然后用我们的genre来动态渲染

通过解构pros简化代码,其中两个props textProperty=”name” valueProperty=”_id” 是为了降低代码的耦合度即不那么依赖Object的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from "react";
const ListGroup = (props) => {
const { items ,textProperty,valueProperty } = props;
return (
<ul className="list-group">
{items.map((item) => (
<li key={item[valueProperty]} className="list-group-item">
{item[textProperty]}
</li>
))}
</ul>
);
};
export default ListGroup;

Filtering- Default Props

这就涉及到代码简洁的问题了,当我们对一个组件的传入越少props,那么他就会好用,而我们对ListGroup组件传入了4个props,其中两个只是简单的字符串,那么我们就可以通过组件的defaultProps 进行对一些props设置默认值

我们在listGroup.jsx中加上这样一段代码

1
2
3
4
ListGroup.defaultProps = {
textProperty: "name",
valueProperty: "_id",
};

这样Movies中ListGroup标签中的那两个props就可以删除了,因为他已经在defaultProps中定义了

Filtering- Handling Selection

然后我们要处理的就是点击按钮,实现高亮,那么这应该怎么操作呢?

事实上和分页按钮是相似的。我们在Movies中实现props操作,在ListGroup中通过props判断当前的按钮是否应该被高亮

movie.jsx

1
2
3
4
5
6
7
8
9
10
11
12
//...
handleGenreSelect = (genre) => {
this.setState({ selectedGenre: genre });
};
//....
<ListGroup
items={this.state.genres}
//当点击的时候,通过handleGenreSelect来更新当前的selectedGenre属性
onItemSelect={this.handleGenreSelect}
//通过selectedItem 这个prop 将我们已经更行了的seletedGenre传回给ListGroup
selectedItem={this.state.selectedGenre}
/>

Filtering- Implementing Filtering

在movies组件的渲染器当中,我们通过 const movies = paginate(filtered, currentPage, pageSize);来进行分页操作。如果要实现分类,那么我们就需要在分页之前进行筛选才对

逻辑就是判断当前selectedGenre是否存在,如果存在,那么就筛选出_id和他相等的出来,如果不存在(我默认为null),那么我们就返回所有的电影

1
2
3
4
const filtered =
//
selectedGenre ?
allmovies.filter((m) => m.genre._id === selectedGenre._id) : allmovies;

然后我们要对filter进行分页

1
const movies = paginate(filtered, currentPage, pageSize);

最后我们交给Pagination 的信息数量也要从count变成filtered.length

<h2>Showing {count} movies in the database</h2> 中的count改成filtered.length

Filtering- Adding All Genres

我们现在要加一个All genre按钮,也就是说给genres多一个All genre对象

1
2
3
4
componentDidMount() {
const genres = [{ _id: "", name: "All genre" }, ...getGenres()];
this.setState({ movies: getMovies(), genres });
}

但是当我们再次回到All genre 标签的时候,发现并没筛选出来什么东西。这是因为我们在一开始的判定条件是 selectedGenre? 当我们点了Action之类的genre的时候,react已经把 selectedGenre从null更新称Action了,所以再次回到All genre的时候, selectedGenre其实不是null了,而任何电影信息的id都不等于空,所以就返回了0

解决这个,我们可以加一个判断条件,需要满足 selectedGenre && selectedGenre._id 不为零才进行筛选,否则就全部返回。这样这个问题就完美解决了

1
2
const filtered = selectedGenre && selectedGenre._id ?
allmovies.filter((m) => m.genre._id === selectedGenre._id): allmovies;

别高兴得太早,我们还是有bug没有解决。

我们如果翻到第二页,再点击Triller的话,并不会显示任何得电影

这是为什么呢?因为我们现在的页面其实停留在第2页,而第二页是没有得,电影信息都在第一页中。

所以我们无论何时改变分类,都应该重置页面回第一页

1
2
3
handleGenreSelect = (genre) => {
this.setState({ selectedGenre: genre, currentPage: 1 });
};

至此,我们已经完美解决了这个问题

完整代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import React, { Component } from "react";
import { getMovies } from "../services/fakeMovieService";
import Like from "./common/like";
import Pagination from "./common/pagination";
import { paginate } from "../utils/paginate";
import ListGroup from "./common/listGroup";
import { getGenres } from "../services/fakeGenreService";
import _ from "lodash";

class Movies extends Component {
state = {
movies: [],
genres: [],
pageSize: 4,
currentPage: 1,
selectedGenre: null,
};

componentDidMount() {
const genres = [{ _id: "", name: "All genre" }, ...getGenres()];
this.setState({ movies: getMovies(), genres });
}
handleDelete = (movie) => {
const movies = this.state.movies.filter((m) => m._id !== movie._id);
this.setState({ movies });
};

handleLike = (movie) => {
const movies = [...this.state.movies];
const index = movies.indexOf(movie);
movies[index] = { ...movies[index] };
movies[index].liked = !movies[index].liked;
this.setState({ movies });
};

handlePageChange = (page) => {
this.setState({ currentPage: page });
};

handleGenreSelect = (genre) => {
this.setState({ selectedGenre: genre, currentPage: 1 });
};
render() {
const { length: count } = this.state.movies;
const {
pageSize,
currentPage,
selectedGenre,
movies: allmovies,
} = this.state;
if (count === 0) return <h1>There are no movies in the database </h1>;

const filtered =
selectedGenre && selectedGenre._id
? allmovies.filter((m) => m.genre._id === selectedGenre._id)
: allmovies;

const movies = paginate(filtered, currentPage, pageSize);
return (
<div className="row">
<div className="col-3">
<ListGroup
items={this.state.genres}
selectedItem={this.state.selectedGenre}
onItemSelect={this.handleGenreSelect}
/>
</div>
<div className="col">
<h2>Showing {filtered.length} movies in the database</h2>
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Stock</th>
<th>Rate</th>
<th>Like</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{movies.map((movie) => (
<tr key={movie._id}>
<td>{movie.title}</td>
<td>{movie.genre.name}</td>
<td>{movie.numberInStock}</td>
<td>{movie.dailyRentalRate}</td>
<td>
<Like
liked={movie.liked}
onClick={() => this.handleLike(movie)}
/>
</td>
<td>
<button
onClick={() => this.handleDelete(movie)}
className="btn btn-danger btn-sm"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
itemsCount={filtered.length}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={this.handlePageChange}
/>
</div>
</div>
);
}
}

export default Movies;

listGroup.jsx

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
import React from "react";
const ListGroup = (props) => {
const {
items,
textProperty,
valueProperty,
onItemSelect,
selectedItem,
} = props;
return (
<ul className="list-group">
{items.map((item) => (
<li
key={item[valueProperty]}
className={item===selectedItem?"list-group-item active":"list-group-item "}
onClick={() => onItemSelect(item)}
>
{item[textProperty]}
</li>
))}
</ul>
);
};

ListGroup.defaultProps = {
textProperty: "name",
valueProperty: "_id",
};
export default ListGroup;

Sorting- Extracting MoviesTable 封装MoviesTable

我们之前把表格直接卸载了Movies组件当中,但其实电压表,分页器,筛选器都是属于一个层面的组件,所以我们现在来封装MoviesTable组件

在component文件夹中新建moviesTable.jsx, imr.sfc构建一个stateless functional component,然后把Movies中得table标签全部移到moviesTable中去,并且把this.handleLike ,this.handleDelete 都变成events,也就是说再点击的时候发起onLike,onDelete{在创建的时候先解构props}。最后在Movies中加入Movies Table组件,传入props处理这些事件。

1
2
3
4
5
<MoviesTable
movies={movies}
onLike={this.handleLike}
onDelete={this.handleDelete}
/>

Sorting- Raising the Sort Event

然后我们要在点击每一列的时候实现sort,那么必然要在moviesTable组件发起一个event,老样子

解构中加入onSort,每个tr标签加入onClick

1
2
3
4
5
6
7
8
9
10
class Movies extends Component {
state = {
movies: [],
genres: [],
pageSize: 10,
currentPage: 1,
selectedGenre: null,
sortColumn: { path: "range", order: "asc" },//默认
};
//...

然后在Movies的 中加入onSort = this.handleSort 的prop

接下来我们实现handleSort

Sorting- Implementing Sorting

我们想实现的是默认正序,点击反序,再点击又回到正序这样的功能

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
32
import _ from "lodash";
class Movies extends Component {
state = {
movies: [],
genres: [],
pageSize: 10,
currentPage: 1,
selectedGenre: null,
sortColumn: { path: "range", order: "asc" },
};
//...
handleSort = (path) => {
//我们首先克隆sortColumn
const sortColumn = { ...this.state.sortColumn };
//判断当前的列是否为排序列,如果是的话,再次点击就取反
if (sortColumn.path === path)
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
else {
//如果不是的话,那么就默认正序排列
sortColumn.path = path;
sortColumn.order = "asc";
}
//实现setState更新
this.setState({ sortColumn });
};

//那么我们真正用到排序的是这个地方
//这是一个lodash库,里面有orderBy函数
//第一个传进去的是筛选过的信息,第二个传入的是列,第三个参数是顺序
const sorted = _.orderBy(filtered, [sortColumn.path], [sortColumn.order]);
//重新命名我们的movies为排序过后的信息
const movies = paginate(sorted, currentPage, pageSize);

Sorting- Moving Responsibility

但这样时显示有问题的,当在其他的地方使用这个moviestable实现排序的时候,我们需要把上面的handleSort放到新的程序当中,这样就变得麻烦起来了,所以我们还是选择把moviesTable变成一个class,在class中加一个raiseSort方法,把逻辑改成点击列的时候调用这个raiseSort方法,这个raiseSort方法处理好刚才的排序逻辑的以后,向Movies发起一个events,在Movies通过onSort 接收以后调用handleSort实现更新

moviesTable.jsx

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
32
33
import React, { Component } from "react";
import Like from "./common/like";
class MoviesTable extends Component {
//raiseSort 处理排序逻辑,然后向Movies发起event
raiseSort = (path) => {
const sortColumn = { ...this.props.sortColumn };
if (sortColumn.path === path)
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
else {
sortColumn.path = path;
sortColumn.order = "asc";
}
this.props.onSort(sortColumn);
};

render() {
const { movies, onDelete, onLike } = this.props;
return (
<table className="table">
<thead>
<tr>
{/*通过点击调用类中方法*/}
<th onClick={() => this.raiseSort("title")}>Title</th>
<th onClick={() => this.raiseSort("director")}>Director</th>
<th onClick={() => this.raiseSort("actor")}>Actors</th>
<th onClick={() => this.raiseSort("country")}>Country</th>
<th onClick={() => this.raiseSort("index")}>Range</th>
<th onClick={() => this.raiseSort("score")}>Score</th>
<th onClick={() => this.raiseSort("sort")}>Sort</th>
<th onClick={() => this.raiseSort("time")}>Time</th>
<th>like</th>
</tr>
//...

然后在movie.jsx中只需要 一句简单的更新即可

1
2
3
handleSort = (sortColumn) => {
this.setState({ sortColumn });
};

Sorting- Extracting TableHeader封装表头

接下来我们要封装一个表头,也就是把这张表逐个拆解成几部分,在表头中实现排序逻辑,

表头的运行模式是

我们在common中新建一个tableHeader.jsx,tableHeader中的columns是从moviesTable给他的props中获得的,然后通过map渲染出表头来

tableHeader.jsx

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
32
33
34
35
36
37
import React, { Component } from "react";

//columns:array
//sortColumn
//onSort:function
class TableHeader extends Component {
raiseSort = (path) => {
const sortColumn = { ...this.props.sortColumn };
if (sortColumn.path === path)
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
else {
sortColumn.path = path;
sortColumn.order = "asc";
}
this.props.onSort(sortColumn);
};

render() {
return (
<thead>
<tr>
{this.props.columns.map((column) => (
<th
//每个标签都要有他的key值,那么需要排列的就取path,不需要的就取key
key={column.path || column.key}
//在点击的时候,调用raiseSort方法,raiseSort向Moviecolumn发起一个event
onClick={() => this.raiseSort(column.path)}
>
{column.label}
</th>
))}
</tr>
</thead>
);
}
}
export default TableHeader;

在moviesTable中传入columns(不是state)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 columns = [
{ path: "title", label: "Title" },
{ path: "director", label: "Director" },
{ path: "actor", label: "Actors" },
{ path: "country", label: "Country" },
{ path: "index", label: "Range" },
{ path: "sort", label: "Score" },
{ path: "time", label: "Time" },
{ key: "like" },
{ key: "Delete" },
];
//...在table中传入 <TableHeader/>,把columns,sortColumn,onSort传给表头组件
// columns在MovieTable中定义,sortColumn和onSort则在Movies传给MovieTable的props中
//等于说,表头raise一个event,表再把这个event向Movies抛,然后Movies组件来解决
<TableHeader
columns={this.columns}
sortColumn={sortColumn}
onSort={onSort}
/>

封装好后,代码变得干净很多,表的重复利用的概率也大大提升

Sorting- Extracting TableBody

如法炮制,我们要封装表体

在common文件夹中新建 tableBody.jsx

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
import React, { Component } from "react";
class TableBody extends Component {

render() {
//解构从moviestable传过来的props
const { data, columns } = this.props;
return (
//生成表体,这里的data代表了movies,然后对于每一个movie,生成一行
//在每一行中用渲染出每一列对应的数据
/*
相当于把这些渲染出来
<td>{movie.title}</td>
<td>{movie.director}</td>
<td>{movie.actor}</td>
<td>{movie.time}</td>
//.....
*/
<tbody>
{data.map((item) => (
<tr>
{columns.map(column=><td>{}</td>)}
</tr>
))}
</tbody>
);
}
}
export default TableBody;

Sorting- Rendering Cell Content

现在我们对每一个标签渲染

这里我们要用到lodash中的get方法_.get(item,column.path) 前面传入一个对象item,后面传入的目标属性是可以嵌套的,也就是说我们可以从item中提取出column.path来进行渲染

然后我们对moviesTable中的tbody进行修改,删除原来的行,并且把列和电影信息通过props传给表体

1
<TableBody data={movies} columns={this.columns} />

但这样还是不够的,我们在上面定义的时候,like和delete两列只包含了key而没有具体的操作,如果我们盲目的把\ 标签删去,那么我们将失去这两个功能,所以我们要把这两个标签移动到columns数组中去。

在这里需要理解一点,在jsx中编译的react元素实际上是JavaScript对象。比如说 const x=\ 那么这其实是一个对象,调用h1对象的jsx代码其实就是JavaScript对象。那么我们就可以对columns中的like和delete传入一个jsx表达式。实际上就是让渲染的时候,每一行都渲染出一个like对象和delete对象

那么问题又来了,我们如果直接复制到cotent后面,那么movie.liked中的movie实际上我们是没有定义的啊(本来是通过movies.map((movie) => () 渲染的。所以我们还需要修改一下

我们把content设置成一个函数,传入movie,然后导出一个like对象和一个delete对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
columns = [
{ path: "title", label: "Title" },
{ path: "director", label: "Director" },
{ path: "actor", label: "Actors" },
{ path: "country", label: "Country" },
{ path: "index", label: "Range" },
{ path: "sort", label: "Score" },
{ path: "time", label: "Time" },
{
key: "like",
content: movie=> <Like liked={movie.liked} onClick={() => this.props.onLike(movie)} />,
},
{
key: "Delete",
content: movie=>(
<button onClick={() => this.props.onDelete} className="btn btn-danger btn-sm">
Delete
</button>
),
},
];

现在我们要回到tableBody处理我们的渲染

为了代码简洁,我们在这里写一个函数renderCell,在td标签中调用renderCell。

这个函数的处理逻辑是这样,传入当前的item(电影信息),和column(当前列),然后我们看看这个列的content存在与否(我们知道只有like和delete存在)。

如果存在的话,那么就传入当前movie,让这个content渲染一个对象出来。

那么如果content不存在,我们就返回当前电影信息中名称等于column.path的属性

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
import React, { Component } from "react";
import _ from "lodash";
class TableBody extends Component {
renderCell = (item, column) => {
if (column.content) return column.content(item);
return _.get(item, column.path);
};
state = {};
render() {
const { data, columns } = this.props;
return (
<tbody>
{data.map((item) => (
<tr>
{columns.map((column) => (
<td>{this.renderCell(item, column)}</td>
))}
</tr>
))}
</tbody>
);
}
}

export default TableBody;

这就是我们的组件树了,代码的层次结构变得简单明了,这是我们始终追求的!

Sorting- Unique Keys - Final

给每个迭代的标签加上单独键值,不需要多说

Sorting- Adding the Sort Icon

加入一个箭头表示表示当前排序顺序,源码来自fontawesome(前提得npm啊!)

加在表头上 tablehead.jsx

如果当前的列不是被筛选列,那么我们就什么都不显示

如果是筛选列,那么如果是正序(asc),那就对应\</i>;;

如果是逆序,那么就对应\</i>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  renderSortIcon = (column) => {
const { sortColumn } = this.props;
if (column.path !== this.props.sortColumn.path) return null;
if (sortColumn.order === "asc") return <i class="fa fa-sort-asc"></i>;
return <i className="fa fa-sort-desc"></i>
};
render() {
//...
<th
key={column.path || column.key}
onClick={() => this.raiseSort(column.path)}
>
{column.label}
{this.renderSortIcon(column)}
</th>
//...
}

Sorting- Extracting Table 封装表格组件

现在在我们的moviesTable组件当中,我们看到现在表中的组件

里面有一个表头组件,一个表体组件

我们可以再次优化,那就是吧这个表格抽象成一个组件,包含表头和表体

这样我们在movieTable中只要加入Table组件就可以了,movieTable用来做函数处理,剩下的结构都由子组件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
const { movies, onSort, sortColumn } = this.props;
return (
<table className="table">
<TableHeader
columns={this.columns}
sortColumn={sortColumn}
onSort={onSort}
/>
<TableBody data={movies} columns={this.columns} />
</table>
);
}

那么我们在common文件夹中新建一个table.jsx,把他创建成一个可以重复使用的表格组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import TableHeader from "./tableHead";
import TableBody from "./tableBody";
import React, { Component } from "react";
const Table = (props) => {
//解构props
const { columns, sortColum, onSort, data } = props;
return (
//返回一张表所需要的部分:表头和表体
<table className="table">
<TableHeader
columns={columns}
sortColumn={sortColumn}
onSort={onSort}
/>
<TableBody data={data} columns={columns} />
</table>
);
};

export default Table;

这样我们在moviesTable的render中只要这样就可以了

1
2
3
4
5
6
7
8
9
10
11
12
render() {
const { movies, onSort, sortColumn } = this.props;
return (
//像Table组件中传入他需要的props
<Table
columns={this.columns}
data={movies}
sortColumm={sortColumn}
onSort={onSort}
/>
);
}

总结一下moviesTable组建的实现过程

这个组件定义了我们希望显示的列 columns,这个实现是针对电影的

在render中我渲染了一个Table组件,这个组件相当于一个Wrapper,他拥有这个表格所需要的所有数据

今后我如果想要渲染一个客户信息的表格,那么我需要做的只是重复利用这个表格组件,然后把我们想要渲染的列传进去就可以了

Sorting- Extracting a Method

我们现在的代码已经很简洁了,但是还是不完美在Movies组件中

因为我们写了很多行逻辑方法,filtered,sorted,movies=paginate(…) 那么我们现在把这些写成一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getPagedData = () => {
//先解构,重命名movies
const {
pageSize,
currentPage,
sortColumn,
selectedGenre,
movies: allMovies,
} = this.state;
// 里面封装我选择、排序的逻辑
//这个选择器和老师的不太一样,因为我的数据库是自己爬取下来的豆瓣Top250。
const filtered =
selectedGenre && selectedGenre._id
? allMovies.filter((movie)=>this.inSort(movie, selectedGenre))
: allMovies;

const sorted = _.orderBy(filtered, [sortColumn.path], [sortColumn.order]);
const movies = paginate(sorted, currentPage, pageSize);
// 把filtered.length当作totalCount,movies当作data,然后当成一个对象返回
return { totalCount: filtered.length, data: movies };
};

在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
25
26
27
28
29
30
31
32
33
34
35
36
37
 render() {
const { length: count } = this.state.movies;
const { pageSize, currentPage, sortColumn } = this.state;

if (count === 0) return <p>There are no movies in the database.</p>;
//用一个对象来接收getPageData 返回的数据,其中data还原成movies,为了直观
const { totalCount, data: movies } = this.getPagedData();
//把下面的filter.count 全部替换成totalCount,movies照常(因为我们把data有命名为movie)
return (
<div className="row">
<div className="col-3">
<ListGroup
items={this.state.genres}
selectedItem={this.state.selectedGenre}
onItemSelect={this.handleGenreSelect}
/>
</div>
<div className="col">
<p>Showing {totalCount} movies in the database.</p>
<MoviesTable
movies={movies}
sortColumn={sortColumn}
onLike={this.handleLike}
onDelete={this.handleDelete}
onSort={this.handleSort}
/>
<Pagination
itemsCount={totalCount}
pageSize={pageSize}
currentPage={currentPage}
onPageChange={this.handlePageChange}
/>
</div>
</div>
);
}
}

Destructuring Arguments

common文件夹下面的都是受控组件,我们没有必要再另写一行代码解构props

我们可以这么做

1
2
3
4
5
6
//直接把属性名称写在括号内,就相当于在参数props中挑出了这些属性,然后直接=>{} 即可
const Table = ({ columns, sortColumn, onSort, data }) => {
return (
//...
);
};

同样我们对其他的组件也相应改变

1
2
3
4
5
6
7
8
9
10
11
12
//...
const ListGroup = ({
items,
textProperty,
valueProperty,
selectedItem,
onItemSelect
}) => {
return (
//...
);
};

Summary

Routing

Introduction

Setup

https://www.bilibili.com/video/BV1Sb411P79t?p=91

教程

https://www.filepicker.io/api/file/zceY1kIhS66M1jfqd9Gu

下载

Adding Routing

添加插件:npm install react-router-dom@4.3.1

在index.js中做如下修改

1
2
3
4
5
6
7
8
9
10
11
12
//...
//引入BrowserRouter
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
//用这个标签涵盖App,这样App中的组件就可以用路由了
//BrewserRouter记录了浏览器的历史,并传入了各组件树的成员
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
registerServiceWorker();

然后再app.js中做如下修改

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
//引入 Route
import { Route } from "react-router-dom";
//...
class App extends Component {
render() {
return (
<div>
<NavBar />
<div className="content">
{/*
我想要根据现在的URL渲染 给定的组件。这就是我们使用路由组件的地方
接下来我们就注册一些路由,这个路由很想我们之前创建的组件,这些属性将作为props传递
path代表地址,component就是路由的组件
如果网页匹配了这个地址,那么他就会渲染出这个组件
*/}
<Route path="/products" component={Products} />
<Route path="/posts" component={Posts} />
<Route path="/admin" component={Dashboard} />
<Route path="/" component={Home} />
</div>
</div>
);
}
}

export default App;

但是我们看到了当其他组件产生的时候,Home组件始终存在,这是为什么?

Switch

router的检测算法,会检测当前的地址是否以给定的字符串开始,如果是,那么后面的组件就会被渲染。

也就是说 http://localhost:3000/products/news,这时候news,products,home会同时被渲染

要解决这方法,我们可以利用exact属性

1
<Route path="/" exact component={Home} />

h或者利用Switch组件,用Switch 标签囊括所有的Route

1
2
3
4
5
6
7
8
9
10
import { Route,Switch } from "react-router-dom";
//...
<div className="content">
<Switch>
<Route path="/products" component={Products} />
<Route path="/posts" component={Posts} />
<Route path="/admin" component={Dashboard} />
<Route path="/" component={Home} />
</Switch>
</div>

现在,问题已经解决了

现在的情况就是,当我们点击一个链接的时候,我们会把整个页面全部重载,但是这样在大型程序中这样会非常慢,所以我们需要在点击的时候只重载需要重载的部分而不是整个页面。这个叫做单一页面程序。

我们需要Link组件来达成这个功能

在navbar.js中,我们做了以下修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
//导入 Link 组件
// 把下面的 a 标签全换成 Link,把href全换成to
import { Link } from "react-router-dom";
const NavBar = () => {
return (
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/products">Products</Link>
</li>
<li>
<Link to="/posts/2018/06">Posts</Link>
</li>
<li>
<Link to="/admin">Admin</Link>
</li>
</ul>
);
};

export default NavBar;

然后,我们发现点击的时候并没有更新整个页面。因为这些页面的内容已经在一个叫做bundle的文件当中了

看到我们的Home组件,只是个简单的js函数而已。所有的组件都是bundle的一部分代码,在初始的 时候已经下载了,所以在用户更换页面的时候没有重新下载

Route Props

我们发现products有很多props,但是我们并没有传入这些props

这就是Route组件的功劳了,本质上Route组件时传入component属性组件的wrapper,所有在加载自身的props的时候,还会自动加上这三个props

history属性时浏览器的history对象,可以去往不同页面,location属性表示了现在在哪里,里面有很多属性

match则展现了我们的URL时如何匹配的,里面也有很多属性‘

通过阅读文档加以了解

https://reacttraining.com/react-router/core/guides/philosophy

Passing Props

我们怎么传递其他的props呢?

1
2
3
4
<Route
path="/products"
render={() => <Products sortBy="newest" />}
/>

我们可以改写成这样

但是这样的话那三个默认的属性就不见了。为了修正这个问题我们需要在箭头函数中加入props

1
2
3
4
5
<Route
path="/products"
render={(props) => <Products sortBy="newest" {...pros}/>}
//这里运用了特殊的jsx语法。通过spread,所有的props属性都会展开
/>

Route Parameters

有时候我们需要给路由传递参数。比如说我们选择不同的商品,会在URL上显示不同的产品id;这就是路由参数

http://localhost:3000/products/3

http://localhost:3000/products/2

http://localhost:3000/products/1

我们首先在App中定义一个新的产品详情页面/后面是可以加参数的地方,我们这里是:id。为了定义参数我们需要在参数面前加上冒号,然后这个组件属性写称ProductDetails

1
2
3
<Route path="/products/:id" component={ProductDetails}/>
//我们也可以定义多个参数
<Route path="/posts/:year/:month" component={Posts} />

productDetails.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from "react";

class ProductDetails extends Component {
handleSave = () => {
// Navigate to /products
};

render() {
return (
<div>
{/* 动态渲染当前的id */}
<h1>Product Details -{this.props.match.params.id} </h1>
<button onClick={this.handleSave}>Save</button>
</div>
);
}
}

export default ProductDetails;

Optional Parameters

点击Posts的按钮显示的连接如下

http://localhost:3000/posts/2018/06

如果只保留年,那么却显示home,这是为什么?

http://localhost:3000/posts/2018

因为我们在定义路由的参数时默认情况时必须都要满足,这里我们年月都需要有才能匹配

1
<Route path="/posts/:year/:month" component={Posts} />

那么怎么才能让月份变得可选?那么我们就需要这样写

1
<Route path="/posts/:year?/:month?" component={Posts} />

这是JavaScript中的正则表达式,如果在这之前加入一个问号,那么这个参数就是可选的

回到post我们渲染这个年份和月份

1
2
3
4
5
6
7
8
9
10
11
import React from "react";

const Posts = ({ match }) => {
return (
<div>
<h1>Posts</h1>
Year: {match.params.year}, Month:{match.params.month}
</div>
);
};
export default Posts;

当url=http://localhost:3000/posts的时候

当url=http://localhost:3000/posts/2018的时候

Query String Parameters

我们说可选参数需要尽量避免。与其在路由中使用可选参数,不如在查询字符串当中使用

比如说我传入上面一个url,在react当中我们需要读取这个字符串的意思

也就是search 属性后面的字符串,我们需要下载插件来完成

1
npm install query-string@6.1.0

然后我们在posts.jsx中做如下改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";
import queryString from 'query-string'
//解构 location,也就是上面那个props
const Posts = ({ match,location }) => {
const result= queryString.parse( location.search);
console.log(result);
return (
<div>
<h1>Posts</h1>
Year: {match.params.year}, Month:{match.params.month}
</div>
);
};

export default Posts;

打印结果如下:

Redirects 跳转

我现在向让不存在的url跳转到404 Not Found 而不是Home页面,怎么办?

首先在home这个router中加入exact修饰

1
<Route path="/" exact component={Home} />

然后从react-router-dom 中解构Redirect

1
import { Route, Switch ,Redirect} from "react-router-dom";

然后对return的内容进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (
<div>
<NavBar />
<div className="content">
<Switch>
<Route path="/products/:id" component={ProductDetails} />
<Route
path="/products"
render={(props) => <Products sortBy="newest" {...props} />}
/>
{/* 首先加入一个not-found链接*/}
<Route path="/not-found" component={NotFound} />
<Route path="/posts/:year?/:month?" component={Posts} />
<Route path="/admin" component={Dashboard} />
<Route path="/" exact component={Home} />
{/*Redirect标签代表了如果都没有匹配到,那么就去to所代表的页面*/}
<Redirect to="/not-found" />
</Switch>
</div>
</div>
);

如果有时候我想把网页上的资源从一页转移到另外一页上去,我们用 Redirect from to实现。比如说我添加

1
<Redirect from="/messages" to="/posts" />

那么我们在浏览器输入messages的时候就会自动跳转到posts页面上去

Programmatic Navigation

现在当我们点击保存按钮的时候,我们将用户转回列表页,这就是Programmatic Navigation。那么该如何操作?

这涉及到props.history中的连个属性:push 和 replace

这两个差别在于push会向浏览器添加历史地址,所以我可以通过后退按钮回到上一个页面。而replace是替换当前的页面,所以没有历史记录

如果在productsDetails.jsx中的handleSave方法中这样实现

1
2
3
4
handleSave = () => {
// Navigate to /products
this.props.history.push("/products");
};

那么在浏览器中,我们点击save按钮,回到produts目录页面。如果点击浏览器的返回,那么仍然返回到save页面。

那么如果我这样实现

1
2
3
4
handleSave = () => {
// Navigate to /products
this.props.history.replace("/products");
};

那么我们将无法返回。这就是很多登陆页面做的逻辑,我们一旦登陆后就无法回到登陆时候的页面了

Nested Routing 嵌套路由

我们想要在点击Admin的时候在弹出两个链接,一个是posts,一个是users 这就是嵌套路由了

我们先进入到dashboard.jsx,里面引入一个sidebar.jsx

sidebar.jsx代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import { Link } from "react-router-dom";
import Users from "./users";

const SideBar = () => {
return (
<ul>
<li>
<Link to="/admin/posts">Posts</Link>{/*点击Posts进行跳转*/}
</li>
<li>
<Link to="/admin/users">Users</Link>{/*点击Users进行跳转*/}
</li>
</ul>
);
};

export default SideBar;

然后呢在dashboard.jsx中引入该引入的users,posts 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
const Dashboard = ({ match }) => {
return (
<div>
<h1>Admin Dashboard</h1>
<SideBar />
{/*引入Route组件,当链接为...时,渲染...组件*/}
<Route path="/admin/posts" component={Posts} />
<Route path="/admin/users" component={Users} />
</div>
);
};

export default Dashboard;

Exercises- NavBar and Routing

达到这个效果

Adding React Router

首先在vidly文件夹下安装rrd

在index.js中 引入,包裹……

现在新建了几个组件,Customers,MovieForm,Rentals,NotFound,格式如下

1
2
3
4
5
6
import React from 'react';
const MovieForm = () => {
return <h1>MovieForm</h1>;
}

export default MovieForm

Adding Routes

在App.jsx中这样写路由

默认显示movies页面,如果未匹配,那么就跳转到notFound``

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends Component {
render() {
return (
<main className="container">
<Switch>
<Route path="/movies" component={Movies}></Route>
<Route path="/customers" component={Customers}></Route>
<Route path="/rentals" component={Rentals}></Route>
<Route path="/not-found" component={NotFound}></Route>
<Redirect from="/"exact to="movies"/>
<Redirect to="/not-found"/>
</Switch>
</main>
);
}
}

Adding the NavBar

从bootstrap上拷贝NavBar代码下来,新建一个NavBar 组件

把a全换成NavLink,除了第一个NavBar之外,这样标签就会自动调整样式,把href都换成 to,然后把地址,名称都渲染上去

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from "react";
import { Link, NavLink } from "react-router-dom";
import Movies from "./movies";
import Customers from "./customers";
import Rentals from "./rentals";
const NavBar = () => {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<Link className="navbar-brand" to="#">
Navbar
</Link>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item ">
<NavLink className="nav-link" to="/movies">
Movies <span className="sr-only">(current)</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/customers">
Customers
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/rentals">
Rentals
</NavLink>
</li>
</ul>
</div>
</nav>
);
};

export default NavBar;

Linking to the MovieForm

连接到MovieForm,也就是给表格内容添加链接。

首先在MoviesTable的列中添加content内容,也就是说在渲染的时候,tableBody会渲染出一个Link组件。to属性就是我们渲染的文本模板,我们用它来动态添加字符

1
2
3
4
5
6
7
8
9
10
11
// import{ Link }from "react-router-dom";
columns = [
{
path: "title",
label: "Title",
content: (movie) => (
<Link to={`/movies/${movie._id}`}>{movie.title}</Link>
),
},
//...
];

现在只完成了第一步,就是当我们点击电影的时候,网页链接后面会显示该电影id

http://localhost:3000/movies/5b21ca3eeb7f6fbccd47181a

现在我们需要在App.js中加一条Route

1
<Route path="/movies/:id" component={MovieForm}></Route>

最后我们需要在MovieForm中动态渲染出当前电影的id,利用match属性中的id来渲染当前的电影id,再添加一个回退按钮,利用history.push 中记录着的历史记录来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import { saveMovie } from "./../services/fakeMovieService";
const MovieForm = ({ match, history }) => {
return (
<div>
<h1>MovieForm {match.params.id}</h1>
<button
className="btn btn-primarty"
onClick={() => history.push("/movies")}
>
saveMovie
</button>
</div>
);
};

export default MovieForm;

现在我把自己做的豆瓣表格也通过这种方式加入到了NavBar当中去了

Summary

-------------本文结束,感谢您的阅读-------------