JS原型

前端面试题永远少不了js的原型和原型链。解释起来有点拗口,可以用一张图来讲解:

一、Function和Object

首先说一下,js中自带两个函数Object和Function,Object继承自己,Funtion继承自己,Object和Function互相是继承对方,也就是说Object和Function都既是函数也是对象。

1
2
Function instanceof Object;//true
Object instanceof Function;//true

也就是说Object是Function的实例,Function是自己的实例

二、普通对象和普通函数

js中万物皆对象,函数是对象,函数创建出来的实例也是对象,我们将函数成为函数对象,将实例成为普通对象。

注意函数对象本身又是Function的实例

1
2
3
4
5
function Sample(){};//Sample是一个函数对象
var s=new Sample();//s是一个普通对象

//function Sample(){}等同于
var Sample=new Function();//所以Sample又是Function的实例

三、隐式原型和显示原型

  • 每个对象都有一个名为__proto__的内部属性(隐式原型),它指向所对应构造函数的原型对象
  • 每个函数对象都有一个prototype的属性(显示原型)prototype与__proto__都指向构造函数的原型对象,原型对象下面又有一个constructor属性,指向这个函数对象

四、原型链

js中,每个对象都会在内部生成一个__proto__属性,当我们访问一个对象属性时,如果这个对象不存在就会去__proto__ 指向的对象里面找,一层一层找下去,这就是javascript原型链的概念。

JS中所有的东西都是对象,所有的东西都由Object衍生而来, 即所有东西原型链的终点指向null

五、继承

js中的普通对象是由构造函数(即函数对象)new出来的,这个对象产生于原型却不等于原型;这个普通对象是如何与原型产生联系的呢?就是通过这个叫__proto__完成的。

1
2
3
4
5
function method(){}
var m = new method();
m===m.prototype;//false

m.__proto__====method.prototype;//true

参考:http://baijiahao.baidu.com/s?id=1585354841830289519&wfr=spider&for=pc

https://www.jb51.net/article/123976.htm

React实战4

详情页面的布局

1、首先在detail下面创建布局组件style.js

2、然后在detail/index.js中引入上面的布局组件

3、在style.js中编写详情页的标题组件、内容组件,在内容组件中放置img和p等标签;

4、给详情页面添加redux

  • 在detail文件夹下创建store文件夹,并在里面创建actionCreators.js、constants.js、index.js、reducer.js

  • 在reducer.js中添加defaultState ,需要将带html标签的内容全部包进去

  • 在src/store/reducer.js中,引入详情页的reducer

  • 在detail的index.js中引入reducer,并展示内容,注意当需要展示带有html标签内容时需要使用dangerouslySetInnerHTML

    1
    2
    3
    4
    <DetailContent dangerouslySetInnerHTML={{__html:this.props.content}}>
    //在DetailContent组件中展示html格式的内容
    <DetailContent dangerouslySetInnerHTML={{__html:this.props.content}}>
    </DetailContent>
  • 用活数据代替死数据,componentDidMount触发请求行为,在actionCreators中引入axios发送请求

  • 通过路由,点击不通的item,实现不同详情页的跳转

两种方式:
动态路由

  • 在home的List页面中,将<Link key={index} to='/detail'>改为<Link key={index} to={'/detail/'+item.get('id')}>

  • 进入APP.js,为了匹配所有的detail,将<Route path='/detail' exact component={Detail}></Route>改为<Route path='/detail/:id' exact component={Detail}></Route>

  • 在detail的index.js页面获取页面ID this.props.match.params.id,传递给getDetail方法

React中的异步组件

之前我们的代码,在控制台可以看到无论我们查看哪个页面,访问的js都没有改变。说明所有的js代码全部在一个文件里面,这样会影响速度,我们希望当我们家在首页时只加载首页代码,当我们访问详情页时只加载详情页的代码。

这就需要使用异步组件,就会非常简单。可以使用react-loadable组件

安装

1
2
3
yarn add react-loadable
//或
npm install react-loadable

我们想要家在详情页时,再家在详情页的代码,应该怎么做呢?在detail文件夹下创建loadable.js的文件

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import Loadable from 'react-loadable';

const LoadableComponent = Loadable({
loader: () => import('./'),
loading(){
return <div>正在加载</div>
},
});

export default ()=> <LoadableComponent/>

然后来到App.js中,将饮用Detail的引用改为loadable

1
2
3
4
import Detail from './pages/detail';

//改为
import Detail from './pages/detail/loadable.js';

这时,会报错TypeError: Cannot read property ‘params’ of undefined

这是由于原先我们是直接使用Detail组件,现在变成了使用loadable.js了,而router是跟Detail对应的,现在对不上了,怎么办呢?

在detail的index.js文件中从react-router-dom引入withRouter,然后在使用connect中加入withRouter

1
2
3
import {withRouter} from 'react-router-dom';
。。。
export default connect(mapStateToProps,mapDispatchToProps)(withRouter(Detail));

它就是让detail有能力获取到router中的内容。

这时就实现了对详情页的异步加载

项目上线

将项目打包

1
npm run build

就会生成项目代码,放在后端即可

React实战3

一、路由

1.1 路由和路由规则

React中路由表示根据url的不同显示不同的内容

在安装react中安装路由

1
npm install react-router-dom

进入APP.js,看到大的组件中只显示header,我们希望访问首页时不仅显示header,而且还显示首页。访问详情页时把详情页给显示出来。

首先从react-router-dom中引入BrowserHistory、Router。注意Proveder中只能有一个元素,所以它里面的内容需要用一个div包起来,对BrowserRouter也是只能包含一个要素。

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
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header/index.js';
import store from './store/index.js';

class App extends Component {
render() {
return (
<Provider store={store}>
<div>
<Header />
<BrowserRouter>
<div>
<Route path='/' render={()=><div>home</div>}></Route>
<Route path='/detail' render={()=><div>detail</div>}></Route>
</div>
</BrowserRouter>
</div>
</Provider>

);
}
}

export default App;

这时当我们访问http://localhost:3000和http://localhost:3000/detail时回出现home和homedetail。这时因为当访问http://localhost:3000时/也能匹配上/detail。如果我们访问http://localhost:3000/detail只想看到detail不想看到home怎么办?很简单,只需要加一个exact就可以了。

表示只有路径和指定的路径完完全全相等的时候才显示后面的内容

1
2
<Route path='/' exact render={()=><div>home</div>}></Route>
<Route path='/detail' exact render={()=><div>detail</div>}></Route>

1.2 拆分首页

首先在src目录下创建一个pages的文件夹,表示有多少个页面,然后在pages里面创建两个文件夹分别叫做home和detail。

在home文件夹下创建一个index.js的文件,这时react组件

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react';

