1 项目目录搭建
安装create-react-app,并创建项目jianshu。删掉src目录下文件,只留下index.js(入口文件), index.css, App.js文件
当你在一个js文件中引入css文件时,css不仅在该js文件中有效,在该js所包含的组件.js中也有效。这也就会导致,如果引入css文件的js文件中包含多个组件,所有组件的css样式都会写在这个css文件中,那么这个css文件代码就会很多,并且容易冲突。(我们是希望写css样式时,每个样式是独立的)
所以可以采用styled-components来避免以上问题
styled-components
这是一款样式管理的组件
安装方式:yarn add styled-components
https://www.worldlink.com.cn/osdir/styled-components.html
英文文档:https://styled-components.com/docs/api#primary
中文文档:https://github.com/hengg/styled-components-docs-zh/blob/master/Basics.md
安装后之后就可以吧css文件改成js文件格式(把css样式放入js文件中),即改为style.js文件,在其他js文件引入css样式也自然成了引入放css的那个js文件(这里是style.js)。再去改style.js文件中的内容
为了让默认样式在所有浏览器上做到统一,我们需要使用一个reset.css的文件,也就是将reset.css文件中的代码复制到src下的style.js中
Reset.css
如:http://www.jq22.com/webqd6168
网站百度即可~~~重置浏览器标签的样式表,因为浏览器的品种很多,每个浏览器的默认样式也是不同的,比如button标签,在IE浏览器、Firefox浏览器以及Safari浏览器中的样式都是不同的,所以,通过重置button标签的CSS属性,然后再将它统一定义,就可以产生相同的显示效果。
src/style.js
import { createGlobalStyle } from 'styled-components';
const GlobalStyleOne = createGlobalStyle`
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
`;
export { GlobalStyleOne };
src/index.js
import React,{ Fragment } from 'react';
import ReactDOM from 'react-dom';
import { GlobalStyle } from './style'
import App from './App';
ReactDOM.render(
<Fragment>
<GlobalStyle/>
<App />
</Fragment>,
document.getElementById('root')
);
2 简书头部区块的编写
首先,在src目录下创建一个common的文件夹(用于存放公用的区块,比如简书的头部区块)。在common文件夹下创建一个header文件夹,存放头部的代码,header文件夹下建立header.js来存放头部组件
src/common/header/index.js
import React, {Component} from 'react';
import { HeaderWrapper,
Logo,
Nav,
NavItem,
Navsearch,
Addition,
Button,
Navbeta
} from './style'
class Header extends Component {
render() {
return (
<HeaderWrapper>
<Logo />
<Nav>
<NavItem className='left active'>首页</NavItem>
<NavItem className='left'>下载APP</NavItem>
<Navbeta />
<NavItem className='right'>登录</NavItem>
<NavItem className='right distance'>Aa</NavItem>
<Navsearch></Navsearch>
</Nav>
<Addition>
<Button className='writting'>写文章</Button>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
)
}
}
export default Header;
src/App.js中引入Header组件
import React, { Component } from 'react';
import Header from './common/header/index';
class App extends Component {
render(){
return (
<Header />
);
}
}
export default App;
src/common/header/style.js
import styled from 'styled-components';
import logoPic from '../../statics/logo.png';
import betaPic from '../../statics/beta.png';
//注意不同样式之间的分号
//别忘记结尾的分号
//创建了Div标签,该标签上有所定义样式
export const HeaderWrapper = styled.div`
position: relative;
height:56px;
border-bottom: 1px solid #f0f0f0;
`;
//设置跳转到首页
export const Logo = styled.a.attrs({
href: '/'
})`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background: url(${logoPic});
background-size: contain;
`;
export const Nav = styled.div`
width: 960px;
height: 100%;
padding-right: 30px;
box-sizing: border-box;
margin: 0 auto;
`;
export const NavItem = styled.div`
line-height: 26px;
padding: 15px;
font-size: 17px;
color: #333;
&.left {
float: left;
}
&.right {
float: right;
color: #969696;
}
&.active {
color: #ea6f5a;
}
&.distance {
margin-right: 80px;
}
`;
export const Navbeta = styled.a.attrs({
href: '#'
})`
position: absolute;
display: block;
width: 57px;
height: 25px;
right: 380px;
margin-top: 16px;
background: url(${betaPic});
background-size: contain;
`;
export const Navsearch = styled.input.attrs({
placeholder: '搜索'
})`
width: 160px;
height: 38px;
border: none;
box-sizing: border-box;
padding: 0 40px 0 20px;
outline: none;
border-radius: 19px;
background: #eee;
margin-top: 9px;
font-size: 14px;
margin-left: 20px;
&::placeholder: {
color: #999;
}
`;
export const Addition = styled.div`
position: absolute;
right: 0;
top: 0;
height: 56px;
`;
export const Button = styled.div`
float: right;
margin-top: 7px;
margin-right: 30px;
padding: 0 20px;
line-height: 38px;
border-radius: 19px;
border: 1px solid #ec6149;
font-size: 14px;
&.reg {
color:#ec6149;
}
&.writting {
color: #fff;
background:#ec6149;
}
`;
3 使用iconfont嵌入头部图标
3.1 打开网站
阿里向外提供的(可注册个号登录):https://www.iconfont.cn/
3.2 下载使用icon
在React中使用symbol字体图标
(1)登录
(2)搜索icon,并下载使用
单击购物车
点击下载至本地,并解压缩
在statics目录下创建iconfont文件夹,把下载后的五个有用的文件剪切到iconfont文件夹下,其余下载文件不需要。
配置文件,打开iconfont.css文件,给其添加相对路径(出data外都加 ‘./’ ),将iconfont.css文件改为iconfont.js文件
iconfont/iconfont.js
import { createGlobalStyle } from 'styled-components';
const GlobalStyleTwo = createGlobalStyle`
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1614397935460'); /* IE9 */
src: url('./iconfont.eot?t=1614397935460#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAVAAAsAAAAAChAAAATyAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDXgqGSIVgATYCJAMcCxAABCAFhG0HdRvNCFGU7c0M2UdCbsb0KlQqY6dKn0zy6XHL3s9PgEtYdWq24oq1HrTZWwgDoaJQcTszhT1TTkQgaG7327OcxNIGJpPJorBkMRtKWvH/v1c563jsY9a1b3F5L3MMW4OCSwMUoOzObQuogAOkXCDtErsiTirvjfuBAHi5ZiNLe68eGix0iADIITlFaDpG7AKT0GhUE+cmyAeOpmxnXgSwV/48+VEcGoDBVaAHDe5tNjTO5C+HaCAawIIaRGB/dgDJRQAFzAawQN5JZwBUJ2ej+OpvsvSAWBoM1UyOpEcyI+WRFV8ORaN0G9EkEYuTAIu5pn88DgXSECi1PsOcUkEkmZLAQiSdBAYimSQQiJQLoaoVPgkcfDlkBA1p6v2xgETAb0CGx5gdZxEMfgaHjWZotTGqhIRUbTZL+XiNJkVw7TCGw4QImsbHiZ2GUIgI6N98U76K4Q4eu1Y43OcdOXnN5ZS5h49dumI5ciJt/lH/xOPJ5kOXRKPTYbIv1z8Uwlo/0RtIHAiGjUa3IBAaMj0czvOw0vHIODmGaH/1O8cb7Id33hBcNY5dPLIyHB7G63dMVw4lH02ma8yBw4J1Z9qhtKPe9ZffENQabEa9ybBcbzQd3ZliOBzwBMnONJsXPLIjWX9o5SvE8Uatnr9amDvviPNz+VPpZvEBYSfAB/8ov+NlEjSZgnJVreHOaxG4Ttj5BEGra+cPTxyYsHr1hIGJw3cTJJBB7vzhiXdPrOLu6L2GfvQhvZZew9dcw17LfvghvYZe690k7+F48ySh2PxIfOdEbUmQbJloHtiLhRLxYuckNPZF+m1bN24ym99/H/88OHfu+wW+JmbrnXduZRrXoy81PsT++28+NL/l/D8bOsNR0LGo2dSxsQunyw3MwSeeOEgx4phd98ATTx5kGjwzQrGxxzNLMss+/p1uuuWWTZ6M+g5mE2OxQP7rof6876ablncuP3u2LS2VUC49TRvVZmSoiuJhWdsSzpxZ3nlnKPTtt/TkCX/E33vNNc+d9PvfDCn+5enLt6lNX/kIoL3++0+bouni82jxqKPjtemtr9ljo5aW8Gj9gIFptkj14+w/9T7lqcoCwjXlyzOMUsxCoaex+rNFJuWFjJR/Mqf8NCUXbgAQfYI+Tptj9nmaRxexH6OP0iZ2gAV95NGu5E/5O7jhj75Ji+Nqf9MILADgvcWf/BBI4MJ/lgqV6C8oAg6wtQSZ1Fm2pztBAGr1OPgi9eDxGPDH8yb6YH/qFHA5QGikWGCo5QClMZnCMmYDR1AKKhoNwJul42JBEi+K5FUAZtrsARHvFmDEegSoeC9RWMb7wEn1HajEBwWeMZKuKZgaEt/itYuK3aazrNRJVtntIBaFV5XDdqd/TPS6Esky2r0+KaIryi/sZzrsbrt3Hyf4ljmLFcWhc3hll67de5h9bEzWebzyqN2q5C9VFE91QYFj6oPyrbILvBZediKFnY2OxUo6EiuZm4PPVvCD7x9m5+Q3RuRdaFnxG9l5+UjL6xTJVwigdCjdoJbnst5nGadiCoUj3OfgJXPRafftdmM4KdPxTG8bZWelyLd0i7hHtYJglAOqzD++2vU6LwDw0PXUwRBKWMIRFVETDbJvSXTTG7udit8iifKqpdIqv+ySLaJ7TlGlenTmREX2sz6P5BYcottpE0dhBUUaj+he/NwSTJMIAAAA') format('woff2'),
url('./iconfont.woff?t=1614397935460') format('woff'),
url('./iconfont.ttf?t=1614397935460') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont.svg?t=1614397935460#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`;
export { GlobalStyleTwo };
并在src/index.js中引用该文件
src/index.js
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { GlobalStyleOne } from './style';
import { GlobalStyleTwo } from './statics/iconfont/iconfont';
ReactDOM.render(
<Fragment>
<GlobalStyleOne/>
<GlobalStyleTwo/>
<App />
</Fragment>,
document.getElementById('root')
);
使用iconfont(可查看之前下载文件夹下的demo_unicode.html文件)。在对应位置使用以下代码替换,并改变其图标对应的值
<span className="iconfont hotfont"></span>
(3)React中使用(另一种方法)
点击项目中的symbol
首先引入icon
注意:需要在引入的前面加 http:
<script src="http://at.alicdn.com/t/font_2022701_od5y3fwvobn.js"></script>
添加到对应的位置,需要注意的是
官方:
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xxx"></use>
</svg>
我们需要把xlink:href更改为 xlinkHref,因为在React中识别不了
<svg className="icon" aria-hidden="true">
<use xlinkHref="#iconAa"></use>
</svg>
此外我们也可以修改这个图标的样式
.icon {
width: 20px;
height: 27px;
display: block;
}
4 搜索框动画实现
实现的动画功能,设置对应的事件和属性
src/common/header/index.js
class Header extends Component {
constructor(props) {
super (props);
this.state = {
focused: false // 定义focused参数
}
this.handleInputFocus = this.handleInputFocus.bind(this);
this.handleInputBlur = this.handleInputBlur.bind(this);
}
handleInputFocus() {
this.setState(()=>({
focused: true // 调用函数handleInputFocus时,改变focused参数
}))
}
handleInputBlur() {
this.setState(()=>({
focused: false // 调用函数handleInputBlur时,改变focused参数
}))
}
我们通过值:focused的ture和false来确定是否聚焦。
然后通过事件.handleInputFocus,handleInputBlur来更改其值,使得其在聚焦和失去焦点的时候触发相应的事件。
然后我们安装react-transition-group,(yarn add react-transition-group)了解react-transition-group
github了解链接:点击https://github.com/reactjs/react-transition-group中的Main documentation查看https://reactcommunity.org/react-transition-group/并使用了CSSTransition的组件
import { CSSTransition } from 'react-transition-group'; // 导入CSSTransition
<SearchWrapper>
<CSSTransition
in={focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={focused ? 'focused': ''} // 判断focused值来确定className值,并分别给予不同className不同的style样式
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
></NavSearch>
</CSSTransition>
// 判断focused值来确定className值,并分别给予不同className不同的style样式
// 并设置style背景值改变其显示背景色
<span className={focused ? 'focused iconfont zoom': 'iconfont zoom'}>

</span>
{ this.getListArea() }
</SearchWrapper>
需要注意的是CSSTransition只能包含一个字组件!用导入的CSSTransition包括住实现动画的组件,并传给其timeout(时长 毫秒)in (控制入场动画的)classNames(让她等于对象或字符串)参数。所以它是在SearchWrapper下的,所以在SearchWrapper下使用.slide等来定义其动画。我们需要知道的是,在styled-components中,子组件(父子关系)是使用 .className,本组件(同级关系)使用的是 &.className
src/common/header/style.js
export const SeachWrapper = styled.div`
float: left;
position: relative;
.slide-enter{
transition: all .2s ease-out;
}
.slide-enter-active{
width:240px;
}
.slide-exit {
transition: all .2s ease-out;
}
.slide-exit-active {
width: 160px;
}
.search {
height: 20px;
width: 20px;
position: absolute;
right: 4px;
top:18px;
border-radius: 50%;
&.focused {
background: #777;
border-radius: #fff;
}
}
`;
此外我们还要使得,聚焦后完成动画的最后的宽度保持不变。 代码:20行
export const Navsearch = styled.input.attrs({
placeholder: '搜索'
})`
width: 160px;
height: 38px;
border: none;
box-sizing: border-box;
padding: 0 35px 0 20px;
outline: none;
border-radius: 19px;
background: #eee;
margin-top: 9px;
font-size: 14px;
margin-left: 20px;
cplor: #777;
&::placeholder: {
color: #999;
}
&.focused {
width:240px;
}
`;
5 使用React-Redux进行应用数据的管理
可参考Redux中第六节,先对上一节代码进行优化
5.1 使用React-Redux实现数据的分离
a.首先导入 import { Provider } from 'react-redux';
然后将需要使用redux的组件使用Provider包裹起来,然后
使用store={store}
,来授予该组件使用redux的权利。
b.我们创建store(注意:如果要使用redux-devtools 上github找 其的高级导入用法)
c.创建reducer
下面这是我们已经创建好数据之后的reducer。
分两步:
1、创建变量
2、导出纯函数
d.建立连接并导出使用
connect建立连接,mapStateToProps 使用,mapDispatchToProps 发送action
这里取拿focused数据在state.header中拿,在下一节会讲到。
5.2 将有状态组件改为无状态组件
将所有的数据和行为使用Redux之后,就可以class组件 用函数function组件替代。需要注意的是:改了之后,需要将this去掉。
6 使用combineReducers优化数据管理
6.1 为什么用combineReducers
如果我们的所有的数据,以及对action的处理,都在一个reducer中的话,随着项目的增大,这个代码会越来愈多使得代码特别臃肿,不好维护,因此Redux给我们提供了combineReducers的方法,方便我们进行数据的管理。
6.2 项目目录增加文件
首先我们之前的核心Redux.js 存放在 src/store中,不变。
然后,我们在common/header文件夹下创建store(子store)文件夹,并在store文件夹下创建reducer.js,也就是子reducer。另外index.js是为了我们方便引入我们的reducer。
6.3 代码更改和实现
src/common/header/store/reducer.js(小笔记本)
将我们原来src/store/reducer.js(大笔记本)中的代码都写到这里,因为这部分是负责header页面的,所以存放的都是header使用的数据
const defaultState = {
focused: false
}
export default (state = defaultState, action) => {
if(action.type === 'search_focus'){
return {
focused: true
}
}
if(action.type === 'search_blur'){
return {
focused: false
}
}
return state;
}
src/common/header/store/index.js
import reducer from './reducer';
export { reducer };
src/store/reducer.js(大笔记本)
import { reducer as headerReducer} from '../common/header/store/index.js';
// 因为我们很多小reucer导入了这个大reducer中,所以reducer会重名,因此我们重命名为headerReducer,方便后面增加其他小reducer的导入
import { combineReducers } from 'redux';
// 导入对应的组件
// 所有小的reducer整合成大的reducer
const reducer = combineReducers({
//我们将 导入的headerReducer层,命名为header
header: headerReducer // 这样的话就导致它的数据层就多了一层
});
export default reducer;
这样的话它的数据层就多了一层,如下图:
所以之前取数据都要在state后加上一层header:比如下面中的代码
src/common/header/index.js
const mapStateToProps = (state) => {
return {
focused: state.header.focused // 这里拿数据多了一层header
}
}
7 actionCreators与constants的拆分
这里constants设置常数,就是我们来设置原来使用的actionTypes类型
优化步骤
7.1 store下创建两个文件
actionCreators:用来存放action
constants:用来放常量
index:用来放导入的模块
7.2 代码书写
src/common/header/store/index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
src/common/header/store/constants.js
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
src/common/header/store/actionCreators.js
import * as constants from './constants.js'; //从当前目录下的index导入
export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
});
export const searchBlur = () => ({
type: constants.SEARCH_BLUR
});
src/common/header/store/reducer.js
import * as constants } from './constants.js';
const defaultState = {
focused: false
}
export default (state = defaultState, action) => {
if(action.type === constants.SEARCH_FOCUS){
return {
focused: true
}
}
if(action.type === constants.SEARCH_BLUR){
return {
focused: false
}
}
return state;
}
src/common/header/index.js
//导入
import { actionCreators } from './store/index.js';
//使用
const mapDispatchToProps = (dispatch) => {
return {
handleInputFocus () {
//我们这里不创建action,直接写在括号里即可。使得代码更简洁
dispatch(actionCreators.searchFocus())
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
}
}
}
8 使用immutable.js来管理store中的数据
8.1 为什么使用immutable.js
在reducer中,我们是不可以对state中的代码进行修改的,我们是通过拷贝一份数据然后进行返回,但是有的时候不小心就会修改到state中的值,又很难发现其中的问题。immutable的意思是:不可改变的
因此就需要Immutable.js来进行数据的管理。这是一个第三方的库,需要进行安装
8.2 immutable的安装
https://github.com/immutable-js/immutable-js
npm install --save immutable
8.3 使用
第一步:在 安装好之后,我们需要导入一个fromJS方法
import { fromJS } from 'immutable';
通过该方法将defaultState的变为immutable对象
import { constants } from './';
import { fromJS } from 'immutable'; // 导入immutable模块中fromJS方法
//使用fromJS将其变为immutable数据
const defaultState = fromJS({
focused: false
});
第二步:在header/index.js中需要使用immutable中的get方法才能来获取immutable对象的值。
src/common/header/index.js
const mapStateToProps = (state) => {
return {
//focused: state.header.focused
focused: state.header.get('focused')
}
}
第三步:在header/store/reducer.js中使用immutable中的set方法修改immutable对象的值。
当然这个方法并不会直接修改我们state中的数据,这个方法的实质是:我们使用这个方法,会结合之前的immutable对象,和我们设置的值,来返回一个全新的值
src/common/header/store/reducer.js
import { constants } from './';
import { fromJS } from 'immutable';
//将其变为immutable对象
const defaultState = fromJS({
focused: false
});
export default (state = defaultState, action) => {
if(action.type === constants.SEARCH_FOCUS){
return state.set('focused', true);
//immutable对象的set方法,会结合之前immutable对象
//和设置的值,会返回一个全新的对象
// {
// focused: true
// }
}
if(action.type === constants.SEARCH_BLUR){
return state.set('focused', false);
}
return state;
}
使用完毕!
9 使用redux-immutable统一数据格式
9.1 为什么使用redux-immutable
在如下代码中src/common/header/index.js中mapStateToProps里面的 focused: state.header.get('focused')
这行代码,
其中states 是一个js对象,而state.header是一个immutable对象,所以调用focused属性的时候,得先调用state的‘.’,再去调用header的‘.get’方法。所以说数据获取不统一,前面是按照js对象来获取,后面又是按照immutable对象来获取的,这样不太靠谱。我们希望统一一下,因此我们希望state也是一个immutable对象。
src/common/header/index.js
const mapStateToProps = (state) => {
return {
//focused: state.header.focused
focused: state.header.get('focused')
}
}
思路:找到以上代码state所指位置,并将其改为immutable对象。
9.2 redux-immutable的安装
https://www.npmjs.com/package/redux-immutable
npm i --save redux-immutable
9.3 使用
第一步:在 安装好之后,我们改变combineReducers的导入模块,之前是从redux模块导入combineReducers,现在让它从redux-immutable模块中导入
src/store/reducer
import { reducer as headerReducer } from '../common/header/store';
//我们从redux-immutable导入原来的combineReducers,这个combineReducers里面的数据内容就是immutable的数据内容。
import { combineReducers } from 'redux-immutable';
//这是我们之前combineReducers的导入方式,这个combineReducers不能是里面数据变为immutable数据内容。
//import { combineReducers } from 'redux';
const reducer = combineReducers({
header: headerReducer
});
export default reducer;
第二步:修改src/common/header/index.js中mapStateToProps里面数据获取方法。
const mapStateToProps = (state) => {
return {
//focused: state.header.focused
focused: state.getIn(['header', 'focused'])
//state.get('header').get('focused')
}
}
immutable对象提供了两种方法:
1.state.get('header').get('focused') 通过两次get方法
2.state.getIn(['header', 'focused']) 通过getIn方法 获取 header 中的 focused
使用完毕!
9.4 了解其他immutable中除fromJS,get,getIn等以外的内容,去官网看
官网:https://immutable-js.github.io/immutable-js/
10 热门搜索样式布局
对热门搜索进行布局
修改src/common/header/index.js中代码,添加搜索区域,并添加其相关样式
src/common/header/index.js
import { SearchInfo, SearchInfoTitle, SearchInfoSwitch, SearchInfoList, SearchInfoItem } from './style';
const getListArea = () => {
if (show){
return(
<SearchInfo
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch
onClick={ () => handleChangePage(page, totalPage, this.spinIcon) }
>
<span ref={(icon) => {this.spinIcon = icon}} className="iconfont spin"></span>
换一批
</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList> // 用来触发BFC
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
</SearchInfoList>
</SearchInfo>
)
} else {
return: null;
}
}
...<SearchWrapper>
...
{ this.getListArea(this.props.focused) }
<SearchWrapper>
...
src/common/header/style.js
export const SearchInfo = styled.div`
position: absolute;
left: 0;
top: 56px;
width: 240px;
padding: 0 20px;
box-shadow: 0 0 8px rgba(0, 0, 0, .2);
background: #fff;
`;
export const SearchInfoTitle = styled.div`
margin-top: 20px;
margin-bottom: 15px;
line-height: 20px;
font-size: 14px;
color: #969696;
`;
export const SearchInfoSwitch = styled.span`
float: right;
font-size: 13px;
cursor: pointer;
.spin {
display: block;
float: left;
font-size: 12px;
margin-right: 2px;
transition: all .2s ease-in;
tranform-origin: center center;
}
`;
export const SearchInfoList = styled.div`
overflow: hidden;
`;
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;
`;
11 Ajax获取数据
11.1 安装对应的插件
(1)安装中间件 yarn add redux-thunk
,并配置thunk使用
(2)安装ajax数据 请求插件yarn add axios
11.2 使用ajax获取数据
(1)从哪里获取数据
React框架寻找的特性:
1.工程目录下看对应的路由
2.找不到,会到public目录下的api/headerList.json寻找 然后就会发送出来,因此我们就可以使用假数据
因此,我们在public目录下创建对应的文件
我们使用统一的数据格式:
public/ api/headerList.json
{
"success": true,
"data": ["高考","区块链","三生三世","崔永元","vue","小程序","茶点微小说","萨沙讲史堂","夜幕下的地安门","擦亮你的眼","理财","毕业","手帐","简书交友","spring","古风","故事","暖寄归人","旅行","连载","教育","简书","生活","投稿","历史","PHP","考研","docker","EOS","微信小程序","PPT","职场","大数据","创业","书评","东凤","饱醉豚","雨落荒原","程序员","爬虫","时间管理","kotlin","数据分析","阴阳合同","设计","红楼梦","父亲节","女人和衣服","swift","高考作文"]
}
(2)使用数据
第一步:在src/common/header/store/reducer中
首先,我们新创建了一个变量list。要注意的是:list中数组的类型也是immutable;因此,我们在更新数据的时候也要使得获取的数据是immutable类型的,因此我们最好在获取数据的时候将要传过去的data变为immutable对象(在actionCreators.js中)
src/common/header/store/reducer.js
import { constants } from './';
import { fromJS } from 'immutable';
//将其变为immutable
const defaultState = fromJS({
focused: false,
list: []
});
export default (state = defaultState, action) => {
if(action.type === constants.SEARCH_FOCUS){
return state.set('focused', true);
}
if(action.type === constants.SEARCH_BLUR){
return state.set('focused', false);
}
if(action.type === constants.CHANGE_LIST){
return state.set('list', action.data);
}
return state;
}
第二步:我们要在搜索框聚焦的时候,获得Ajax数据。所以在src/common/header/index.js中派发action来获取Ajax数据(所以间接使用thunk中间件)
src/common/header/index.js
//class Header extends Component 中
getListArea() {
if(this.props.focused){ //如果聚焦 就显示以下的内容
return (
<SearchInfo>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
{
this.props.list.map((item) => {
return <SearchInfoItem key={item}>{item}</SearchInfoItem>
})
}
</SearchInfoList>
</SearchInfo>
)
} else{
return null;
}
}
//我们在jsx语法中的引入
{ this.getListArea()}
const mapStateToProps = (state) => {
return {
//focused: state.header.focused
focused: state.getIn(['header', 'focused']),
list: state.getIn(['header', 'list']) //获取数据
//state.get('header').get('focused')
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleInputFocus () {
dispatch(actionCreators.getList()); //聚焦的时候发送行为
dispatch(actionCreators.searchFocus())
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
}
}
}
src/common/header/store/actionCreators中定义getList方法
我们导入模块import { fromJS } from 'immutable';
将data导入的类型 data: fromJS(data)
更改为immutable类型
import { constants } from './';
import axios from 'axios';
import { fromJS } from 'immutable';
export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
});
export const searchBlur = () => ({
type: constants.SEARCH_BLUR
});
export const changelist = (data) => ({
type: constants.CHANGE_LIST,
data: fromJS(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");
})
}
}
成功!
12 代码优化reducer微调
12.1.比较传统的代码优化
比如:在使用这些变量前先定义,const { focused, handleInputFocus, handleInputBlur } = this.props;
然后就可以直接通过名字使用了
2.reducer的更改
随着我们要处理的代码增多,我们的reducer变成如下。if语句会越来越多。
src/common/header/store/reducer.js修改前
import { constants } from './';
import { fromJS } from 'immutable';
//将其变为immutable
const defaultState = fromJS({
focused: false,
list: []
});
export default (state = defaultState, action) => {
if(action.type === constants.SEARCH_FOCUS){
return state.set('focused', true);
}
if(action.type === constants.SEARCH_BLUR){
return state.set('focused', false);
}
if(action.type === constants.CHANGE_LIST){
return state.set('list', action.data);
}
return state;
}
因此我们选择新的方式:使用switch进行代码的优化,这样看起来更简洁。我们为什么不用break,因为return 之后,switch函数就会结束。
src/common/header/store/reducer.js修改后
import { constants } from './';
import { fromJS } from 'immutable';
//将其变为immutable
const defaultState = fromJS({
focused: false,
list: [],
page: 1,
totalPages: 1
});
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.CHANGE_LIST:
return state.set('list', action.data).set('totalPages', action.totalPages);
default:
return state;
}
}
13 实现换一批功能
13.1.创建新的变量
我们需要实现换一换,就类似于实现换页的功能,因此我们新增三个变量,一个是总的页面:totalPages,一个是当前页面:page
因为我们要展示的时候是在鼠标移入移出来确定该显示框的开启和关闭: mouseIn,目前的reducer中变量:
src/common/header/store/reducer.js
const defaultState = fromJS({
focused: false,
mouseIn: false,
list: [],
page: 1,
totalPages: 1
});
2.使用新的变量
我们如何获得总的页数呢?
在AJAX获取数据的时候,我们通过计算,可以获得总的页数
src/common/header/store/actionCreators.js
const changelist = (data) => ({
type: constants.CHANGE_LIST,
data: fromJS(data),
totalPages: Math.ceil(data.length / 10) // 增加总页数计算,math.ceil方法用来取整
});
我们获取数据之后:
在行为:当INput聚焦或者鼠标移入了之后显示,移除之后消失。
我们主要更改的是代码中的getListArea()的方法
需要注意的:我们获取的list中的数据是immutable类型,是无法通过list[i]方式显示,我们使用方法toJS将其转为JS类型,并且赋值给newList
实现步骤:
第一步:显示分页内容。
我们获取当前页面的信息,通过如下6~12行代码,获取到当前页面需要展示的信息
并且将JSX的类型,通过push存储到该数组中。之后我们通过变量{pageList} 在页面显示即可。
getListArea() {
const {focused, list, page, mouseIn, totalPages, handleMouseEnter, handleMouseLeave, handleChangePage} = this.props;
const newList = list.toJS(); //转换为普通数组 immutable类型 不能list[i]
const pageList = [];
if(newList.length) {
for(let i = ((page-1) * 10); i< page * 10; i++){
pageList.push(
<SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
)
}
}
if(focused || mouseIn){
return (
<SearchInfo
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch onClick={ () => handleChangePage(page, totalPages)}>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
{pageList}
</SearchInfoList>
</SearchInfo>
)
} else{
return null;
}
}
改善下功能:确保聚焦后点击下方SearchInfo区域时,不会收缩回去。我们创建如下的两个事件,在移入的时候mouseIn为true,移出的时候mouseIn为false,因此我们通过 focused || mouseIn 来判断是否显示,onMouseEnter={handleMouseEnter}
,onMouseLeave={handleMouseLeave}
在后面mapDispatchToProps中写handleMouseEnter,handleMouseLeave函数,并派发action给reducer(派发的action去actionCreators中写)
我们现在可以正常的显示框了
第二步:实现换一批的功能
我们在换一批的标签上添加事件,<SearchInfoSwitch onClick={ () => handleChangePage(page, totalPages)>
,这里我们为什么要传值呢?因为我们要让我们当前页面和总的页面做一个对比如下:
当前页面没有到达最终页数:我们点击就会跳到下一页。当我们到达了最后一页:我们单击,返回到第一页
handleChangePage(page,totalPages) {
if( page < totalPages){
dispatch(actionCreators.ChangePage(page+1));
}else{
dispatch(actionCreators.ChangePage(1));
}
同时我们会通过actionCreators将当前的页面page传值,然后在reducer中,进行值的修改。这样page就是一个可以变化的量,而不是一个固定的值。然后就实现了我们的翻页效果。
//actionCreators
export const ChangePage = (page) => ({
type: constants.CHANGE_PAGE,
page
})
//reducer
case constants.CHANGE_PAGE:
return state.set('page', action.page);
3.需要注意的点
我们实现代码之后,每次打开网页都会报错
原因是什么?
如下:当我们加载网页,它就会渲染我们的网页,但是此时,我们没有获得AJAX数据,也就是说,我们的list中是没有值的,进而就导致key=undefined 就会报错。
解决的方法很简单,如下:当我们list有值的时候,我们的newList也有值,我们通过看newList.length它的长度来判断是否有值,只有存在值时,我们才会运行之后的for循环,这样就不会报错了
getListArea() {
const {focused, list, page, mouseIn, totalPages, handleMouseEnter, handleMouseLeave, handleChangePage} = this.props;
const newList = list.toJS(); //转换为普通数组 immutable类型 不能list[i]
const pageList = [];
if(newList.length) {
for(let i = ((page-1) * 10); i< page * 10; i++){
pageList.push(
<SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
)
}
}
实现效果如下:
14 实现换一批前面的动画效果
14.1 去使用对应的icon
我们将其放在换一批的前面
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch onClick={ () => handleChangePage(page, totalPages, this.spinicon)}>
<svg ref={(icon) => {this.spinicon = icon}}className="spin" aria-hidden="true">
<use xlinkHref="#iconspin"></use>
</svg>
换一批
</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoLise>
{pageList}
</SearchInfoLise>
</SearchInfo>
14.2 设置样式
(1)调整该icon到合适的位置
(2)设置动画(这里我们不使用CSStransition)
设置过渡效果
transition: all .2s ease-in;
transform-origin: center center;
export const SearchInfoSwitch = styled.span`
float: right;
font-size: 13px;
cursor: pointer;
.spin {
display: block;
position: relative;
top:5px;
float: left;
width: 12px;
height: 12px;
color: red;
margin-right: 2px;
transition: all .2s ease-in;
transform-origin: center center;
}
`
(3)设置逻辑
<SearchInfoSwitch onClick={ () => handleChangePage(page, totalPages, this.spinIcon)}>
<svg ref={(icon) => {this.spinIcon = icon}}className="spin" aria-hidden="true">
<use xlinkHref="#iconspin"></use>
</svg>
换一批
</SearchInfoSwitch>
我们通过ref来获取此标签当前的真实节点。然后传值给handleChangePage
handleChangePage(page, totalPages, spin) {
let originAngle = spin.style.transform.replace(/[^0-9]/ig, ''); // 用来取字符串中的值,非数字就用''代替
// 第一次拿到的originAngle为空
if(originAngle) {
originAngle = parseInt(originAngle);
}else{
originAngle = 0;
}
spin.style.transform = 'rotate('+ (originAngle + 360) +'deg)';
if( page < totalPages){
dispatch(actionCreators.ChangePage(page+1));
}else{
dispatch(actionCreators.ChangePage(1));
}
}
获得该节点之后:我们需要获取spin.style.transform的值,但是我们不设置的时候,其为空
我们通过正则表达式将 "rotate(360deg)"中的数字提取,spin.style.transform.replace(/[^0-9]/ig, '');
然后我们判断是否存在值:若存在 则 获取里面的数值然后 + 360;不存在 则 使其等于0 然后+360;这样我们就实现了图标的旋转
15 避免无意义的请求发送
15.1 什么是无意义的请求发送?
当我们聚焦的时候,会发送AJAX的数据请求来获取数据,当我们关闭了,再打开,它会再次发送数据,这样会降低性能。
我们只需要获取一次数据就够了。
15.2 代码修改
我们通过list来获取对应的值,我们将list传递给该函数,当list.size为 0 的时候,就是没有获取数据,当list.size为 50 的时候,就是获取到了数据
因此,我们只在list.size === 0 的时候来获取数据,这样就只会发送一次请求。
<Navsearch
className={focused ? 'focused' : ''}
onFocus={ () => handleInputFocus(list)}
onBlur={handleInputBlur}
></Navsearch>
handleInputFocus (list) {
if(list.size === 0){
dispatch(actionCreators.getList());
}
dispatch(actionCreators.searchFocus())
},