0%

React 简书实战

react简书实战

day1

实战环节

环境的搭建->Header组件的编写->首页的编写->详情页的编写->登陆相关业务逻辑的处理->项目开发结束上线

核心技术点

  1. creat-react-app脚手架工具进行项目的搭建

  2. 组件化思维进行项目开发

  3. 深入学习jsx模板语法

  4. 项目的开发调试

  5. 虚拟DOM和diff算法,生命周期

  6. 项目react-transition-group第三方的模块实现复杂的动画效果

  7. Redux Antd UI组件 容器组件 无状态组件等等内容

  8. redux使用之中的redux中间键 比如redux-thunk、redux-saga这些新的技术

  9. 项目样式布局采用最新的Styled-components技术来实现CSS样式的编码,避免组件之间样式的互相影响

  10. 在进行redux之中数据管理的时候,使用react推出的数据框架 Immutable.js,这个库有效的避免我们对数据的误操作

    如果使用这个库还会涉及redux-immutable这个redux的中间键

  11. 使用axios这个工具发送ajax请求

react基础知识的巩固

React开发环境的准备:引入.js文件来使用React->通过脚手架工具来编码->Create-react-app官网提供的脚手架

  • 创建脚手架:

    1. 检查node -v的版本 和 npm -v的版本

    2. 输入命令npm install -g create-react-app

    3. 输入命令create-react-app 文件夹名

    4. 创建之后切换到创建的文件夹 cd 文件名夹

    5. 运行yarn start即可显示 React 主界面

  • 三个文件:

    (1)public下的index.html文件 :页面上显示的html的内容

    ​ (2)src下的index.js: 整个react项目的入口文件 会引入App文件

    ​ (3)src下的App.js : 负责页面上显示Hello Word的内容

day2

React中的组件

​ 定义组件:定义的那个类继承React.Component

  • 只要是jsx语法想在react之中正常的运行,就要引入react 例如 组件(组件标签都需要大写开头) render()下的

    (小写开头的就是h5标签)这些都属于jsx语法

  • index.js ``文件中通过ReactDOM将APP这个组件显示在``id=root这个标签下面

  • render返回的必须是在一个大的元素之中,但是有时候又不想让这个div显示就可以用的占位符来替代最外层的div,不会渲染为任何标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<input 
value={this.state.inputvalue} --inputvalue就是输入框的值
onChange={this.handleInputChange} --通过调用该函数来改变改值
/>

handleInputChange=(e)=>{
this.state.inputValue = e.target.vaue
console.log(e.target.value) --e.target是Input框返回的DOM节点,e.target.value返回的就是value值
console.log(this)
this.setState({
inputValue:e.target.value
})
}
/*如果需要实现改变inputvalue的值就在handleInputChange函数里写this.state.inputValue = e.target.vaue
如果handleInputChange这个函数没有使用箭头函数,此时的this为undefined,可以使用ES6的语法将this的指向指向为Todolist组件
onChange={this.handleInputChange.bind(this)}即可。
  • 改变state里的数据不能像上述引用的方式去改变,必须调用setState的方法去改变state里的数据。

  • 由于react中state是不允许做任何的改变的immutable的特性,因此我们在做list操作的时候需要先做一个拷贝,对拷贝的数据进行处理 如果改变之后做性能优化的时候就会出现问题。

JSX细节语法补充

1
2
3
4
5
6
7
8
9
10
11
1.两种注释方法:
{
//注释内容
}
或者
{/*注释内容*/}
2.为了防止某些标签的内容不被转义 也就是在输入框输入<h1>啦啦啦<h1/>点击提交的时候是显示啦啦啦
return <li key={index} onClick={this.handledelete} dangerouslySetInnerHTML={{__html:item}}></li>
3.在输入框前添加label标签的时候 点击babel标签的内容光标会自动聚焦到输入框上
<label htmlFor='insertArea'>输入内容</label>
<input id='insertArea' className='input' value={inputvalue}

注意

1
2
3
4
5
6
7
8
9
10
新版的react  setState返回的不是一个对象了 要把对象替换成函数
之前: handlechange=(e)=>{
this.setState(
{inputvalue:e.target.value}
)}

现在: const value = e.target.value //对value值做保存
this.setState(()=>({
inputvalue:value})) //写成函数就会变成异步的setState 如果直接写e.target.value就会有问题
}

React优点

  1. 声明式开发:使得我们能减少大量DOM操作代码量
  2. 可以与其他框架并存
  3. 组件化
  4. 单向数据流
  5. 视图层框架
  6. 函数式编程 好处:用react写出来的项目更容易实现前端自动化测试

思考

单向数据流:父组件可以向子组件传值,但是子组件不能改变这个值,只能单向进行传递,不能反过来修改。这就说明为什么在修改item项目的时候,为什么不向子组件传递一个list,而要传递index与item了。

再或者说如果一个父组件有很多个子组件且子组件都共用list,如果将list传给某一个子组件且在该子组件进行修改,会导致其余四个组件中list也发生变化,这样页面出现bug的时候,就不知如何去定位这个bug,对代码的调试是很不方便的,因此要有单向数据流的概念。

如果真的想修改:首先父组件传递给子组件一个方法,在子组件去调用父组件传的这个方法,然后去做删除。

React是一个视图层的框架:react并不是什么问题都解决,只帮助你解决数据和页面渲染上的一些东西,至于组件之间怎么传值(就如1给2传递的话就不能按一层一层传了),并不负责,交给其他组件来做,做到大型项目时,react不够用,只能借助它去搭建视图,做组件传值的时候还要借助redux等的数据层的框架来做额外的支撑。

image-20211117154738062

巩固知识

  • 父组件给子组件传值的时候,子组件要对传来的值做类型的限制。如果对传来的值做了isRequired的限制,如果不传就会报错。
1
2
3
4
5
6
7
8
9
10
11
限制类型有两种放置方法:
1.TodoItem.propTypes={
item:PropTypes.string,
dele:PropTypes.func,
index:PropTypes.number
} --放置在定义完组件的下方(最下方)
2.TodoItem.propTypes={
item:PropTypes.string,
dele:PropTypes.func,
index:PropTypes.number
} --放置在render的上方
  • 当组件的state和props发生改变的时候,render函数就会重新执行。

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

虚拟DOM(本质上就是一个js对象,之所以能提高性能,因为js去比较js对象不怎么耗性能,去比较真实的DOM很耗性能)

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
真实DOM和虚拟DOM性能的差异:
使用真实DOM
1. state数据 **初始数据state
2. JSX 模版 **对应render里的jsx代码
3.数据+模版结合,生成真实的DOM,来显示
4. state 发生改变
5.数据+模版结合,生成真实的DOM,并不直接替换原始的DOM
6.新的DOM 和原始的DOM做比对,找差异
7.找出input框发生了变化
8.只用新的DOM中的input元素,替换掉老的DOM中的input元素
**相比全部替换原始DOM性能提升
**但是还是消耗了一部分性能:新的DOM和原始DOM做比对的性能 性能提升也不是很明显

使用虚拟DOM:
1. state数据
2. JSX 模版
3. 数据+模版结合,生成真实的DOM,来显示
假设真实DOM为:<div id='abc'><span>hello world</span></div>
4.生成虚拟DOM(虚拟DOM就是一个JS对象,用它来描述真实DOM)(相比上面第四步没有生成虚拟DOM 实看是损耗了性能)
[ 'div', {id: 'abc'}, [ 'span',{},'hello world ']] //标签名 标签属性 标签内容 **JS对象 也就是虚拟DOM
5. state 发生变化
6. 数据+模版生成新的虚拟DOM(极大的提升了性能相比创建真实DOM,js对象创建的过程性能是极低的)
[ 'div', {id: 'abc'},[ 'span', {},'bye bye' ]]
7.比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中内容(极大的提升性能)
**比对的是原始的js对象和新的js对象两个js对象的比较是极不损耗性能的。
8.直接操作DOM,改变span中的内容

