简书Header组件开发(热门搜索样式布局、Ajax获取推荐数据、代码优化)

热门搜索样式布局

热门搜索样式布局

那么我们就需要在style.js中创建以下组件

组件名 | 类型 | 含义 | 备注
—|—|—
SearchInfo | div | 整个外框 | 绝对定位
SearchInfoTitle | div | “热门搜索” |
SearchInfoSwith | span | “换一批” | 右浮动
SearchInfoList | div | 所有标签的外框 | overflow:hidden
(BFC解决浮动元素的父元素高度塌陷
SearchInfoItem | a | 各个标签 | display:block,左浮动

热门搜索框SearchInfo

1.header-style.js定义组件SearchInfo(绝对定位):

1
2
3
4
5
6
7
8
9
export const SearchInfo = styled.div`
position: absolute;
left: 0;
top: 56px;
width: 240px;
padding:0 20px;
height:100px;
box-shadow:0 0 8px rgba(0, 0, 0, .2);
`;

2.header-index.js引入、使用组件SearchInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<SearchWrapper>
<CSSTransition
in={props.focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={props.focused ? "focused" : ""}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}
/>
</CSSTransition>
<span className={props.focused ? "focused iconfont" : "iconfont"}>
&#xe62d;
</span>
<SearchInfo>

</SearchInfo>
</SearchWrapper>

效果


“热门搜索”SearchInfoTitle

1.header-style.js定义组件 SearchInfoTitle:

1
2
3
4
5
6
7
export const SearchInfoTitle = styled.div`
margin-top: 20px;
margin-bottom: 15px;
line-height: 20px;
font-size:14px;
color: #969696;
`;

2.header-index.js引入、使用组件 SearchInfoTitle:

1
2
3
<SearchInfo>
<SearchInfoTitle>热门搜索</SearchInfoTitle>
</SearchInfo>

效果


“换一批”SearchInfoSwitch

1.header-style.js定义组件 SearchInfoSwitch(右浮动):

1
2
3
4
export const SearchInfoSwitch = styled.div`
float:right;
font-size:13px;
`;

2.header-index.js引入、使用组件 SearchInfoSwitch:

1
2
3
4
5
6
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
</SearchInfo>

效果


标签 SearchInfoItem

1.header-style.js定义组件 SearchInfoItem(左浮动,块级以定义宽高):

1
2
3
4
5
6
7
8
9
10
11
12
export const SearchInfoItem = styled.a`
display: block;
float: left;
line-height: 20px;
padding: 0 5px;
margin-right: 10px;
margin-bottom: 10px;
font-size: 12px;
border: 1px solid #ddd;
color: #787878;
border-radius: 3px;
`;

2.header-index.js引入、使用组件 SearchInfoItem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoItem>理财</SearchInfoItem>
<SearchInfoItem>毕业</SearchInfoItem>
<SearchInfoItem>手帐</SearchInfoItem>
<SearchInfoItem>简书交友</SearchInfoItem>
<SearchInfoItem>spring</SearchInfoItem>
<SearchInfoItem>古风</SearchInfoItem>
<SearchInfoItem>故事</SearchInfoItem>
<SearchInfoItem>暖寄归人</SearchInfoItem>
</SearchInfo>

效果
可以看到此时标签没有撑起框,因为之前我们把外边框高度写死了


SearchInfoList 所有标签的外框

1.header-style.js中,删除SearchInfo中的死高度100px,定义组件 SearchInfoList:

1
2
3
export const SearchItemList = styled.div`
overflow: hidden;
`;

2.header-index.js引入、使用组件 SearchInfoList:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchItemList>
<SearchInfoItem>理财</SearchInfoItem>
<SearchInfoItem>毕业</SearchInfoItem>
<SearchInfoItem>手帐</SearchInfoItem>
<SearchInfoItem>简书交友</SearchInfoItem>
<SearchInfoItem>spring</SearchInfoItem>
<SearchInfoItem>古风</SearchInfoItem>
<SearchInfoItem>故事</SearchInfoItem>
<SearchInfoItem>暖寄归人</SearchInfoItem>
</SearchItemList>
</SearchInfo>

效果


实现点击显示效果

  • 实现效果:点击搜索框则显示“热门搜索框”,搜索框失去焦点时“热门搜索框”消失。
  • 实现思路:
    1. 新建一个函数getListArea(),该函数接收一个参数show,show为true时显示“热门搜索框”,为false时隐藏“热门搜索框”。
    2. show为true时,返回“热门搜索框”的所有组件
    3. show为false时,返回null(空对象)。
    4. **在SearchWrapper组件中调用函数getListArea(),参数为props.focused**。

1.header-index.js新建函数getListArea():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const getListArea = (show) => {
if (show) {
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchItemList>
<SearchInfoItem>理财</SearchInfoItem>
<SearchInfoItem>毕业</SearchInfoItem>
<SearchInfoItem>手帐</SearchInfoItem>
<SearchInfoItem>简书交友</SearchInfoItem>
<SearchInfoItem>spring</SearchInfoItem>
<SearchInfoItem>古风</SearchInfoItem>
<SearchInfoItem>故事</SearchInfoItem>
<SearchInfoItem>暖寄归人</SearchInfoItem>
</SearchItemList>
</SearchInfo>
)
} else {
return null;
}
}