class Home extends Component{
render(){
return (
<div>Home</div>
);
}
export default Home;

同理在detail文件夹下也创建一个index.js的文件。

然后在App.js文件中引入这两个文件,并在Route标签中引入这两个组件。在标签中引入组件使用compinent

1
2
3
4
5
import Home from './pages/home';
import Detail from './pages/detail';
...
<Route path='/' exact component={Home}></Route>
<Route path='/detail' exact component={Detail}></Route>

然后在home目录下创建一个style.js的文件,

1
2
3
4
5
6
7
8
import styled from 'styled-components';

export const HomeWrapper=styled.div`
width:960px;
margin:0, auto;
height:300px;
background:red;
`;

home/index.js中引入HomeWrapper组件即可

1
2
3
4
5
6
7
8
9
...
import { HomeWrapper } from './style.js';
class Home extends Component{
render(){
return (
<HomeWrapper></HomeWrapper>
);
}
}

在style.js中创建HomeLeft和HomeRight,并在home/index.js中引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import styled from 'styled-components';

export const HomeWrapper=styled.div`
overflow:hidden;
width:960px;
margin:0, auto;
`;

export const HomeLeft=styled.div`
float:left;
margin-left:15px;
padding-top:30px;
width:625px;
`;

export const HomeRight=styled.div`
width:240px;
float:right;
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react';
import { HomeWrapper , HomeLeft, HomeRight} from './style.js';
class Home extends Component{
render(){
return (
<HomeWrapper>
<HomeLeft>left</HomeLeft>
<HomeRight>right</HomeRight>
</HomeWrapper>
);
}

}
export default Home;

如何在left中引入banner图呢?可以在HomeLeft中引入img标签并且在home/style.js文件中添加样式,这样图片就正常显示了

1
2
3
<HomeLeft>
<img className='banner-img' src="//upload.jianshu.io/admin_banners/web_images/4516/cd9298634ca88ca71fc12752acf47917967a5d31.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/1250/h/540" />
</HomeLeft>
1
2
3
4
5
6
7
8
9
10
export const HomeLeft=styled.div`
float:left;
margin-left:15px;
padding-top:30px;
width:625px;
.banner-img {
width:625px;
height:270px;
}
`;

1.3 划分首页组件

可以把首页内容划分成几个组件

在home文件夹下创建一个名为components的文件夹,在里面创建Recommend.js、List.js、Writer.js、Topic.js的文件。

1.3.1 Topic的编程

由于我们首页的几个组件过于简单,如果给每一个小组件都设计一个style就显得过于设计。所以统一给它们上一层目录的style

进入到Topic.js文件,使用TopicWrapper标签,并且在home/style.js文件中创建该样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';
import { TopicWrapper , TopicItem} from '../style';

class Topic extends Component{
render(){
return (
<TopicWrapper>
<TopicItem>
<img className='topic-pic' src="//upload.jianshu.io/collections/images/283250/%E6%BC%AB%E7%94%BB%E4%B8%93%E9%A2%98.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64" />
手绘手绘
</TopicItem>
</TopicWrapper>
);
}
}

export default Topic;
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
export const TopicWrapper=styled.div`
overflow:hidden;
padding: 20px 0 10px 0;
margin-left:-18px;
`;

export const TopicItem=styled.div`
float:left;
height:32px;
line-height:32px;
padding-right:10px;
margin-bottom:18px;
margin-left:18px;
background:#f7f7f7;
font-size:14px;
color:#000;
border:1px solid #dcdcdc;
border-radius:4px;
.topic-pic {
display:block;
margin-right:10px;
float:left;
width:32px;
height:32px;
}
`;
1.3.2 创建reducer

src/store/reducer中大的reducer将若干个小的reducer组合一起。所以home页面应该由自己的reducer管理自己的数据。

在home目录下创建一个store的文件夹,并且在里面创建reducer.js文件,参照header下的reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
import { fromJS } from 'immutable'

const defaultState=fromJS({

});

export default (state=defaultState,action)=>{
switch(action.type){

default:
return state;
}
}

我们的topicItem肯定是一个数组来管理的,所以reducer中就需要定义一个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { fromJS } from 'immutable'

const defaultState=fromJS({
topicList:[
{id:1,title:'手绘',imgUrl:'//upload.jianshu.io/collections/images/283250/%E6%BC%AB%E7%94%BB%E4%B8%93%E9%A2%98.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:2,title:'读书',imgUrl:'//upload.jianshu.io/collections/images/4/sy_20091020135145113016.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:3,title:'自然科普',imgUrl:'//upload.jianshu.io/collections/images/76/12.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:4,title:'故事',imgUrl:'//upload.jianshu.io/collections/images/95/1.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:5,title:'简书电影',imgUrl:'//upload.jianshu.io/collections/images/21/20120316041115481.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:6,title:'摄影',imgUrl:'//upload.jianshu.io/collections/images/83/1.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},
{id:7,title:'旅行 在路上',imgUrl:'//upload.jianshu.io/collections/images/13/%E5%95%8A.png?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'},

]
});

export default (state=defaultState,action)=>{
switch(action.type){

default:
return state;
}
}

并且在src/store/reducer中引入该ruducer,同理我们不想让外部直接饮用该reducer,需要在pages/home/store下创建index.js,来引入和导出该reducer

1
2
3
4
5
6
7
8
import {combineReducers} from 'redux-immutable';
import { reducer as headerReducer } from '../common/header/store';
import { reducer as homeReducer } from '../pages/home/store';

export default combineReducers({
header:headerReducer,
home:homeReducer
})
1
2
3
4
//pages/home/store下创建index.js
import reducer from './reducer';

export { reducer };
1.3.3 连接Topic和reducer

这时候就需要让Topic和store做连接了,怎么连接?在Topic.js中引入react-redux的connect即可

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 { connect } from 'react-redux';
import { TopicWrapper , TopicItem} from '../style';

class Topic extends Component{
render(){
return (
<TopicWrapper>
{
this.props.list.map((item)=>{
return (
<TopicItem key={item.get('id')}>
<img
className='topic-pic'
src={item.get('imgUrl')} />
{item.get('title')}
</TopicItem>
);
})
}

</TopicWrapper>
);
}
}

const mapStateToProps=(state)=>{
return {
list:state.get('home').get('topicList')
}
}

export default connect(mapStateToProps,null)(Topic);
1.3.4 文章列表

和Topic没什么区别,只是再走一遍流程,不再重复

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 { connect } from 'react-redux';
import { ListItem , ListInfo} from '../style';

class List extends Component{
render(){
return (
<div>
{
this.props.list.map((item)=>{
return (
<ListItem key={item.get('id')}>
<img className='list-pic' src={item.get('imgUrl')} />
<ListInfo>
<h3 className='title'>{item.get('title')}</h3>
<p className='desc'>{item.get('desc')}</p>
</ListInfo>
</ListItem>
)
})
}
</div>
);
}
}

const mapStateToProps=(state)=>{
return {
list:state.get('home').get('articleList')
}
}

export default connect(mapStateToProps,null)(List);
1
2
3
4
5
6
7
8
9
10
const defaultState=fromJS({
...
articleList:[
{id:1,title:'全城百姓举牌欢迎,不料被将军看到牌后一字,悄悄对手下说:屠城',desc:'文/历史大农 简短一句话,剖析一段史。让“历史大农”用简洁的语言,为您讲述一段历史辛密。 军事家、战略家——“明太祖”朱元璋 明太祖朱元璋,作为...',imgUrl:'//upload-images.jianshu.io/upload_images/11919269-0a22c403002a1371?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240'},
{id:2,title:'每天最重要的2小时:让自己成为高效的人',desc:'工作、加班、学习、运动、陪伴家人,当然还有娱乐,我们每天都被时间的脚步追赶着,每一刻都是忙碌的,而忙碌永远没有劲头。 当下,时间俨然已经成为最宝...',imgUrl:'//upload-images.jianshu.io/upload_images/10746568-26f4733a3ad87c62.png?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240'},
{id:3,title:'是可忍,孰不可忍!|140字微小说',desc:'多少个夜晚 你真心相伴 不离不弃 多少个夜晚 你亲昵纠缠 不眠不休 你已经在我心底留下了深深的印迹 我想你 魂牵梦萦 如今 我终于将你捧在了手心...',imgUrl:'//upload-images.jianshu.io/upload_images/12309992-4e21d79f92914f76.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240'},
{id:4,title:'还在纠结秋冬穿什么?8套秋冬搭配轻松搞定',desc:'第一套: 杏色的针织荷叶边的垂感中长裙搭配同色系的折线宽松毛衣,视觉上看着很舒服,比较优雅的颜色搭配,适合皮肤中性偏白的女生,很显气质,同时竖条...',imgUrl:'//upload-images.jianshu.io/upload_images/12861608-90ae71664fbb9d99?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240'},

]
});

可是这时候控制台会报一些警告,这时由于使用img标签都要填一个alt属性,只需要给img添加一个alt标签即可。

1
alt=''
1.3.5 组件向样式传递属性

有时候我们希望在组件中定义一个属性,然后在style中使用,怎么办呢?

我们可以在组件RecommendItem中定义一个imgUrl的属性,然后在style中通过props来传递

1
2
3
//Recommend.js
<RecommendItem imgUrl="http://cdn2.jianshu.io/assets/web/banner-s-3-7123fd94750759acf7eca05b871e9d17.png">
</RecommendItem>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//style.js
//这时原始的
export const RecommendItem=styled.div`
width:280px;
height:50px;
margin-bottom:6px;
background:url(http://cdn2.jianshu.io/assets/web/banner-s-3-7123fd94750759acf7eca05b871e9d17.png);
background-size:contain;
`;

//这时最新的
export const RecommendItem=styled.div`
width:280px;
height:50px;
margin-bottom:6px;
background:url(${(props)=>props.imgUrl});
background-size:contain;
`;
1.3.6 通过ajax获取数据

实际上我们hone页面的数据是从后台获取的,所以不能自reducer中把数据写死。

首先我们在public/api/下建立一个home.json文件,并写入home的数据

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
{
"success": true,
"data": {
"topicList": [{
"id": 1,
"title": "社会热点",
"imgUrl": "//upload.jianshu.io/collections/images/261938/man-hands-reading-boy-large.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64"
}, {
"id": 2,
"title": "手手绘",
"imgUrl": "//upload.jianshu.io/collections/images/21/20120316041115481.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64"
}],
"articleList": [{
"id": 1,
"title": "胡歌12年后首谈车祸",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "//upload-images.jianshu.io/upload_images/2259045-2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 2,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "//upload-images.jianshu.io/upload_images/2259045-2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 3,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "//upload-images.jianshu.io/upload_images/2259045-2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 4,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "//upload-images.jianshu.io/upload_images/2259045-2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}],
"recommendList": [{
"id": 1,
"imgUrl": "http://cdn2.jianshu.io/assets/web/banner-s-3-7123fd94750759acf7eca05b871e9d17.png"
}, {
"id": 2,
"imgUrl": "http://cdn2.jianshu.io/assets/web/banner-s-5-4ba25cf5041931a0ed2062828b4064cb.png"
}]
}
}

然后将home/store/reducer中的defaultState各个数据设置为空

1
2
3
4
5
const defaultState=fromJS({
topicList:[],
articleList:[],
recommendList:[]
});

然后在home/index.js中添加生命周期函数componentDidMound方法中发送ajax请求。表示当页面挂载完毕时发送请求;将获取的数据传给store,通过actioin来完成;这时候需要dispatch来发送,可是如何将home/index.js于store建立连接呢,使用connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mport axios from 'axios';
...
componentDidMount(){
axios.get('api/home.json').then((res)=>{
const result=res.data.data;
const action={
type:'change_home_data',
topicList:result.topicList,
articleList:result.articleList,
recommendList:result.recommendList,
}
this.props.changeHomeData(action);
})
}
...
const mapDispatchToProps=((dispatch)=>{
return {
changeHomeData(action){
dispatch(action);
}
}
})
export default connect(null,mapDispatchToProps)(Home);

这样就可以在/page/home/store/reducer.js中处理该action 了

1
2
3
4
5
6
7
8
9
10
11
12
export default (state=defaultState,action)=>{
switch(action.type){
case 'change_home_data':
return state.merge({
topicList:fromJS(action.topicList),
articleList:fromJS(action.articleList),
recommendList:fromJS(action.recommendList)
});
default:
return state;
}
}
1.3.7 异步代码的拆分

home/index.js应该是一个UI组件,它不应该有过多的逻辑处理,而componentDidMount中ui组件发ajax请求然后更新store,更新UI。这样不合适,可以将ajax请求直接放在changeHomeData,直接对返回结果进行派发,这样就将ui组件中逻辑剔除了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
componentDidMount(){
this.props.changeHomeData();
}
。。。
const mapDispatchToProps=((dispatch)=>{
return {
changeHomeData(action){
axios.get('api/home.json').then((res)=>{
const result=res.data.data;
const action={
type:'change_home_data',
topicList:result.topicList,
articleList:result.articleList,
recommendList:result.recommendList,
}
dispatch(action);
})

}
}
})

其实异步操作,这么放也不合理。我们可以使用redux-thunk,将它放在action中去管理

我们在home/store目录下创建一个actionCreators.js 的文件,并且在store/index.js中引入并输入,然后在home/index.js中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//actionCreator.js
import axios from 'axios';

export const getHomeInfo=()=>{
return (dispatch)=>{
axios.get('api/home.json').then((res)=>{
const result=res.data.data;
const action={
type:'change_home_data',
topicList:result.topicList,
articleList:result.articleList,
recommendList:result.recommendList,
}
dispatch(action);
})
}
}
1
2
3
4
5
//home/store/index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators.js';

export { reducer , actionCreators};
1
2
3
4
5
6
7
8
9
//home/index.js
const mapDispatchToProps=((dispatch)=>{
return {
changeHomeData(){
const action=actionCreators.getHomeInfo();
dispatch(action);
}
}
})

这样我们就把home/index.js中的逻辑移动到actionCreators中去了,这里的getHomeInfo还可以进一步优化,将action提出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const changeHomeData=(result)=>({
type:'change_home_data',
topicList:result.topicList,
articleList:result.articleList,
recommendList:result.recommendList,
})

export const getHomeInfo=()=>{
return (dispatch)=>{
axios.get('api/home.json').then((res)=>{
const result=res.data.data;
dispatch(changeHomeData(result));
})
}
}

同样可以创建一个constants.js文件,保存type常量

1
export const CHANGE_HOME_DATA='home/CHANGE_HOME_DATA';
1.3.8 创建阅读更多

当将列表滑动到最下面时,会出现阅读更多的按钮,点击该按钮会在列表中加载更多内容。它既可以放在列表之内,也可以放在列表之外。我们将它放在列表之内

我们在List组件中添加一个LoadMore的组件,并在style中创建该样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render(){
return (
<div>
{
this.props.list.map((item)=>{
return (
<ListItem key={item.get('id')}>
<img className='list-pic' src={item.get('imgUrl')} alt=''/>
<ListInfo>
<h3 className='title'>{item.get('title')}</h3>
<p className='desc'>{item.get('desc')}</p>
</ListInfo>
</ListItem>
)
})
}
<LoadMore>更多内容</LoadMore>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//style
export const LoadMore=styled.div`
width: 100%;
height: 40px;
line-height:40px;
margin: 30px 0;
text-align: center;
font-size: 15px;
border-radius: 20px;
color: #fff;
background-color: #a5a5a5;
display: block;
cursor:pointer;
`;

然后给LoadMore添加点击事件,这时只需要创建mapDispatchToProps即可,创建action等不再赘述

1
2
3
4
5
6
7
8
9
const mapDispatchToProps=(dispatch)=>{
return {
getMoreList(){
dispatch(actionCreators.getMoreList());
}
}
}

export default connect(mapStateToProps,mapDispatchToProps)(List);

真实情况其实需需要翻页,只需要给reducer中添加一个articlePage的字段即可,每次getMoreList时,将该字段加1即可,不再详述。

1.4 编写返回顶部

这代码量很小,单独写一个组件不合适。可以将它写在home的index.js中.

只需要在样式中定义该组件的样式;

然后添加点击事件,这里的点击事件和reducer关联不大,只是返回顶部

1
2
3
4
5
handlerScrollTop(){
window.scrollTo(0,0);
}
...
<BackTop onClick={this.handlerScrollTop}>回到顶部</BackTop>

而官网的效果时,当页面往下拖的时候才会出现,而首屏时是不会出现的,这时需要通过变量来保存了。在reducer中添加

1
2
3
4
5
6
7
const defaultState=fromJS({
topicList:[],
articleList:[],
recommendList:[],
articlePage:1,
showScroll:false
});

然后打开home下index.js,在里面拿showScroll数据,注意当组件移除时需要将该监听移出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{this.props.showScroll?
<BackTop onClick={this.handlerScrollTop}>回到顶部</BackTop>:
null}
...
componentDidMount(){
this.props.changeHomeData();

this.bindEvents();
}
componentWillUnmount(){
this.removeEvents();
}

bindEvents(){
window.addEventListener("scroll",this.props.changeScrollShow);
}
removeEvents(){
window.removeEventListener("scroll",this.props.changeScrollShow);
}

这个时候,它不显示了。

这时就需要监听滚动方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
componentDidMount(){
this.props.changeHomeData();
this.bindEvents();
}

bindEvents(){
window.addEventListener("scroll",this.props.changeScrollShow);
}
...
const mapDispatchToProps=((dispatch)=>{
return {
changeHomeData(){
const action=actionCreators.getHomeInfo();
dispatch(action);
},
changeScrollShow(e){
if(document.documentElement.scrollTop>100){
dispatch(actionCreators.toggleTopShow(true));
}else{
dispatch(actionCreators.toggleTopShow(false));
}
}
}
})

然后就可以通过reducer来操作了.

此时reducer中switch代码好像有点多,可以将每个case中返回的内容提取出来

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
const changeHomeData=(state,action)=>{
return state.merge({
topicList:fromJS(action.topicList),
articleList:fromJS(action.articleList),
recommendList:fromJS(action.recommendList)
});
}
const addArticleList=(state,action)=>{
return state.merge({
'articleList':state.get('articleList').concat(action.list),
'articlePage':action.nextPage
})
}
export default (state=defaultState,action)=>{
switch(action.type){
case constants.CHANGE_HOME_DATA:
return changeHomeData(state,action);
case constants.ADD_ARTICLE_LIST:
return addArticleList(state,action);
case constants.TOGGLE_SCROLL_SHOW:
return state.set('showScroll',action.show)
default:
return state;
}
}

1.5 路由跳转

由于页面很多时候并且不是所有的操作都要出发render。这时可以使用shouldComponentUpdate,来闭关频繁触发render,但是如果所有的子组件中都重写这个方法就太麻烦了,react内置了一个新的组件类型,叫做PureComponent,这个组件内部实现了shouldComponentUpdate,就不需要我们重复重写这个方法了。我们只要对List、Topic、Writer、Recommend组件都即成PureComponent就可以了。

注意:这里之所以可以使用PureComponent,是因为这个项目中我们使用了框架immutable.js,PureComponent和immutable数据才能无缝对接,如果没使用immutable可能就会出现问题。所以当使用PureComponent时建议使用immutable来管理数据

接下来来实现首页跳转到详情页

可以通过添加a标签,来实现页面的跳转,这时每跳转一次都会加载一次html,这就比较耗性能。这里我们要使用react-router,因为它可以实现单页面跳转,但也面跳转是指在页面跳转过程中只会加载一次html。可以使用react-router-dom中的Link来代替a标签

1
2
3
4
5
6
7
8
9
10
import { Link } from 'react-router-dom';
<a key={index} href='/detail'>
<ListItem>
</ListItem>
</a>
--------
<Link key={index} to='/detail'>
<ListItem>
</ListItem>
</Link>

同理我们对头部组件的logo组件也要使用Link,注意使用Link的组件必须在Router的内部,所以我们需要在App.js中,将Header组件放在Router的内部

1
2
3
<Link to='/'>
<Logo>
</Link>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<Route path='/' exact component={Home}></Route>
<Route path='/detail' exact component={Detail}></Route>
</div>
</BrowserRouter>
</Provider>

);
}
}

react实战2

一、修改搜索框

1.1、修改搜索框文字颜色和间距

当在搜索框填写的内容过长时,间距就不够了,并且输入的文字颜色有点深。我们首先调整一下文字颜色,在common/header/style.js中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const NavSearch=styled.input.attrs({
placeholder:'搜索'
})`
width:160px;
height:38px;
padding:0 30px 0 20px;
box-sizing:border-box;
border:none;
outline:none;
margin-top:9px;
margin-left:20px;
border-radius:19px;
background:#eee;
font-size:14px;
color:#666;
&::placeholder{
color:#999;
}
`;

1.2、修改搜索框鼠标动画

下面我们来添加一下鼠标的移入移出效果,搜过框的宽度锁着鼠标的变化会自动的变化。当聚焦的时候,搜索框就会变长。

react不建议我们直接操作dom,所以我们可以通过定义数据来实现,首先我们在common/header/index.js中添加constructor函数,并在里面定义state;并且在NavSearch标签中添加className={this.state.focused},

1
2
3
4
5
6
7
8
9
10
11
constructor(props){
super(props);
this.state={
focused:false,
};
}
...
<SearchWrapper>
<NavSearch className={this.state.focused ? 'focused':''}></NavSearch>
<i className='iconfont'>&#xe6dd;</i>
</SearchWrapper>

然后我们在common/header/style.js中添加focused的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const NavSearch=styled.input.attrs({
placeholder:'搜索'
})`
width:160px;
height:38px;
padding:0 35px 0 20px;
box-sizing:border-box;
border:none;
outline:none;
margin-top:9px;
margin-left:20px;
border-radius:19px;
background:#eee;
font-size:14px;
color:#666;
&::placeholder{
color:#999;
}
&.focused {
width:200px;
}
`;

并且注意当搜索框变长时,右边搜索按钮背景色也发生了变化,此时需要修改common/header/index.js中的i标签中添加新的className,并且在common/header/style.js中SearchWrapper标签的样式中设置iconfont搜索按钮的背景颜色

1
2
3
4
5
6
7
8
9
10
<SearchWrapper>
<NavSearch className={this.state.focused ? 'focused':''}></NavSearch>
<i className='iconfont'>&#xe6dd;</i>
</SearchWrapper>

//改为
<SearchWrapper>
<NavSearch className={this.state.focused ? 'focused':''}></NavSearch>
<i className={this.state.focused ? 'focused iconfont':'iconfont'}>&#xe6dd;</i>
</SearchWrapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const SearchWrapper=styled.div`
float:left;
position:relative;
.iconfont {
position:absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
border-radius:15px;
text-align:center;
&.focused{
background:#777;
color: #fff;
}
}
`;

1.3、实现搜索框事件绑定

接下来实现,通过事件来实现长短切换,首先focused默认时false,然后在NavSearch标签中添加onFocus属性,在constructor实现对this的绑定,然后就可以添加handleInputFocus函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
constructor(props){
super(props);
this.state={
focused:false,
};
this.handleInputFocus=this.handleInputFocus.bind(this);
}
...
<NavSearch
className={this.state.focused ? 'focused':''}
onFocus={this.handleInputFocus}>
</NavSearch>
...
handleInputFocus(){
this.setState({
focused:true
});
}

这时就实现了当搜索框聚焦时,就会变长,并且搜索按钮背景色变暗。可以失焦时,却没有变短,这很好办,只需添加onBlur属性即可

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
constructor(props){
super(props);
this.state={
focused:false,
};
this.handleInputFocus=this.handleInputFocus.bind(this);
this.handleInputBlur=this.handleInputBlur.bind(this);
}
...
<NavSearch
className={this.state.focused ? 'focused':''}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}>
</NavSearch>
...
handleInputFocus(){
this.setState({
focused:true
});
}
handleInputBlur(){
this.setState({
focused:false
});
}

此时就实现了聚焦和失焦的效果,但是此时还没有动画的效果,下面添加动画。

1.4、添加动画变长变短

首先在控制台,进入jianshu项目,安装react-transition-group的插件,

1
npm install react-transition-group --save

然后在common/header/index.js中引入CSSTransition,然后用CSSTransition将NavSearch包裹起来

1
2
3
4
5
6
7
8
9
10
11
12
13
import { CSSTransition } from 'react-transition-group';
。。。
<CSSTransition
in={this.state.focused} //入场动画
timeout={200} //动画时长
classNames="slide"
>
<NavSearch
className={this.state.focused ? 'focused':''}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}>
</NavSearch>
</CSSTransition>

此时就会在外层挂载几个样式,然后找到SearchWrapper标签的样式,在common/header/style.js中SearchWrapper标签的样式中设置slide-enter和slide-enter-active设置展开效果,通过slide-exit和slide-exit-active设置缩回效果

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
export const SearchWrapper=styled.div`
float:left;
position:relative;
.slide-enter {
width:160px;
transition:all .2s ease-out;
}
.slide-enter-active {
width:240px;
}
.slide-exit {
width:240px;
transition:all .2s ease-out;
}
.slide-exit-active {
width:160px;
}
.iconfont {
position:absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
border-radius:15px;
text-align:center;
&.focused{
background:#777;
color: #fff;
}
}
`;

二、使用React-Redux管理数据

首先我们看CSSTransition标签,它会给内部的NavSearch标签自动添加样式,这些样式在它外层样式(SearchWrapper)中定义,包含slide-enter和slide-enter-active、slide-exit和slide-exit-active。而实际中,应该将这些样式写在NavSearch标签上。

首先我们将SearchWrapper样式中的slide-enter和slide-enter-active、slide-exit和slide-exit-active样式剪切到NavSearch样式中,注意在前面添加&表示同级样式

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
export const NavSearch=styled.input.attrs({
placeholder:'搜索'
})`
width:160px;
height:38px;
padding:0 35px 0 20px;
box-sizing:border-box;
border:none;
outline:none;
margin-top:9px;
margin-left:20px;
border-radius:19px;
background:#eee;
font-size:14px;
color:#666;
&::placeholder{
color:#999;
}
&.focused {
width:240px;
}
&.slide-enter {
width:160px;
transition:all .2s ease-out;
}
&.slide-enter-active {
width:240px;
}
&.slide-exit {
width:240px;
transition:all .2s ease-out;
}
&.slide-exit-active {
width:160px;
}
`;

这里用到了state来管理数据,如果是小型项目这么写没问题,但是大型项目时需要使用redux来管理数据。为了便于维护,所有的数据尽量放在redux中

2.1 安装使用redux

首先在jianshu目录先安装redus和react-redux

1
2
3
4
//安装redux数据框架
npm install --save redux
//方便在react中使用redux
npm install --save react-redux

然后在src目录下创建一个store的文件夹,在store文件夹下创建一个index.js的文件;并且创建一个reducer(src/store/reducer.js)给它

1
2
3
4
5
6
import { createStore } from 'redux';
import reducer from './reducer';

const store=createStore(reducer);

export default store;
1
2
3
4
5
const defaultState={};

export default (state=defaultState,action)=>{
return state;
}

这时就创建好了store,就需要往store中存数据和取数据 。

2.2 使用store存取数据

打开src/App.js,引入store,并且从react-redux中引入Provider,然后在reder

函数句将Header标签用Provider包裹起来,设置store属性,表示Provider把store中的数据都提供给了它内部的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header/index.js';
import store from './store';

class App extends Component {
render() {
return (
<Provider store={store}>
<Header />
</Provider>

);
}
}

export default App;

然后在src/common/header/index.js文件中,做一下数据的连接,首先从react-redux引入connect,并且利用connect进行输出

1
2
3
4
5
6
7
8
9
10
11
12
import React , { Component } from 'react';
import { connect } from 'react-redux';
。。。
const mapStateToProps=(state)=>{
return {}
}

const mapDispatchToProps=(dispatch)=>{
return {}
}

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

然后就可以清理src/common/header/index.js文件中的数据了,删除里面的数据定义

this.state={
focused:false,
};

src/store/index.js文件中定义默认的state,包含focus

1
2
3
4
5
6
7
const defaultState={
focused:false
};

export default (state=defaultState,action)=>{
return state;
}

这样就把focused的数据放在了redux的仓库里。接着就应该将仓库里的focused映射到props中去,可以在mapStateToProps中操作

1
2
3
4
5
const mapStateToProps=(state)=>{
return {
focused:state.focused
}
}

此时就可以将src/common/header/index.js中涉及focus的数据,改为this.props.focused了。

这时也可以把之前的handle函数全部删除了,因为不直接操作state了。修改onFocus和onBlur,并在mapDispatchToProps中创建相应的action,然后做分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<NavSearch 
className={this.props.focused ? 'focused':''}
onFocus={this.props.handleInputFocus}
onBlur={this.props.handleInputBlur}></NavSearch>
。。。
const mapDispatchToProps=(dispatch)=>{
return {
handleInputFocus(){
const action={
type:'search_focus'
};
dispatch(action);
},
handleInputBlur(){
const action={
type:'search_blur'
};
dispatch(action);
}
}
}

然后在src/store/reducer.js中对action类型进行判断,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

此时,我们的页面又有聚焦和失焦功能了。

此时我们也就没必要使用构造函数了,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
import React from 'react';
import { connect } from 'react-redux';
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper} from './style.js';
import { CSSTransition } from 'react-transition-group';

const Header= (props)=>{
return (
<HeaderWrapper>
<Logo href='/'/>
<Nav>
<NavItem className='left active'>首页</NavItem>
<NavItem className='left'>下载App</NavItem>
<NavItem className='right'>登陆</NavItem>
<NavItem className='right'>
<i className='iconfont'>&#xe636;</i>
</NavItem>
<SearchWrapper>
<CSSTransition
in={ props.focused }
timeout={200}
classNames="slide"
>
<NavSearch
className={props.focused ? 'focused':''}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}></NavSearch>
</CSSTransition>
<i className={props.focused ? 'focused iconfont':'iconfont'}>&#xe6dd;</i>
</SearchWrapper>
</Nav>
<Addition>
<Button className='writting'>
<i className='iconfont'>&#xe60e;</i>
写文章
</Button>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
);
}

const mapStateToProps=(state)=>{
return {
focused:state.focused
}
}

const mapDispatchToProps=(dispatch)=>{
return {
handleInputFocus(){
const action={
type:'search_focus'
};
dispatch(action);
},
handleInputBlur(){
const action={
type:'search_blur'
};
dispatch(action);
}

}
}
export default connect(mapStateToProps,mapDispatchToProps)(Header);

这时我们还不能使用redux开发者工具,怎么办呢,使用redux-devtools-extension,这时需哟啊对store对什么处理呢?从redux中引入compose,然后使用composeEnhancers增强功能即可

1
2
3
4
5
6
7
8
import { createStore, compose } from 'redux';
import reducer from './reducer';


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

export default store;

####2.3 reducer分类

reducer可以看作管理员手册,但是当手册管理内容增多时,怎么办呢?可以对手册作个分类,这样图书馆管理员再次查找手册时,就可以先找分类,然后再找内容。redux就可以将一个reducer拆分成很多小的reducer。

因为focused就是header的数据,我们可以在src/common/header/下创建文件夹store,并在里面创建reducer.js的文件,然后将src/store/reducer中的数据剪切到src/common/header/store/reducer.js之中。

这时src/store/reducer已经为空,项目也会报错,相当于把src/store/reducer中的笔记本拆分出去了,这里如何将拆分出去的笔记本整合起来呢?Redux提供了combineReducers使用,它可以将很多小的reducer合并成大的reducer,可以在src/store/reducer中如下

1
2
3
4
5
6
import {combineReducers} from 'redux';
import headerReducer from '../common/header/store/reducer.js';

export default combineReducers({
header:headerReducer
})

此时页面也恢复正常了,通过redux-devtools-extension工具可以看到focused位于了header下,而不再是顶层。但是这时搜索框的动画却没了,怎么回事呢

因为我们在src/common/header/index.js中的mapStateToProps是直接使用state 下的focused,这里需要将它们改造成state.header.focused

1
2
3
4
5
const mapStateToProps=(state)=>{
return {
focused:state.header.focused
}
}

这时我们的页面就恢复正常了

2.3 总结1

我们可以对每一部分,创建自己的reducer,然后在总的reducer中引入各个部分的reducer,并利用redex的combineReducers来创建一个总的reducer,并输出

1
2
3
4
5
6
7
import {combineReducers} from 'redux';
import headerReducer from '../common/header/store/reducer.js';

const reducer=combineReducers({
header:headerReducer
})
export default reducer;

这里还有一个可优化的地方,我们看到在引入headerReducer时路径非常长,我们可以在src/common/header/store下新建一个index.js的文件,然后在里面引入自己的reducer,并且输出;然后我们在总的reducer文件中就可以直接引用该src/common/header/store就可以,它会自动寻找该路径下的index.js文件;为了避免命名冲突可以使用as关键字。

src/common/header/store/index.js

1
2
3
import reducer from './reducer.js';

export { reducer };

src/store/index.js

1
2
3
4
5
6
7
import {combineReducers} from 'redux';
~~import headerReducer from '../common/header/store/reducer.js';~~
import { reducer as headerReducer } from '../common/header/store';

export default combineReducers({
header:headerReducer
})

2.4 使用actionCreator创建action

这里还要注意,创建action时尽量不要使用字符串,我们使用actionCreator来通过常量创建action。

我们在src/common/header/store/下创建一个名为actionCreators.js的文件。

1
2
3
4
5
6
7
export const searchFocus=()=>({
type:'search_focus'
});

export const searchBlur=()=>({
type:'search_blur'
});

然后在src/common/header/index.js中引入该creator,然后修改mapDispatchToProps中相应ation的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as actionCreators from './store/actionCreators';
。。。
const mapDispatchToProps=(dispatch)=>{
return {
handleInputFocus(){
const action=actionCreators.searchFocus();
dispatch(action);
},
handleInputBlur(){
const action=actionCreators.searchBlur();
dispatch(action);
}

}
}

更进一步,将src/common/header/store/actionCreator.js中的type用常量给替换掉。在

src/common/header/store/下创建一个constants.js的文件;并且在src/common/header/store/actionCreator.js和在src/common/header/store/reducer.js文件中 中引用该常量;

1
2
export const SEARCH_FOCUS='header/SEARCH_FOCUS';
export const SEARCH_BLUR='header/SEARCH_BLUR';
1
2
3
4
5
6
7
8
9
import * as constants from './constants.js';

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

export const searchBlur=()=>({
type:constants.SEARCH_BLUR
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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中引用了src/common/header/store/actionCreator.js文件中的内容,而我们希望store下只有一个出口文件,而不是一个一个引入store下具体的文件,我们在src/common/header/store/index.js中作统一处理,这样这个index文件就引入了store文件夹下的所有内容,在外部只需引入这一个index文件即可

1
2
3
4
5
import reducer from './reducer.js';
import * as actionCreators from './actionCreators.js';
import * as constants from './constants.js';

export { reducer , actionCreators , constants};

这时也需要将src/common/header/index.js中引入的creator换一下

1
2
~~import * as actionCreators from './store/actionCreators';~~
improt {actionCreators} from './store/index.js';

2.5 总结2

这时,store文件夹下的内容就非常清晰了,将所有输出都通过index.js作统一处理,外部只需要引用index.js文件就可以。

2.6 使用immutable来管理store中的数据

reducer只能获取state中的数据,而不能改变原始state中的数据,而时创建一个新的state返回。这样很容易出错,immutable就是解决这个问题。

immutable就是不可变更的,如果将state设成immutable,那么这个state就是不可改变的,这样reducer就不会出现问题 ,

首先安装immutable.js,

1
2
3
npm install immutable
//或
yarn add immutable

接下来就需要将state变成immutable对象了,来到src/common/header/store/reducer.js,引入immutable中的fromJS,他可以帮助我们把一个js对象转化为一个immutable对象,然后就可以将defaultState转化为immutable对象了

1
2
3
4
5
import { fromJS } from 'immutable'
...
const defaultState=fromJS({
focused:false
});

然后打开src/common/header/index.js文件,修改里面mapStateToProps方法中的数据

1
2
3
4
5
6
const mapStateToProps=(state)=>{
return {
~~focused:state.header.focused~~
focused:state.header.get('focused ')
}
}

但是此时,点击搜索框还是会报错,这是由于这时在reducer中返回的是普通对象,需要使用immutable的set方法来做更改,immutable对象set方法会结合之前immutable对象的值和设置的值返回一个全新的对象。

1
2
3
4
5
6
7
8
9
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);
}
return state;
}

这样就可以避免不小心更改state的错误了。

2.7 使用redux-immutable统一数据格式

我们注意看一下 focused:state.header.get(‘focused ‘)这个代码,state是一个js对象,而state.header是一个immutable对象,所以要调用focused的时候需要xian 调用.再调用.get()。这种混在的方式不是很好。

我们可以考虑将state变成immutable对象,也就是对src/store/reduce.js,这时我们需要依赖一个第三方的模块,redux-immutable,

1
npm install redux-immutable

之前我们redux从redux中来,赖在我们修改它从redux-immutable中来

1
2
~~import {combineReducers} from 'redux';~~
import {combineReducers} from 'redux-immutable';

但是此时页面会报错,我们需要改变一下src/common/header/index.js,里面的state方法就不能用.了,需要用get()方法了,这样整个对数据的操作就统一了

1
2
3
4
5
const mapStateToProps=(state)=>{
return {
focused:state.get('header').get('focused ')
}
}

immutable中还有很多写法,比如可以将focused:state.get('header').get('focused ')用getIn方法改写focused:state.getIn(['header','focused']),数组表示取header下面的focused项。

三、热门搜索布局

当我们鼠标聚焦在搜索框时,会弹出热门搜索框,,接下来我们做搜索部分的热门搜索的布局。

3.1 添加SearchInfo

首先我们需要在src/common/header/index.js中添加一个SearchInfo组件,并且在style中添加这个组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper,SearchInfo} from './style.js';
...
<SearchWrapper>
<CSSTransition
in={ props.focused }
timeout={200}
classNames="slide"
>
<NavSearch
className={props.focused ? 'focused':''}
onFocus={props.handleInputFocus}
onBlur={props.handleInputBlur}></NavSearch>
</CSSTransition>
<i className={props.focused ? 'focused iconfont':'iconfont'}>&#xe6dd;</i>
<SearchInfo></SearchInfo>
</SearchWrapper>

然后打开src/common/header/style.js中编写SearchInfo

1
2
3
4
5
6
7
8
9
export const SearchInfo=styled.div`
position:absolute;
left:0;
top:50px;
width:240px;
height:100px;
padding:0 20px;
background:green;
`;

然后设置阴影,从简书官网获取,添加阴影,就可以去掉background了

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

3.2 添加SearchInfoTitle

然后在SearchInfo里面添加新标签SearchInfoTitle了

首先在src/common/header/index.jsSearchInfo组件内添加SearchInfoTitle,并且引入SearchInfoTitle,然后在style中定义该title

1
2
3
4
5
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper,SearchInfo,SearchInfoTitle} from './style.js';
。。。
<SearchInfo>
<SearchInfoTitle>热门搜索</SearchInfoTitle>
</SearchInfo>
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;
`;

3.3 添加写换一批

我们在SearchInfoTitle中添加组件SearchInfoSwitch组件,同样引入并定义该样式

1
2
3
4
5
6
7
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper,SearchInfo,SearchInfoTitle, SearchInfoSwitch} from './style.js';
。。。
<SearchInfo>
<SearchInfoTitle>热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
</SearchInfo>
1
2
3
4
5
6
7
8
export const SearchInfoSwitch=styled.div`
float: right;
font-size: 13px;
color: #969696;
background-color: transparent;
border-width: 0;
padding: 0;
`;

3.4 添加提示内容

然后在SearchInfoTitle下方添加一个div,并且在里面添加组件SearchInfoItem,同样引入并创建该样式,它是一个a标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper,SearchInfo,SearchInfoTitle, SearchInfoSwitch, SearchInfoItem} from './style.js';
。。。
<SearchInfo>
<SearchInfoTitle>热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<div>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
</div>
</SearchInfo>
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:15px;
font-size:12px;
border:1px solid #ddd;
color:#333;
border-radius:3px;
`;

3.5 添加SearchInfoList

这时发现item越界了,这时由于在外层SearchInfo中把高度写死了,去掉下面的height就可以了。这时就可以了 。

这时我们将外层的div改为SearchInfoList,引入并创建该样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { HeaderWrapper , Logo , Nav , NavItem , NavSearch, Addition,Button,SearchWrapper,SearchInfo,SearchInfoTitle, SearchInfoSwitch, SearchInfoItem, SearchInfoList} from './style.js';
。。。
<SearchInfo>
<SearchInfoTitle>热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
</SearchInfoList>
</SearchInfo>
1
2
3
export const SearchInfoList=styled.div`
overflow:hidden;
`;

3.6 聚焦显示失焦隐藏

我们在src/common/header/index.js中声明一个方法,getListArea,接受一个参数,参数是真就返回列表,参数为假就不返回列表,然后在之前SearchInfo标签处添加该方法

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
const getListArea=(show)=>{
if(show){
return (
<SearchInfo>
<SearchInfoTitle>热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>教育</SearchInfoItem>
</SearchInfoList>
</SearchInfo>
)
}else{
return null;
}
}
。。。
<SearchWrapper>
<CSSTransition
。。。
</CSSTransition>
<i className={props.focused ? 'focused iconfont':'iconfont'}>&#xe6dd;</i>
{getListArea(props.focused)}
</SearchWrapper>

这样热门搜索框就做好了。

3.7 再次换一批

header会越来约庞大,如果还用无状态组件的话会特别麻烦,需要将它换成一个常规的Component组件,继承自component,在render函数中直接返回就可以;

这里面也没有props这个参数了,需要将它们全部转为this.props;

同时需要将getListArea方法,添加到累内部,在调用的地方换为this.getListArea

1
...

当我们第一次聚焦input框的时候,会发送ajax请求,获取热门搜索关键字,再次聚焦的时候不会发送请求,而是直接使用之前的关键字。

所以在header的reducer中不仅要保存focus的数据,还要保存热门关键字数组list,

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

而发送ajax请求,我们统一放在redux-thunk中去处理,首先安装redux-thunk

1
npm install redux-thunk

它应该在创建store时被使用,我们是在src/index.js中创建的store,在里面引入并使用

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


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

export default store;

然后就可以在action中做异步操作了,来到src/common/header/index.js文件,在mapDispatchToProps方法中派发一个action,

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

并且在actionCreators.js文件中添加一个函数,之前时返回一个对象,现在不返回对象了而是返回一个函数,在这个函数中发出一个一步请求,这就可以使用一个第三方模块axios了,首先安装

1
npm install axios
1
2
3
4
5
6
7
8
9
export const getList=()=>{
return (dispatch)=>{
axios.get('api/headerList.json').then((res)=>{//成功

}).catch(()=>{//失败
console.log("error");
})
}
}

这时很明显不会获取到数据,因为我们没有这个后台接口,这时我们可以利用create-react来模拟数据,首先在public目录下创建一个文件夹api,在里面创建一个问价headList.json,在里面随意添加几个文字,这时在浏览器中输入localhost:3000/api/headerList.json,就可以访问你写入的内容了。通过这个特性我们可以写一些假数据

在json文件中添加:

1
2
3
4
{
'success':ture,
"data":["行距杯2018征文","区块链","小程序","vue","毕业","PHP","故事","flutter","理财","美食","投稿","手帐","书法","PPT","穿搭","打碗碗花","简书","姥姥的澎湖湾","设计","创业","交友","籽盐","教育","思维导图","疯哥哥","梅西","时间管理","golang","连载","自律","职场","考研","慢世人","悦欣","一纸vr","spring","eos","足球","程序员","林露含","彩铅","金融","木风杂谈","日更","成长","外婆是方言","docker"]
}

这时我们就能在请求成功之后派发action了,如下:

1
2
3
4
5
6
7
8
9
10
axios.get('api/headerList.json').then((res)=>{//成功
const data=res.data;
const action={
type:'change_list',
data:data.data
}
dispatch(action);
}).catch(()=>{//失败
console.log("error");
})

由于请求本身就是在actionCreators.js文件中的,所以可以这么改:

1
2
3
4
5
6
7
8
9
10
11
const changeList=(data)=>{
type:constants.CHANGE_LIST,
data
}
...
axios.get('api/headerList.json').then((res)=>{//成功
const data=res.data;
dispatch(changeList(data));
}).catch(()=>{//失败
console.log("error");
})

接下来就可以去src/common/header/store/reducer.js中继续写代码了

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

注意这么写肯定是有问题的,因为我们在defaultState是一个immutable对象,里面的list也是一个immutable对象,而action.data是一个普通的js数组,可定会出错的。解决方法很简单,只需要在actionCreators.js中将data变成一个immutable对象就可以了

1
2
3
4
5
import {fromJS} from 'immutable';
const changeList=(data)=>({
type: constants.CHANGE_LIST,
data: fromJS(data)
})

然后就可以在src/common/header/index.js中取数据了

在mapStateToProps方法中补充list属性

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

然后修改getListArea,首先将参数去掉直接在方法内部调用this.props.focused,然后将SearchInfoList内部写一个循环表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}

...
{ this.getListArea() }

3.8 简化代码

(1)

这时我们的src/common/header/store/actionCreators.js中大部分内容需要暴漏出去,但是像changeList的方法不用暴漏出去。像这种不暴漏的方法建议要么都放在顶部要么都放在底部;

(2)

src/common/header/index.js中,导出都用this.props.list或this.props.focused,其实可以精简一下,在getListArea方法中添加入戏内容,这样就无需每次都使用this.props.XXX了,就可以直接使用focused和list了

1
2
3
4
getListArea(){
const {focused,list}=this.props;
...
}

同理在render方法中我们可以使用,来简化

1
2
3
4
render(){
const {focused,handlerInputFocused,handlerInputBlur}
...
}

(3)

src/common/header/store/reducer.js文件中,大量的使用了if语句,可以使用switch语句来做替换

3.9 实现换一批功能

3.9.1给提示框分页

我们热门索搜提示显示了所有的内容,而简书官网每次只显示十个内容。我们先在src/common/header/store/reducer.js文件中给defaultStatus添加page和totalPage两个属性,表示当前页码和总页数

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

然后在src/common/header/store/actionCreators.js文件中给changeList方法,补充totalPage属性

1
2
3
4
5
const changeList=(data)=>({
type: constants.CHANGE_LIST,
data: fromJS(data),
totalPage:Math.ceil(data.length/10),
})

这样当取完列表项数据时,就可以知道总页数了;action会被派发给reducer,需要在src/common/header/store/reducer.js中对CHANGE_LIST方法不仅哟啊改变list,而且还要改变totalPage内容

1
2
3
4
5
6
7
8
9
10
11
12
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("totalPage",action.totalPage);
default:
return state;
}
}

然后在src/common/header/index.js拿到page页码

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

在getListArea中使用page,来创建10个item,放置在相应位置。注意list由于是immutable对象不能直接使用下标,需要将它转化为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
getListArea(){
const {focused,list,page}=this.props;
const newList=list.toJS();
const pageList=[];
for (let i = (page-1)*10; i <page*10; i++) {
pageList.push(
<SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
);
}
if(focused){
return (
<SearchInfo>
<SearchInfoTitle>热门搜索
<SearchInfoSwitch>换一批</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
{
pageList
}
</SearchInfoList>
</SearchInfo>
)
}else{
return null;
}
}

此时我们的热门索搜提示就展示10条内容了。

3.9.1实现换一批换页

此时我们点击换一批,发现提示框直接消失了;简书官网则不是这样的,只有在鼠标移出搜索框时才隐藏。所有提示框不仅仅是依赖focused来隐藏的,我们需要额外监听一个属性,鼠标是否在提示框内mouseIn,首先给src/common/header/store/reducer.js添加mouseIn属性

1
2
3
4
5
6
7
const defaultState=fromJS({
focused:false,
list:[],
mouseIn:false,
page:1,
totalPage:1,
});

给constants添加MOUSE_IN和MOUSE_OUT常量

1
2
export const MOUSE_IN="header/MOUSE_IN";
export const MOUSE_OUT="header/MOUSE_OUT";

给SearchInfo添加onMouseEnter和onMouseLeave;在mapStateToProps中引入mouseIn;在mapDispatchToProps中添加handlerMouseEnter,handlerMouseLeave操作;在actionCreators中创建mouseEnter和mouseLeave事件;在reducer中处理这两个事件

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
const {focused,list,page,handlerMouseEnter,handlerMouseLeave}=this.props;
....
<SearchInfo
onMouseEnter={handlerMouseEnter}
onMouseLeave={handlerMouseLeave}
>
....
const mapStateToProps=(state)=>{
return {
focused:state.get('header').get('focused'),
list:state.getIn(['header','list']),
page:state.getIn(['header','page']),
mouseIn:state.getIn(['header','mouseIn']);
}
}
const mapDispatchToProps=(dispatch)=>{
return {
handleInputFocus(){
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
handleInputBlur(){
dispatch(actionCreators.searchBlur());
},
handlerMouseEnter(){
dispatch(actionCreators.mouseEnter());
},
handlerMouseLeave(){
dispatch(actionCreators.mouseLeave());
}

}
}
1
2
3
4
5
6
7
8
...
export const mouseEnter=()=>({
type:constants.MOUSE_IN
});

export const mouseLeave=()=>({
type:constants.MOUSE_OUT
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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("totalPage",action.totalPage);
case constants.MOUSE_IN:
return state.set('mouseIn',true);
case constants.MOUSE_OUT:
return state.set('mouseIn',false);
default:
return state;
}
}

然后就可以对getListArea方法,foucese的判断再添加一个mouseIn了

1
2
3
if(focused || mouseIn){
...
}

接下来就可以实现点击事件了 给SearchInfoSwitch添加点击事件

1
<SearchInfoSwitch onClick={()=>handlerChangePage(page,totalPage)}>换一批</SearchInfoSwitch>

其余略,同上

3.9.3 给immutable对象修改多个属性

如果给immutable修改多个属性,连续使用set会和长,这时可以使用merge代替

1
2
3
4
5
6
state.set('list',action.data).set("totalPage",action.totalPage);
//改为
state.merge({
list:action.data,
totalPage:action.totalPage,
});

3.10 实现刷新旋转按钮

3.10.1 添加iconfont

首先去iconfont将spin按钮Tina 驾到购物车,然后将jianshu项目下的iconfont重新下载到本地。将iconfont.eot、

iconfont.svg、iconfont.ttf、iconfont.woff文件拷贝到我们的src/statics/iconfont/下,然后打开我们的iconfont.js文件,将我们解压缩文件中的iconfont.css文件将里面的内容复制到iconfont.js中,将@font-face中的内容替换掉,其他的不用变。

这样我们项目中的iconfont就替换成了最新的,对老的功能不会有影响。

打开src/common/header/index.js,在SearchInfoSwitch标签内部添加一个i 标签,写入

1
2
3
4
<SearchInfoSwitch onClick={()=>handlerChangePage(page,totalPage)}>
<i className='iconfont'>&#xe851;</i>
换一批
</SearchInfoSwitch>

此时看一些页面,发现改图标出现在了我们提示框的右下角,这时由于它跟上边的放大镜按钮的“靠右居下”的布局影响了,我们在src/common/header/style.js中可以看到SearchWrapper中所有的iconfont都是“绝对定位,居右居下”的样式修饰。我们可以把这个iconfont的样式换名为zoom,首先给放大镜的i标签中添加一个zoom属性

1
<i className={focused ? 'focused iconfont zoom':'iconfont zoom'}>&#xe6dd;</i>

然后将SearchWrapper中所有的iconfont换成zoom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const SearchWrapper=styled.div`
float:left;
position:relative;
.zoom {
position:absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
border-radius:15px;
text-align:center;
&.focused{
background:#777;
color: #fff;
}
}
`;

这时在看我们的刷新图标,就位于“换一批”左边了。然后我们就可以给我们的刷新图标添加一个spin的样式,在SearchInfoSwitch样式中书写改样式

1
<i className='iconfont spin'>&#xe851;</i>
1
2
3
4
5
6
7
8
9
10
11
12
export const SearchInfoSwitch=styled.div`
float: right;
font-size: 13px;
color: #969696;
background-color: transparent;
border-width: 0;
padding: 0;
.spin {
font-size:12px;
margin-right:2px;
}
`;
3.10.2 点击换一换实现图标旋转

我们给spin样式一个transition动画,设置动画时间200ms,动画类型是ease-in,设置旋转角度,和旋转中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const SearchInfoSwitch=styled.div`
float: right;
font-size: 13px;
color: #969696;
background-color: transparent;
border-width: 0;
padding: 0;
.spin {
display:block;
float:left;
font-size:12px;
margin-right:2px;
transition:all .2s ease-in;
transform:rotate(0deg);
transform-origin:center,center;
}
`;

所以当换一批被点击时,只需要让i 标签rotate的值发生变化,加360度就可以了。ref属性可以获取i 标签的真实节点给它放置一个函数;当我们的SearchInfoSwitch被点击时,会触动handlerChangePage方法,我们可以把this.spinIcon也传进去,然后就可以在handlerChangePage方法获取spin对应的dom了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<SearchInfoSwitch onClick={()=>handlerChangePage(page,totalPage, this.spinIcon)}>
<i ref={(icon)=>{this.spinIcon=icon}} className='iconfont spin'>&#xe851;</i>
换一批
</SearchInfoSwitch>
。。。
const mapDispatchToProps=(dispatch)=>{
return {
...
handlerChangePage(page,totalPage,spin){
spin.style.transform="rotate(360deg)";
if(page<totalPage){
dispatch(actionCreators.changePage(page+1));
}else{
dispatch(actionCreators.changePage(1));
}
},
}
}

这时点击换一批就有旋转效果了,不过当再次点击时就不再旋转了。有无rotate一直时360不再发生变化了。所以我们需要先看它之前的角度,然后在原油角度的基础上增加360

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handlerChangePage(page,totalPage,spin){
let originAngle=spin.style.transform.replace(/[^0-9]/ig,'');
if(originAngle){
originAngle=parseInt(originAngle,10);
}else{
originAngle=0;
}
spin.style.transform="rotate(" + (originAngle+360) + "deg)";
if(page<totalPage){
dispatch(actionCreators.changePage(page+1));
}else{
dispatch(actionCreators.changePage(1));
}
},

四、规避不必要的ajax请求

之前我们的代码,会在每次给搜索框聚焦时发送ajax请求,其实改请求获取一次就足够了。我门可以给handleInputFocus方法传递一个list的参数,当list大小为0时才发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render(){
const {focused,handleInputFocus,handleInputBlur,list}=this.props;
return (
...
onFocus={()=>handleInputFocus(list)}
)
}
。。。
const mapDispatchToProps=(dispatch)=>{
return {
handleInputFocus(list){
(list.size===0) && dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
。。。。
}
}

现在就解决问题了。

接下来我们解决一个小问题,当鼠标放在SearchInfoSwitch时变成小手的样式,给SearchInfoSwitch的样式设置一个样式cursor的样式等于pointer就可以了。

react实战1

React实战-简书

一、安装React

创建名为jiashu的react-app,并且清理工程目录,仅保留index.js、index.css和App.js文件。步骤略。

二、css样式管理

2.1 react中css使用

在index.css文件中添加样式

1
2
3
.dell {
background:red;
}

然后在App.js文件中,给div添加className,这时页面的div就变成了红色。

注意:并没有给App.js文件引入index.css,只有文件index.js中引用了index.css文件,但是我们在App.js中依然可以使用其中的样式文件。这是因为css文件一旦在一个文件被引入,那么就会全局生效,即全局任何一个文件都能使用这里面的样式。

但是这样容易产生混乱,不建议这么做,这时推荐使用一个第三方的css管理,名为styled-compenents

2.2 react中使用styled-compenents管理css

首先安装该包

1
2
3
npm install --save styled-components
//或
yarn add styled-components

然后我们给index.css文件改名为style.js,然后在index.js文件中修改对应的引用

1
2
~~ import './index.css'; ~~
import './style.js';

然后在style.js中如何创建全局样式呢,如下

1
2
3
4
5
6
7
8
9
10
11
import { injectGlobal } from 'styled-components';
injectGlobal`
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.dell {
background:red;
}
`

这时再看页面,又能正常显示了。

那么如何数据跟组件一对一的样式呢,后面会有介绍。

由于浏览器的差异,如何做到让不同浏览器的样式统一呢?这时需要用到一个reset.css的文件

2.3 完成浏览器样式的统一

不同浏览器内核中对body等标签默认样式的设置是不同的,为了代码再所有浏览器上表现是一致的,这就可以使用reset.css,将里面的内容,拷贝到我们的全局样式里面

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 { injectGlobal } from 'styled-components';
injectGlobal`
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;
}
/* HTML5 display-role reset for older browsers */
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;
}
`

三、头部区块的编写

一般头部区块,是很多页面都要公用的。

3.1 创建header组件

首先在src目录下创建一个文件夹common,在common文件夹中再创建一个header的文件夹,然后再header文件夹中创建index.js文件,这个就是header的组件。接下来我们来定义这个组件

1
2
3
4
5
6
7
8
9
10
11
import React , { Component } from 'react';
class Header extends Component{
render(){
return {
<div>
header
</div>
}
}
}
export default Header;

然后再src/App.js文件中引入Header组件,并使用

1
2
3
4
5
6
7
8
9
10
import React,{Component} from 'react';
import Header from './common/header/index.js';
class App extends Component{
render(){
return {
<Header />
}
}
}
export default App;

3.2创建header样式

src/common/header/文件夹下,创建一个style.js的样式文件,在里面编写Header自己用的样式

1
2
3
4
5
import styled from 'styled-components';
export const HeaderWrapper=styled.div`
height:56px;
background:red;
`;

这样就可以在Header组件(src\common\header\index.js)中,引入该样式并使用了

1
2
3
4
5
6
7
8
9
10
11
12
import React , { Component } from 'react';
import {HeaderWrapper} from './style.js';
class Header extends Component{
render(){
return {
<HeaderWrapper>
header
</HeaderWrapper>
}
}
}
export default Header;

这时header的简易样式就出来了。

查看简书官网可以看到header有58px高,并且有个1px的边框,可以这么修改

1
2
height:58px;
border-bottom:1px soid #f0f0f0;

首先去简书官网,将左上角的logo下载到我们项目的src/state/目录下,命名为logo.png

接下来我们在src/common/header/style.js中定义一个Logo的组件,由于点击该组件可以进行条状,所以该组件是一个a标签

注意在属性url的不能这么写background:url('../../statics/logo.png')因为webpack打包时会当成字符串,而因该使用引用的方式引用该图片,然后使用${}形式进行使用;

图片有点大,还要注意给图片设置大小background-size

1
2
3
4
5
6
7
8
9
10
11
12
import logoPic from '../../common/statics/logo.png';
...
export const Logo=styled.a`
position: absolute;
top: 0;
left: 0;
display:block;
width: 100px;
height: 58px;
background:url(${logoPic});
background-size:contain;
`;

然后就可以在/src/common/header/index.js文件中使用它了,a标签是一个链接,如何让点击时回到首页呢?可以直接给Logo设置href="/"属性

1
2
3
4
5
6
7
8
9
10
11
12
import React , { Component } from 'react';
import { HeaderWrapper , Logo} from './style.js';
class Header extends Component{
render(){
return (
<HeaderWrapper>
<Logo href='/' />
</HeaderWrapper>
)
}
}
export default Header;

当然也可以在style.js中设置href属性,可以这么写style

1
2
3
4
5
6
7
8
9
10
11
12
export const Logo=styled.a.attrs({
href:'/'
})`
position: absolute;
top: 0;
left: 0;
display:block;
width: 100px;
height: 58px;
background:url(${logoPic});
background-size:contain;
`;

然后我们编写header的中间组件

3.4 编写header中间部分Nav

可以现在/src/common/header/index.js文件中添加一个新组件Nav,再编写

1
2
3
4
5
6
7
8
9
10
11
12
13
import React , { Component } from 'react';
import { HeaderWrapper , Logo, Nav} from './style.js';
class Header extends Component{
render(){
return (
<HeaderWrapper>
<Logo href='/' />
<Nav />
</HeaderWrapper>
)
}
}
export default Header;

然后在src/common/header/style.js中定义一个Nav的组件

1
2
3
4
5
6
export const Nav=styled.div`
width:960px;
height:100%;
margin:0px;
background:green;
`;

3.5 填充Nav

这样就能看到我们的中间了,接下来我们往Nav中添加东西,添加4个NavItem,为了给它们添加浮动,给它们添加样式

1
2
3
4
5
6
7
8
import { HeaderWrapper , Logo , Nav ,NavItem} from './style.js';
...
<Nav>
<NavItem className='left'>首页</NavItem>
<NavItem className='left'>下载App</NavItem>
<NavItem className='right'>登陆</NavItem>
<NavItem className='right'>Aa</NavItem>
</Nav>

接下来我们就能在src/common/header/style.js中定义一个NavItem的组件了,其中&.left表示NavItem组件有left属性的话,就执行向左浮动;

同时它们也有一些共有的属性如line-height、padding、font-size等;

1
2
3
4
5
6
7
8
9
10
11
export const NavItem=styled.div`
line-height:58px;
padding:0 15px;
font-size:17px;
&.left{
float:left;
}
&.right{
float:right;
}
`;

同时也可以给文字指定颜色,比如让首页为红色,其它设为黑色,右侧文字黑色浅一些:

1
<NavItem className='left active'>首页</NavItem>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const NavItem=styled.div`
line-height:58px;
padding:0 15px;
font-size:17px;
color:#333;
&.left{
float:left;
}
&.right{
float:right;
color:#969696;
}
&.active{
color:#ea6f5a;
}
`;

3.6 编写NavSearch组件

再Nav中添加NavSearch组件

1
2
3
4
5
6
7
8
9
import { HeaderWrapper , Logo , Nav ,NavItem, NavSearch} from './style.js';
...
<Nav>
<NavItem className='left'>首页</NavItem>
<NavItem className='left'>下载App</NavItem>
<NavItem className='right'>登陆</NavItem>
<NavItem className='right'>Aa</NavItem>
<NavSearch></NavSearch>
</Nav>

然后在src/common/header/style.js中定义一个NavSearch的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const NavSearch=styled.input.attrs({
placeholder:'搜索'
})`
width:160px;
height:38px;
padding:0 20px;
box-sizing:border-box;
border:none;
outline:none;
margin-top:9px;
margin-left:20px;
border-radius:19px;
background:#eee;
font-size:14px;
&::placeholder{
color:#999;
}
`;

其中box-sizing:border-box; 表示input框不拉伸;&::placeholder表示框内文字颜色;

3.7 写注册组件

首先引入Addition,仔里面添加两个Button然后编写style,通过给Button设置className来自定义属性,相应的再style中可以使用&.XXX来编写对应的样式

1
2
3
4
5
6
7
8
9
import { HeaderWrapper , Logo , Nav ,NavItem, NavSearch,Addition,Button} from './style.js';
...
<Nav>
...
</Nav>
<Addition>
<Button className='writting'>写文章</Button>
<Button className='reg'>注册</Button>
</Addition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const Addition=styled.div`
position:absolute;
right:0;
top:0;
height:56px;
`;
export const Button=styled.div`
float:right;
margin-top:9px;
margin-right:20px;
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.8 使用iconfont

首先进入iconfont.cn下载几个iconfont图标,放在src/statics/iconfont/文件夹下,包括下面五个文件:

  • iconfont.css
  • iconfont.eot
  • iconfont.svg
  • iconfont.ttf
  • iconfont.woff

然后打开iconfont.css文件,在里面将iconfont开头内容,改为添加相对引用的,并且可以删除下面没用的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//before
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1536939256958'); /* IE9*/
src: url('iconfont.eot?t=1536939256958#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont.ttf?t=1536939256958') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg?t=1536939256958#iconfont') format('svg'); /* iOS 4.1- */
}
//after
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1536939256958'); /* IE9*/
src: url('./iconfont.eot?t=1536939256958#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./iconfont.ttf?t=1536939256958') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('./iconfont.svg?t=1536939256958#iconfont') format('svg'); /* iOS 4.1- */
}

iconfont我们倾向于将它们全局使用

首先我们将iconfont.css文件改名为iconfont.js,并且引入

1
import { injectGlobal } from 'styled-components';

然后就可以将里面的内容引入全局语法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { injectGlobal } from 'styled-components';

injectGlobal`
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1536939256958'); /* IE9*/
src: url('./iconfont.eot?t=1536939256958#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAW0AAsAAAAACEAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFY8hEjPY21hcAAAAYAAAABeAAABnLRjHYpnbHlmAAAB4AAAAdIAAAIA2xiyuGhlYWQAAAO0AAAALwAAADYSoy/taGhlYQAAA+QAAAAcAAAAJAfeA4VobXR4AAAEAAAAAA4AAAAQEAAAAGxvY2EAAAQQAAAACgAAAAoBSgCEbWF4cAAABBwAAAAeAAAAIAESAFluYW1lAAAEPAAAAUUAAAJtPlT+fXBvc3QAAAWEAAAAMAAAAEHS0b6peJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkYWCcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBye8T27y9zwv4EhhrmBoQEozAiSAwDusgzOeJztkEEOgDAIBAdbTWOML/Fo+iBPfrpXvlGBevARLhkCmw0HgBlIxmFkkBvBdZkr4SfW8DPV9mI1ge5atfX+nUISiRJXxZOy8GuLfr5b8q8NYq4D/7e2AdMDWkcVrgAAeJw1jk1PE1EUhs+ZO/ejfsw47W3HkrY6HWamk0g7zrRTg7VIQg1SExsSiAsVo1GwwkpXJGyElf4G2PAPuurKVBP30BUr/oihtYWQnMWb532S94AKMB6TAQHgoMMsVAAs27O4jVGKuJ7NOKFxFObRrtnMLrq16gJW7SI3NUzLTBTGT1AZfFoenbU+ov5+6QtlCuVdPAuaXx+htViud98+na9slPL3Z5xgOCQw8nHhjmunRr9p7tvfShz469qttvOK5rLpXOgUAECZ/NQnf8gypMECcJpYK6OnIS+gGcZ1MxPGVc+ZoCbWJ0hDZWh75PTw6ERVT44aO5VzY65oXPw76BHSO9jvqWrvorLTuKoPT4lz79ywHyS/q739a+Vyc0z2CIG78GKyKZnnMq5hAetNNAsYNaehjOgWdbzMU56ZKtfW9GLXK+MVMzmTmbCOcZU839rcO2YJdkMm1B/bfis/31CEcjs9Gggpdul6J/gw50c8tbrpddwEvSkT/pofdbIWTeorrYdb1dfbss91KXBDrLC8V/YES6UYZVbJ0JS8yqWh4DMhdks/V4O2KYXIHHe9RZPJJKUzS370OWi1dYMVsy+jN78e94XUOb7jEuA/i1FlEwAAeJxjYGRgYADiF3ZLN8bz23xl4GZhAIHrB7/+QND/d7AwMHsAuRwMTCBRAHx7DUcAeJxjYGRgYG7438AQw8IAAkCSkQEVsAAARwoCbXicY2FgYGBBwgAAsAARAAAAAAAAAEoAhAEAAAB4nGNgZGBgYGHwBWIQYAJiLiBkYPgP5jMAABCXAWwAAHicZY9NTsMwEIVf+gekEqqoYIfkBWIBKP0Rq25YVGr3XXTfpk6bKokjx63UA3AejsAJOALcgDvwSCebNpbH37x5Y08A3OAHHo7fLfeRPVwyO3INF7gXrlN/EG6QX4SbaONVuEX9TdjHM6bCbXRheYPXuGL2hHdhDx18CNdwjU/hOvUv4Qb5W7iJO/wKt9Dx6sI+5l5XuI1HL/bHVi+cXqnlQcWhySKTOb+CmV7vkoWt0uqca1vEJlODoF9JU51pW91T7NdD5yIVWZOqCas6SYzKrdnq0AUb5/JRrxeJHoQm5Vhj/rbGAo5xBYUlDowxQhhkiMro6DtVZvSvsUPCXntWPc3ndFsU1P9zhQEC9M9cU7qy0nk6T4E9XxtSdXQrbsuelDSRXs1JErJCXta2VELqATZlV44RelzRiT8oZ0j/AAlabsgAAAB4nGNgYoAALgbsgIWRiZGZkYWRlYHJMZErLTEvPSUxKzMvnbUiM7Uqk4EBAFR3Bwg=') format('woff'),
url('./iconfont.ttf?t=1536939256958') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('./iconfont.svg?t=1536939256958#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;
}
`