深入了解虚拟DOM

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
实际的流程是:
1. state数据

2. JSX 模版(类似于div)

/*JSX -> createElement方法 -> JS对象 -> 真实的DOM
return <div>item<div> =>等价为
return React.createElement('div',{},'item')
*/

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', {},'bye bye' ]]

7.比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中内容(极大的提升性能)

8.直接操作DOM,改变span中的内容

虚拟DOM的好处: 1.性能提升:因为生成真实DOM要耗费很大的性能
2.它使得跨端应用得以实现

第2点好处的底层原理:没有虚拟DOM React是无法写原生应用的,根本无法在原生(React Native)的应用里被使用。
例如:没有虚拟DOM,流程就是准备好state数据和模板之后就会渲染DOM,渲染DOM在浏览器是没有问题的,可是在移动端的原生应用里,比如安卓或者ios机器上的代码是不存在DOM这个概念的,所以没有虚拟DOM,在原生的应用里根本无法被使用,所以代码只能运行在浏览器里面,但是有虚拟DOM就不一样了,代码首先会被转化为虚拟DOM(是个js对象)js对象在浏览器可以被识别,在原生的应用里也可以被识别。
换句话说:虚拟DOM生成好了在浏览器下面,可以将虚拟DOM变成真实DOM,浏览器下面会显示出真实的页面。假设在原生应用里,虚拟DOM生成之后可以不生成真实DOM,而让虚拟DOM生成些原生的组件,这样的话同样的代码包括state,jsx的代码都可以被复用,原生的应用里面可以把对应的页面展示出来。因为生成的虚拟DOM,在哪里都可以运行,只需要在网页上将虚拟DOM转化为真实DOM,这样的话网页就可以跑起来。这使得React既能开发网页应用,还可以开发原生应用

虚拟DOM中的Diff算法

1
2
3
4
5
6
比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中内容(极大的提升性能)
当数据发生改变的时候(state和props的改变,其实props的改变实际上也是父组件的state发生改变),虚拟DOM和真实DOM才会发生比对。
归根到底,当你调用setState的时候,数据才发生改变,然后虚拟DOM才发生比对。(setState是异步的,实际上是为了提升react底层的性能,原因:假设连续调用三次setState,变更三组数据,React会把三次setState合并成一次,只是做了一次虚拟DOM的比对,然后去更新一次DOM,可以额外减去那两次DOM比对带来的性能上的耗费)--性能优化
Diff算法就是两个虚拟DOM比对的算法,React采用的是同层的虚拟DOM比对的算法。左边的为虚拟DOM,当数据改变的时候会生成新的虚拟DOM。同层比对,如果一层不满足要求,之后的也不比对了,后面的就直接废弃掉,整个就替换掉,这样会提升性能。(直观上如果第一层不一样后面都一样,导致后面一样的节点没办法复用,性能很低,怎么会提升性能。实际上是因为同层比对的算法很简单,算法简单带来的是比对的速度会非常快。虽然可能会导致DOM重新渲染上的浪费,但是它大大的减少了两个虚拟DOM之间比对的算法上性能的消耗。)
key值要保持相对稳定,在项目中能不用index做key值就不用。
同层比对和key值比对这些比对的方式都是Diff算法的一部分。--性能优化

image-20211130220031980

ref

如果ref和setState一起使用的时候,要注意setState是异步函数,不会立马执行。有时候会使得无法正确输出页面DOM的真实情况

为了避免这种情况,setState还有第二个参数,它是一个回调函数,执行的时机是在等异步的完全结束了再执行。

生命周期函数

生命周期函数指在某一个时刻组件会自动调用的函数(当state和pros变化的时候render就会被调用,render函数就是生命周期函数) 组件一创建就会被调用 constructor函数和生命周期函数

每一个组件都可以有生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shouldComponentUpdate(nextProps,nextState) {
if(nextProps.content !==this.props.content) {
return true;
}else {
return false;
}
}
设置shouldComponentUpdate这个声明周期函数提升了组件的性能,性能提升在避免组件做无谓的render操作。render重新执行就意味着react底层需要对组件重新生成一份虚拟DOM,这样就要和之前的虚拟DOM做比对,虽然虚拟DOM的比对比真实DOM的比对性能要快的多,但是能省略这个比对过程,当然可以节约更多的性能。--性能优化

一般把ajax请求放到ComponentDidMount()来做

如何发送ajax请求,首先我们要借助一个库 在todolist输入命令行:yarn add axios

componentDidMount() {
axios.get('/api/todolist')

.then(()=>{alert( 'success' )})

.catch(()=>{alert( 'error' )})
}

day3

CSS过渡动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以使用关键帧
@keyframes identifier {
0% {
opacity: 1;
color: red;
}
50% {
opacity: 0.5;
color: salmon;
}
100%{
opacity: 0;
color: blue;
}
}
.hide{
animation: identifier 2s ease-in forwards; //如果没有forwards 动画执行结束
}
注意:关键帧当动画执行结束后,最后一帧动画的CSS效果不会被保存下来。加了forwards属性就可以保存下来了
实际上用CSS3开发出的动画具有局限性,涉及js动画的时候就没法处理了。因此得使用下方的模块

使用react-transition-group实现动画

使用这个可能实现更加复杂的动画效果

