(08)Header 组件开发——⑧ 热门搜索“换一换”功能实现 | React.js 项目实战:PC 端“简书”开发
转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 需求
紧接上一篇的代码,在“简书”官网效果中,“热门搜索”区域的“关键字”(AJAX 获取到的“数据”)每页只显示 10 项:
截止目前,我们项目的效果是将所有的“数据”全部都循环出来了,这样肯定是不行的。
2 让每一页仅放置 10 个“关键字”
1️⃣打开 header 目录下 store 中的 reducder.js
文件,我们增加两个初始的“数据项” page: 1
、 totalPage: 1
:
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST} from "./actionTypes";
import {fromJS} from "immutable";
const defaultState = fromJS({ /*❗️*/
refresh: false,
list: [],
// ❗️增加两个“数据项”~
page: 1,
totalPage: 1
})
export default (state=defaultState, action) => {
if(action.type === CHANGE_CLASS_NAME) {
return state.set("refresh", true);
}
if(action.type === RESUME_CLASS_NAME) {
return state.set("refresh", false);
}
if(action.type === CHANGE_LIST) {
return state.set("list", action.data)
}
return state;
}
2️⃣然后,当“聚焦 onfocus” SearchInput
样式组件时,会去获取 AJAX 数据。“数据”获取到后,会执行 changeListAction
这个 action。
2️⃣-①:我们打开 header 目录下 store 中的 actionCreators.js
文件;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST} from "./actionTypes";
import axios from "axios";
import {fromJS} from "immutable";
export const changeClassNameAction = () => ({
type: CHANGE_CLASS_NAME
})
export const resumeClassNameAction = () => ({
type: RESUME_CLASS_NAME
})
const changeListAction = (data) => ({
type: CHANGE_LIST,
data: fromJS(data),
// ❗️❗️❗️2️⃣-②:我们同时返回一个“总页码”出去;
totalPage: Math.ceil(data.length / 10) // ❗️注意我们是怎样得出这个“总页码”的!
})
export const initLabelAction = () => {
return(dispatch) => {
axios.get("/api/headerList.json")
.then((res) => {
const data = res.data
const action = changeListAction(data.data);
dispatch(action)
})
.catch(() => {alert("error")})
}
}
3️⃣接着,action 会传递给 reducer。
3️⃣-①:打开 header 目录下 store 中的 reducer.js
文件;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST} from "./actionTypes";
import {fromJS} from "immutable";
const defaultState = fromJS({
refresh: false,
list: [],
page: 1,
totalPage: 1
})
export default (state=defaultState, action) => {
if(action.type === CHANGE_CLASS_NAME) {
return state.set("refresh", true);
}
if(action.type === RESUME_CLASS_NAME) {
return state.set("refresh", false);
}
if(action.type === CHANGE_LIST) {
/*
❗️3️⃣-②:返回给 store 的数据还应包括修改后的“总页码”~
return state.set("list", action.data)
*/
return state.set("list", action.data).set("totalPage", action.totalPage)
}
return state;
}
4️⃣接下来,Header 组件就可以去获取到这两个“数据项”了。
4️⃣-①:打开 header 目录下的 index.js
文件;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
import {actionCreators} from "./store";
class Header extends Component {
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput
onFocus={() => this.props.handleInputFocus(this.props.list)}
/>
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
>
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
{
this.props.list.map((item) => {
return <LabelLink key={item} href="/">{item}</LabelLink>
})
}
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.getIn(["header", "refresh"]),
list: state.getIn(["header", "list"]),
// ❗️❗️❗️4️⃣-②:获取到新增的那两个“数据项”;
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction();
dispatch(action)
},
handleInputFocus(list) {
if(list.size === 0) {
const action = actionCreators.initLabelAction();
dispatch(action)
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
5️⃣组件获取到数据后,我们就可以来利用这些“数据”进行“需求”的实现了——让每一页仅放置 10 个“关键字”。
5️⃣-①:返回 header 目录下的 index.js
中;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
import {actionCreators} from "./store";
class Header extends Component {
// 5️⃣-③:在 render 函数的上边写 getPanels 方法;
getPanels() {
const newList = this.props.list.toJS(); /*
5️⃣-⑦:由于 list 是“immutable 对象”,
“immutable 对象”是没办法通过 list[] 的形式
获取到值的。故,我们要将“immutable 对象”转化
为“JS 对象”;
*/
const pageLabels = []; // 5️⃣-⑤:新建一个空数组;
// 5️⃣-④:首先,我们可以通过 page 来获取到每一页内容的“索引”,然后进行 for 循环;
for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) {
pageLabels.push( // 5️⃣-⑨:将循环出的每一项都增加到数组 pageLabels 中;
// 5️⃣-⑥:循环将 list 对应“索引 i”的内容渲染到“样式组件 LabelLink”中;
<LabelLink key={newList[i]} href="/">
{newList[i]} {/* 5️⃣-⑧:“JS 对象”就可以通过 newList[i] 取到具体索引的值了; */}
</LabelLink>
)
}
return pageLabels; // ❗️❗️❗️5️⃣-⑩:请一定记得让函数返回一个值!
}
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput
onFocus={() => this.props.handleInputFocus(this.props.list)}
/>
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
>
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
{/* ❗️5️⃣-②:由于这里的逻辑代码很多,所以我们专门写成一个函数来调用; */}
{this.getPanels()}
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.getIn(["header", "refresh"]),
list: state.getIn(["header", "list"]),
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction();
dispatch(action)
},
handleInputFocus(list) {
if(list.size === 0) {
const action = actionCreators.initLabelAction();
dispatch(action)
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
返回页面查看效果:
3 点击“换一换”切换页面
6️⃣给 PanelChange
样式组件增加一个 onclick 事件。
6️⃣-①:打开 header 目录下的 index.js
文件;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
import {actionCreators} from "./store";
class Header extends Component {
getPanels() {
const newList = this.props.list.toJS();
const pageLabels = [];
for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) {
pageLabels.push(
<LabelLink key={newList[i]} href="/">
{newList[i]}
</LabelLink>
)
}
return pageLabels;
}
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput
onFocus={() => this.props.handleInputFocus(this.props.list)}
/>
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
> {/*
❗️❗️❗️6️⃣-②:绑定一个 onclick 事件。并用“箭头函数”的形式传递出“
当前 page”和“totalPage”;
*/}
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
{this.getPanels()}
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.getIn(["header", "refresh"]),
list: state.getIn(["header", "list"]),
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction();
dispatch(action)
},
handleInputFocus(list) {
if(list.size === 0) {
const action = actionCreators.initLabelAction();
dispatch(action)
}
},
// 6️⃣-③:当用户“点击”时,向 store 发送相应满足条件的 action;
handleChangePage(page, totalPage) {
if(page < totalPage) {
dispatch(actionCreators.changePageAction(page + 1))
}else {
dispatch(actionCreators.changePageAction(1))
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
7️⃣到 header 目录下 store 中的 actionTypes.js
文件中定义“方法”的“类型”。
7️⃣-①:打开 header 目录下 store 中的 actionTypes.js
文件;
export const CHANGE_CLASS_NAME = "change_class_name";
export const RESUME_CLASS_NAME ="resume_class_name";
export const CHANGE_LIST="change_list";
export const CHANGE_PAGE="change_page"; // ❗️定义好常量!
7️⃣-②:打开 header 目录下 store 中的 actionCreators.js
文件,定义 action;
// 7️⃣-③:引入“常量”;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST, CHANGE_PAGE} from "./actionTypes";
import axios from "axios";
import {fromJS} from "immutable";
export const changeClassNameAction = () => ({
type: CHANGE_CLASS_NAME
})
export const resumeClassNameAction = () => ({
type: RESUME_CLASS_NAME
})
const changeListAction = (data) => ({
type: CHANGE_LIST,
data: fromJS(data),
totalPage: Math.ceil(data.length / 10)
})
export const initLabelAction = () => {
return(dispatch) => {
axios.get("/api/headerList.json")
.then((res) => {
const data = res.data
const action = changeListAction(data.data);
dispatch(action)
})
.catch(() => {alert("error")})
}
}
// 7️⃣-④:定义 action;
export const changePageAction = (page) => ({
type: CHANGE_PAGE,
page: page
})
7️⃣-⑤:打开 header 目录下 store 中的 reducer.js
文件,传递 action;
// 7️⃣-⑥:引入“常量”;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME, CHANGE_LIST, CHANGE_PAGE} from "./actionTypes";
import {fromJS} from "immutable";
const defaultState = fromJS({
refresh: false,
list: [],
page: 1,
totalPage: 1
})
export default (state=defaultState, action) => {
if(action.type === CHANGE_CLASS_NAME) {
return state.set("refresh", true);
}
if(action.type === RESUME_CLASS_NAME) {
return state.set("refresh", false);
}
if(action.type === CHANGE_LIST) {
return state.set("list", action.data).set("totalPage", action.totalPage)
}
// 7️⃣-⑦:编写返回的“数据”;
if(action.type === CHANGE_PAGE) {
return state.set("page", action.page);
}
return state;
}
返回页面查看效果(除了 key 值报警告外,一切正常):
4 解决 key 值报警告的 bug
以上视频效果中,我们清晰地发现,当页面刷新时,key 值就报警告了(即,一开始就报了警告)。
8️⃣回到 header 目录下的 index.js
文件中:
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
import {actionCreators} from "./store";
class Header extends Component {
getPanels() {
const newList = this.props.list.toJS();
const pageLabels = [];
for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) {
// 8️⃣-①:❗️我们在控制台打印一下这里的 newList[i] 是什么东西;
console.log(newList[i])
pageLabels.push(
<LabelLink key={newList[i]} href="/">
{newList[i]}
</LabelLink>
)
}
return pageLabels;
}
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput
onFocus={() => this.props.handleInputFocus(this.props.list)}
/>
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
>
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
{this.getPanels()}
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.getIn(["header", "refresh"]),
list: state.getIn(["header", "list"]),
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction();
dispatch(action)
},
handleInputFocus(list) {
if(list.size === 0) {
const action = actionCreators.initLabelAction();
dispatch(action)
}
},
handleChangePage(page, totalPage) {
if(page < totalPage) {
dispatch(actionCreators.changePageAction(page + 1))
}else {
dispatch(actionCreators.changePageAction(1))
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
返回控制台查看:
发现没有?当我们还没请求 AJAX 数据时,for 循环里边的代码也是执行了的。而循环一旦执行,里边也就有了 key 值(即使 key={newList[i]}
为 undefined
),key 值都为 undefined
肯定就会报警告。
❓怎么解这个 bug 呢?
很简单,我们规定只有当 newList
有“数据”后,才执行 for 循环即可!
8️⃣-②:返回 header 目录下的 index.js
文件中;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
import {actionCreators} from "./store";
class Header extends Component {
getPanels() {
const newList = this.props.list.toJS();
const pageLabels = [];
if(newList.length) { /*
❗️❗️❗️8️⃣-③:设置一个“条件”,只有 newList 中有“数据”时(
即,AJAX 获取到数据后)才执行 for 循环!
*/
for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) {
pageLabels.push(
<LabelLink key={newList[i]} href="/">
{newList[i]}
</LabelLink>
)
}
return pageLabels;
}
}
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput
onFocus={() => this.props.handleInputFocus(this.props.list)}
/>
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
>
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
{this.getPanels()}
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.getIn(["header", "refresh"]),
list: state.getIn(["header", "list"]),
page: state.getIn(["header", "page"]),
totalPage: state.getIn(["header", "totalPage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction();
dispatch(action)
},
handleInputFocus(list) {
if(list.size === 0) {
const action = actionCreators.initLabelAction();
dispatch(action)
}
},
handleChangePage(page, totalPage) {
if(page < totalPage) {
dispatch(actionCreators.changePageAction(page + 1))
}else {
dispatch(actionCreators.changePageAction(1))
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
返回页面查看效果(一切正常显示):
OK,随着本篇结束,我们的 Header 组件就成功地完成了,讲的比较细,文字也比较多,但都是重点!彻底把 Header 组件掌握后,后边的组件都是一样的套路,没有太多难度。
当然,代码还有一些优化的小地方,但不多(比如,可以用 ES6 的结构赋值减少很多 this.props.xxx
的书写)。我为了讲解清楚思路,故都写的比较细。大伙可以自行稍微优化一下,我们之前的文章都讲过。
下一篇,我们开始“首页”的开发,大家一定理清思路,多敲几次代码!
祝好,qdywxs ♥ you!
转载自:https://juejin.cn/post/7345076635609448474