1 UI组件与容器组件的拆分
UI组件叫做傻瓜组件 (负责页面的渲染)(特殊情况也可以做一些简单的逻辑),容器组件叫做聪明组件 (负责页面逻辑处理),之前我们把渲染(render)和逻辑都在一个组件(todolist)里写着
现在我们首先在src下创建一个TodoListUI.js,并创建TodoListUI组件和导出
将原Todolist.js渲染的部分(render)部分剪切出来到TodoListUI.js中。在TodoList.js组件中导入TodoListUI组件,在其render函数中返回该UI组件。
在TodoListUI.js中
1.将todolist中的UI组件拷贝过来
2.然后将其它的‘antd’组件引入
3.父组件传值给子组件
例如:在render里面组件上传值<TodoListUI inputValue={this.state.inputValue}/>
,然后TodoListUI.js组件就可以接收使用了,写成this.props.inputValue即可
4.父组件传函数方法给子组件和传值一样
例如:在render里面组件上传值<TodoListUI inputValue={this.state.inputValue} handleInputChange={this.handleInputChange}/>
,然后TodoListUI.js组件就可以接收使用了,写成this.props.handleInputChange即可
5.需要注意的是函数中有传值的问题
必须用箭头函数进行传值onClick={(index) => {this.props.handleItemDelete(index)}}
如果写成如下:onClick={this.props.handleItemDelete(index)}
就会发生错误,因为传值有要求,必须是纯函数
TodoListUI.js
import React, { Component } from 'react';
import { Input } from 'antd';
import { Button } from 'antd';
import { List } from 'antd';
class TodoListUI extends Component {
render() {
return (
<div style={{marginTop: '10px',marginLeft: '20px'}}>
<div >
<Input
value={this.props.inputValue} // 修改的地方,接收使用父组件传的值
placeholder="todo info"
style={{width: '300px'}}
onChange={this.props.handleInputChange} // 修改的地方,接收使用父组件传的方法
/>
<Button
type='primary'
style={{marginLeft:'20px'}}
onClick={this.props.handleBtnClick} // 修改的地方,接收使用父组件传的方法
>
提交
</Button>
</div>
<List
style={{marginTop: '20px',width: '300px'}}
bordered
dataSource={this.props.list}
renderItem={(item,index) => (
// 下面是修改的地方,接收使用父组件传的方法(并解决函数传值问题)
<List.Item onClick={() => {this.props.handleItemDelete(index)}}>
//上面这里前面已经传index值了,所以箭头函数前面不需要再传。
{item}
</List.Item>
)}
/>
</div>
)
}
}
export default TodoListUI;
todolist.js
import React, {Component} from 'react';
import 'antd/dist/antd.css';
import store from './store/index';
import { getInputChangeAction,getAddItemAction,getDeleteItemAction } from './store/actionCreators'
import TodoListUI from './todolistUI';
class TodoList extends Component {
constructor(props) {
super (props);
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete=this.handleItemDelete.bind(this);
//store的内容只要发生改变,函数就会执行
store.subscribe(this.handleStoreChange);
}
render() {
return (
<TodoListUI // 这里都是修改的地方
inputValue={this.state.inputValue} // 传值给子组件TodoListUI
list={this.state.list} // 传值给子组件TodoListUI
handleInputChange={this.handleInputChange} // 传方法给子组件TodoListUI
handleBtnClick={this.handleBtnClick} // 传方法给子组件TodoListUI
handleItemDelete={this.handleItemDelete} // 传方法给子组件TodoListUI
/>
)
}
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
handleStoreChange() {
console.log('store change');
this.setState(store.getState());
}
handleBtnClick() {
const action = getAddItemAction();
store.dispatch(action);
}
handleItemDelete(index) {
const action = getDeleteItemAction(index);
store.dispatch(action)
}
}
export default TodoList;
2 无状态组件
无状态组件就类似于一个函数
什么情况下使用无状态组件呢?当一个普通的组件只有render函数的时候,我们可以通过无状态组件,替换普通组件
无状态组件的优势是什么?之前普通组件是执行一个class类的组件,也会执行生命周期函数。现在改成函数,就只是执行了一个函数。所以无状态组件性能明显比较高
所以如果我们将刚刚的UI组件改为无状态组件,就需要首先定义一个函数,函数接收props参数(父组件参数)返回之前render中渲染返回的内容(所以函数里也就可以直接使用props参数,不需要写成this.props啦)。
我们将原来的UI组件TodoListUI.js改为无状态组件
TodoListUI.js
import React, { Component } from 'react';
import { Input } from 'antd';
import { Button } from 'antd';
import { List } from 'antd';
//当一个普通的组件只有render函数的时候
//我们可以通过无状态组件,替换普通组件
//无状态组件性能比较高
function TodoListUI(props) {
return(
<div style={{marginTop: '10px',marginLeft: '20px'}}>
<div >
<Input
value={props.inputValue}
placeholder="todo info"
style={{width: '300px'}}
onChange={props.handleInputChange}
/>
<Button
type='primary'
style={{marginLeft:'20px'}}
onClick={props.handleBtnClick}
>
提交
</Button>
</div>
<List
style={{marginTop: '20px',width: '300px'}}
bordered
dataSource={props.list}
renderItem={(item, index) => (
<List.Item onClick={() => {props.handleItemDelete(index)}}>
{item}
</List.Item>
)}
/>
</div>
)
}
export default TodoListUI;
3 Redux中发送异步请求获取数据
todolist.js
componentDidMount() {
axios.get('/todolist.json')
.then((res) => {
const data = res.data;
const action = initListAction(data); // 将请求到数据放入store中,这里initListAction为actionCreators.js中封装的action
store.dispatch(action); // 然后将action传给store
})
}
actionCreators.js
import {CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION} from './actionType' // 新增导入type
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_ACTION,
data
})
actionType.js
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
export const INIT_LIST_ACTION = 'init_list_action'; // 新增
reducer.js
import {CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION} from './actionType'
const deafultState = {
inputValue: '',
list: []
}
//记录本
//state 图书馆书籍的信息-> 存储的数据
//state -> 上次存储的数据 action -> 用户传过来的那句话
//reducer 可以接受state,但是不可以修改state
export default (state = deafultState, action) => {
if(action.type === CHANGE_INPUT_VALUE){
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
}
if(action.type === INIT_LIST_ACTION){
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.data;
return newState
}
return state;
}
4 使用Redux-thunk中间件进行ajax请求
4.1 中间件的安装
网站:https://github.com/reduxjs/redux-thunk
安装命令:npm install --save redux-thunk
查看上面网站信息:
然后修改我们src/store/index中的引入
4.2 thunk作用
可以将异步请求或者复杂的逻辑放到actionCreators中的action里面去处理和管理
4.3 导入
我们按文档要求修改
src/store/index.js
import { createStore,applyMiddleware } from 'redux';
import reducer from'./reducer';
import thunk from 'redux-thunk';
const store = createStore(
reducer,
applyMiddleware(thunk),
);
export default store;
在引入之后,发现Redux插件不能使用,查看文档来解决:
https://github.com/zalmoxisus/redux-devtools-extension
继续修改src/store/index.js
import { createStore,applyMiddleware,compose } from 'redux'; // 引入applyMiddleware和compose
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;
这样配置后reducer和thunk都可以使用了。在actionCreators.js中,如果不使用thunk中间件的话,我们就不能return一个函数(只能return对象,例如:type对象)给action,就会报错。
现在我们可以返回一个函数,所以就可以将Ajax请求数据封装在actionCreatoes.js中,然后在组件(TodoList.js)componentDidMount中创建action时调用所封装的函数。因为需要使用axios,所以把组件中axios的导入剪切到actionCreators.js中。
src/TodoList.js 添加代码
import { getInputChangeAction, getAddItemAction, getDeleteItemAction,getTodoList} from './store/actionCreators'
componentDidMount() {
const action = getTodoList(); //这里调用getTodoList返回的是一个函数,getTodoList在actionCreators中封装
//当把函数发送给store,该函数会自动执行
store.dispatch(action);
}
src/store/actionCreators.js
import axios from 'axios';
//函数方法可以接收到dispatch方法
//可以使用dispatch给store发送action
export const getTodoList = () => {
return (dispatch) => {
axios.get('/todolist.json')
.then((res) => {
const data = res.data;
const action = initListAction(data);
dispatch(action);
})
}
}
由上述代码,我们的getTodoList返回的是一个函数,然后todolist通过 store.dispatch来执行该函数。函数执行的时候,我们通过axios获取到了json数据,然后将其赋值给data
之后我们创建action来更改数据,所以再执行initListAction的方法,此时的action就是一个对象了,然后我们通过dispatch来发送action给store。这里需要将display方法传递过来使用。
最后会在Reducer中执行,返回给store新的state
if(action.type === INIT_LIST_ACTION){
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.data;
return newState
}
最终由组件中 store.subscribe(this.handleStoreChange)监听store数据变化来执行handleStoreChange
使用setState来更新数据。
handleStoreChange() {
this.setState(store.getState());
}
4.4 到底什么是Redux中间件?
中间:指的是Action和Store之间。中间件都是对store的Dispatch方法的升级,使其具有其他能力。
比如:thunk中间件使Dispatch可以派发action定义的函数(之前只能是对象),又比如logger中间件使Dispatch在派发action时打印日志。
这里thunk中间件的使用,使得Dispatch派发action时,如果传递过来的是对象,直接传递给store;如果传递过来的是函数,不会直接传递给store(先调用函数);让函数先执行,需要调用store才调用store
5 Redux-saga中间件的使用
5.1 Redux-saga的安装
https://github.com/redux-saga/redux-saga
参考文档:https://redux-saga-in-chinese.js.org/docs/basics/index.html
npm install redux-saga --save
yarn add redux-saga
5.2 Redux-saga的使用
首先在store文件夹下创建sagas.js文件。然后修改store中的代码(index.js)。
src/store/index.js
import { createStore,applyMiddleware,compose } from 'redux';
import reducer from'./reducer';
import createSagaMiddleware from 'redux-saga'
import TodoSagas from './sagas'
const sagaMiddleware = createSagaMiddleware()
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
}) : compose;
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoSagas);
export default store;
这里首先导入 Redux的模块:import { createStore,applyMiddleware,compose } from 'redux';
然后导入 saga模块:import createSagaMiddleware from 'redux-saga'
之后导入我们自己创建的模块:import TodoSagas from './sagas'
为了使得Redux tool 和 saga都能使用,因此使用了类似redux-thunk里面的方法,最后:sagaMiddleware.run(TodoSagas);
src/store/sagas.js
function* mySaga() {
}
export default mySaga;
先默认saga.js内容写成上面这样,防止报错。配置sagas后,dispatch派发的action不止store(默认给reducer)能接收到action,sagas.js也就能接收到dispatch派发的action了。
5.3 Todolist代码修改(使用Redux-saga完成功能.1)
修改如下的代码
src/TodoList.js
import { getInitList } from './actionCreators'; // 增加getInitList引入
componentDidMount() {
const action = getInitList();
store.dispatch(action);
}
src/store/actionCreators.js
import { GET_INIT_LIST } from './actionType'; // 增加这个action的type常量的导入
export const initListAction = (data) => ({
type: INIT_LIST_ACTION,
data
})
export const getInitList = () => ({ // 创建这个action
type: GET_INIT_LIST
})
src/store/actionTypes.js
export const GET_INIT_LIST = 'get_init_list'; // 增加这个type常量的定义
5.4 使用Redux-saga完成功能.2
可以参考官方文档:https://github.com/redux-saga/redux-saga
首先需要在saga.js中导入takeEvery,put方法:import { takeEvery,put } from 'redux-saga/effects';
其中takeEvery意思是捕捉每一个类型
src/store/sagas.js
import { takeEvery,put } from 'redux-saga/effects'; // 导入takeEvery,put方法
import { GET_INIT_LIST } from './actionType';
import axios from 'axios';
import { initListAction } from './actionCreators';
function* mySaga() {
//当捕捉到type为GET_INIT_LIST的action,就会执行后面的getInitList方法。以前是在reducer中判断type来执行方法
yield takeEvery(GET_INIT_LIST, getInitList);
}
function* getInitList() {
//ajax 获取失败
try {
const res = yield axios.get('/todolist.json'); // yield指等后面执行完成之后再往下进行
const action = initListAction(res.data);
yield put(action); // 之前我们使用store.dispatch(action)来派发,不过这里没有store这个仓库,不过saga中间件新增了put方法。
}catch(e){
console.log('list.json 网络请求失败');
}
}
export default mySaga;
这里saga.js接收到store.js派发的action后,来判断action的type类型,从而执行对应的函数方法。按generator函数规则来写,并把store.dispatch换成put方法。
注:saga与thunk的区别
thunk只是拓展了action的功能,使得action能够返回一个函数,同时也是和action混合在一起使用的。而saga则将这些异步请求的代码,单独放到了一个文件中进行处理,防止对action污染。此外saga还拓展了许多API,可以实现不同的功能。可以参考如下的文档。
https://redux-saga-in-chinese.js.org/docs/basics/index.html
6 React-Redux的使用
之前文件全部清除,只留下一个src/index.js文件,重新来。创建Todolist组件并引入到index.js中。新建store文件夹内容(store/index.js,store/reducer.js)
6.1 React-Redux的安装
yarn add react-redux
npm install --save react-redux
6.2 使用React-Redux
它可以让我们更方便的在React 中使用Redux
(1)创建Provider(react-redux的第一个核心API)
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './todolist';
import { Provider } from 'react-redux'; //导入Provider
import store from './store';
const App = (
//提供器连接了store,那么Provider里面的所有组件,都有能力获得store中的内容
<Provider store={store}>
<TodoList />
</Provider>
);
ReactDOM.render(
App,
document.getElementById('root')
);
我们创建了一个App,里面有一个Provider 提供器,然后里面是我们的组件TodoList,只要写在Provider 中的组件,都有能力获取store中的内容。
(2)TodoList(使用Provider后store数据的获取)
src/TodoList.js(connect为react-redux的第二个核心API)
import React,{ Component }from 'react';
import { connect } from 'react-redux';
class TodoList extends Component {
render() {
return (
<div>
<div>
<input
value={this.props.inputValue}
onChange={this.props.changeInputValue}
/>
<button>提交</button>
</div>
<ul>
<li>hello</li>
</ul>
</div>
)
}
}
//把Store中的内容映射到props中,这里state就是指store里面的数据,props指公共组件的里面的数据(reducer.js)
const mapStateToProps = (state) => {
return {
// 这里inputValue实际上在props中
inputValue: state.inputValue // 使得store里面的inputValue映射到props里面的inputValue
}
}
//把store的dispatch方法映射到props中
const mapDispatchToProps = (dispatch) => {
return {
// 这里changeInputValue实际上在props中,所以可以直接使用store的dispatch方法
changeInputValue(e) {
const action = {
type: 'change_input_value',
value: e.target.value
}
dispatch(action);
}
}
}
//connect将TodoList这个UI组件和mapStateToProps,mapDispatchToProps连接形成容器组件。
// 所以实际上返回的是连接后的容器组件。
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
connect这个API使得这个组件和store做连接(这个组件在Provider里面)
mapStateToProps, mapDispatchToProps是衍生出的连接方式
之后changeInputValue发送action到reducer中进行一个处理:
src/store/reducer.js
const defaultState = {
inputValue: 'Hello world',
list: []
}
export default(state = defaultState, action) => {
if(action.type === 'change_input_value'){
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
return state;
}
6.3 代码补全及其优化
connect连接了数据,页面跟着数据改变。所以也不需要我们像以前那样还需要用store的subscribe方法来监测数据的改变,从而进一步重新渲染页面。
以下代码:Todolist是一个UI组件,connect 将UI组件和逻辑相结合,只负责渲染,组件里只有一个render方法中,所以我们可以将其写成一个无状态组件。
src/TodoList.js
import React from 'react';
import { connect } from 'react-redux';
function TodoList(props) {
const { inputValue,
changeInputValue,
handleClick,
list,
handleDelete } = props;
return (
<div>
<div>
<input
value={inputValue}
onChange={changeInputValue}
/>
<button onClick={handleClick}>提交</button>
</div>
<ul>
{
list.map((item, index)=>{
return (
<li onClick={() => handleDelete(index)} key={index}>
{item}
</li>
)
})
}
</ul>
</div>
)
}
//把Store中的内容映射到props中,这里state就是指store里面的数据,props指公共组件的里面的数据(reducer.js)
const mapStateToProps = (state) => {
return {
// 这里inputValue实际上在props中
inputValue: state.inputValue, // 使得store里面的inputValue映射到props里面的inputValue
list: state.list
}
}
//把store的dispatch方法映射到props中
const mapDispatchToProps = (dispatch) => {
return {
// 这里changeInputValue实际上在props中,所以可以直接使用store的dispatch方法
changeInputValue(e) {
const action = {
type: 'change_input_value',
value: e.target.value
};
dispatch(action);
},
handleClick() {
const action = {
type: 'add_item'
};
dispatch(action);
},
handleDelete(index) {
const action = {
type: 'delete_item',
index
}
dispatch(action);
}
}
}
//connect将TodoList这个无状态组件(UI组件改的)组件和mapStateToProps,mapDispatchToProps连接形成容器组件。
// 所以实际上返回的是连接后的容器组件。
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
容器组件:包含组件渲染和业务逻辑。
UI组件:只有渲染,没有逻辑。
无状态组件:是一个函数,UI组件只有render函数时,可改为无状态组件。
src/store/index.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
src/store/reducer.js
const defaultState = {
inputValue: '',
list: []
}
export default(state = defaultState, action) => {
if(action.type === 'change_input_value'){
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState
}
if(action.type === 'add_item'){
const newState = JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.inputValue = "";
return newState
}
if(action.type === 'delete_item'){
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState
}
return state;
}
其实,上述代码还没有完全的精简
还需将代码中的方法 actionCreators 还有actionType 将代码进行一个分离。