[官网文档API网址](React Transition Group (reactcommunity.org)

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
App.js文件里
对单个元素实现效果:
<CSSTransition>组件

<CSSTransition
in={this.state.show} //显示的属性
timeout={1000} //显示的时间
classNames='fade' //入场和出场动画属性名匹配
unmountOnExit
onEntered={(el)=>{el.style.color='blue'}}//入场动画结束时显示的
appear={true}//在入场动画开始前就显示样式 同时在style.css要加上appear属性
>
<div>hello</div>
</CSSTransition>
style.css文件里
.fade-enter, .fade-appear {
opacity: 0;
}

.fade-enter-active , .fade-appear-active{
opacity: 1;
transition: opaciy 1s ease-in ;
}

.fade-enter-done {
opacity: 1;

}
补充:很多相关的API都可以展示效果(例如出场动画第一帧,中间帧等等)

对多个元素实现效果:
<TransitionGroup>
{this.state.list.map((item,index)=>{
return( <CSSTransition
timeout={1000}
classNames='fade'
unmountOnExit
onEntered={(el)=>{el.style.color='blue'}}
appear={true}
key={index}
>
<div>{item}</div>
</CSSTransition>)

})}
</TransitionGroup>

注意:如果CSSTransition实现不了的效果就可以去浏览TransitionAPI。(API位置在官网文档最下方)

redux(数据层框架)

用一张图很好的表示有无Redux之间的区别(数据的传递更加简化,便捷)

有Redux不管组件的层次结构有多深,走的流程都是一样的。组件改变->数据修改->其余组件取用

img

整体redux流程:react要去改变store里的数据,首先要一个派发action,action会通过dispatch方法传递给store,store再把之前的数据state和action转发给reducers,reducers是一个函数,当reducers接收到state和action之后,做一些处理之后,会返回一个新的state给到store,store用这个新的state替换掉之前store里的数据,store数据发生改变之后,react组件会感知到store发生改变(用的是store.subcribe()来监测store里状态的改变)这个时候就会从store里面重新取出数据更新组件的内容,页面就发生变化了(this.setState(store.getState()))。

对于todolist的案例要使点击某项实现删除某项的功能 首先先绑定一个删除的回调函数,派发一个action(某项的索引index会传进去)之后在reducers值删除(由于reduces可以接收state,但是决不能修改state)这里就要对之前的状态做一个深拷贝

1
2
3
4
5
6
7
if(action.type === 'delete_todo_item'){
const newstate = JSON,parse(JSON.stringfy(state));
newState.list.splice(action.index,1)//做删除
return newstate
}
**注意** 讲解简书这个项目的老师和尚硅谷的老师(好像不需要深拷贝,用的另外一个方法)对redux基础知识的讲解有点出处。
之后会做整合的博客

对ActionTypes的拆分:目的是为了拼写错误难以找出错误,有了拆分的文件就拼写错误控制台就会直接定位报错的位置

1
export const CHANGE_INPUT_VALUE='change_input_value'

利用actionCreators对action进行管理:目的提高代码的可维护性,加上前端有自动化测试的工具,如果你把action写在一个文 件里,自动化测试也会非常方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
管理前:handleInputChange(e){
const action = {
type:CHANGE_INPUT_VALUE,
value:e.target.value
};
store.dispatch(action);
}

管理后:在actionCreators文件内:
export const getInputChangeAction=(value)=>({
type:CHANGE_INPUT_VALUE,
valuee.target.value
})
handleInputChange(e){
const action = getInputChangeAction(e.target.value)
store.dispatch(action);
}
**高代码的可维护性
*自动化测试也会非常方便

补充

1
2
3
4
5
6
7
8
1. store是唯一的。
2. 只有store能改变自己的内容。
3. Reducer必须是纯函数:给定固定的输入,就一定会有固定的输出,而且不会有副作用。
4。rudux核心API
-creatStore 帮助创建一个store
-store.dispatch 帮助派发action,action会传递给store
-store.getState 帮助获取store里的数据内容
-store.subscribe 帮助订阅store里的改变,只要store改变,store.subscribe接收的这个函数里的回调函数就会执行

UI组件和容器组件

UI组件负责页面的渲染,容器组件负责页面的逻辑

无状态组件

当一个组件只有render函数的时候,就可以使用无状态组件去替换这个普通组件

无状态组件相对于普通组件的优势在于:性能比较高,因为无状态组件仅仅是一个函数,普通组件是js的一个类,类生成的对象还有生命周期函数render,执行的东西要远比无状态组件要多得多。

1
2
3
4
5
6
7
8
9
10
11
无状态组件:
const 组件名 = (props)=>{
return
}
普通组件
class 组件名 extends Component{
render(){
return
}
}
前提:一个组件只有一个render的时候就可以用无状态组件去替换普通组件

Redux中发送异步请求获取数据

通过axios发送请求,将后台返回的数据传给store,但是传之前要做几个流程。首先,创建一个actionCreartors的一个函数initListAction,然后在actionTypes(防止拼写错误的文件)定义一个常量

1
2
3
4
5
6
7
8
9
10
11
axios.get('./list.json').then((res)=>{
const data = res.data;
const action = initListAction(data)
store.dispatch(action)
})
然后在reducers文件里写
if(action.type === INIT_LIST_ACTION){
const newstate = JSON,parse(JSON.stringfy(state));
newState.list = action.data;//这里的data就是后台传过来的数据
return newstate
}

使用Redux-thunk中间件进行ajax请求发送

这个中间件可以使我们把异步请求或者复杂的逻辑放到action里处理

使用redux-thunk可使得创建actionCreartors定义的函数返回的结果可以是一个函数(之前返回的是一个对象),这样就可以发异步请求。

1
2
3
4
5
6
7
8
9
10
11
12
componentDidMount(){
const action = getTodoList();
store.dispatch(action)
}
export const getTodoList = () => {
return (dispatch) =>{
axios.get( '/list.json').then((res)=>{
const data = res.data;
const action = initListAction(data);
dispatch(action); ***最终执行的
})
解释:componentDidMount当组件挂载时执行,store派发函数的action实则是派发给store,但是它先会默认自动执行getTodoList()这个函数,随之就会去执行那个异步action,最终派发的也就是dispatch(action)

day4

redux的标准流程

view在redux之中会派发一个action,action通过store的dispatch方法,分发给store,store接收到action连同之前的state一起传给reducer,reducer返回一个新的数据给store,store拿到新的状态去更新旧的状态。

redux的中间件(redux-saga中间件的使用)

中间件的概念是指action和store之间的中间,指的是redux的中间键(dispatch的升级)因为只有redux有action和store的概念。

redux-sage远比redux-thunk复杂的多,里面内置了很多API

有一点区别的是如果上方store.dispatch(action)store传递的action是一个函数,store接收到action之后它不仅可以派发到reducer还可以派发到一个sagas.js的文件里,可以在sagas.js文件里做一些异步逻辑的操作。

React-Redux的使用

  1. 给组件包裹一个即可让组件使用store里的数据,是react-redux的核心组件。

  2. 通过redux的connect方法,将组件和store做一个连接 export default connect(mapStateToProps,null)(组件)

  3. 做连接就有一个规则,规则就在第一个参数mapStateToProps(翻译过来就是把store里的数据映射给组件,变成组件的props)

1
2
3
4
5
6
const mapStateToProps = (state) => { //state就是store里的数据
return {
inputvalue:state.inputvalue// 这一步映射到组件的props <input value={this.props.inputValue}/>
list:state.list
}
}
  1. connect的第二个参数是mapDispatchToProps(把store的diapatch方法挂载到props)
1
2
3
4
5
6
7
8
9
10
11
12
13
const mapDispatchToProps = (dispatch) => { //state就是store里的数据
return {
changeInputvalue(e){
const action={
type:'change_input_value' //组件里就是onclick={this.props.changeInputvalue}
value:e.target.value
}
dispatch(action); //这一派发就要去写reducer了(匹配类型返回一个新的newState->newState有inputvalue和list)
}
}
}
上述的代码还需像之前一样创建actiontypes和actionCreaters文件
connect生成返回的结果就是一个容器组件(可以理解成业务逻辑,数据,方法对UI组件进行包装)

day5

开始实战

1、引入reset.css文件

使用reset.css的好处:不同浏览器的内核里面对html,body这样的标签,默认样式得设置是不同的,有可能在某一个浏览器上margin间距值是10,而在另外一个浏览器上这个间距值可能就是8。为了能让我们代码在所有浏览器表现形式都是一致的,首先就得对这些浏览器默认对这些标签的实现进行统一,reset.css就是干这个活,使得这些标签在任何浏览器上的margin padding默认都是一样的。一般在开发项目的时候,首先就要先引入reset.css文件。


2、简书网站头部区块的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
与之前不一样的是,对样式的处理需要创建一个style.js文件(样式写在这个文件里,实际上是一个个的组件,不会影响其他页面的组件样式)
也就是我们这个页面用这个组件时候。这些样式只作用这个组件上面,对其他任何地方都没有影响,有效避免写项目潜在的可能css样式冲突的问题
import styled from 'styled-components'
export const HeaderWrapper = styled.div`
height:56px;
background:red;
`
HeaderWrapper是带样式的div标签
用法在header组件里引入后使用 <HeaderWrapper>header</HeaderWrapper>

注意点:
1.在style.js写的样式都要加分号``;``不然样式显示不出来
2.a标签不能设置宽高,必须转换为块元素
3.头部导航条设置padding 不然会导致设置的图标重合

image-20211120103759703

就像这样 因此需要去设置头部导航条

1
2
3
4
5
6
7
8
9
10
  width:960px;
height:100%;
margin: 0 auto;
+padding-right:70px;
+box-sizing:border-box
+号为添加的代码,设置一个内聚变以便使其隔开,但是为了保证盒子宽度不变,还得将盒子设置成border-box

由于搜索框和图标是独立的,因此需要<SearchWrapper></SearchWrapper>标签将其包裹
由于该标签被Nav标签包裹且在Nav标签里的子标签都设置了浮动,宽度失效。如果SearchWrapper不设置浮动,宽度就会继承Nav的宽度
就与搜索框的宽度不一致,因此也得将SearchWrapper设置浮动。

day6

整体交换的流程

创建store文件夹->创建index.js ,reducer.js->编写相应的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
index.js:
import {createStore} from 'redux'
import reducer from './reducer'

const store = createStore(reducer)
export default store;

reducer.js:
const defaultState = {};

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

之后回到App.js引入

1
2
3
4
5
6
7
8
9
import store from './store/index'
import {Provider} from 'react-redux'
export default class App extends Component {
render () {
return (
<Provider store={store}>
<Header/>
</Provider>
}

回到header组件引入connect,,将组件和store做一个连接

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

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

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

由于初始化的数据需要放在reducer.js上 所以将组件里的数据进行转移 —>形成版本1

1
2
3
4
5
6
7
8
9
10
11
12
Header/index.js 将状态转移后删除
constructor(props){
super(props)
this.state={
focused:false
}
}

reduer.js
const defaultState = {
focused:false
}

由于header组件里有很多是用到state里的focused,因此得将从state拿改成从props拿, 此时就需要mapState…方法

将store里状态的focused映射到组件的Props里去,因此就可以将下方图中的state替换成props

image-20211122095058838

1
2
3
4
5
6
7
8
9
10
11
handleInputFocus=()=>{     
this.setState({
focused:true
})
}
handleInputBlur=()=>{
this.setState({
focused:false
})
}
这两个方法之前是为了去改变state里的数据 现在数据都不在state里存放了因此就可以连同方法删掉

之后我们要实现当聚焦的时候要改变focused的值 ->一整套流程

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
当你聚焦的时候,改变store里的数据,就要去调用store的dispatch方法,就要把方法定义在mapDispatch...里
Focus={this.handleInputFocus}
onBlur={this.handleInputBlur}
改成
onFocus={this.props.handleInputFocus}
onBlur={this.props.handleInputBlur}
有这个方法之后,首先要改变store里的数据首先要创建一个action
const mapDispatchToProps=(dispatch)=>{
return{
handleInputFocus(){
const action = {
type:'search_focus'
};
dispatch(action)
},
handleInputBlur(){
const action = {
type:'search_blur'
};
dispatch(action)
}}}

reducers.js
export default (state = defaultState,action)=>{
if(action.type==='search_focus'){
return{
focused:true
}
}
if(action.type==='search_blur'){
return{
focused:false
}
}
return state;

}

总结

我们把之前组件里的数据移除了,转移到redux公共存储的框架进行存储。Header组价就变成了一个无状态组件

1
2
3
4
5
无状态组件的写法 const Header = (props)=>{
1.render以下的内容(不包括render)
2.所有的this去掉
}
到这形成第2

day7

安装redux-devtools-extension工具使得网页可以显示redux组件操作状态的变化

1
2
3
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer,composeEnhancers())

image-20211122212415727

由于reducers存放数据和数据的处理,随着功能的强大,数据量会越来越大,因此不能都放在reducers里存储。

于是在head文件夹下创建一个store的文件夹,创建一个header下的reducer,将store里的reuders移至header的reducers

全局的reducer作为一个仓库,专门来放置各个组件部分的reducers。 此时功能尚未实现,因为header下的index.js文件mapState…方法用的是全局的reducer,因此得修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mapStateToProps=(state)=>{
return {
focused: state.header.focused
}
}

仓库reducers
import {combineReducers} from 'redux'
import {reducer as headreducer} from '../common/header/store' //间接引入reducers 目录结构会少两层

const reducer = combineReducers({

header:headreducer

})
export default reducer
间接引入reducers 目录结构会少两层 因为header下的reduces间接由header下的index.js文件管理了 。引入的时候就方便了

day8

由于现在在header组件下创建了一个头部的reducers,由于在头部组件里改变数据的时候派发action,然后reducer接收action然后去修改数据,之前课程说过action不要写成对象,类型也不要写成字符串。用actionCreate去创建action,同时类型定义成常量。

按这个做下去逻辑太绕了,不易理解。大改文件夹存放的位置

image-20211123111850307

在reducer.js文件里,不能对原始的state做修改,为了防止这一点得引入facebook团队研发出来的一个库:immutable.js

它会帮助我们生成一个immutable对象 (不可改变的)如果state是immutable对象就是不可以被改变,就不会出问题。

immutable这个库提供了一个fromJS方法,可以帮助我们把一个js对象转化为immutable对象,用这个方法把数据对象(js对象)

转化为immutable对象。此时header下的index文件的mapState方法里的数据已经转化为immutable类型的数据(state.header)

如果是immutable类型的数据,再进行state.header.focused这种语法是不合理的了。immutable.js里面调用state.header里的属性不能通过点focused的方式来调用,得通过点get的方法传入focused的属性来获取对应的属性。此时不会报错,但是点击的时候就会报错报错的原因在与当你派发一个action的时候,传入reducers返回的是一个普通对象,普通对象时没有get方法的 为了防止这种情况应该这样写(没修改前与修改前的代码)

总结:借助immutable这个库避免编写改变数据里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
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
未修改前的reducers:
import {SEARCH_FOCUS,SEARCH_BLUR} from '../constant'
const defaultState = {
focused:false
}
const reducer = (state = defaultState,action)=>{
if(action.type===SEARCH_FOCUS){
return{
focused:true}
}
if(action.type===SEARCH_BLUR){
return{
focused:false}
}
return state;
}
export default reducer

修改前的header下的index.js
const mapStateToProps=(state)=>{
return {
focused:state.header.focused
}
}

修改后的reducers:
import {SEARCH_FOCUS,SEARCH_BLUR} from '../constant'
import {fromJS} from 'immuatble'
const defaultState = fromJS({
focused:false
})
const reducer = (state = defaultState,action)=>{
if(action.type===SEARCH_FOCUS){
// immutable对象的set方法,会结合之前immutable对象的值和设置的值,返回一个全新的对象,并没有改变原始的state
return state.set('focused',true) //这里的变更不是直接变更
}
if(action.type===SEARCH_BLUR){
return state.set('focused',false)
}
return state;
}
export default reducer

修改后的header下的index.js
const mapStateToProps=(state)=>{
return {
focused:state.header.get('focused')
}
}

即便是修改后的index.js 由于state是js对象,state.header是immutable对象 所以去调用focused属性的时候,数据获取的行为是不统一的。(不太靠谱)因此我们想让state也成为immutable对象,而不是js对象

state对应的数据是在redux下reducers里的index.js里创建的,因此我们要依赖第三方的模块 redu-immutable

1
2
3
4
import {combineReducers} from 'redux-immutable'//只要combineReducers是由这个库引入的reducer就是immuable对象了  
之后回到header下改如下代码即可
focused:state.get('header').get('focused')
可以简写为 focused:state.getIn(['header','focused'])

下一阶段(搜索区域的布局) F11退出全屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <SearchList>
<SearchInfoItem>教育</SearchInfoItem>
<SearchInfoItem>简书</SearchInfoItem>
<SearchInfoItem>生活</SearchInfoItem>
<SearchInfoItem>投稿</SearchInfoItem>
<SearchInfoItem>历史</SearchInfoItem>
</SearchList>
由于子元素设置浮动了 父元素的高度无法被撑起所以给父元素设置一个BFC
export const SearchList = styled.div`
overflow:hidden;//此时只是开启BFC使其高度和宽度不丢失
`;
之后就是该列表聚焦的时候显示,不聚焦的时候不显示,设置一个函数,给函数接收一个变量show。
如果show为真就返回整个列表,假就返回null
最后再调用一下这个方法 {getListArea(props.focused)} 传入focused的值即可

换一批的功能

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
此时的header是一个无状态组件,之后还要添加很多功能,就会出现函数嵌套函数,就会使Header变得越来越庞大,不好维护
因此还是使用Component组件 将原来props前加一个this 且将显示列表的方法定义在组件里接口

由于列表里的内容实际上是通过ajax数据获取的,因此header里不进要有focused的数据还得有列表的内容
因此在header的reducer里加一个list:[];
当导航的input框做了聚焦之后,就要获取ajax数据,也就是onFocus的时候,onFocus执行的是handleInputFocus这个方法
找到这个方法在方法里获取ajax数据,之前的课程有说过,获取ajax数据一般来说不会直接写在组件里,会把异步的操作放到action,或者放到redux-saga里面进行处理,这里统一使用redux-thunk,把异步操作都放至action里处理
redux-thunk其实就是action和store之间的中间件 其实就是对dispatch方法的升级
因此得写在创建store之前被使用
import {createStore,compose,applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducers'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer,composeEnhancers(
applyMiddleware(thunk)
))
export default store;
有了redux-thunk之后,就可以去action里面做异步操作了
dispatch(getList());去派发这样一个action
以前的action返回的是一个对象,现在可以返回函数
函数里写个输出123 只要聚焦控制台就会显示123
发请求前要先安装包axios
axios.get('/api/headerList.json').then((res)=>{
}).catch(()=>{
console.log('ERROR');
})
}
在public文件夹下创建一个api文件夹之后在创建headerList.json的文件输入一下数据 之后在网页访问这个链接页面就可以显示数据
原理:首先先到功能目录下看看有没有对应的路由,找不到就会去public目录下找api的headerList.json,找到后就会把文件的内容输出出来
通过这个特性,我们可以创建一些假数据,保存。
模拟的数据要和后端对的数据要保持一直,就是定一下格式
public下的api下的header.json添加模拟的数据 此时:
axios.get('/api/headerList.json').then((res)=>{
console.log(res // 当你点击搜索的时候,控制台已经有输出了 见下图
}).catch(()=>{
console.log('ERROR');
})
}

image-20211123162311226

之前header下的reducer里定义了一个list:[],现在发送了一个ajax请求,获取到了结果,现在就是希望能用获取的结果替换那个空数组

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
action.js文件里
export const getList = () =>{
return (dispatch)=>{ //返回函数,就要就是去派发一个异步的请求
axios.get('/api/headerList.json').then((res)=>{
const data = res.data;
const action = changeList(data.data);
dispatch(action)
}).catch(()=>{
console.log('ERROR');

})}}

reducer.js文件里
if(action.type===CHANGELIST){
return state.set('list',action.data)
//console.log(action);//返回的是{type: "change_list", data: Array(50)}
return state
这样写实际上还是有问题的:创建store,默认list是一个空数组,fromJS方法会将js对象转换为immuable对象,list是个数组,也会变成immuable的数组,但是调用set去改变list的时候,action.data实际上是一个普通的数组, list数组会由immuable数组变成一个普通的数组,这样的话数据类型就会变了,解决这个问题
export const changeList = (data) =>({
type:CHANGELIST,
data:fromJS(data)
})
这样转换一下 const action = changeList(data.data);//输出data后的结果
这里派发的时候数据就会转换成immuable的数据类型。这样reducer里接收到action的数据类型也就是immuable类型了。
做到这里就可以使用redux的插件去查看显示header里面就list的数组了

image-20211123170830305

之后就是将list里的数组逐一放至getListArea的方法里展示

总结

实际上讲的就是项目中用react-thunk发送ajax数据。获取到数据之后,存到store里面,然后在页面上进行显示,有几个关键点

第一个我们把异步获取的逻辑都拆分到actions文件夹下,如果你想拆分到这里来,就要求action返回的对象不能是一个js对象,而是一个函数,如果要返回的是一个函数就必要要用redux-thunk这个中间件,这是第一个重点

第二个当我们获取到数据,想要改变store里的数据的时候,我们要注意redux里的list因为外层包裹了一个fromJS,它会在创建的时候,会把内部的list定义的数组由一个普通的数组转换为immuable类型的数组,但是想直接改变这个状态的数组,就会把之前immuable类型变成普通数组的类型,因为我们就要再action之前传递action之前就要把数据转换为immuable类型的数据,这样数据统一就不会有问题。

页面循环展示数据,就要调用map方法通过对每一项进行循环展示,对对应的内容都展示在页面上 —功能实现

避免无意义的ajax请求发送(上方衔接最后补充的内容)

每点击一次焦点的时候就会发送一次ajax请求。实际上列表中的数据获取一次就可以了

解决问题:给handleInputFocus(list)传入list 然后输出list可以看出第一次请求的时候list.size是0,之后就是50,

所以控制当size为0的时候才发送请求,之后不等于0的时候就不发生了,这样就避免了每次获取焦点的时候都发一个ajax请求,从而做到性能上的调优。

1
2
3
4
5
6
7
8
 handleInputFocus(list){
if(list.size===0){
dispatch(getList());
} 或者 (list.size>0)&&dispatch(getList());意思都一样
dispatch(searchFocus());
},

样式加一个cursor:pointer 可使鼠标放上去有个小手的标志。

路由

1
2
3
4
5
import {BrowserRouter,Route} from 'react-router-dom'
BrowserRouter代表路由 Route代表路由规则
首先先引入路由组件
下载包:npm add react-router-dom
**注意** 前两周这个包刚更新过如果按之前的编码规则会使得路由组件无法显示(太坑了官网也不发布,害我整了半天)

image-20211124201227148

把首页组件拆分成一个个小的组件

外层元素:margin-left: -18px;

内层元素:margin-left: 18px;、

这样就可以充分利用左侧栏的宽度,使其保持一直

1
2
3
4
5
6
7
8
9
10
   <TopicItem>
<img
className='topic-pic'
src='//upload.jianshu.io/collections/images/261938/man-hands-reading-boy-large.jpg? imageMogr2/auto-orient/strip' alt='niao'
/>
社会热点
</TopicItem>
这是一个标题数据,之后我们拿来用的时候需要从redux里取 数据都存在store里,而Topic是home的自组件 而home是APP的子组件
因此Topic是APP的自组件 想使用store里的数据需要connect做连接
定义一个获取状态的方法 mapState,从reducer获取到list给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
import React, { Component } from 'react'
import {TopicWrapper,TopicItem} from '../style'
import {connect} from 'react-redux'
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') } //reducer.js定义好的
alt="nihao"/>
{item.get('title')}
</TopicItem>
)
})}