现在这个样式文件还没有全局生效,要生效该怎么做呢?

我们在src/index.js文件中引入该样式

1
import './statics/iconfont/iconfont.js';

这时我们看页面控制台没有报错,就说明引用成功,接下来我们就可以使用它了。

进入src/common/header/index.js文件,用iconfont标签插入到相应的组件即可,可以通过i标签引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
render(){
return (
<HeaderWrapper>
<Logo href='/'/>
<Nav>
<NavItem className='left active'>首页</NavItem>
<NavItem className='left'>下载App</NavItem>
<NavItem className='right'>登陆</NavItem>
<NavItem className='right'>
<i className='iconfont'>&#xe636;</i>
</NavItem>
<NavSearch></NavSearch>
<i className='iconfont'>&#xe6dd;</i>
</Nav>
<Addition>
<Button className='writting'>
<i className='iconfont'>&#xe60e;</i>
写文章
</Button>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
)
}

此时我们的页面就展示出来了iconfont元素了。

接下来对搜索地方进行优化,创建一个SeachWrapper样式,将NavSearch和搜索标签包起来,并且在src/common/header/style.js中定义该样式,如何给里面的iconfont添加样式呢?只需要使用.iconfont{}即可

1
2
3
4
5
6
7
8
9
10
11
12
13
export const SearchWrapper=styled.div`
float:left;
position:relative;
.iconfont {
position:absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
border-radius:15px;
text-align:center;
}
`;