2.header-index.js调用函数getListArea():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<SearchWrapper>
<CSSTransition
in={props.focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={props.focused ? "focused" : ""}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}
/>
</CSSTransition>
<span className={props.focused ? "focused iconfont" : "iconfont"}>
&#xe62d;
</span>
{getListArea(props.focused)}
</SearchWrapper>

Ajax获取推荐数据

上面我们显示的标签是写死的,现在我们要实现点击搜索框以后 通过AJAX获取数据
通过AJAX获取数据

思路:

  1. 往组件的state中增添一个**list数组用于存储标签数据**
  2. 使用redux-thunk中间件使action可以是函数
    • 中间件指的是action和store的“中间”,实际是对dispatch的一个升级
    • 原本actionCreators中返回的action都只能是对象,使用redux-thunk后可返回函数(该函数接受dispatch方法作为参数)
    • 创建store时添加中间件(dispatch是store的方法)
  3. public文件夹下新建api文件夹,新建json文件,模拟假数据
    • 注意:当js页面中向某个接口发出请求获取数据时,项目会先去node中找,找不到就会去public文件夹中找相应的路径下是否有数据
    • 所以假设接口是/api/headerList.json,那可以在public文件夹下=>新建api文件夹=>新建headerList.json,放置相应假数据
  4. AJAX请求写在函数action中,异步AJAX请求需要借助第三方模块axios
  5. reducer处理action,将AJAX获取到的数据放到state的list中
  6. 组件使用connect的参数1获取state的list,使用map()循环渲染list数组于页面上

将Header改回容器组件

目前header-index.js中Header组件是无状态组件,只能通过函数嵌套来添加函数,但接下来会添加比较多东西,所以先将Header组件转回 容器组件

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
class Header extends Component {
getListArea(show) {
if (show) {
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchItemList>
<SearchInfoItem>理财</SearchInfoItem>
<SearchInfoItem>毕业</SearchInfoItem>
<SearchInfoItem>手帐</SearchInfoItem>
<SearchInfoItem>简书交友</SearchInfoItem>
<SearchInfoItem>spring</SearchInfoItem>
<SearchInfoItem>古风</SearchInfoItem>
<SearchInfoItem>故事</SearchInfoItem>
<SearchInfoItem>暖寄归人</SearchInfoItem>
</SearchItemList>
</SearchInfo>
)
} else {
return null;
}
}