</TopicWrapper>
)
}
}
const mapState =(state) =>({
list: state.get('home').get('topicList')
})
export default connect(mapState,null)(Topic)//因为不需要修改状态 所以暂且先传入一个null

day9

页面的组件,首先闲在reducer的初始状态定义好相应的信息

1
2
3
4
5
6
7
8
9
10
11
articleList:[{
id: 1,
title:'大数据分析可视化平台',
desc:'支持直连MySQL、SQL Server、PostgreSQL、Oracle、SAP HANA、Hive等数据源,也可上传本地Excel/Csv文件或通过API连接数据。可完成多源交互分析',
imgUrl:'https://lupic.cdn.bcebos.com/20200412/3048304592_14_800_600.jpg'
},{
id: 2,
title:'大数据分析可视化平台',
desc:'支持直连MySQL、SQL Server、PostgreSQL、Oracle、SAP HANA、Hive等数据源,也可上传本地Excel/Csv文件或通过API连接数据。可完成多源交互分析',
imgUrl:'https://lupic.cdn.bcebos.com/20200412/3048304592_14_800_600.jpg'
}],

之后在组件里通过调用connect 里调用因为这个状态是immuable数据类型 因此需要用get方法来引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import {RecommendWrapper,RecommendItem} from '../style'
import {connect} from 'react-redux'
// import imgone from '../1.jpg'
class Recommend extends Component {
render() {
return (
<RecommendWrapper>
{
this.props.list.map((item)=>{
return <RecommendItem imgUrl={item.get('imgUrl')} key={item.get('id')}/>
})
}
</RecommendWrapper>
)
}
}
const mapState=(state)=>({
list: state.getIn(['home','recommendList'])
})
export default connect(mapState)(Recommend)