这里给iconfont搜索按钮添加了圆角框,是为了以后动画服务。

redux中间件

Redux中间件

一、解决action类型太多的问题

在之前的例子中,我们在TodoList文件中创建了多种action,并且在reducer文件中对多种action做了区分处理。如果action的数量变得多了以后,就很容易出现错误并且不好调试。如何解决这个问题呢?

我们可以在store下创建一个actionType.js的文件。在里面将不同的action定义成常量,这样就可以在其他文件中直接使用了,又不容易出现错误

actionType.js文件内容可以如下:

1
2
3
export const CHANGE_INPUT_VALUE="change_input_value";
export const ADD_TODO_ITEM="add_todo_item";
export const DELETE_TODO_ITEM="delete_todo_item";

然后在TodoList中引入这三个常量即可:

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 {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './actionType.js';
...
handleInputChange(event){
const action={
type:CHANGE_INPUT_VALUE,
value:event.target.value,
};
store.dispatch(action);
}

//当感知到数据发生变化时,就回去新的state数据
handleStoreChange(event){
this.setState(store.getState());
}

handleBtnClick(event){
const action={
type:ADD_TODO_ITEM,
};
store.dispatch(action);
}

handleItemDelete(index){
const action={
type:DELETE_TODO_ITEM,
index,
};
store.dispatch(action);
}

同理也需要对reducer中引入这些常量。

二、创建action创建器

在之前的例子中,我们都是直接创建action对象,虽然方便,但是这种方式却不便于管理。我们需要一个action创建器来统一创建相应的action。

在store下面创建一个actionCreators.js的文件

输入

1
2
3
4
5
6
import {CHANGE_INPUT_VALUE} from './actionType.js';

export const getInputChangeAction=(value)=>({
type=CHANGE_INPUT_VALUE,
value,
})

然后我们在TodoList文件中修改该action的创建方式

1
2
3
4
5
6
import { getInputChangeAction } from './store/actionCreators.js';
...
handleInputChange(event){
const action=getInputChangeAction(event.target.value);
store.dispatch(action);
}

同理对其他的action做同样的处理

三、Redux使用三原则

  • store唯一:整个工程只有一个store
  • 只有store能够改变自己的内容:很多同学认为是reducer改变的store中的内容,其实不是的,reducer只是拿到了之前的数据,然后创建一个新的数据,将新的数据返回给了store;其实还是store将数据进行更新
  • Reducer必须是存函数:存函数给定固定的输入就一定会有固定的输出,而且不会有任何副作用(除了返回东西不会做任何改变)。

store最常用的四个方法:

  • createStore
  • store.dispatch
  • store.getState
  • store.subscribe

四、拆分UI组件和容器组件

在之前的例子中我们在组件TodoList中既处理UI部分也处理逻辑部分,这样会造成很多混乱,可以考虑将组件拆分成UI组件和容器组件。

4.1编写UI组件

在UI组件中只展示UI,容器部分只处理逻辑

我们可以新建一个TodoListUI.js;

将UI编写UI部分;

此时该组件中的设计的逻辑部分只能通过父组件传递过来,使用this.props.XXX的方式;

最后将该组件输出export

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 { Input , Button , List} from 'antd';

class TodoListUI extends Component{
render(){
return (
<div style={{marginTop:'10px' , marginLeft:'10px'}}>
<div>
<Input
value={this.props.inputValue} placeholder="todo info"
style={{width:'300px',marginRight:'10px'}}
onChange={this.props.handleInputChange}/>
<Button type="primary" onClick={this.props.handleBtnClick}>提交</Button>
</div>
<List
style={{width:'300px',marginTop:'10px'}}
bordered
dataSource={this.props.list}
renderItem={(item,index) => (<List.Item onClick={()=>{this.props.handleItemDelete(index)}}>{item}</List.Item>)}
/>
</div>
);
}
}
export default TodoListUI;

4.2 修改逻辑组件部分

在TodoList.js文件中,引入TodoListUI,直接添加到render方法中。

然后将TodoListUI需要用到的一些逻辑通过属性传递过去

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
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store/index';
import { getInputChangeAction , getAddItemAction , getDeleteItemAction } from './store/actionCreator.js';
import TodoListUI from './TodoListUI';

class TodoList extends Component {
constructor(props){
super(props);
this.state=store.getState();
this.handleInputChange.bind(this);
this.handleStoreChange=this.handleStoreChange.bind(this);
this.handleBtnClick=this.handleBtnClick.bind(this);
this.handleItemDelete=this.handleItemDelete.bind(this);

store.subscribe(this.handleStoreChange);
}

render() {
return <TodoListUI
inputValue={this.state.inputValue}
list={this.state.list}
handleInputChange={this.handleInputChange}
handleStoreChange={this.handleStoreChange}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>;
}

handleInputChange(event){
const action=getInputChangeAction(event.target.value);
store.dispatch(action);
}

//当感知到数据发生变化时,就回去新的state数据
handleStoreChange(event){
this.setState(store.getState());
}

handleBtnClick(event){
const action=getAddItemAction();
store.dispatch(action);
}

handleItemDelete(index){
const action=getDeleteItemAction(index);
store.dispatch(action);
}
}

export default TodoList;

此时UI组件和逻辑组件就分开了,功能同样实现

五、无状态组件

一个组件如果没有构造函数,就可以把它写成无状态组件,比如我们上面的TodoListUI,可以改成下面的样式

这时使用props就不用使用this了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {Component} from 'react';
import { Input , Button , List} from 'antd';

const TodoListUI=(props)=>{
return (
<div style={{marginTop:'10px' , marginLeft:'10px'}}>
<div>
<Input
value={props.inputValue} placeholder="todo info"
style={{width:'300px',marginRight:'10px'}}
onChange={props.handleInputChange}/>
<Button type="primary" onClick={props.handleBtnClick}>提交</Button>
</div>
<List
style={{width:'300px',marginTop:'10px'}}
bordered
dataSource={props.list}
renderItem={(item,index) => (<List.Item onClick={()=>{props.handleItemDelete(index)}}>{item}</List.Item>)}
/>
</div>
);
};
export default TodoListUI;

这时候,就完成了,无状态组件性能比较高。因为它只是一个函数,而不需要一些生命周期函数.

六、Redux发送ajax请求

想初始化我们的列表,首先在actionType文件中添加一个action类型

1
export const INIT_LIST="init_list";

然后在actionCreator中创建该action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST} from './actionType.js';

export const getInputChangeAction=(value)=>({
type:CHANGE_INPUT_VALUE,
value,
});
export const getAddItemAction=()=>({
type:ADD_TODO_ITEM,
});
export const getDeleteItemAction=(index)=>({
type:DELETE_TODO_ITEM,
index,
});
export const initListAction=(data)=>({
type:INIT_LIST,
data,
});

在TodoList中引入该action,并且引入axios,添加componentDidMount生命周期函数,在该函数中创建action,然后将action传递给store

1
2
3
4
5
6
7
8
9
10
import { getInputChangeAction , getAddItemAction , getDeleteItemAction,initListAction } from './store/actionCreator.js';
import axios from 'axios';
...
componentDidMount(){
axios.get('/list.json').then((res)=>{
const data=res.data;
const action=initListAction(data);
store.dispatch(action);
});
}

最后写reducer,在reducer中完成对数据的处理