render() {
return (
<HeaderWrapper>
<Logo />
<Nav>
<NavItem className="left active">首页</NavItem>
<NavItem className="left">下载App</NavItem>
<NavItem className="right">登录</NavItem>
<NavItem className="right">
<span className="iconfont">&#xe636;</span>
</NavItem>
<SearchWrapper>
<CSSTransition
in={this.props.focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={this.props.focused ? "focused" : ""}
onFocus={this.props.handleInputFocus}
onBlur={this.props.handleInputBlur}
/>
</CSSTransition>
<span className={this.props.focused ? "focused iconfont" : "iconfont"}>
&#xe62d;
</span>
{this.getListArea(this.props.focused)}
</SearchWrapper>
</Nav>
<Addition>
<Button className="writting">
<span className="iconfont">&#xe6e5;</span>
写文章
</Button>
<Button className="reg">注册</Button>
</Addition>
</HeaderWrapper>
);
}
}
  1. 使用class和Component,将props转回this.props(可使用ctrl+d多选辅助)
  2. 将函数getListArea放入容器组件中,使用this.getListArea()进行调用
    (注意:写在组件中的函数是getListArea(){...},写在组件外的函数是const getListArea = ()=>{...})

state中增添一个list数组

headet-store-reducer.js往state中增添一个list数组用于存储标签数据:

1
2
3
4
const defaultState = fromJS({
focused: false,
list: []
});

配置redux-thunk中间件

使用redux-thunk使action中可以写函数,我们将AJAX请求写在函数action中

1.安装redux-thunk

1
yarn add redux-thunk

复习中间件:指的是action和store的“中间”,实际是对dispatch的一个升级。

2.创建store时(即src-store-index.js)添加中间件:

1
2
3
4
5
6
7
8
9
10
import { createStore, compose, applyMiddleware } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk"

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
));

export default store;

通过redux提供的applyMiddleware可将redux-thunk中引入的thunk添加进store。

public下模拟假数据

  • AJAX请求的接口下得返回数据给页面,那么返回的数据就需要我们进行模拟。
  • 注意:当js页面中向某个接口发出请求获取数据时,项目会先去node中找,找不到就会去public文件夹中找相应的路径下是否有数据
  • 所以假设接口是/api/headerList.json,那可以在public文件夹下=>新建api文件夹=>新建headerList.json,放置相应假数据
  • 注意:这种写法就不能使用mock.js去模拟假数据,因为public下的文件中获取到的内容是被直接显示的,不会进行编译。而且mockjs会帮助拦截请求,写到public下也没办法让使用AJAX的组件引入mockjs。

public文件夹下=>新建api文件夹=>新建headerList.json:

1
2
3
4
{
"sucess":true,
"data":["简书","理财","毕业","手帐","简书交友","spring","古风","故事","暖寄归人"]
}

组件中发送含有AJAX的action

当搜索框NavSearch聚焦时会触动handleInputFocus函数,所以我们将AJAX获取数据的action(即actionCreators.js中的getList())写在handleInputFocus函数中即可:

1
2
3
4
5
6
7
8
9
10
11
const mapDispatchProps = (dispatch) => {
return {
handleInputFocus() {
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
}
}
}

action中使用axios库进行AJAX请求

  • 异步请求需要借助第三方模块axios

1.安装axios库

1
yarn add axios

2.header-store-actionCreators.js中,引入axios并创建getList()用于获取AJAX数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const changeList = (data) => ({
type: constants.CHANG_LIST,
data
});

export const getList = () => {
return (dispatch) => {
axios.get("/api/headerList.json").then((res) => {
const data = res.data;
dispatch(changeList(data.data));
}).catch(() => {
console.log("error");
});
};
};

原本actionCreators中返回的action都只能是对象,使用redux-thunk后可返回函数(该函数接受dispatch方法作为参数)

3.注意:fromJS会自动将对象中的数组变成immutable类型的数组,所以在reducer中使用set()修改数组的时候要求修改的数据也是immutable类型的,否则数据类型就乱了。所以我们要在actionCreators.js中使用fromJS使data也成为immutable类型

1
import { fromJS } from "immutable";
1
2
3
4
export const changeList = (data) => ({
type: constants.CHANG_LIST,
data: fromJS(data)
});

reducer处理action中AJAX获取到的数据

action发出以后就要到reducer.js中处理这个action,将AJAX获取到的数据放到list中:

1
2
3
if (action.type === constants.CHANG_LIST) {
return state.set("list", action.data);
}

获取list数据循环显示在页面上

原本我们把标签数据都写死了,现在我们要在connect方法的参数1 mapStateToProps函数中将store的list取到,然后修改getListArea方法,循环显示list中的数据