首先所有的数据内容都存在前端的代码里,都是写死的,实际做项目的时候,通过接口来获取的,不能写死。

在public下的api定义一个接口home.json。之后在home组件下借助componentDidMount这个声明周期函数来发ajax请求去获取数据,获取了数据,(当组件挂载完毕)。就要将获取的数据取修改store里初试的数据

所以要创建一个action,派发给store,需要store和组件建立连接

修改store里的数据也就是状态 就得使用connect的第二个参数,通过这个函数就可以定义一个方法将action派发给store

因为UI组件不能和store直接通信,只能通过容器组件。派发给store之后store就会派发给reducer。

1
2
3
4
5
case 'change_home_data':
console.log(action);//返回的就是ajax请求获取出来的内容
state.set('topicList',action.topicList)
//action.topicList是JS对象而上面的topicList是immuable对象不能替换因此要在前面加一个fromJS(action.topicList)
由于是多个对象可以用state的一个方法做一个合并 这样就实现了通过ajax异步的形式获取到了首页的内容

上述的操作都是不合适的因为UI组件不应该有太多的业务逻辑,发ajax请求不应该在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
40
41
42
43
44
45
46
47
48
修改前
componentDidMount(){
axios.get('./api/home.json').then((res)=>{
const result = res.data.data;
console.log(res);//如果成功会打印 因为这个路径下有这个文件 因此是可以成功的
const action={
type:'change_home_data',
topicList:result.topicList,
articleList:result.articleList,
recommendList:result.recommendList

}
// console.log(res.data.data);
this.props.changeHomeData(action)

})
}
}
const mapDispatch = (dispatch)=>({
changeHomeData(action){
dispatch(action);
}
})
export default connect(null,mapDispatch)(Home)
第一次修改后:
componentDidMount(){

// console.log(res.data.data);
this.props.changeHomeData()


}}
const mapDispatch = (dispatch)=>({
changeHomeData(){
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);
})
}
})