1
2
3
4
5
6
7
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST} from './actionType.js';
。。。
if(action.type===INIT_LIST){
const newState=JSON.parse(JSON.stringify(state));
newState.list=action.data;
return newState;
}

七、Redux-thunk中间件发送ajax请求

如果把过多的异步请求或逻辑放在一个组件中,会显得组件特别臃肿。我们希望将它们移除在其它地方进行统一管理。放在哪里呢?Redux-thunk可以将它们放在action 中去处理。Redux-thunk如何使用呢?

7.1 安装redux-thunk

首先在github官网上查看,在工程目录下输入

1
2
3
4
npm install --save redux-thunk

//或
yarn add redux-thunk

那该如何使用中间件来创建store呢?

首先在store/index.js文件中,引入redux的applyMiddleware,并且引入thunk

1
2
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

然后在createStore中使用applyMiddleware方法,将thunk放在里面里面就行了。

1
2
3
const store = createStore(reducer,
applyMiddleware(thunk)
);

7.2 redux同时使用dev-tools和thunk中间件

如果想将之前的window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()和thunk同时使用该怎么办呢?它们全都是Redux的中间件,可以去github的REDUX_DEVTOOLS 中查看使用方式

  • 首先声明window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()

  • 然后扩展thunk

  • 最后使用即可
1
2
3
4
5
6
7
8
9
10
11
import { createStore , applyMiddleware , compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers =window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(thunk),
);
const store = createStore(reducer,enhancer);

export default store;

这样我们的代码就可以既支持devtools,又支持thunk了

7.3 使用thunk编写代码

之前我们的aciton都是一个函数,函数返回的是一个对象,如下

1
2
3
4
export const initListAction=(data)=>({
type:INIT_LIST,
data,
});

在使用thunk之后,我们action,函数返回的也可以是一个函数

首先我们将TodoList的componentDidMount函数中的异步代码删除。

然后在actionCreators.js中,放进来,注意这里不用在引入store了,因为返回的函数自动接收了store的dispatch方法,可以直接将里面创建的action派发出去了。

1
2
3
4
5
6
7
8
9
export const getTodoList=()=>({
return (dispatch)=>{
axios.get('/todolist.json').then((res)=>{
const data=res.data;
const action=initListAction(data);
dispatch(action);
});
}
});

这里我们使用axios,所以需要在actionCreators.js文件中添加两个引用

1
import axios from 'axios';

然后在TodoList.js组件的componentDidMount函数中创建getTodoList的action,注意需要先import

1
2
3
4
5
6
7
import { getTodoList, getInputChangeAction , getAddItemAction , getDeleteItemAction,initListAction } from './store/actionCreator.js';
...

componentDidMount(){
const action=getTodoList();
store.dispatch(action);
}

注意这里之所以能将函数发送出去,就是因为使用了redux-thunk中间件

八、什么是Redux的中间件

已经使用redux中间件redux-thunk了,到底什么是中间件呢

redux的中间件指的是action和store之间,thunk就是对store的dispatch方法做了一个升级,之前的是只能接受一个对象,现在可以接收一个函数,当接受一个函数时先执行函数再分发

九、Redux-saga中间件

这里再强调一下,中间件指的是action和store之间,因为只有redux才有store和action。之前我们学习的redux-thunk将异步过程封装到了action之中,这样有助于我们做自动化测试,以及异步代码拆分管理。redux-saga也是做这种异步代码拆分的工作,可以用redux-saga完全代替redux-thunk。

9.1 安装Redux-saga

首先进入官网,进行Redux-saga的安装

1
2
3
npm install --save redux-saga
//或
yarn add redux-saga

然后在store/index.js组件中引入createSagaMiddleware,用于创建中间件

1
2
3
4
import createSagaMiddleware from 'redux-saga'
...
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()

之前在redux-thunk中是通过composeEnhancers将中间件传进入,redux-saga也一样

1
2
3
4
5
const composeEnhancers =window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware),
);
const store = createStore(reducer,enhancer);

在store文件夹下创建一个sagas.js的文件,然后在store/index.js文件中引入该文件,并执行saga文件

1
2
3
4
5
import todoSagas from './sagas';
...

// then run the saga
sagaMiddleware.run(todoSagas)

sagas.js文件中写什么呢?一定要导出一个generator函数

1
2
3
4
function* todoSagas() {
}

export default todoSagas;

9.2 使用redux-saga

在actionCreator.js创建一个action,是一个函数返回一个对象.

首先在actionType.js中创建GET_INIT_LIST

1
export const GET_INIT_LIST="get_init_list";
1
2
3
4
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,GET_INIT_LIST} from './actionType.js';
export const getInitList=()=>({
type:GET_INIT_LIST
});

然后在TodoList中使用

1
2
3
4
componentDidMount(){
const action=getInitList();
store.dispatch(action);
}

此时我们不仅能在reducer中接收action了,还能在sagas.js中接收了,然后将异步过程放在这里。在这里将创建的action如何发布出去呢,可以使用redux-saga中的put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { takeEvery , put} from 'redux-saga/effects';
import { GET_INIT_LIST } from './actionType';
import axios from 'axios';
import { initListAction } from './store/actionCreator.js';

//generator函数
function* todoSagas() {
//takeEvery:捕捉每一个XX类型的action,然后调用fetchUser这个方法
yield takeEvery(GET_INIT_LIST, getInitList);
}

function* getInitList(){
//这里无需使用promise了
const res=yield axios.get('/todolist.json');
const action=initListAction(res.data);
yield put(action);
}

export default todoSagas;

注意:这是在getInitList函数中如果yield axios.get('/todolist.json');获取成功,肯定是没问题的,但是如果获取不成功呢,ajax失败情况怎么办呢,可以使用try-catch

1
2
3
4
5
6
7
8
9
10
function* getInitList(){
try{
//这里无需使用promise了
const res=yield axios.get('/todolist.json');
const action=initListAction(res.data);
yield put(action);
}catch(e){
console.log("网络请求失败");
}
}

redux-thunk基本没有api,它只是让我们在action不仅仅返回对象,还能返回函数;

而redux-saga又很多常用的API,在大型项目中很适合

十、React-Redux的使用

React-Redux是一个第三方的模块,可以帮助我们在react中更加方便的使用redux。首先我们先将我们的工程清空,只剩一个index文件。

10.1 安装React-Redux

1
2
3
npm install --save react-redux
//或
yarn add react-redux

然后编写TodoList.js文件

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 TodoList extends Component{
render(){
return (
<div>
<div>
<input />
<button>提交</button>
</div>
<ul>
<li>VUE</li>
</ul>
</div>
);
}
}

export default TodoList;

10.2 创建store

首先创建store文件夹,并在下面创建index.js文件,在里面输入创建store(管理员)

1
2
3
4
5
6
import { createStore } from 'redux';
import { reducer } from './reducer';

const store=createStore();

export default store;

并且在store目录下创建一个reducer.js(记事本),这是一个纯函数。并在里面定义一些初始值

1
2
3
4
5
6
7
8

const defaultState={
inputValue:'',
list:[],
};
export default (state=defaultState,action)=>{
return state;
}

这样我们redecer和store就创建好了。

10.3 在TodoList.js中使用store中的数据

首先在TodoList.js中引入store;

并在构造函数中通过store设置setState;

然后在input中加入对state的引用

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,{ Component } from 'react';
import store from './store/index';

class TodoList extends Component{
constructor(props){
super(props);
this.state=store.getState();
}
render(){
return (
<div>
<div>
<input value={this.state.inputValue}/>
<button>提交</button>
</div>
<ul>
<li>VUE</li>
</ul>
</div>
);
}
}

export default TodoList;

那我们该如何使用React-Redux呢?

10.4 使用React-Redux

在index.js文件中,之前是直接渲染TodoList,现在我们引入react-redux,并且声明一个App,将TodoList传给App,最后将App传给RenderDom使用

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import ReactDom from 'react-dom';
import TodoList from './TodoList';
import { Provider } from 'react-redux';

const App=(
<Provider>
<TodoList />
</Provider>
);
RenderDom.render(App, document.getElementById("root"));
//RenderDom.render(<TodoList />, document.getElementById("root"));

这里将TodoList组件传给了Provider组件,Provider组件来自于react-redux。

provider组件可以做什么呢?provider可以提供链接store的能力

我们继续引入store,然后在provider组件中使用如下,这样Provider内的所有组件都有获取store的能力

1
2
3
4
5
6
7
import store from './store';

const App=(
<Provider store={store}>
<TodoList />
</Provider>
);

此时TodoList组件有获取store的能力了,它怎么获取store呢?

此时我们在TodoList.js中就可以改变获取store的方式了,首先引入react-redux中的connnect,然后到处connect方法。connect的方法是让TodoList和store做连接

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

class TodoList extends Component{
render(){
return (
<div>
<div>
<input value={this.state.inputValue}/>
<button>提交</button>
</div>
<ul>
<li>VUE</li>
</ul>
</div>
);
}
}

export default connect(null,null)(TodoList);

connect是怎么做连接的呢?首先我们可以创建一个mapStateToProps,然后将它放在connect的一个参数处

1
2
3
4
5
6
7
//将state中的数据映射给props
const mapStateToProps=(state)=>{
return {
inputValue:state.inputValue,
};
};
export default connect(mapStateToProps,null)(TodoList);

这时我们input中的value就不是等于{this.state.inputValue}了,不使用state了改为props,需要改为

1
<input value={this.props.inputValue}/>

然后我们实现onChange方法,在input标签中添加该监听,并且编写一个mapDispatchToProps,将它放在connect的第二个参数处。然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<input value={this.props.inputValue} onChange={this.props.changeInputValue)}/>
...

handleInputChange(event){

}
//store.dispatch 挂载到props上
const mapDispatchToProps=(dispatch)={
return {
changeInputValue(e){
const action={
type:'change_input_value',
value:e.target.value,
};
dispatch(action);
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(TodoList);

然后就可以在reduce中做处理了

1
2
3
4
5
6
7
8
9
10
11
12
const defaultState={
inputValue:'',
list:[],
};
export default (state=defaultState,action)=>{
if(action.type==='change_input_value'){
cost newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
return state;
}

这里对react-redux的使用就讲解的差不多了,

接下来我们给button添加一个监听,然后在mapDispatchToProps中添加这个监听函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<button onClick={this.props.handleClick}>提交</button>
...

const mapDispatchToProps=(dispatch)={
return {
changeInputValue(e){
const action={
type:'change_input_value',
value:e.target.value,
};
dispatch(action);
},
handleClick(e){
//...
}
}
}

然后在做action类型的处理即可,不再重复。

同理对于列表,可以这么处理

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul>
{
this.props.list.map((item,index)=>({return <li key={index}>{item}</li>}));
}
</ul>

...
const mapStateToProps=(state)=>{
return {
inputValue:state.inputValue,
list:state.list,
};
};

这是代码中出现太多的this.props.XXX,其实可以精简一下,在render函数中使用结构赋值,就可以在直接使用相应的函数了

1
2
3
render(){
const {inputValue,list,changeInputValue,handleClick} =this.props;
}

此时的TodoList就是一个UI组件,可以改为无状态组件,因为无状态组件性能比较高,可以直接写成函数的形式

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
import React,{ Component } from 'react';
import { connect } from 'react-redux';

const TodoList=(props)=>{
const {inputValue,list,changeInputValue,handleClick} =props;
return (
<div>
<div>
<input value={inputValue} onChange={changeInputValue}/>
<button onClick={handleClick}>提交</button>
</div>
<ul>
{list.map((item,index)=>({return <li key={index}>{item}</li>}));}
</ul>
</div>
);
}
const mapStateToProps=(state)=>{
return {
inputValue:state.inputValue,
list:state.list,
};
};
const mapDispatchToProps=(dispatch)={
return {
changeInputValue(e){
const action={
type:'change_input_value',
value:e.target.value,
};
dispatch(action);
},
handleClick(e){
//...
}
}
}

export default connect(mapStateToProps,mapDispatchToProps)(TodoList);

react中Redux的使用

Redux

react只是一个轻量级的视图框架,想用它做一个大型应用是不可能的,所以需要一个数据层的框架跟它结合起来使用,这时就可以使用redux了。

一、Redux简介

Redux是一个数据层框架。再react中如果想在组件间传递数据,需要繁琐的数据传递。使用redux,将组件的数据放在一个公共的区域,这样每当这个区域的数据发生的变化时,所有相关的组件都会随着更新。

Redux=Reducer+Flux

二、Redux的工作流程

1
2
3
4
5
6
7
借书人               借书语句         管理员	 图书记事本
dispatch (previousState
action) ,action)
(newState)
ReactComponents---ActionCreators-->Store<-->Reducers
^ |
|------------------------------

三、使用antD创建todoList布局

3.1 初始化一个工程

初始化一个名为todolist的工程,清洁里面只剩一个index.js的文件;

然后创建一个TodoList的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react';

class TodoList extends Component {
render() {
return (
<div>
hello world!
</div>
);
}
}

export default TodoList;

3.2 安装AntD

首先进入官网,查看安装教程。

进入工程todolistantd,运行下面的命令,安装Ant D

1
2
3
4
npm install antd --save

//或使用yarn安装
yarn add antd

3.2 使用AntD

安装完成后,如何使用AntD呢

首先在组件中引入antD的样式,就可以使用了

1
import 'antd/dist/antd.css';
3.2.1 使用antD的Input

若要使用antD的组件可以直接在使用文档中查找相关组件,通过showCode直接将代码粘贴过来。如我们使用一个input,首先引入Input,然后就能使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input } from 'antd';

class TodoList extends Component {
render() {
return (
<div>
<div><Input placeholder="todo info"/></div>
</div>
);
}
}

export default TodoList;

这样就可以在页面上看到一个input框了,和预计的样式一样。当然,可以给这个Input设置自己的样式,如下面的样式可以将input框宽度设为300px

1
<Input placeholder="todo info" style={{width:'300px'}}/>

此时我们想在Input右边添加一个按钮怎么办呢?同理去antD中找Button使用

3.2.2 使用antD的Button

首先引入Button的引用,然后直接使用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input , Button} from 'antd';//引入Button

class TodoList extends Component {
render() {
return (
<div>
<div>
<Input placeholder="todo info" style={{width:'300px'}}/>
<Button type="primary">Primary</Button>
</div>
</div>
);
}
}

export default TodoList;

添加间距

此时觉得input框和button间没有间距,直接连接不太好,想给它们添加个10px的间距,可以这么做:

1
<Input placeholder="todo info" style={{width:'300px',marginRight:'300px'}}/>

这时这两个标签都会紧贴着浏览器,想给它们整体添加一个间距,怎么做呢

可以在最外层的div中添加样式文件

1
2
3
4
5
6
7
8
9
return (
<div style={{marginTop:'10px',marginLeft:'10px'}}>
<div>
<Input placeholder="todo info" style={{width:'300px'}}/>
<Button type="primary">Primary</Button>
</div>
</div>
);
}

此时,我们的Input和Button展示的有点样子了,接下来

3.2.3 使用List

首先引入List组件,

然后在TodoList组件外边创建全局列表数据,

在render中添加List组件

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
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input , Button,List } from 'antd';//引入Button