header-index.js:
1.使用mapStateToProps函数将store的list取到:

1
2
3
4
5
6
const mapStateToProps = (state) => {
return {
focused: state.getIn(["header", "focused"]),
list: state.getIn(["header", "list"])
}
}

2.修改getListArea方法,循环显示list中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getListArea() {
if (this.props.focused) {
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchItemList>
{
this.props.list.map((item) => {
return <SearchInfoItem key={item}>{item}</SearchInfoItem>
})
}
</SearchItemList>
</SearchInfo>
)
} else {
return null;
}
}

代码优化

actionCreator.js需要导出的函数放一起

  • actionCreator中作为AJAX请求的函数changeList是不需要导出的,建议将不需要导出的函数统一放在顶部/底部
  • 明确表示哪些函数是自己用的,哪些函数是导出给别人用的。

actionCreator.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
27
import { constants } from "../store";
import axios from "axios";
import { fromJS } from "immutable";

const changeList = (data) => ({
type: constants.CHANG_LIST,
data: fromJS(data)
});

export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
});

export const searchBlur = () => ({
type: constants.SEARCH_BLUR
});

export const getList = () => {
return (dispatch) => {
axios.get("/api/headerList.json").then((res) => {
const data = res.data;
dispatch(changeList(data.data));
}).catch(() => {
console.log("error");
});
};
};

函数顶部解构赋值一次性获取props上的值

组件中很多地方会重复使用this.props.xxx来取值,那么我们可以在函数顶部使用const {xx,xxx,xxx} = this.props;来一次性获取props上的值,接下来只要使用xx即可。

header-index.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
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
import React, { Component } from 'react'
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { CSSTransition } from 'react-transition-group';
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
NavSearch,
Addition,
Button,
SearchWrapper,
SearchInfo,
SearchInfoTitle,
SearchInfoSwitch,
SearchInfoItem,
SearchItemList,
} from "./style";

class Header extends Component {
getListArea() {
const { focused, list } = this.props;
if (focused) {
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchItemList>
{
list.map((item) => {
return <SearchInfoItem key={item}>{item}</SearchInfoItem>
})
}
</SearchItemList>
</SearchInfo>
)
} else {
return null;
}
}

render() {
const { focused, handleInputFocus, handleInputBlur } = this.props;
return (
<HeaderWrapper>
<Logo />
<Nav>
<NavItem className="left active">首页</NavItem>
<NavItem className="left">下载App</NavItem>
<NavItem className="right">登录</NavItem>
<NavItem className="right">
<span className="iconfont">&#xe636;</span>
</NavItem>
<SearchWrapper>
<CSSTransition
in={focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={focused ? "focused" : ""}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
</CSSTransition>
<span className={focused ? "focused iconfont" : "iconfont"}>
&#xe62d;
</span>
{this.getListArea()}
</SearchWrapper>
</Nav>
<Addition>
<Button className="writting">
<span className="iconfont">&#xe6e5;</span>
写文章
</Button>
<Button className="reg">注册</Button>
</Addition>
</HeaderWrapper>
);
}
}

const mapStateToProps = (state) => {
return {
focused: state.getIn(["header", "focused"]),
list: state.getIn(["header", "list"])
}
}

const mapDispatchProps = (dispatch) => {
return {
handleInputFocus() {
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
}
}
}

export default connect(mapStateToProps, mapDispatchProps)(Header);

用switch替换大量if

  • 在reducer中我们使用了大量的if语句,使用switch替换会更好。
  • 一般要使用 break 来阻止代码自动地向下一个 case 运行。但由于我们使用了return,也就不需要break了。

header-store-reducer.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as constants from "./constants";
import { fromJS } from "immutable";

// immutable对象 是不可改变的
const defaultState = fromJS({
focused: false,
list: []
});

export default (state = defaultState, action) => {
switch (action.type) {
case constants.SEARCH_FOCUS:
return state.set("focused", true);
case constants.SEARCH_BLUR:
return state.set("focused", false);
case constants.CHANG_LIST:
return state.set("list", action.data);
default:
return state;
}
}

,