export default connect(null,mapDispatch)(Home)

再次升级 ajax请求不放在容器组件里 放在action里 通过调用那个函数

1
2
3
4
5
6
7
8
componentDidMount(){      
// console.log(res.data.data);
this.props.changeHomeData()
}}
const mapDispatch = (dispatch)=>({
changeHomeData(){
dispatch(getHomeInfo())//因为派发的action是一个函数 且返回的是一个函数。如果这个action是个函数,就会顺序执行这个函数,执行这个函数action就会去派发 页面就重新执行了
}});

而后action里代码可以做升级

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
action下home.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);
})

}
}
优化后
import axios from 'axios'
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;
const action = changeHomeData(result)
dispatch(action); 或者写成 dispatch(changeHomeData(result))
})

}
}
之后让type成常量



<LoadMore onClick={this.props.getMore}>更多文字</LoadMore>*** 不用写()

对于页数的编码,首先先在reducers定义一个初始状态,定义完之后就可以回到List组件里定义mapState方法中的page:state…

定义完之后将UI组件里的LoadMore里的onClick的回调函数设置为箭头函数 传page进去, 下面调用方法的函数也传page

之后发异步action

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
 <LoadMore onClick={()=>this.props.getMore(this.props.page)}>更多文字</LoadMore>
</div>

)
}
}
const mapState =(state)=>({
list:state.get('home').get('articleList'),
page:state.get('home').get('articlePage')
})

const mapDispatch=(dispatch)=>({
getMore(page){
dispatch(getMoreList(page))
}})

异步action
export const getMoreList=(page)=>{
return (dispatch)=>{

axios.get('./api/homeList.json?page=' + page).then((res)=>{
const result = res.data.data;
const action = AddHomeData(result,page+1) //这里每次点击更多的时候页面加1
dispatch(action);
});

}}
每一个异步action都有一个同步action
const AddHomeData = (list,nextpage)=>({
type:ADD_HOME_LIST,
list:fromJS(list),
nextpage

})
之后就回到reducer 去更新原始页面的状态

回到顶部

回到顶部
1
2
3
4
5
handleScrollTop() {
window.scrollTo(0, 0);//回到顶部
}

之后实现页面再顶部的时候不显示回到顶部这个图标 在底部才实现

然后在home.js下的传入connect的第一个参数

1
2
3
4
5
const mapState =(state)=>({
showScroll:state.getIn(['home','showScroll']) //由于reducer的初始值有定义
})
控制这个值的truefalse 来实现是否显示回到顶部的功能
{this.props.showScroll ? <BackTop onClick={this.handleScrollTop}>回到顶部</BackTop>:null}

day10

在组件刚挂载的时候绑定一个监听事件来监测滚轮是是否滑动

1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount(){
this.props.changeHomeData()
this.bindEvents();
}
bindEvents(){
window.addEventListener('scroll',this.props.changeScrollTopShow)
}
}

调用的方法:
changeScrollTopShow(e){
console.log(e); //打印的就是下图一个个事件
}

image-20211126095729529

如果不打印事件 换做

1
2
3
4
5
document.documentElement.scrollTop(获取滚动条位置)要获取当前页面的滚动条纵坐标位置
changeScrollTopShow(){
console.log(document.documentElement.scrollTop);
}
我也不明白为什么我打印出的是小数

image-20211126100207963