const data = [
'React',
'Vue',
'Android',
'IOS',
'WebGL',
];
class TodoList extends Component {
render() {
return (
<div>
<div>
<Input placeholder="todo info" style={{width:'300px'}}/>
<Button type="primary">Primary</Button>
</div>
<h3 style={{ marginBottom: 16 }}>Default Size</h3>
<List
bordered
dataSource={data}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
}

export default TodoList;

这是list也是左右满屏,可以做一下微调,可以给list添加样式,如下

1
2
3
4
5
6
<List
style={{width:'300px',marginTop:'10px'}}
bordered
dataSource={data}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>

这样,List就和Input等宽了。关于antD的其它空间,可以直接去官网查看用法。antD一般用来开发后台页面,效果很快。

四、Store的创建

4.1 安装Redux

如果要使用Redux,首先得先安装Redux,可用下面命令安装

1
2
3
npm install --save redux
//或
yarn add redux
4.1 创建Store

在工程的src目录下创建一个名为“store”的文件夹;

在store的文件夹下,创建一个文件叫做index.js;

然后在index.js中引入redux的一个名为createStore方法,利用这个方法就可以创建一个store出来;

如此我们就创建了一个公共数据存储仓库。

1
2
3
4
5
import { createStore } from 'redux';

const store=createStore();

export store;

此时我们’’图书管理员’’已经创建好了;

但是他却记不住如何管理数据,此时他需要一个”图书记事本”来辅助管理图书;

所以创建’’图书管理员’’时,需要同时创建”图书记事本”,也就是Reducer

4.2 创建Reducer

在store目录下,创建名为reducer.js的文件;

在里面返回一个函数;

1
2
3
4
//state表示整个store中存储的数据
export default (state,action)=>{
return state;
}

如果我们想要返回指定的state怎么办呢?可以写一个结构,返回如下:

1
2
3
4
5
6
7
const defaultState={
inputValue:'',
list:[],
}
export default (state=defaultState,action)=>{
return state;
}

我们还要把state传给store,怎么传呢?

在store/index.js文件中引入reducer,并且在创建store时,将reducer传给store

1
2
3
4
5
6
import { createStore } from 'redux';
import reducer from './reducer';

const store=createStore(reducer);

export default store;//注意一定要加上default

这时,’’图书管理员’’和”图书记事本”已经绑定起来了?

我们该怎么将store中的数据跟TodoList组件绑定起来呢?刚才我们demo展示的data是写死的数据,可以将之清掉。

然后在TodoList控件中引入该store,

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
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input , Button,List } from 'antd';
import store from './store/index.js';//引入store

class TodoList extends Component {
constructor(props){
super(props);
this.state=store.getState();
//console.log(store.getState());
}
render() {
return (
<div>
<div>
<Input value={this.state.inputValue} placeholder="todo info" style={{width:'300px'}}/>
<Button type="primary">Primary</Button>
</div>
<h3 style={{ marginBottom: 16 }}>Default Size</h3>
<List
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
}

export default TodoList;

自此已完成store和reducer的创建,那么该如何编写它们呢?

4.3 store和reducer的编写
1、Redux DevTools调试工具的安装

为了调试redux,首先需要先安装一个redux的插件,名为Redux DevTools

然后在创建store时,添加第二个参数,参考如下:

1
2
const store = createStore(reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

这个时候我们就能在浏览器中使用Redux DevTools了,进行redux的调试了。

下面要走改变store中数据的流程

2、改变store中数据

首先我们想,当我们网input中输入东西时,store中的inputValue发生改变;

我们可以给Input标签一个onChange事件,传入自定义函数handleInputCahnge ,记得在constructor中对该函数进行this的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
constructor(){
...
this.handleInputChange.bind(this);
}
...

onChange={this.handleInputChange}


...
handleInputChange(event){
console.log(event.tarent.value);
}

接下来我们要创建一个action,接受input的值,然后将action传给store;

怎么将action的内容传给store呢?store提供了一个方法叫dispatch

1
2
3
4
5
6
7
handleInputChange(event){
const action={
type:"change_input_value",
value:event.target.value,
};
store.dispatch(action);
}

store接收到action传来的数据,该怎么处理呢?它需要查”图书记事本”reducer,它需要拿着action和当前store中数据一起转发给reducer,然后reducer告诉store来做什么。

幸运的是store给reducer传递数据已经自动化完成了,无需我们再做。我们只需要再reducer中对当前的数据和action进行数据的更新。比如可以如下:

1
2
3
4
5
6
7
8
9
10
//store表示当前数据,也就是preState,action表示动作
export default (state=defaultState,action)=>{
if(action.type==='change_input_value'){
//对当前state做一个深拷贝
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
return state;
}

注意:reducer可以接收state,但是绝对不能修改state

3、完成action的传递和TodoList中Input的更新

此时reducer返回的state就给了store,然后store就可以把新数据替换掉老数据。如何让组件能实时更新呢?

需要让TodoList组件订阅store,在订阅接口中编写我们的更新函数即可;

更新函数中,只需要对store重新getState,然后setState即可;

这样store发生改变,TodoList就会发生更新了

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
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input , Button , List} from 'antd';
import store from './store/index';

class TodoList extends Component {
constructor(props){
super(props);
this.state=store.getState();
this.handleInputChange.bind(this);
this.handleStoreChange=this.handleStoreChange.bind(this);
store.subscribe(this.handleStoreChange);
}

render() {
return (
<div style={{marginTop:'10px' , marginLeft:'10px'}}>
<div>
<Input
value={this.state.inputValue} placeholder="todo info"
style={{width:'300px',marginRight:'10px'}}
onChange={this.handleInputChange}/>
<Button type="primary">提交</Button>
</div>
<List
style={{width:'300px',marginTop:'10px'}}
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}

handleInputChange(event){
const action={
type:"change_input_value",
value:event.target.value,
};
store.dispatch(action);
}

//当感知到数据发生变化时,就回去新的state数据
handleStoreChange(event){
this.setState(store.getState());
}
}

export default TodoList;
4、完成提交按钮,实现list的更新

希望点击按钮时,把input中的数据存在store中的list数组中

首先给Button绑定一个onClick事件,(记得在constructor中绑定this)

1
<Button type="primary" onClick={this.handleBtnClick}>提交</Button>

然后编写handleBtnClick的方法,先创建一个action,

1
2
3
4
5
6
handleBtnClick(event){
const action={
type:"add_todo_item",
};
store.dispatch(action);
}

注意这里action只定义了事件类型,没有传值,这是因为要改变的数据可以放在reducer中处理,此时可以在reducer中处理这个事件了

1
2
3
4
5
6
7
8
if(action.type==='add_todo_item'){
const newState=JSON.parse(JSON.stringify(state));
//将input中的值添加到list
newState.list.push(newState.inputValue);
//清空input
newState.inputValue='';
return newState;
}

五、完成TodoList的删除

在上面已经熟悉了redux的流程,这里就当回顾,顺便实现TodoList的删除的删除

首先我们对List组件的每一项进行事件绑定,List的每一项是通过List.Item进行绑定的,所以我们只需要对List.Item进行事件绑定即可

1
2
3
4
5
6
<List
style={{width:'300px',marginTop:'10px'}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => (<List.Item onClick={this.handleItemDelete.bind(this,index)}>{item}</List.Item>)}
/>

然后我们写handleItemDelete这个方法, 接受一个index 的参数,在里面创建action,传递index,然后传递action

1
2
3
4
5
6
7
handleItemDelete(index){
const action={
type:"delete_todo_item",
index,
};
store.dispatch(action);
}

最后在reducer中判断delete_todo_item事件,根据index在list中删除相应下标的数据

1
2
3
4
5
if(action.type==='delete_todo_item'){
const newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.index,1);
return newState;
}

最终我们的reducer.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
const defaultState = {
inputValue:'',
list: []
}

export default (state=defaultState,action)=>{
if(action.type==='change_input_value'){
//对当前state做一个深拷贝
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}

if(action.type==='add_todo_item'){
const newState=JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.inputValue='';
return newState;
}
if(action.type==='delete_todo_item'){
const newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.index,1);
return newState;
}

return state;
}

react组件细节

一 、React细节基础

1、Fragment,去掉多余的外出div

在组件的类中,render方法中所有的标签都得包含在一个div中,这样在生产的html页面中就会始终多一个div,要去掉这个最外层多余的div怎么办呢?

1
2
3
4
5
6
7
8
9
10
<div>	
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button className='red-btn' onClick={this.handlerBtnClick}>add</button>
</div>
<ul>
<li>学英语</li>
<li>学汉语</li>
</ul>
</div>

在react16的版本中,提供了一个叫做Fragment的占位符,这样就不会出现多余的外层div了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//如果没有这句话,只能使用<React.Fragment>
import React, { Component , Fragment } from 'react';
。。。

。。。
<Fragment>
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button className='red-btn' onClick={this.handlerBtnClick}>add</button>
</div>
<ul>
<li>学英语</li>
<li>学汉语</li>
</ul>
</Fragment>

2、React中响应式设计思想

​ react不直接操作dom来响应页面,而是操作数据,让react自动来响应布局。

​ react如何创建数据呢?

2.1首先需要在组件的构造函数中声明状态,然后把数据定义在这个状态中

1
2
3
4
5
6
7
8
constructor(props){
super(props);
//this.state就是组件的状态
this.state={
list:[],
inputValue:""
};
}

2.2然后让标签和状态里中数据做一个绑定,该怎么做呢?

1
2
//比如input中的值是有value属性决定的,就可以如下和数据绑定
<input value={this.state.inputValue} />

这样input标签就与状态中的inputValue数据绑定在了一起

3、React中事件绑定

​ 原生的标签事件绑定都是使用的小写字母,在react 中必须使用驼峰式的规则。

1
2
3
4
5
6
<input value={this.state.inputValue} onchange=...>
//必须改为
<input value={this.state.inputValue} onChange=.../>

//在jsx的语法里使用表达式,必须将表达式包括在打括号中,其中handlerInputChange函数在组件内定义,与render方法平行
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>

注意此时在handlerInputChange函数中直接使用this.state会报错,显示this指向为undefined,这是由于此处this指向的是input标签,而不是该组件类,如果让它执行该组件类呢,需要绑定this

1
2
//这是在handlerInputChange函数中直接使用this了
<input value={this.state.inputValue} onChange={this.handlerInputChange.bind(this)}/>

为了书写的简洁,可以对所有的函数在constructor中进行this的绑定,这样在标签中就不用书写bind语法了。

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
constructor(props){
super(props);
this.state={
list:[],
inputValue:""
};
this.handlerInputChange=this.handlerInputChange.bind(this);
this.handlerBtnClick=this.handlerBtnClick.bind(this);
this.handlerDelete=this.handlerDelete.bind(this);

}
//注意改变state中的数据,不能直接使用this.state进行改变,必须使用this.setState方法操作
handlerInputChange(event){
this.setState({
inputValue:event.target.value
});

}
...
...
render() {
return (
<Fragment>
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button className='red-btn' onClick={this.handlerBtnClick}>add</button>
</div>
<ul>{this.getTodoItems()}</ul>
</Fragment>
);
}

每一个标签最好有一个key值,特别是当多个相同的标签同时使用时。

在react中,不允许直接改变state的数据,必须通过setState来改变

4、jsx

4.1 jsx中写注释

在jsc语法中写注释需要将注释放在大括号中

1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
return (
<Fragment>
{/*下面是div*/}
{//下面是div}
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button className='red-btn' onClick={this.handlerBtnClick}>add</button>
</div>
<ul>{this.getTodoItems()}</ul>
</Fragment>
);
}

4.2 jsx中如果不希望对字符串转义

例如输入“

hello

”,不希望在页面输出“

hello

”,而是直接显示“hello”,此时可以使用dangerouslySetInnerHTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getTodoItems(){
return (
this.state.list.map((item,index) => {
return (
<Item
delete={this.handlerDelete}
key={index}
{//表示解析item中的html标签,不被转义}
dangerouslySetInnerHTML={{__html:item}}
index={index}
/>
)
})
);
}

4.3 光标的聚焦

在html中,可以使用label标签做聚焦,例如想通过点击某一文字让光标聚焦在input中,本来使用for属性,就可以,但是在jsx语法中需要使用htmlFor属性来代替

1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
return (
<Fragment>
<div>
{//给htmlFor传递某标签的id即可}
<label htmlFor="insertArea" >哈哈哈</label>
<input id="insertArea" value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button className='red-btn' onClick={this.handlerBtnClick}>add</button>
</div>
<ul>{this.getTodoItems()}</ul>
</Fragment>
);
}

4.4 组件数据的传递

父组件可以通过属性想子组件传递信息

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
//TodoList组件
。。。
getTodoItems(){
return (
this.state.list.map((item,index) => {
return (
<Item
delete={this.handlerDelete}
key={index}
content={item}
index={index}
/>
)
})
);
}

//item组件
。。。
render(){
const {content}=this.props;
return (
<div onClick={this.handlerDelete}>{content}</div>
)
}

子组件想要调用父组件中的方法,可以在父组件中通过属性将父组件中的方法传递给子组件,供子组件调用

代码同上,注意需要绑定父组件的this

4.5 setState参数

在使用setState改变状态数据时,可以设置一个参数为prevState表示上一个状态的数据

1
2
3
4
5
6
7
handlerDelete(index){
this.setState((prevState)=>({
const list=[...prevState.list];
list.splice(index,1);
return {list};
}));
}

注意:父组件可以向子组件传值,但是子组件不能改变这个值,这就是react中单向数据流的概念。如果非要在子组件中修改这个值,可以通过父组件传给子组件的方法来操作。

4.6、子组件的传值

react中子组件间的传值会非常复杂,因此建议使用其它框架或方法完成子组件间的值传递。这也是react只是视图层框架的原因。

5深入React

5.1 chrome的react调试

安装扩展React Developer Tools

5.2 PropTypes和DefaultProps

虽然子组件可以接受父组件传来的属性,但是子组件如果不能定义该属性的类型的话,就容易发生错误。此时需要对属性做校验,例如对Item组件添加组件交验:

  • 首先引入PropTypes
  • 然后在代码中添加属性类型校验
1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component }from "react";
import PropsTypes from 'prop-types';//引入包

class Item extends Component{
...
}
//定义子组件接受属性的类型校验
Item.propTypes={
content:PropTypes.string,//限定父组件传来的属性content必须是字符串
delete:PropTypes.func,//限定父组件传来的属性delete必须是函数
index:propTypes.number,//限定父组件传来的属性index必须是数字
}
export default Item;

这样的话,如果父组件传来的属性不符合要求的话,就会给出明显的错误提示。

注意:如果在子组件中为某属性定义了某一类型,并且还使用了该属性,但是父组件没有传递该属性,程序是不会报错的。如果想改变这种情况,可以在类型校验中添加isRequired

1
2
3
4
5
6
7
//定义子组件接受属性的类型校验
Item.propTypes={
test:PropTypes.string.isRequired,//test为字符串,且必须传递,否则或报错警报
content:PropTypes.string,
delete:PropTypes.func,
index:PropTypes.number,
}

此时父组件必须给子组件传递test的属性,不传递就会报错。但是想要如果父组件不传递时给test一个默认值,如何处理呢,此时就用到了DefaultProps

1
2
3
4
5
6
7
Item.propTypes={
test:PropTypes.string.isRequired,//test为字符串,且必须传递,否则或报错警报
}
//这是test是必传的,如果不传,则使用默认值。
Item.defaultProps={
test:"hello world"
}

当然还有很多高级的应用,比如允许一个属性既可以是字符串也可以是数字,可以使用PropsTypes.oneOfType([PropTypes.number,PropTypes.string]),详情请见https://reactjs.org/docs/typechecking-with-proptypes.html

5.3 Props、State和render函数

当组件的props或state发生改变的时候,render函数就会重新执行。

所以当你输入input时会触发handlerInputChange 函数;而handlerInputChange 函数里面改变了state的状态,进而引起render函数的重新执行,这时render就可以拿到state中新的数据来渲染。

当父组件的render函数被运行时,它的子组件的render都将被重新运行。

react中频繁的重绘,得益于它有虚拟dom

6 React中的虚拟dom

6.1 虚拟dom

假设让我们来实现虚拟dom,会有哪些步骤呢?

1、stateshju

2、JSX模版

3、数据+模版,结合生成真实的dom,进行展示

4、state 发生改变

5、数据+模版,结合生成真实的dom,进行展示

常规思路就这样,但是这样会有什么问题呢?

缺陷:

第一次生成一个完整的dom片段,第二次也生成一个完整的dom片段,然后第二次的dom替换第一次的dom,非常消耗性能

如何改良呢?

1、stateshju

2、JSX模版

3、数据+模版,结合生成真实的dom,进行展示

4、state 发生改变

5、数据+模版,结合生成真实的dom,并不直接替换原始的dom

6、新的dom和原始dom做比较,找差异

7、找出input框发生变化,

8、只用新的dom中的input元素,替换掉老dom中的input元素

此时虽然有省去了dom替换的性能,但是增加了新老dom做比较的性能。此时性能的提升并不明显。

而react怎么做的呢(操作dom消耗性能很大,但是操作js对象不消耗性能)

1、stateshju

2、JSX模版

3、生成虚拟dom(虚拟dom是一个js对象,用它来描述真实dom)

["div",{id:"abc"},["span",{},"hello world"]]

4、用虚拟dom的结构生成真实的dom,进行展示

<div id=“abc”><span>hello world</span></div>

5、state 发生变化

6、数据+模版 生成新的虚拟dom

["div",{id:"abc"},["span",{},"byebye"]]

7、比较原始虚拟dom和新的虚拟dom的区别,找到区别是span中的内容(比较js对象不消耗性能)

8、直接操作dom,改变span中的内容

6.2 render中的流程

render函数中的JSX模版,会先转化成js对象(虚拟dom),再转化为真实dom。

所以render中返回的看似htnl标签,其实不是,它是jsx模版。

假如render函数如下:

1
2
3
4
5
6
7
8
9
10
render(){
return <div>hello</div>
//return <div><span>hello</span></div>
}

//效果等同于
render(){
return React.createElement("div",{},"hello");
//return React.createElement("div",{},React.createElement("span",{},"hello"));
}

实际上React.createElement是更偏向于底层的一个接口。而上边的返回div的真实流程是

jsx 调用createElement,转化为虚拟dom(js对象),生成真实dom.

但是当我们的结构比较复杂时 ,调用createElement就会显得更加混乱,而jsx语法就是为了简洁,方便写代码。

虚拟dom有什么好处呢?

1、性能提升 dom的比较变成了js对象的比较

2、它使得跨端应用得以实现,react native(可以将虚拟dom 生成原生组件)

6.3 react中虚拟dom的diff算法

react是如何比较新旧虚拟都没的呢?diff算法

要么state改变,要么props改变(其实props改变也会state),就会触发虚拟dom的比对。

什么时候数据会发生变化,都是调用setState时数据才发生变化。

setState是异步的,如果你连续快速调用三次setState,那么react会把这三次setState合并成一个setState,只需进行一次虚拟dom的比对,这样就不会造成浪费了

diff的比对,遵循同层比对,若此层dom有差别,则不会再比较下层dom。

下面说一下为什么在for循环中,为啥不要使用index作为key值,通过key值可以建立组件一对一的关联,如果使用index作为key时,当仅仅是删除一项时,会带动剩下所有组件key值的变化,相当于重新组装了所有的组件;如果使用另一种唯一值作为key值,即使删除某一项,剩余项的key值也不会发生改变,此时dom只需删除被删除的那一项即可。

7、react中ref的使用

ref是帮助在react中直接获取dom元素的,实际上不推荐使用ref

如何使用ref,先看原来的input框流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render() {
return (
<Fragment>
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
</div>
<ul>{this.getTodoItems()}</ul>
</Fragment>
);
}

handlerInputChange(event){
//console.log(event.target.value);
this.setState({
inputValue:event.target.value
});
}

此时我们是通过event.target来获取数据的,而ref就可以不通过event来获取数据,而是直接通过input来获取数据,怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
render() {
return (
<Fragment>
<div>
<input
value={this.state.inputValue}
onChange={this.handlerInputChange}
//这里第一个括号里的input可以任意命名,大括号里的等号后边input就是这个input标签,
ref={(input)=>{this.input=input}}/>
</div>
<ul>{this.getTodoItems()}</ul>
</Fragment>
);
}

handlerInputChange(event){
//这里就可以直接使用ref了
const value=this.input.value;
this.setState({
inputValue:value
});
}

注意直接操作dom对于新手有时会出现意想不到的效果,比如当我们每次点击按钮时项计算一下当前ul中的li的个数,通常会这么做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
render() {
return (
<Fragment>
<div>
<input value={this.state.inputValue} onChange={this.handlerInputChange}/>
<button
className='red-btn'
onClick={this.handlerBtnClick}
>add</button>
</div>
<ul ref={(ul)=>{this.ul=ul}}>{this.getTodoItems()}</ul>
</Fragment>
);
}

handlerBtnClick(){
this.setState({
list:[...this.state.list,this.state.inputValue],
inputValue:""
});
console.log(this.ul.querySelectorAll("div").length);
}

这是你会发现,每次输出的数量都会比实际的要少1,为什么呢?这是因为setState是异步函数,每次还没更新页面你的console就已经执行了,这时如何避免这种情况呢?

setState第二个参数提供了回调函数,供我们使用,这样就能正常获取正确的数值了

1
2
3
4
5
6
7
8
handlerBtnClick(){
this.setState({
list:[...this.state.list,this.state.inputValue],
inputValue:""
},{}=>{
console.log(this.ul.querySelectorAll("div").length);
});
}

本笔记参考慕课网视频https://coding.imooc.com/class/229.html

react中的css

3、react中的css

3.1 react中的css过度动画

下面我们实现一个过渡动画效果,页面一个文字,一个按钮,点击按钮则文字逐渐消失,再点击,文字逐渐显示。

1、新建一个react组件APP

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 React { Component , Fragment} from 'react';
> import './App.css';//引入css文件
>
> class App extends Component{
> constructor(props){
> super(props);
> this.state={
> show:true
> }
> this.handleToggole=this.handleToggole.bind(this);
> }
> render(){
> return ({
> <Fragment>
> <div className={this.state.show?'show':'hide'}>hello</div>
> <button onClick={this.handleToggole}>tonggle<button>
> </Fragment>
> });
> }
> handleToggole(){
> this.setState({
> //若为true则变成false,若为false则变成true
> show:this.state.show?false:true;
> });
> }
> }
>

2、新建App.css,定义show和hide的样式

1
2
3
4
5
6
7
8
9
10
> .show {
> opacity:1;
> transition:all 1s ease-in;//css3的动画样式,在1s内实现过渡
> }
>
> .hide {
> opacity:0
> transition:all 1s ease-in;
> }
>

3.2 react中的css动画效果

什么是css的动画效果呢?其实是指通过 @keyframes定义的一些css动画

可以在App.css文件中这个定义并使用动画

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
.show {
opacity:1;
transition:all 1s ease-in;//css3的动画样式,在1s内实现过渡
}

//使用动画
.hide {
animation:hide-item 2s ease-in;
}
//定义动画
@keyframes hide-item{
0% {//当进度为0%时 的状态
opacity:1;
color:red;
}
50% {//当进度为50%时 的状态
opacity:0.5;
color:green;
}
100% {//当进度为100%时 的状态
opacity:0;
color:blue;
}

}

这是我们的文字具有动画效果了,可是此时会发现问题,当文字动画执行完之后,文字并没有消失,

这是因为,我们动画的最后一帧并没有保存,该怎么做呢,很简单,

只需要在animation最后添加 forwards即可

1
animation:hide-item 2s ease-in forwards;

这样就实现了想要效果,同理可以对show样式做同样的处理

到此刻就可以实现纯css3的动画,如果想实现js辅助动画,这种方法就解决不了了。那该如何做呢?可以使用第三方框架

3.3 使用react-transition-group实现动画

可以使用react-transition-group实现js辅助动画

3.3.1 CSSTransition的css方式实现动画

1、首先安装React Transition Group模块,参考React Transition Group

1
2
3
> # npm
> npm install react-transition-group --save
>

然后重新启动自己的项目

在可以看到有三个组成

1
2
3
4
> Transition: //更底层的组件,当高层组件不够用时才用它
> CSSTransition
> TransitionGroup
>

下面首先介绍一下CSSTransition

2、在组件APP中引入CSSTransition,

1
2
> import { CSSTransition } from 'react-transition-group';
>

然后在render函数中,就可以使用CSSTransition。只需要将div包裹在CSSTransition标签内就可以,,而不再需要那些自定义的样式,CSSTransition组件会自动的帮我们做一些class名字的增加和移除的工作,如下

1
2
3
4
5
6
7
8
9
10
11
12
>  render(){
> return ({
> <Fragment>
> <CSSTransition>
> //<div className={this.state.show?'show':'hide'}>hello</div>
> <div>hello</div>
> </CSSTransition>
> <button onClick={this.handleToggole}>tonggle<button>
> </Fragment>
> });
> }
>

3、CSSTransition有一些属性,需要了解下

1
2
3
4
5
6
>  * in  :CSSTransition什么时候感知到样式
> * timeout : 动画时间 单位毫秒
> * fade-enter:入场动画将要执行的第一瞬间,还没有执行时,就会被安排在div标签上
> fade-enter-active:入场动画执行的第二个时刻,到入场动画执行结束之前,就会被安排在div标签上
> fade-enter-done: 整个入场动画执行完成之后,就会被安排在div标签上
>

对于上面fade属性在使用时,需要设置前缀传给classNames,当然可以使用fade开头,也可以使用其它单词开头

4、入场动画

最终的render函数可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> render(){
> return ({
> <Fragment>
> <CSSTransition
> in={this.state.show}
> timeout={1000}
> classNames='fade'>
> <div>hello</div>
> </CSSTransition>
> <button onClick={this.handleToggole}>tonggle<button>
> </Fragment>
> });
> }
>

css文件中这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
> .fade-enter {
> opacity:0;
> }
>
> .fade-enter-active {
> opacity:1;
> transition:opacity 1s ease-in;
> }
>
> .fade-enter-done {
> opacity:1;
> }
>

5、出场动画

这时候已经有入场动画了,我们可以把出场动画也给做了,这是就使用fade-exit,fade-exit-active,fade-exit-done

1
2
3
4
> * fade-exit:出场动画将要执行的第一瞬间,还没有执行时,就会被安排在div标签上
> fade-exit-active:出场动画执行的第二个时刻,到出场动画执行结束之前,就会被安排在div标签上
> fade-exit-done:整个出场动画执行完成之后,就会被安排在div标签上
>

我们只需要在css文件中这么添加,render无需添加任何更新(因为已经有了fade前缀了)

1
2
3
4
5
6
7
8
9
10
11
> .fade-exit {
> opacity:1;
> }
> fade-exit-active {
> opacity:0;
> transition:opacity 1s ease-in;
> }
> fade-exit-done{
> opacity:0;
> }
>

5、消除标签

此时我们的效果是 点击按钮实现文字的消失和再显。

如果我们想实现当文字消失时同时这个div标签也移除,而不是占的位置显示空白

此时我们可以使用unmountOnExit属性,就可以达到目的,最终render函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> render(){
> return ({
> <Fragment>
> <CSSTransition
> in={this.state.show}
> timeout={1000}
> classNames='fade'
> unmountOnExit>
> <div>hello</div>
> </CSSTransition>
> <button onClick={this.handleToggole}>tonggle<button>
> </Fragment>
> });
> }
>

以上时通过css的方式实现动画,CSSTransition还提供了通过js实现动画的方式防范如下

3.3.2 CSSTransition的js方式实现动画

比如钩子onEntered就是当入场动画结束之后才会被调用,可以作为CSSTransition的属性调用,函数的参数el指的就时div的元素,这就可以实现js控制动画效果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
render(){
return ({
<Fragment>
<CSSTransition
in={this.state.show}
timeout={1000}
classNames='fade'
unmountOnExit
onEntered={(rl)=>{el.style.color="blue"}}>
<div>hello</div>
</CSSTransition>
<button onClick={this.handleToggole}>tonggle<button>
</Fragment>
});
}
3.3.3 CSSTransition实现首次展示动画

此时还有一个问题,我们首次展示时,文字并没有动画效果,如何让页面首次展示时也有动画效果呢,很简单,直接在CSSTransition标签中添加appear属性为true即可。

1
2
3
4
5
6
7
8
9
<CSSTransition
in={this.state.show}
timeout={1000}
classNames='fade'
unmountOnExit
onEntered={(rl)=>{el.style.color="blue"}}
appear={true}>
<div>hello</div>
</CSSTransition>

此时CSSTransition会在div首次展示时添加动画,但是这个动画并不叫fade-enter,而是叫fade-appear,fade-appear-active,所以我们还需要再css中添加它们

1
2
3
4
5
6
7
8
9
10
11
12
.fade-enter .fade-appear{
opacity:0;
}

.fade-enter-active .fade-appear-active{
opacity:1;
transition:opacity 1s ease-in;
}

.fade-enter-done {
opacity:1;
}

3.3.4 CSSTransition的group方式实现动画

上边我们讲的是针对一个元素的动画,下面讲一下,如何实现多个元素的动画效果。这就需要使用到TransitionGroup了,

1、首先我们引入该组件

1
import { CSSTransition , TransitionGroup} from 'react-transition-group';

现在我门state的show不在存储,而已存储一个数组,如下

1
2
3
this.state={
list:[]
}

2、然后修改render中的动画 标签,注意TransitionGroup还需要配合CSSTransition使用,使用CSSTransition修饰每一个单独的div

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
render(){
return ({
<Fragment>
<TransitionGroup>
{
this.state.list.map((item,index)=>{
return (
<CSSTransition
in={this.state.show}
timeout={1000}
classNames='fade'
unmountOnExit
onEntered={(rl)=>{el.style.color="blue"}}
appear={true}>
<div key={index}>{item}</div>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={this.handleAddItem}>tonggle<button>
</Fragment>
});
}

handleAddItem(){
this.setState((prevState)=>{
return {
list:[...prevState.list,"item"]
}
});
}

这样我们就实现了多个元素的动画了。


本笔记参考慕课网视频https://coding.imooc.com/class/229.html

react生命周期函数

react生命周期函数

1、什么是生命周期函数

生命周期函数是指组件在某一刻会自动执行的函数

  • 1、初始化initialization

    在组件创建时,被执行的函数;进行props和state的设置,可以放在constructor函数中

  • 2、挂载mounting

    当组件被挂载的时候执行,该周期包括如下

    • componentWillMount:组件被挂载之前执行,通常组件只会挂载一次
    • render:组件被渲染,可执行多次
    • componentDidMount:组件被挂载之后执行,通常组件只会挂载一次
  • 3、更新update

    包括props和state更新

    • 3.1 props

      • componentWillReceiveProps

      一个组件从父组件接受参数,只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行。

      如果这个组件第一次存在于父组件中,不会执行;

      如果这个之前已经存在于父组件中,才会执行

      • shouldComponentUpdate:返回true则继续执行,返回false则下一步不执行

      • componentWillUpdate

      • render

      • componeneDidUpdate

    • 3.2 state

      • shouldComponentUpdate:返回true则继续执行,返回false则下一步不执行

      • componentWillUpdate

      • render

      • componeneDidUpdate

  • 4、卸载unmounting

    当一个组件即将被从页面剔除的时候,执行

    componentWillUnmount:

注意:每个组件都存在生命周期函数

2、生命周期函数使用场景

生命周期函数中,除了render之外,都可以不存在。

这是由于react中所有的组件都是继承自React.Component,在Component中内置了所有的其余生命周期函数,唯独没有内置render函数的实现。

2.1 shouldComponentUpdate性能提升

在todolistdemo中,当我们在input中输入东西时,都是都会同时更新input组件和所有的子组件item。之前我们讲过组件重新渲染的条件时state或props变化时,回重新渲染;但是现在item的数据并没有变化,为啥为跟着父组件重新渲染呢?这是因为父组件的render函数执行时,子组件的render都会被执行,这样就会造成很多无谓的渲染,如何避免这种情况呢?

这时shouldComponentUpdate就派上了用场,在内部直接返回false,就可以实现,该组件只在挂载时执行一次,而不会随着数据的更新再次渲染

1
2
3
shouldComponentUpdate(){
return false;//此时该组件不会随着数据的更新而发生重新渲染
}

这样强制返回,简单暴力,不是很合适,可以根据实际情况来判断,当我们用到的数据真的发生改变时就返回true,否则就返回false。可以根据shouldComponentUpdate函数的两个参数来判断

1
2
3
4
5
6
7
8
//在渲染时我们只用到了state中的content的内容
shouldComponentUpdate(nextProps,nextState){
if(nextProps.content!==this.props.content){
return true;
}else{
return false;
}
}

这样就提升了我们组件的性能,减少了无谓的组件渲染。

目前为止,我们讲到提升react的性能的方式有以下几种了

1、将自定义函数与this的绑定,放在构造函数中

2、标签的key值不能使用index

3、使用shouldComponentUpdate,避免无效的组件渲染

2.2 react发生ajax请求

如果我们需要在组件中发送ajax请求,这个请求该放在什么地方呢?比如我们要请求我们要展示多少条,如果放在render中,那么随着render函数的多次调用,ajax请求也会被执行多次。如何让这个ajax请求只执行一次呢?

这时componentDidMount就派上了用场,因为这个函数只会在组件被挂载之后执行一次,之后就不会再被执行了。componentWillMount也有此作用,但是如果放在componentWillMount中可能会与后期的高级技术发生冲突(不再详述),所以不放这里。

react并没有封装ajax请求,此时我们可以使用第三方的框架,axios

1、首先通过控制台,进入项目目录todolist,

2、输入 yarn add axios,此时就能将axios载入到项目之中了

3、在TodoList组件中,引入axios

import axios from 'axios'

4、在componentDidMount中发送请求

1
2
3
4
5
6
> componentDidMount(){
> axios.get('/spi/todolist')
> .then(()=>{alert('success')})//成功
> .catch(()=>{alert('error')});//失败
> }
>

2.3 使用charles实现本地数据的mock

当前开发模式都是前后端分离,为了方便测试需要进行数据的模拟,这就是mock。而charles就可以实现这种模拟。如何模拟呢?参考下面步骤

1、首先下载charles

2、在桌面创建模拟数据todolist.json,在里面填写要模拟的数据

1
2
> ["react","vue","webgl"]
>

3、打开cahrles工具:tools -> Map Loacal

进行url(localhost:3000/api/todolist)的配置即可,并且map到桌面上指定的todolist.json文件

4、然后进入刷新我们的页面,之前的请求就可以得到数据了

5、在componentDidMount中对ajax请求的结果进行处理,setState即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> componentDidMount(){
> axios.get('/spi/todolist')
> .then((res)=>{
> this.setState(()=>{
> return {
> list:res.data
> }
> });
> })//成功
> .catch(()=>{alert('error')});//失败
> }
>
> //或者这么简写
> componentDidMount(){
> axios.get('/api/todolist')
> .then((res)=>{
> //小括号即表示return
> this.setState(()=>({list:[...res.data]}));
> })//成功
> .catch(()=>{alert('error')});//失败
> }
>

如此就实现了ajax请求,并且和组件建立关系

charles可以抓到浏览器对外的一些请求,作为代理服务器


本笔记参考慕课网视频https://coding.imooc.com/class/229.html