react基础2(分页分类排序+路由)
这篇文章的教程
从第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 | import React, { Component } from "react"; |
显然这个Pagination是Movies的一个受控组件,全部有props传递数据,所以我们在Movies中建一个\
1 | //新建一个handlePageChange接口,完成翻页操作 |
Pagination- Displaying Pages
利用bootstrap的文档,利用快捷键nav>ul.pagination>li.page-item>a.page-link 生成翻页标签
然后我们要动态渲染页面。我们已经知道了props中传入了总信息条数,和每页包含的信息数量,那么我们就可以通过计算的方式得出有多少页码
1 | import React, { Component } from "react"; |
Pagination- Handling Page Changes
接下来我们来实现当点击页码的时候,console会显示该页的页码,并且让其高亮。
那么我们就需要在点击页码的时候发起一个event,然后让父组件Movies去更新当前的页面,然后再通过props重新生成,传回给Pagination组件,实现页码更新+高亮
下面就是在pagination.jsx中修改的部分,我们首先解构props
1 | const { itemsCount, pageSize, onPageChange, currentPage } = props; |
在movies.jsx中我们也有要改的
1 | class Movies extends Component { |
这时候点击页码按钮,就会实现高亮
Pagination- Paginating Data
接下来我们正式处理翻页,因为翻页功能可能在其他的app中也会有用,所以我们新建一个文件夹utils存放这类函数。我们在utils文件夹中 新建一个 paginate.js文件
现在我们要理解这段话的意思
slice(items,startIndex)这个方法,将从起始位置开始切割这个数组
take(pageSize) 这个方法 传入截取的信息条数
value() 就是把这个lodash wrapper转化成一个 常规的数组
然后把他return掉
1 | import _ from "lodash"; |
然后在movies中引入import { paginate } from “../utils/paginate”;
接下来我们就需要在render渲染器中把原来的movies覆盖掉
1 | //length:count 是解构中的一个语法,就是说给movies中的length属性一个叫做count的别名 |
接下来就看看成效吧
Pagination- Type Checking with PropTypes
我们如果向props中传入了invalid 的参数,那么虽然不会报错,却会产生很严重的bug
这时候我们需要引入一个外部的库,来帮我们检查是否出现了参数类型错误
1 | cnpm i prop-types@15.6.2 |
在pagination.jsx 中引入这个包,然后做检测
1 | import PropTypes from "prop-types"; |
当我们给itemsCount传入一个字符串的时候,就会得到上面的Warning
Exercise- ListGroup Component
接下来我们要做一个旁边的类型筛选器来是先筛选操作。
Filtering- Component Interface 组件接口
我们首先在common文件夹中新建一个listGroup.jsx
imrc,cc,新建一个ListGroup组件
我们看到上面的效果,是左面一列,右边一列,所以说在一个div当中我们要做两列,第一列放ListGroup,第二列来放我们刚才的表
那么修改Movies 的render() ,通过快捷键 div.col-2+div.col快捷键新建
1 | <div className="col-2">{/*存ListGroup*/}</div> |
然后在ListGroup中传入props
1 | <div className="col-2"> |
同时新建一个handleGenreSelect来处理
1 | handleGenreSelect = genre =>{ |
对了,我们这里只是在本地调用一些数据,以后我们将调用数据库中的数据,那么在state中直接声明就变得不可行了,所以我们修改一下state中的movies和genres,先声明一个空数组,然后通过componentDidMount这个hook来赋值
movies.jsx:
1 | state = { |
Filtering- Displaying Items
通过快捷键 ul.list-group>li.list-group-item来新建,然后用我们的genre来动态渲染
通过解构pros简化代码,其中两个props textProperty=”name” valueProperty=”_id” 是为了降低代码的耦合度即不那么依赖Object的属性。
1 | import React, { Component } from "react"; |
Filtering- Default Props
这就涉及到代码简洁的问题了,当我们对一个组件的传入越少props,那么他就会好用,而我们对ListGroup组件传入了4个props,其中两个只是简单的字符串,那么我们就可以通过组件的defaultProps 进行对一些props设置默认值
我们在listGroup.jsx中加上这样一段代码
1 | ListGroup.defaultProps = { |
这样Movies中ListGroup标签中的那两个props就可以删除了,因为他已经在defaultProps中定义了
Filtering- Handling Selection
然后我们要处理的就是点击按钮,实现高亮,那么这应该怎么操作呢?
事实上和分页按钮是相似的。我们在Movies中实现props操作,在ListGroup中通过props判断当前的按钮是否应该被高亮
movie.jsx
1 | //... |
Filtering- Implementing Filtering
在movies组件的渲染器当中,我们通过 const movies = paginate(filtered, currentPage, pageSize);来进行分页操作。如果要实现分类,那么我们就需要在分页之前进行筛选才对
逻辑就是判断当前selectedGenre是否存在,如果存在,那么就筛选出_id和他相等的出来,如果不存在(我默认为null),那么我们就返回所有的电影
1 | const filtered = |
然后我们要对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 | componentDidMount() { |
但是当我们再次回到All genre 标签的时候,发现并没筛选出来什么东西。这是因为我们在一开始的判定条件是 selectedGenre? 当我们点了Action之类的genre的时候,react已经把 selectedGenre从null更新称Action了,所以再次回到All genre的时候, selectedGenre其实不是null了,而任何电影信息的id都不等于空,所以就返回了0
解决这个,我们可以加一个判断条件,需要满足 selectedGenre && selectedGenre._id 不为零才进行筛选,否则就全部返回。这样这个问题就完美解决了
1 | const filtered = selectedGenre && selectedGenre._id ? |
别高兴得太早,我们还是有bug没有解决。
我们如果翻到第二页,再点击Triller的话,并不会显示任何得电影
这是为什么呢?因为我们现在的页面其实停留在第2页,而第二页是没有得,电影信息都在第一页中。
所以我们无论何时改变分类,都应该重置页面回第一页
1 | handleGenreSelect = (genre) => { |
至此,我们已经完美解决了这个问题
完整代码如下:
1 | import React, { Component } from "react"; |
listGroup.jsx
1 | import React from "react"; |
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 | <MoviesTable |
Sorting- Raising the Sort Event
然后我们要在点击每一列的时候实现sort,那么必然要在moviesTable组件发起一个event,老样子
解构中加入onSort,每个tr标签加入onClick
1 | class Movies extends Component { |
然后在Movies的
接下来我们实现handleSort
Sorting- Implementing Sorting
我们想实现的是默认正序,点击反序,再点击又回到正序这样的功能
1 | import _ from "lodash"; |
Sorting- Moving Responsibility
但这样时显示有问题的,当在其他的地方使用这个moviestable实现排序的时候,我们需要把上面的handleSort放到新的程序当中,这样就变得麻烦起来了,所以我们还是选择把moviesTable变成一个class,在class中加一个raiseSort方法,把逻辑改成点击列的时候调用这个raiseSort方法,这个raiseSort方法处理好刚才的排序逻辑的以后,向Movies发起一个events,在Movies通过onSort 接收以后调用handleSort实现更新
moviesTable.jsx
1 | import React, { Component } from "react"; |
然后在movie.jsx中只需要 一句简单的更新即可
1 | handleSort = (sortColumn) => { |
Sorting- Extracting TableHeader封装表头
接下来我们要封装一个表头,也就是把这张表逐个拆解成几部分,在表头中实现排序逻辑,
表头的运行模式是
我们在common中新建一个tableHeader.jsx,tableHeader中的columns是从moviesTable给他的props中获得的,然后通过map渲染出表头来
tableHeader.jsx
1 | import React, { Component } from "react"; |
在moviesTable中传入columns(不是state)
1 | columns = [ |
封装好后,代码变得干净很多,表的重复利用的概率也大大提升
Sorting- Extracting TableBody
如法炮制,我们要封装表体
在common文件夹中新建 tableBody.jsx
1 | import React, { Component } from "react"; |
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而没有具体的操作,如果我们盲目的把\
在这里需要理解一点,在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 | columns = [ |
现在我们要回到tableBody处理我们的渲染
为了代码简洁,我们在这里写一个函数renderCell,在td标签中调用renderCell。
这个函数的处理逻辑是这样,传入当前的item(电影信息),和column(当前列),然后我们看看这个列的content存在与否(我们知道只有like和delete存在)。
如果存在的话,那么就传入当前movie,让这个content渲染一个对象出来。
那么如果content不存在,我们就返回当前电影信息中名称等于column.path的属性
1 | import React, { Component } from "react"; |
这就是我们的组件树了,代码的层次结构变得简单明了,这是我们始终追求的!
Sorting- Unique Keys - Final
给每个迭代的标签加上单独键值,不需要多说
Sorting- Adding the Sort Icon
加入一个箭头表示表示当前排序顺序,源码来自fontawesome(前提得npm啊!)
加在表头上 tablehead.jsx
如果当前的列不是被筛选列,那么我们就什么都不显示
如果是筛选列,那么如果是正序(asc),那就对应\</i>;;
如果是逆序,那么就对应\</i>
1 | renderSortIcon = (column) => { |
Sorting- Extracting Table 封装表格组件
现在在我们的moviesTable组件当中,我们看到现在表中的组件
里面有一个表头组件,一个表体组件
我们可以再次优化,那就是吧这个表格抽象成一个组件,包含表头和表体
这样我们在movieTable中只要加入Table组件就可以了,movieTable用来做函数处理,剩下的结构都由子组件实现
1 | render() { |
那么我们在common文件夹中新建一个table.jsx,把他创建成一个可以重复使用的表格组件
1 | import TableHeader from "./tableHead"; |
这样我们在moviesTable的render中只要这样就可以了
1 | render() { |
总结一下moviesTable组建的实现过程
这个组件定义了我们希望显示的列 columns,这个实现是针对电影的
在render中我渲染了一个Table组件,这个组件相当于一个Wrapper,他拥有这个表格所需要的所有数据
今后我如果想要渲染一个客户信息的表格,那么我需要做的只是重复利用这个表格组件,然后把我们想要渲染的列传进去就可以了
Sorting- Extracting a Method
我们现在的代码已经很简洁了,但是还是不完美在Movies组件中
因为我们写了很多行逻辑方法,filtered,sorted,movies=paginate(…) 那么我们现在把这些写成一个方法
1 | getPagedData = () => { |
在render中
1 | render() { |
Destructuring Arguments
common文件夹下面的都是受控组件,我们没有必要再另写一行代码解构props
我们可以这么做
1 | //直接把属性名称写在括号内,就相当于在参数props中挑出了这些属性,然后直接=>{} 即可 |
同样我们对其他的组件也相应改变
1 | //... |
…
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 | //... |
然后再app.js中做如下修改
1 | //引入 Route |
但是我们看到了当其他组件产生的时候,Home组件始终存在,这是为什么?
Switch
router的检测算法,会检测当前的地址是否以给定的字符串开始,如果是,那么后面的组件就会被渲染。
也就是说 http://localhost:3000/products/news,这时候news,products,home会同时被渲染
要解决这方法,我们可以利用exact属性
1 | <Route path="/" exact component={Home} /> |
h或者利用Switch组件,用Switch 标签囊括所有的Route
1 | import { Route,Switch } from "react-router-dom"; |
现在,问题已经解决了
Link
现在的情况就是,当我们点击一个链接的时候,我们会把整个页面全部重载,但是这样在大型程序中这样会非常慢,所以我们需要在点击的时候只重载需要重载的部分而不是整个页面。这个叫做单一页面程序。
我们需要Link组件来达成这个功能
在navbar.js中,我们做了以下修改
1 | import React from "react"; |
然后,我们发现点击的时候并没有更新整个页面。因为这些页面的内容已经在一个叫做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 | <Route |
我们可以改写成这样
但是这样的话那三个默认的属性就不见了。为了修正这个问题我们需要在箭头函数中加入props
1 | <Route |
Route Parameters
有时候我们需要给路由传递参数。比如说我们选择不同的商品,会在URL上显示不同的产品id;这就是路由参数
http://localhost:3000/products/3
http://localhost:3000/products/2
http://localhost:3000/products/1
我们首先在App中定义一个新的产品详情页面/后面是可以加参数的地方,我们这里是:id。为了定义参数我们需要在参数面前加上冒号,然后这个组件属性写称ProductDetails
1 | <Route path="/products/:id" component={ProductDetails}/> |
productDetails.jsx
1 | import React, { Component } from "react"; |
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 | import React from "react"; |
当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 | import React from "react"; |
打印结果如下:
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 | return ( |
如果有时候我想把网页上的资源从一页转移到另外一页上去,我们用 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 | handleSave = () => { |
那么在浏览器中,我们点击save按钮,回到produts目录页面。如果点击浏览器的返回,那么仍然返回到save页面。
那么如果我这样实现
1 | handleSave = () => { |
那么我们将无法返回。这就是很多登陆页面做的逻辑,我们一旦登陆后就无法回到登陆时候的页面了
Nested Routing 嵌套路由
我们想要在点击Admin的时候在弹出两个链接,一个是posts,一个是users 这就是嵌套路由了
我们先进入到dashboard.jsx,里面引入一个sidebar.jsx
sidebar.jsx代码如下
1 | import React from "react"; |
然后呢在dashboard.jsx中引入该引入的users,posts 文件
1 | // ... |
Exercises- NavBar and Routing
达到这个效果
Adding React Router
首先在vidly文件夹下安装rrd
在index.js中 引入,包裹……
现在新建了几个组件,Customers,MovieForm,Rentals,NotFound,格式如下
1 | import React from 'react'; |
Adding Routes
在App.jsx中这样写路由
默认显示movies页面,如果未匹配,那么就跳转到notFound``
1 | class App extends Component { |
Adding the NavBar
从bootstrap上拷贝NavBar代码下来,新建一个NavBar 组件
把a全换成NavLink,除了第一个NavBar之外,这样标签就会自动调整样式,把href都换成 to,然后把地址,名称都渲染上去
1 | import React from "react"; |
Linking to the MovieForm
连接到MovieForm,也就是给表格内容添加链接。
首先在MoviesTable的列中添加content内容,也就是说在渲染的时候,tableBody会渲染出一个Link组件。to属性就是我们渲染的文本模板,我们用它来动态添加字符
1 | // import{ Link }from "react-router-dom"; |
现在只完成了第一步,就是当我们点击电影的时候,网页链接后面会显示该电影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 | import React from "react"; |
现在我把自己做的豆瓣表格也通过这种方式加入到了NavBar当中去了