状态管理
Flux
核心组成部分
- Store
- 存储状态(数据)
- 通过事件的订阅和发布更新视图
- Views
- 视图,使用React 组件充当
- Actions
- 动作创建者
- 视图中的数据交互和用户交互
- 动作创建者
- Dispatch
- 派发器
- 修改数据
- 派发器
Flux流程
-
要想使用FLux架构思维,需要通过一个工具进行使用, 这个工具就是flux
yarn add flux
-
在src目录下新建store文件,里面新建index.js
- 存储数据
- 使用事件的订阅和发布来更新视图
- 使用node中的events模块来完成
- on
- emit
const events = require( 'events' ) //使用events模块 const EventEmitter = events.EventEmitter const store = { ...EventEmitter.prototype, //获得其中的方法,如on state: { //state就是用来存储数据的 count: 0 }, getState () { // 这个方法是帮助我们获取store里面的状态的 return this.state } } export default store
-
将store中的数据显示在组件(视图)中
import store from './store' class xxx extends React.Component{ constructor () { super() this.state = { count: store.getState().count //获取数据 } } render () { return ( <div> <p> { this.state.count } </p> </div> ) } }
-
用户操作,用户点击按钮,执行当前组件中的方法,这个方法的逻辑实际上是actionCreators中的方法
-
创建actionCreators.js(名字/方式不固定)
- actions的发送要通过dispatcher来发送
import * as type from './type' //方法名 import dispatcher from './dispatcher'; //引入dispatcher,可以用{ }形式 const actionCreators = { increment () { // 创建动作 let actions = { type: type.INCRMENT } // dispatcher来通过dispatch 发送actions dispatcher.dispatch( actions ) } } export default actionCreators
-
创建dispatcher.js
import { Dispatcher } from 'flux'; import * as type from './type' import store from './state' const dispatcher = new Dispatcher() // dispatcher.register( callback ) dispatcher.register( ( actions) => {//actions就是actions发送过来的 switch ( actions.type ) { case type.INCRMENT: // 用户操作了 store.state.count++ break; default: break; } }) export default dispatcher
-
通过store的事件的发布和订阅进行 当前组件中 state 的重新赋值
- 以点击为案例
- 组件的生命周期中,数据可以进行一次修改的 componentWillMount // componentDidMount
- 当我们点击按钮的时候,就要修改当前组件的state,也就是要进行事件的订阅
import React from 'react'; import logo from './logo.svg'; import './App.css'; import store from './store' import actionCreators from './store/actionCreators'; class App extends React.Component { constructor () { super() this.state = { count: store.getState().count } } increment () { actionCreators.increment() store.emit('count') //调用count } componentDidMount () {//store的订阅 store.on('count', () => { //定义count this.setState({ count: store.getState().count }) }) } render () { return ( <div> <h3> flux </h3> <button onClick = { this.increment }> + </button> <p> count: { this.state.count } </p> </div> ) } } export default App;
- 以点击为案例
Redux
- 使用方式
- 数据不分块使用
- 数据分块使用
- 方便维护和更新
核心组成部分
- Store 数据的管理者和数据的存储者
- actionCreators 动作的创建者,发送动作给 reducers
- react Components 组件( 用来充当视图层 )
- reducers 数据的修改者,返回一个新的 newstate 给store
其他
React 只是 DOM 的一个抽象层,并不是 Web 应用的完整解决方案。有两个方面,它没涉及。
- 代码结构
- 组件之间的通信
2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
如果你不知道是否需要 Redux,那就是不需要它
只有遇到 React 实在解决不了的问题,你才需要 Redux
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。
- 用户的使用方式非常简单
- 用户之间没有协作
- 不需要与服务器大量交互,也没有使用 WebSocket
- 视图层(View)只从单一来源获取数据
需要使用Redux的项目:
- 用户的使用方式复杂 【 电商类型 后台管理系统 】
- 不同身份的用户有不同的使用方式(比如普通用户和管理员) 【 权限验证 】
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了WebSocket
- View要从多个来源获取数据
从组件层面考虑,什么样子的需要Redux:
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
Redux的设计思想:
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面(唯一数据源)。
注意:flux、redux都不是必须和react搭配使用的,因为flux和redux是完整的架构,在学习react的时候,只是将react的组件作为redux中的视图层去使用了。
Redux的使用的三大原则:
- Single Source of Truth(唯一的数据源)
- State is read-only(状态是只读的)
- Changes are made with pure function(数据的改变必须通过纯函数完成)
实现Redux步骤
-
redux是一个架构思维,我们实现需要一个工具,这个工具叫做redux
yarn add redux
-
在src目录下新建store目录,里面新建index.js
import { createStore } from 'redux' import reducer from './reducer' const store = createStore( reducer ) // 不加new createStore() 参数不是一个 Object 而是一个Function export default store
-
在store目录下新建一个state.js (数据,可直接写在reducer.js中)
const state = { todos: [ { id: 1, task: '任务一' } ] } export default state
-
在 store下新建一个 reducer.js
import state from './state' const reducer = ( previousState = state , action ) => {//赋初始值、纯函数 const newState = { ...previousState // 解构的原因是为了做深拷贝,我们操作newState,不会影响state } //浅 return newState } export default reducer
//数据分块时 import { combineReducers } from 'redux' const reducer = combineReducers({ //这里面放分片的reducer })
-
在你想要使用的组件中直接引用 store
import React, { Component,Fragment } from 'react' import store from '../store' class Content extends Component{ constructor () { super() this.state = { todos: store.getState() //获得数据 } } render () { return ( <Fragment> <div> </div> </Fragment> ) } } export default Content
-
进行用户交互 React component — > actionCreators,在store下新建(非硬性)actionCreators.js
import * as type from './type' import store from './index' const actionCreators = { add_todos_item ( val ) { //动作的创建 const action = { type: type.ADD_TODOS_ITEM, payload: val // 负载数据 } // 动作的发送 store.dispatch( action ) } } export default actionCreators
-
在pages中触发 actionCreators中 的方法
import React, { Component,Fragment } from 'react' import actionCreators from './../store/actionCreators'; class Button extends Component{ add = () => { let val = this.input.value actionCreators.add_todos_item( val ) //调用动作中的方法 this.input.value = '' } render () { return ( <Fragment> <div> <input type = "text" ref = { el => this.input = el } /> <br/> <button onClick = { this.add }> + </button> </div> </Fragment> ) } } export default Button
-
在 reducer中修改数据
import state from './state' const state = require( './state' ) import * as type from './type' const reducer = ( previousState = state,action) => { let newState = { ...previousState } //判断用户进行了那个用户交互 ,操作新状态 switch ( action.type ) { case type.ADD_TODOS_ITEM: //修改新状态 newState.todos.push({ id: newState.todos.length + 1, task: action.payload }) break; default: break; } return newState } export default reducer
-
进行数据个更新,通过store的订阅功能进行更新,也就是组件需要重新赋值一次数据
- 在Content组件中进行订阅
componentDidMount () { store.subscribe( () => { this.setState({ todos: store.getState().todos }) }) }
Redux进阶
- redux-thunk是一个redux的中间件,用来处理redux中的复杂逻辑,中间件都是对store.dispatch()的增强,比如异步请求;
redux-thunk
中间件可以让action
创建函数先不返回一个action
对象,而是返回一个函数;- react-redux相当于一个适配react的一个redux插件;redux本身可以在任何项目中使用,react-redux带来了更适合react的方法{ connect };
- redux就是来管理数据的一个仓库了。
- redux-saga是一个用于管理redux应用异步操作的中间件,redux-saga通过创建sagas将所有异步操作逻辑收集在一个地方集中处理,可以用来代替redux-thunk中间件
- store/index.js
//创建store
import { createStore,applyMiddleware } from 'redux' //applyMiddleware是用来执行中间件 , 中间件就是一个函数
import reducer from 'reducer'
// 统一完成数据请求处理
import thunk from 'redux-thunk' //需要安装
const store = createStore( reducer,applyMiddleware( thunk ) ) //这个reducer是总的reducer
export default store
actionCreator的方法就可以返回一个能接收到dispatch的一个函数,我们可以在这个函数中进行异步操作之后,将actionCreator创建好的action给发送
- reducer/index.js
/*
这个文件是统一的reducer,,它的作用是为了管理分片的reducer
*/
import { combineReducers } from 'redux'
import homeReducer from './homeReducer'
const reducer = combineReducers({
// 分片的reducer
homeReducer
})
export default reducer
- reducer/homeReducer
import {
MOVIE_ON_INFO_LIST
} from 'action/actionType' //引入动作类型
const initState = { //初始化数据
movieIds: null,
movieLists: null
}
const homeReducer = ( state = initState,action ) => {
const newState = {
...state
}
// 判断用户是进行了什么动作
switch ( action.type ) {
case MOVIE_ON_INFO_LIST:
//修改数据
newState.movieIds = action.payload.data.movieIds
newState.movieLists = action.payload.data.movieList
break;
default:
break;
}
return newState
}
export default homeReducer
- action/index.js
import { getMovieOnInfoList } from './homeAction'
const actions = {
getMovieOnInfoList
}
export default actions
export const MOVIE_ON_INFO_LIST = 'MOVIE_ON_INFO_LIST'
actionType.js- action/homeAction.js
import request from 'utils/request' //封装好的axios
import api from 'api' //接口
import { MOVIE_ON_INFO_LIST } from './actionType';
export const getMovieOnInfoList = () => {
// 先进行数据请求,然后在创建动作,发送动作 ,
// 注意: 使用了redux-thunk之后要求返回一个参数为dispatch的回调函数
return async dispatch => {
const result = await request({ //数据请求
url: api.home.list,
params: {
token: ''
}
})
const action = {
type: MOVIE_ON_INFO_LIST,
payload: result
}
dispatch( action ) //发送动作给reducer
}
}
- 使用的组件
import React, { Component } from 'react';
import actions from 'action' //引入动作
//import { 动作名 } from 'action' //引入具体动作
import { connect } from 'react-redux' // 需要安装
import { bindActionCreators } from 'redux'
//使用的时候调用actions对象中的方法
this.props.getMovieOnInfoList()
//调用的时候 homeReducer为名字
this.props.homeReducer.xxx
const mapStateFromProps = state => {
return state //传递state
}
const mapDispatchToProps = dispatch => {
return bindActionCreators( actions,dispatch )
}
//使用connect
export default connect( mapStateFromProps,mapDispatchToProps )(Home)
-
react-redux
src目录下index.js修改
import store from 'store' import { Provider } from 'react-redux' ReactDOM.render( <Provider store = { store }> <Router> <App /> </Router> </Provider> , document.getElementById('root'));
react-redux提供两个核心的api:
- Provider: 提供store
- connect: 用于连接容器组件和展示组件
-
Provider
根据单一store原则 ,一般只会出现在整个应用程序的最顶层。(容器组件)
-
connect
语法格式为
connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)
一般来说只会用到前面两个,它的作用是:
- 把
store.getState()
的状态转化为展示组件的props
- 把
actionCreators
转化为展示组件props
上的方法
- 把
特别强调:
官网上的第二个参数为mapDispatchToProps, 实际上就是actionCreators
只要上层中有
Provider
组件并且提供了store
, 那么,子孙级别的任何组件,要想使用store
里的状态,都可以通过connect
方法进行连接。如果只是想连接actionCreators
,可以第一个参数传递为null
-
src/api 后端接口文档
路由监听
- 可以在LayOut中做统一的路由监听
componentDidMount () {
// 这个钩子我们用来监听页面第一次加载
this.checkLogin()
this.watchTabBar()
}
componentWillReceiveProps ( nextProps ) {
//这个钩子可以监听到 页面中路由的切换
this.watchTabBar( nextProps ) //nextProps
}
checkLogin = () => {
const token = localStorage.getItem( 'token' )
if ( !token ) {
this.props.history.push('/err')
}
}
watchHead = () => {
//有的页面有头部,有的页面没有头部
}
watchTabBar = ( nextProps ) => {
// 有的页面有底部,有的页面没有底部
// 底部没有的有 mine login register
//判断是刷新还是跳转页面
const { pathname } = nextProps && nextProps.location || this.props.location
const arr = ['/mine','/account/login','/account/register','/recommend']
const f = arr.some( item => item == pathname )
if ( f ) {
//条件成立,证明我们在当前路由上,需要去掉底部
this.setState({
tab_bar_flag: false
})
} else {
this.setState({
tab_bar_flag: true
})
}
}
better-scroll上拉加载事件
import BScroll from 'better-scroll' //安装
import _ from 'loadsh' //分组工具
componentDidMount () {
this.bscroll = new BScroll('.home-list',{
click: true, // 解锁页面的点击(原因:有一层覆盖)
pullUpLoad: {// 开启上拉加载,50表示拉动的距离是50时进行上拉的事件的触发
threshold: 50
},
pullDownRefresh:{//下拉刷新
threshold:50,
stop:30
}
})
// 上拉加载的事件
let count = 0 //计数,用作判断
this.bscroll.on('pullingUp',() => {
Toast.loading('Loading...', 1 );
// 触发多次
// 重新发起数据请求,修改数据
// 将id进行数据分组 - 使用loadsh来完成
// const id = _.chunk( 数组,几个分一组 )
const id = this.props.homeReducer.movieIds && _.chunk( this.props.homeReducer.movieIds.slice( 12 ), 10 )
const idStr = id[ count ].join(',') //传入参数形式要求
if ( count < id.length - 1 ) {
//表示可以拉动
this.props.moreComingMovies( idStr ) // id是第一次数据请求之后的所有(id - 12 ) / 10
count ++
}
if ( id.length - 1 == count ) {
// 拉动结束了,没有数据了
console.log('拉动结束了')
}
this.bscroll.finishPullUp() //一次拉动的结束
})
this.bscroll.on('pullingDown',async()=>{
Toast.success('已成功为您推荐新的内容!',1)
count + 8 < 99 ? count+=8 : count=count-99+8
const video = await http({
url:'/video/group',
params:{
id: this.state.idList[count].id
}
})
// console.log('111',video.data.datas)
this.setState({
videos:video.data
})
// console.log('shang',count)
this.bscroll.finishPullDown();
})
}
axios拦截器
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
//这里写加载之前
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
Mobx
当使用 mobx-react
时可以定义一个新的生命周期钩子函数 componentWillReact
。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact
会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。
componentWillReact
不接收参数componentWillReact
初始化渲染前不会触发 (使用componentWillMount
替代)componentWillReact
对于 mobx-react@4+, 当接收新的 props 时并在setState
调用后会触发此钩子- 要触发
componentWillReact
必须在render里面用到被观察的变量 - 使用Mobx之后不会触发
componentWillReceiveProps
CRA配置mobx
-
使用CRA创建好项目
create-react-app mobx_pro
-
在项目中进行配置文件抽离
yarn eject
-
安装mobx mobx-react
-
yarn add mobx mobx-react
- mobx 是状态管理工具
- mobx-react 是做数据分片和数据获取
-
注意: 如果git冲突
解决: 我们要原文件先放到本地暂存盘
git add .
git commit -m ‘’然后 : 安装mobx mobx-react’
注意不要git push
-
-
配置装饰器( 修饰器 es6 ) babel
- yarn add babel-plugin-transform-decorators-legacy -D
- yarn add @babel/preset-env -D
- yarn add babel-plugin-transform-class-properties -D
- yarn add @babel/plugin-proposal-decorators -D
-
配置package.json
"babel": { "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ], "transform-class-properties" ], "presets": [ "react-app", "@babel/preset-env" ] }, //注意: 以下两个配置顺序不可更改 [ "@babel/plugin-proposal-decorators", { "legacy": true } ], "transform-class-properties"
项目中搭建mobx
-
新建store目录
src/store
home/index.js
user/index.js
index.js
-
在入口文件中用Provider ---------src/index.js
import store from './store' import { Provider } from 'mobx-react' ReactDOM.render( <Provider store = {store}> <Show /> </Provider> , document.getElementById('root'));
-
使用的组件注入 inject
import React, { Component } from 'react'; import { inject,observer } from 'mobx-react' @inject('store') @observer //监听,可以实现数据更新 class Todo extends Component{ ... } export default Todo
-
打造mobx数据包
import { observable, computed,action } from 'mobx' class Home { @observable //监听 age age = 18 @computed //当age发生改变时, 自动触发 get doubleAge(){ return this.age *= 2 } @action // 用户操作 事件调用 increment(){ this.props.store.home.age ++ console.log( this.props.store.home.age ) //数据请求 fetch('/data/data.json') .then(res => res.json()) .then( result => console.log( result )) .catch( error => console.log( error )) } } const home = new Home() export default home
-
打造store/index.js
import home from './home' const store = { //实例 home } export default store
-
组件内使用数据
-
this.props.store.xxx 可以拿到数据
-
注意:
-
this.porps里面没有找到 @action 装饰器定义的方法, 但是可以直接使用,
-
this会丢失
observable, computed,action
} from ‘mobx’
class Home {
@observable //监听 age
age = 18@computed //当age发生改变时, 自动触发
get doubleAge(){
return this.age *= 2
}@action // 用户操作 事件调用
increment(){
this.props.store.home.age ++
console.log( this.props.store.home.age ) -
//数据请求
fetch(’/data/data.json’)
.then(res => res.json())
.then( result => console.log( result ))
.catch( error => console.log( error ))
}
}
const home = new Home()export default home
-
-
打造store/index.js
import home from './home' const store = { //实例 home } export default store
-
组件内使用数据
-
this.props.store.xxx 可以拿到数据
-
注意:
-
this.porps里面没有找到 @action 装饰器定义的方法, 但是可以直接使用,
-
this会丢失
this.props.store.home.increment.bind(this)
-
-