之后将这个值设置大于400的时候显示回到顶部,否则隐藏,因此我们要改reducer里的数据,通过发action进行改

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
changeScrollTopShow(){
if(document.documentElement.scrollTop>400){
dispatch(toggleTopShow(true))
}else{
dispatch(toggleTopShow(false))
}

action.js文件
export const toggleTopShow=(show)=>({
type:TOGGLE_SCROLL_TOP,
show
})

reducer.js文件
case TOGGLE_SCROLL_TOP:
return state.set('showScroll',action.show)
功能实现 但是需要注意的是创建组件组件挂载的时候,往window上绑定的scroll事件的监听的时候,一定要把scroll事件监听在window上移除
这样组件的事件就不会影响其他的组件
reducer.js代码优化:

case CHANGE_HOME_DATA:
return state.merge({
topicList:fromJS(action.topicList),
articleList:fromJS(action.articleList),
recommendList:fromJS(action.recommendList)
})

定义一个方法
const changeHomeData=(state,action)=>{
return state.merge({
topicList:fromJS(action.topicList),
articleList:fromJS(action.articleList),
recommendList:fromJS(action.recommendList)
})
}
然后调用:
case CHANGE_HOME_DATA:
return changeHomeData(state,action) ***修改部分
case ADD_HOME_LIST:
return addhomedata(state,action)

首页性能调优

因为首页的index.js都调用了connect的方法,和store做了连接,这就会产生一个问题,只要store发生了改变,那么每一个组件都会被重新渲染,也就是每个函数的render函数会被重新执行,可能有些数据发生改变了,但是那个数据和这个组件一点关系都没有,但是这个组件依然会被重新渲染,导致性能不好。提高组价性能 shouldComponentUpdata,可以在这里做性能优化的代码,判断只有与这个组件相关的数据发生改变才让render函数重新执行(重新渲染),否则return false不让render函数重新执行。

通过这种方式来避免虚拟DOM的比对,提高性能,react也考虑到了这点,如果你在每个特组件都去自己写should….太麻烦了,react内置了新的组件类型,引入PureComponent,区别就是PureComponent内在自己底层实现了should…,这样就不用我们手写收should……做性能优化。之后就是每个UI组件都替换为PureComponent

之所以项目用PureComponent,是因为项目的数据管理用了immuable.js的框架,它可以保证我们的数据是immuable的,这样PureComponent和immuablejs数据格式的管理相结合,使用PureComponent一点问题都没有,但是如果你在你的项目里面,没有使用immuable.js管理你的数据,那么使用PureComponen有的时候会遇到坑。(偏底层的坑)

实现点击title跳转至详情页面

当我们在react实现页面跳转的时候,我们要用到react-router-dom第三方模块,它的这种跳转是单页应用跳转,所谓单页应用跳转就是做一个单页应用的时候,单页应用指的是,不管怎么做页面的跳转,整个网站只会加载一个html文件,只会加载一次html文件,那么来看使用a标签来做页面的跳转,符不符合单页应用的定义。network里Doc可以看加载的html文件,使用a标签就是会重新加载html文件,会由原来的localhost变为detail的html,增加了一次html请求,比较耗性能,因为借助react-router-dom的时候,完全没必要重新加载页面,做页面的跳转,正确的做法是用Link代替a标签。

注意:link标签要在Router里面否则会报错,在做头部点击简书logo的时候,因为link在head里,而APP组件的Header是在BrowseRouter外部因此违背了这个规则 导致报错

image-20211126111958584

或者是报这个错

img

day11

详情页面的布局

从reducer里导入初试状态的content时候会导致标签转义,所以页面显示的内容不是该有的内容。

1
2
3
dangerouslySetInnerHTML={{__html: this.props.content} 这样写不会被转义

之后这些数据想在后端被获取而不是通过store里的原始数据取获取。就需要发ajax请求,步骤都是和之前一样

实现点击不同的title显示不同的详情页(匹配id)

在List.js文件夹下 路径下加上一个id 打开浏览器不显示了

原因是在APP.js组件里路由组件匹配的路径是/detail 而不是/detail/1 因此得在路由组件匹配的时候出换一个‘/detail/:id’

获取参数的第一种方法:动态获取参数

首先打开详情页面的代码 在render和return之间先 打印输出log(this.props)//在控制台里可以看出props里match里的params里有id 然后就是在组件挂载时传入id

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
action.js
export const getDetail=(id)=>{
return (dispatch)=>{
axios.get('/api/detail.json?id='+ id).then((res)=>{//把id传给后端
const result=res.data.data;
// console.log(res.data.data);
dispatch(changeDetail(result.title,result.content))

})
}
}
detail下的index.js
class Detail extends Component {
render() {
return (
<DetailWrapper>
<Header>{this.props.title}</Header>
<Content dangerouslySetInnerHTML={{__html: this.props.content}}/>
</DetailWrapper>
)
}
componentDidMount(){
const {id} = this.props.param
this.props.getDetail(id);//通过后端去返回
}
}
const mapState=(state)=>({
title: state.get('detail').get('title'),
content: state.get('detail').get('content')
})
const mapDispatch=(dispatch)=>({
getDetail(id){
dispatch(getDetail(id));
}
})

然后传递路由参数出现了问题 是由于使用了react-router-dom v6的版本 下面有解释
export default connect(mapState, mapDispatch)((props)=>(
<Detail
{...props}
param={useParams()}
/>
))

巨坑由于使用了react-router-dom v6的版本 使得组件props参数里没有location history等属性。如果使用v5的话就完全避免了这个问题(使用v5下载包的时候附上版本号yarn add react-router-dom@5.3.0)

image-20211126192533225

image-20211126192612881

登陆组件

创建reducers 定义初始状态,login:false 之后在去头部的map方法里那login组件里的login

1
2
3
4
5
login:state.getIn(['login','login'])
{
login ? <NavItem className='right'>退出</NavItem>:<NavItem className='right'>登陆</NavItem>
}
就可以在页面的头部看到是否处于登陆状态还是退出状态

接下来如果用户没有登陆不是显示这个字样 而是跳转到登陆页面

1
2
3
4
5
6
7
8
9
10
11
12
13
 <Link to='/login'><NavItem className='right'>登陆</NavItem></Link>
回到登陆组件
<Button onClick={() => this.props.login(this.account, this.password)}>登录</Button>
因为登陆需要调用ajax接口,把账号密码传递过去的。这时候需要把它写进mapDispatch里 因为要发异步的请求,异步action
只有它才能派发异步action

<Input placeholder="账号" ref={(input) => {this.account = input}} />
<Input placeholder="密码" type="password" ref={(input) => {this.password = input}} />
这两句话的意思就是:通过ref把这两个DOM分别存在this.account和this.password里
这样在登陆的时候就可以调用箭头函数把account和password传进去
之后就是发ajax请求 action reducers 常见的套路
实现的结果就是输入账号密码之后头部的登陆变成了退出 现在就是要实现点击登陆跳转到首页
重定向到首页 又是版本的问题,之前的重定向Redirect也不能用了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const {loginState} = this.props;//false
if(!loginState){
return (
<LoginWrapper>
<LoginBox>
<Input placeholder="账号" ref={(input) => {this.account = input}} />
<Input placeholder="密码" type="password" ref={(input) => {this.password = input}} />
<Button onClick={() => this.props.login(this.account, this.password)}>登录</Button>
</LoginBox>
</LoginWrapper>
)
}else{
return < Navigate to='/' />
}
之后做退出的功能回到header组件的index.js下
1
2
3
4
5
6
7
8
 {
login ?
<NavItem className='right'>退出</NavItem>:
<Link to='/login'><NavItem className='right'>登陆</NavItem></Link>
}
在这里添加一个点击事件onClick={logout}通过调用该回调函数去修改login的值,所以要去派发action
因为这是在header.js下的 要引用login的action
修改数据得到的结果也就是头部的显示结果 就是点击退出的时候上面的退出会改成登陆

创建写文章(只有在登陆状态下才能写)

对于一个组件的实现流程:首先建立writer文件夹下的index.js 把login复制过来,沿用它的一些逻辑

如果登陆了就写文章,如果没有登陆就跳转到登陆见面 然后把这个组件放到APP.js里面 。

同时在头部组件里的一个写文章外部加一个Link 跳转到Writer组件里之后就不做了

代码优化

  1. 在每个方法后面加分号;
  2. 在action里发异步请求的时候都默认发成功的请求,在企业级项目都有错误的情况.catch()异常逻辑进行补充

异步组件

首先点开network组件 JS刷新页面, 跳转页面,登陆页面,都是加载bundle.js,存在一个问题:所有页面对应的组件代码,都是bundle.js,现在访问的是首页,实际上登陆和详情页面的代码一起都加载了,这样首页加载的速度就有点慢。解决问题:当你访问首页的时候,你只加载首页的代码,访问详情页的时候再去加载详情页的代码 使用异步组件来实现,底层比较复杂,但是使用封装起来的第三方模块会非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
npm add react-loadable
异步组件的时候会有一个问题如果是v5版本的话
detail下的index.js是有用到params参数的,以前可以直接获取是因为
因为APP.js里以前引入的Detail组件,路由对应的组件就是这个组件import Detail from './pages/detail';
<Route path='/detail/:id' exact component={Detail}></Route>
如果是这样detail可以直接拿到路由信息
但是现在不一样了引入的是import Detail from './pages/detail/loadable.js';
所以loadable.js可以获得所有的路由信息,但是它下面的组件获取不了路由内容,因为它不是Router直接对应的组件
箭头所示的就是下面的组件
解决方法就是在index.js组件里加一个withRouter **这是v5的 v6不需要以上操作
调用该方法让Detail组件有能力获取到Router里所以的参数和内容
export default connect(mapState, mapDispatch)(withRouter(Detail));

image-20211126220845485

注意:使用jsx语法要引入React

day12

补充(之前漏看的视频)

首先在header组件的初始状态定义一个page和totalpage。page的作用是通过页码对内容进行控制,totalpage指的是整体的数据一共有多少页。 通过点击输入框就可以将获取列表,在列表里的action获取到数据后在同步action里面定义总共的页码。

之后在把页面传给reducers,打开reudx点击搜索就可以看到如下获取的列表和总共的页码

1
之后在map方法里获取页码  ``  page:state.getIn(['header','page']),``

image-20211127192232109

之后在map方法里获取页码 page:state.getIn(['header','page']),

让它一页显示10个

1
2
3
4
5
6
7
8
9

const {focused,list,page}=this.props
const pageList=[];
const Listpage = list.toJS(); //因为从props里获取的list是immuable数据类型 该数据类型不能使用Listpage[i]方法,要先转换
for(let i=(page-1)*10;i<page*10;i++){
pageList.push(
<SearchInfoItem key={Listpage[i]}>{Listpage[i]}</SearchInfoItem>
)
}

之后点击搜索显示的就是10个内容 但是才存在一个问题,点击换一换的时候input框会失焦,一失焦下面的区块就隐藏掉了,什么时候隐藏,其实并不是靠这个input的focus来控制的。实际点击区块的白色部分的时候,已经失焦但是区块不会隐藏,把鼠标移出去的时候才会隐藏。因此下面区块的展示和隐藏并不是完全由focus的状态来决定的还由一个mouseleft和mousemove决定的。

因此再加一个属性 mouseIn:false,,因为一开始鼠标就是没进入这个区块,鼠标移入进去,变成true。

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
<SearchInfo onMouseEnter={this.props.handleMouseEnter}//定义一个方法 派发action
改变mouseIn:false,这个的状态
同理再加一个 onMouseLeave={this.props.handleMouseLeave}这个方法
一直我们是通过focus来控制显示还是隐藏,其实现在focus不管用,还得借助mouseIn这个变量一起来控制,因此要从mapstate拿到mouseIn
if(focused||mouseIn)如果mouseIn为true也显示
下面就是点击换一换 下面的内容也要跟着变
定义方法onClick={this.props.handleChangePage}
handleChangePage(){
dispatch(changePage())//派发action
},
之后就是修改page的状态由于状态修改page不能直接加1
因此还要获取totalPage的状态
handleChangePage(page,totalPage){
if(page<totalPage){
dispatch(changePage(page+1))
}else{
dispatch(changePage(1))
}
把这个函数设为箭头函数之后把page和totalpage传进去之后进行判断 之后去修改状态即可
但是还是会报错原因就是输出for循环的时候输出的是undefined。原因是因为当你刚进入页面的时候,header就会被渲染,初始顺序就是list是个空,page是个1, 然后for循环一定会执行,所以会循环19 但是list是一个空数组,那么空数组list[0]都是undefined
那么key就是undefined。 因此外层要包裹一个if判断
if(pageList.length){
for(let i=(page-1)*10;i<page*10;i++){
pageList.push(
<SearchInfoItem key={Listpage[i]}>{Listpage[i]}</SearchInfoItem>
)
}
}

之后点换一换的时候让i标签的动画动一下

1
2
3
4
5
6
7
<SearchInfoSwitch
onClick={()=>handleChangePage(page,totalPage,this.spinIcon)}
>
//将this.spinIcon传进onClick方法
<i ref={(icon)=>{this.spinIcon = icon}} className="iconfont spin">&#xe675;</i>
换一批
</SearchInfoSwitch>

之前换一批图标功能未实现,且iconfont图标在下侧,原因是因为之前设置的iconfont样式覆盖了它

只要把之前的iconfont样式改成zoom即可

当你点击换一批的时候,我们希望i标签的值发生变化, 有一个ref的内容可以回去i标签真实的DOM节点

之后把相应的值传进,之后在方法下方输出spin 控制台就会输出

image-20211128094949731

就获取到spin对应的DOM了

  
1
2
3
4
5
6
7
8
9
10
11
spin.style.transform='rotate(360deg)';//当你点击的时候就会旋转360 但是只会执行一次因为是转360
可以获取上一次旋转的角度之后每一次加360度 下一次就是720
使用单纯CSS transition动画效果来实现
let originAngle = spin.style.transform.replace(/[^0-9]/ig,'')//不是0-9的数字都替换为空
if(originAngle){
originAngle=parseInt(originAngle,10)
} else{
originAngle=0;
}
spin.style.transform='rotate('+(originAngle+ 360)+'deg)'//0-360 360-720都会有过渡效果
react是面向数据编程,最难的是在redux里如何对数据进设计

1、react是面向数据编程,最难的是在redux里的数如何的被设计


2、改变它里面的数据,遵循redux里单向数据流的流程,首先先派发action给store,store再给reducers,reducers返回一个新的数组再给store,store去变更自己的内容,当数据发生变更,页面就自动发生变化了。

day13

简书项目上线

首先在后端的htdocs文件夹下创建一个api文件夹,后端的人在api文件夹下的json所各种复杂的运算处理,然后前端就可以将简书项目public文件夹下的api文件夹删除了,因为开发要做接口的模拟,然后在控制台运行npm run build,就会生成一个build文件夹,在build目录下有很多项目需要上线的文件,把build下的所有文件复制粘贴至htdocs文件夹下(和api一个同级)这样就完成了项目的上线。

整体的流程就是将前端的代码拷贝到后端的项目,访问的也就是后端的项目(访问路径就是之前的一个软件XAMPP)。

image-20211128153306716

image-20211128153010553

点start就可以在本地启动一个服务器(启动一个Apache服务器),会启动在localhost:80端口上(可以简写localhost)

完美撒花

  1. react的基础语法
  2. redux数据层框架
  3. react-redux如何方便我们在react之中使用redux
  4. react-router(本课程用的是4.0的版本 而我采用的是6.0天呐)
  5. immuable.js 相关的第三方模块
万一真有土豪呢!!!