33
Redux + React-router 的入门📖和配置👩🏾💻教程
source link: https://juejin.im/post/5dcaaa276fb9a04a965e2c9b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2019年11月15日
阅读 7288
Redux + React-router 的入门📖和配置👩🏾💻教程
React
是单向数据流,数据通过props
从父节点传递到子节点。如果顶层的某个props
改变了,React
会重新渲染所有的子节点。注意⚠️:props
是只读的(即不可以使用this.props
直接修改props
),它是用于在整个组件树中传递数据和配置。- 每个组件都有属于自己的
state
,state
和props
的区别在于state
只存在于组件内部。注意 ⚠️:只能从当前组件调用this.setState
方法修改state
值(不可以直接修改this.state
)。 - 可见,更新子组件有两种方式,一种是改变子组件自身的
state
值,另一种则是更新子组件从父组件接收到的this.props
值从而达到更新。 - 在
React
项目开发过程中,我们大多时候需要让组件共享某些数据。一般来说,我们可以通过在组件间传递数据(通过props
)的方式实现数据共享,然而,当数据需要在非父子关系的组件间传递时操作起来则变得十分麻烦,而且容易让代码的可读性降低,这时候我们就需要使用state
(状态)管理工具。 - 常见的状态管理工具有
redux
,mobx
。由于redux
提供了状态管理的整个架构,并有着清晰的约束规则,适合在大型多人开发的应用中使用。本文介绍的是如何在React
项目中使用redux
进行状态管理。
进入正题 🥰
- 本节主要介绍
redux
和react-router
相关的基础知识 📖和相关配置 👩🏾💻。
redux
redux
适用于多交互、多数据源的场景。从组件角度看,如果我们的应用有以下场景,则可以考虑在项目中使用redux
:- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
- 当我们的应用符合以上提到的场景时,若不使用
redux
或者其他状态管理工具,不按照一定规律处理状态的读写,项目代码的可读性将大大降低,不利于团队开发效率的提升。
- 如上图所示,
redux
通过将所有的state
集中到组件顶部,能够灵活的将所有state
各取所需地分发给所有的组件。 redux
的三大原则:- 整个应用的
state
都被存储在一棵object tree
中,并且object tree
只存在于唯一的store
中(这并不意味使用redux
就需要将所有的state
存到redux
上,组件还是可以维护自身的state
)。 state
是只读的。state
的变化,会导致视图(view
)的变化。用户接触不到state
,只能接触到视图,唯一改变state
的方式则是在视图中触发action
。action
是一个用于描述已发生事件的普通对象。- 使用
reducers
来执行state
的更新。reducers
是一个纯函数,它接受action
和当前state
作为参数,通过计算返回一个新的state
,从而实现视图的更新。
- 整个应用的
- 如上图所示,
redux
的工作流程大致如下:- 首先,用户在视图中通过
store.dispatch
方法发出action
。 - 然后,
store
自动调用reducers
,并且传入两个参数:当前state
和收到的action
。reducers
会返回新的state
。 - 最后,当
store
监听到state
的变化,就会调用监听函数,触发视图的重新渲染。
- 首先,用户在视图中通过
- 放一张图加深理解 ⚡⚡️⚡️️:
store
store
就是保存数据的地方,整个应用只能有一个store
。redux
提供createStore
这个函数,用来创建一个store
以存放整个应用的state
:
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);
复制代码
- 可以看到,
createStore
接受reducer
、初始state
(可选)和增强器作为参数,返回一个新的store
对象。
state
store
对象包含所有数据。如果想得到某个时点的数据,就要对store
生成快照。这种时点的数据集合,就叫做state
。- 如果要获取当前时刻的
state
,可以通过store.getState()
方法拿到:
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);
const state = store.getState();
复制代码
action
state
的变化,会导致视图的变化。但是,用户接触不到state
,只能接触到视图。所以,state
的变化必须是由视图发起的。action
就是视图发出的通知,通知store
此时的state
应该要发生变化了。action
是一个对象。其中的type
属性是必须的,表示action
的名称。其他属性可以自由设置,社区有一个规范可以参考:
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux' // 可选属性
};
复制代码
- 上面代码定义了一个名称为
ADD_TODO
的action
,它携带的数据信息是Learn Redux
。
Action Creator
view
要发送多少种消息,就会有多少种action
,如果都手写,会很麻烦。- 可以定义一个函数来生成
action
,这个函数就称作Action Creator
,如下面代码中的addTodo
函数:
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
复制代码
redux-actions
是一个实用的库,让编写redux
状态管理变得简单起来。该库提供了createAction
方法用于创建动作创建器:
import { createAction } from "redux-actions"
export const INCREMENT = 'INCREMENT'
export const increment = createAction(INCREMENT)
复制代码
- 上边代码定义一个动作
INCREMENT
, 然后通过createAction
创建了对应Action Creator
:- 调用
increment()
时就会返回{ type: 'INCREMENT' }
- 调用
increment(10)
返回{ type: 'INCREMENT', payload: 10 }
- 调用
store.dispatch()
store.dispatch()
是视图发出action
的唯一方法,该方法接受一个action
对象作为参数:
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
复制代码
- 结合
Action Creator
,这段代码可以改写如下:
import { createStore } from 'redux';
import { createAction } from "redux-actions"
const store = createStore(reducer, [preloadedState], enhancer);
const ADD_TODO = 'ADD_TODO';
const add_todo = createAction('ADD_TODO'); // 创建 Action Creator
store.dispatch(add_todo('Learn Redux'));
复制代码
reducer
store
收到action
以后,必须给出一个新的state
,这样视图才会进行更新。state
的计算(更新)过程则是通过reducer
实现。reducer
是一个函数,它接受action
和当前state
作为参数,返回一个新的state
:
const reducer = function (state, action) {
// ...
return new_state;
};
复制代码
- 为了实现调用
store.dispatch
方法时自动执行reducer
函数,需要在创建store
时将将reducer
传入createStore
方法:
import { createStore } from 'redux';
const reducer = function (state, action) {
// ...
return new_state;
};
const store = createStore(reducer);
复制代码
- 上面代码中,
createStore
方法接受reducer
作为参数,生成一个新的store
。以后每当视图使用store.dispatch
发送给store
一个新的action
,就会自动调用reducer
函数,得到更新的state
。 redux-actions
提供了handleActions
方法用于处理多个action
:
// 使用方法:
// handleActions(reducerMap, defaultState)
import { handleActions } from 'redux-actions';
const initialState = {
counter: 0
};
const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
initialState,
);
复制代码
拆分、合并 reducer
- 前面提到,在一个
react
应用中只能有一个store
用于存放应用的state
。组件通过调用action
函数,传递数据到reducer
,reducer
根据数据更新对应的state
。 - 对于大型应用来说,应用的
state
必然十分庞大,导致reducer
的复杂度也随着变大。
- 在这个时候,就可以考虑将
reducer
拆分成多个单独的函数,让每个函数负责独立管理state
的一部分。
redux
提供了combineReducers
辅助函数,可将独立分散的reducer
合并成一个最终的reducer
函数,然后在创建store
时作为createStore
的参数传入。- 我们可以根据业务需要,把所有子
reducer
放在不同的目录下,然后在在一个文件里面统一引入,最后将合并后的reducer
导出:
// src/model/reducers.ts
import { combineReducers } from 'redux';
import UI from './UI/reducers';
import user from './user/reducers';
import content from './content/reducers';
const rootReducer = combineReducers({
UI,
user,
content,
});
export default rootReducer;
复制代码
中间件及异步操作
- 对
redux
而言,同步指的是当视图发出action
以后,reducer
立即算出state
(原始的redux
工作流程),而异步指的是在action
发出以后,过一段时间再执行reducer
。 - 同步通常发生在原生
redux
的工作流程中,而在大多数实际场景中,更多的是需要异步操作:action
发出以后,在进入reducer
之前需要先完成一个异步任务,比如发送ajax
请求后拿到数据后,再进入reducer
执行计算并对state
进行更新。 - 显然原生的
redux
是不支持异步操作的,这就要用到新的工具——中间件(middleware)来处理这种业务场景。从本质上来讲中间件是对store.dispatch
方法进行了拓展。 - 中间件提供位于
action
发起之后,到达reducer
之前的扩展点:即通过store.dispatch
方法发出的action
会依次经过各个中间件,最终到达reducer
。 - 我们可以利用中间件来进行日志记录(
redux-logger
)、创建崩溃报告(自己写crashReporter
)、调用异步接口(redux-saga
)或者路由(connected-react-router
)等操作。 redux
提供了一个原生的applyMiddleware
方法,它的作用是将所有中间件组成一个数组,依次执行。假如要使用redux-logger
来实现日志记录功能,用法如下:
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
复制代码
- 如果有多个中间件,则将中间件依次作为参数传入
applyMiddleware
方法中:
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
const logger = createLogger(); // 日志记录
const sagaMiddleware = createSagaMiddleware(); // 调用异步接口
let middleware = [sagaMiddleware];
middleware.push(logger);
const store = createStore(
reducer,
// 可传initial_state
applyMiddleware(...middleware)
);
复制代码
- 需要注意⚠️的是:
createStore
方法可以接受整个应用的初始状态作为参数(可选),若传入初始状态,applyMiddleware
则需要作为第三个参数。- 有的中间件有次序要求,使用前要查一下文档(如
redux-logger
一定要放在最后,否则输出结果会不正确)。
react-redux
- 前面小节介绍的
redux
本身是一个可以结合react
,vue
,angular
甚至是原生javaScript
应用使用的状态库。 - 为了让
redux
帮我们管理react
应用的状态,需要把redux
与react
连接,官方提供了 react-redux库(这个库是可以选用的,也可以只用redux
)。 react-redux
将所有组件分成 UI 组件和容器组件两大类:- UI 组件只负责 UI 的呈现,不含有状态(
this.state
),所有数据都由this.props
提供,且不使用任何redux
的 API。 - 容器组件负责管理数据和业务逻辑,含有状态(
this.state
),可使用redux
的 API。
- UI 组件只负责 UI 的呈现,不含有状态(
- 简而言之,容器组件作为 UI 组件的父组件,负责与外部进行通信,将数据通过
props
传给 UI 组件渲染出视图。 react-redux
规定,所有的 UI 组件都由用户提供,容器组件则是由react-redux
自动生成。也就是说,用户负责视觉层,状态管理则是全部交给react-redux
。
connect 方法
react-redux
提供了connect
方法,用于将 UI 组件生成容器组件:
import { connect } from 'react-redux'
class Dashboard extends React.Component {
...
// 组件内部可以获取 this.props.loading 的值
}
const mapStateToProps = (state) => {
return {
loading: state.loading,
}
}
// 将通过 connect 方法自动生成的容器组件导出
export default connect(
mapStateToProps, // 可选
// mapDispatchToProps, // 可选
)(Dashboard)
复制代码
- 从上面代码可以看到,
connect
方法接受两个可选参数用于定义容器组件的业务逻辑:mapStateToProps
负责输入逻辑,即将state
映射成传入 UI 组件的参数(props
)mapDispatchToProps
负责输出逻辑,即将用户对 UI 组件的操作映射成action
- 注意⚠️:当
connect
方法不传入任何参数时,生成的容器组件只可以看作是对 UI 组件做了一个单纯的包装,不含有任何的业务逻辑:- 省略
mapStateToProps
参数, UI 组件就不会订阅store
,即store
的更新不会引起 UI 组件的更新。 - 省略
mapDispatchToProps
参数, UI 组件就不会将用户的操作当作action
发送数据给store
,需要在组件中手动调用store.dispatch
方法。
- 省略
mapStateToProps
mapStateToProps
是一个函数,它的作用就是建立一个从state
对象(外部)到 UI 组件props
对象的映射关系。该函数会订阅 整个应用的store
,每当state
更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。mapStateToProps
的第一个参数总是state
对象,还可以使用第二个参数(可选),代表容器组件的props
对象:
// 容器组件的代码
// <Dashboard showType="SHOW_ALL">
// All
// </Dashboard>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.showType === "SHOW_ALL",
loading: state.loading,
}
}
复制代码
- 使用
ownProps
作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
mapDispatchToProps
mapDispatchToProps
是connect
函数的第二个参数,用来建立 UI 组件的参数到store.dispatch
方法的映射。- 由于在项目中大多使用
mapDispatchToProps
比较少,这里不进行细讲。关于mapStateToProps
、mapDispatchToProps
和connect
的更详细用法说明可以查看文档。
Provider 组件
- 使用
connect
方法生成容器组件以后,需要让容器组件拿到state
对象,才能生成 UI 组件 的参数。 react-redux
提供了Provider
组件,可以让容器组件拿到state
,具体用法是需要用Provider
组件包裹项目的根组件(如App),使得根组件所有的子组件都可以默认获取到state
对象:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './store/configureStore';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
复制代码
react-router
react-router
是完整的react
的路由解决方案,它保持UI
与URL
的同步。在项目中我们使用的是最新的v4
版。- 需要注意⚠️的是,在开发中不应该直接安装
react-router
,因为👉:在v4
版中react-router
被划分为三个包:react-router
,react-router-dom
和react-router-native
,它们的区别如下:react-router
:提供核心的路由组件和函数。react-router-dom
:提供浏览器使用的路由组件和函数。react-router-native
:提供react-native
对应平台使用的路由组件和函数。
- 当我们的
react
应用同时使用了react-router
和redux
,则可以将两者进行更深度的整合,实现:- 将
router
的数据与store
进行同步,并且可以从store
访问router
数据,可使用this.props.dispatch
方法发送action
。 - 通过
dispatch actions
导航,个人理解是可使用store.dispatch(push('routerName'))
切换路由。 - 在
redux devtools
中支持路由改变的时间旅行调试。
- 将
- 想要实现以上的目标,则可以通过
connected-react-router
和history
两个库进行实现,步骤如下:- 在创建
store
的文件添加配置,包括创建history
对象、使用connected-react-router
提供的connectRouter
方法和history
对象创建root reducer
、使用connected-react-router
提供的routerMiddleware
中间件和history
对象实现dispatch actions
导航。
import { connectRouter, routerMiddleware } from 'connected-react-router'; import createHistory from 'history/createBrowserHistory'; import { createStore, applyMiddleware } from 'redux'; import { createLogger } from 'redux-logger'; import createSagaMiddleware from 'redux-saga'; import reducer from '../model/reducers'; export const history = createHistory(); const sagaMiddleware = createSagaMiddleware(); // 调用异步接口 let middleware = [sagaMiddleware, routerMiddleware(history)]; const logger = createLogger(); // 日志记录 middleware.push(logger); const initialState = {}; const store = createStore( connectRouter(history)(reducer), initialState, applyMiddleware(...middleware) ); 复制代码
- 在项目入口文件
index.js
中为根组件中添加配置,包括使用connected-react-router
提供的ConnectedRouter
组件包裹路由,将ConnectedRouter
组件作为Provider
的子组,并且将在store
中创建的history
对象引入,将其作为props
属性 传入ConnectedRouter
组件:
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' import App from './App' import rootSaga from './model/sagas'; import { store, history } from './store/configureStore'; ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, document.getElementById('root'), ); 复制代码
- 在创建
- 以上则完成了
react-router
和redux
的深度整合 ✌️。
- 本文介绍的是如何在
React
项目中使用redux
进行状态管理,并对相关基础知识进行介绍和展示了完整的代码。 - 在进行业务代码开发前通常会对项目进行的一些特殊配置,有利于后期的工程开发,具体内容可参考 👉:react + typescript 项目的定制化过程。
以上内容如有遗漏错误,欢迎留言 ✍️指出,一起进步💪💪💪
如果觉得本文对你有帮助,🏀🏀留下你宝贵的 👍
参考资料 📖
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK