分享个人 Full-Stack JavaScript 项目开发经验
本文将介绍怎么借助react-redux集成Redux的 Store 与 React 组件 的具体步骤。包括 Store 的创建、react-redux 的基本使用方式和 React 组件需要注意的问题等。为了化繁为简,突出它们三者的协作关系,本文先跳过哈希路由和异步请求等内容。
React 的 State 管理会随着应用程序规模的扩张而变得繁琐,最好的做法是在 React 之外管理 State,确保大部分的 React 组件可以是无状态的 UI 组件。
这并不意味 React 组件不能再使用 State,而是在必要的情况下谨慎使用。
Facebook 开发了一种可用于替代 MVC 的设计模式,Flux。它旨在保持数据的单向流动。Redux 就是其中一个流行的类 Flux 脚本库。
安装 Redux:
yarn add redux
创建 Redux 的开发目录:
src/
├── actionCreator/
├ ├── 数据节点A的Action生成器.js
├ ├── 数据节点B的Action生成器.js
├ ├── ......
├ └── 公共数据节点的Action生成器.js
├── actionTypes/
├ └── index.js
├── components/
├ ├── 模块A的UI组件/
├ ├── 模块B的UI组件/
├ ├── ......
├ └── 公共UI组件/
├── containers/
├ ├── 模块A的容器组件.js
├ ├── 模块B的容器组件.js
├ └── ......
├── reducers/
├ ├── index.js
├ ├── 数据节点A的reducer.js
├ ├── 数据节点B的reducer.js
├ └── ......
├── store/
├ ├── index.js
├ └── initialState.js
├── App.js
└── index.js
上面为在项目中创建一个基于 Redux 的源码目录 src,它最后会被打包到客户端运行。
最外层 index.js
它为 webpack 打包入口主文件。在这里获取初始化 Store 对象,应用 react-redux 和 react-router-dom 来封装 App 组件并 render 到 DOM 中。
App.js
它为 React 应用程序的根组件,在这里分配路由到各个容器组件。
store/initialState.js
它为应用程序的初始化 State 对象。要了解有关 State 范式化的说明,请点击这里。
store/index.js
它为构建 Store 的主文件。在这里将合成所有 reducers、传入初始化 State 和应用如 redux-thunk、redux-logger、redux-devtools-extension 等的中间件。
reducers/index.js
reducers 是以模块化方式描述 State 对象的函数。它一般按数据节点拆分,然后通过 index.js 整合成一个导出对象。要了解有关 reducers 的拆分逻辑,请点击这里。
containers/
App 要使用的容器组件。容器组件即将 UI 组件和数据相连的组件。它们将映射组件属性到 State,并将回调函数属性传递给 Store 的 dispatch 方法,来达到渲染 UI 组件的目的。这些容器组件往往是一个功能模块或主菜单的顶级组件。
components/
UI 组件,即只关注于 UI 渲染的组件,大多是无状态的。它们通过属性接收数据,并通过回调函数属性,以状态提升的方式把数据回传给容器组件。一般地,可以以功能模块或菜单为单位对 UI 组件进行划分。再抽取公共的 UI 组件,以组件合成的方式给各功能模块或菜单重用。
actionTypes/index.js
它把应用程序所有的 Action 映射到一个常量对象的属性中。这样做的好处是,当 Action 拼写错误时,undefined 的 Action 能导致浏览器抛出错误。若是一个错误的字符串,则不会触发任何警告信息,只是 State 不会预期变更。同时这也方便编辑器的代码提示和自动补全,并且可以一览应用程序所有功能模块或菜单的 Action。
actionCreator/
Action 是一个简单的 JavaScript 对象。它至少包含一个 type 属性,描述变更 State 的指令名称。它一般还包含执行变更必须要的数据。Action 告诉了我们变更的内容是什么,还有创建变更时使用到哪些数据。而 Action 生成器就是根据用户操作返回对应 Action 的一个函数。这个函数还可能包含服务器请求等的异步操作。最终通过 dispatch 发出这个 Action。一般地,actionCreator 对应着 reducers,以数据节点来拆分和维护。对于各个容器组件共享的数据节点,可以设置为一个公共数据节点 Action 生成器。
下面来具体介绍各个部分的代码实例。
1、分析应用程序需要使用到的 State 数据,包括用于展示的数据和 UI 控制的 State,并按照范式创建 store/initialState.js。
const initialState = {
// 加载中(ui)
loading: {
show: false,
lockPage: false
},
//(专题管理)
topic: {
isFetching: false,
// ......
},
//(清理冗余)
clear: {
isFetching: false,
// ......
},
// ......
};
export default initialState;
2、依据 State 结构和功能模块或菜单的划分,添加对应的 actionTypes:
const actionsTypes = {
// state.topic (专题管理)
FETCHING_TOPIC: 'FETCHING_TOPIC',
// ......
// state.clear (清理冗余)
FETCHING_CLEAR: 'FETCHING_CLEAR',
// ......
};
export default initialState;
3、根据 actionTypes 创建响应的 actionCreator,如 actionCreator/clear.js:
export function resetClear() {
return {
type: actionTypes.RESET_CLEAR,
isFetching: false,
clearSuccess: false
}
}
// ......
4、根据 actionCreator 的返回 Action 创建对应 reducer,如 reducers/clear.js:
import actionTypes from '../actionTypes';
export function clear( state = { isFetching: false, clearSuccess: false }, action ) {
switch (action.type) {
case actionTypes.RESET_CLEAR: {
return {
isFetching: action.isFetching,
clearSuccess: action.clearSuccess
};
}
// ......
default: {
return state;
}
}
}
可以看出,reducer 接收当前 State 节点和 action 作为参数,并通过这些参数创建并返回一个新的 State 节点。
5、在 store/index.js 中创建 Store 的初始化方法:
import {combineReducers, createStore} from 'redux';
import initialState from './initialState';
import reducers from '../reducers';
const configureStore = () =>
createStore(
combineReducers(reducers),
initialState
);
export default configureStore;
为了简练,这里已把处理异步操作和调试的中间件省略。
6、在最外层 index.js 主入口文件中调用 Store 的初始化方法:
import configureStore from "./store";
const store = configureStore();
// store.dispatch(your_action_creator());
// store.getState();
// const unsubscribeA = store.subscribe(()={//...})
// unsubscribeA();
到此为止,Redux 的 Store 已经构建完毕。现在已经可以调用 Store 的相关方法了,但一般不需要直接使用它们。下面介绍如何使用 react-redux 集成 Store 和 React 组件,简化显式通过上下文传递 Store 的复杂性。
在没有使用 react-redux 之前,我们可以通过组件属性传递 store 或者显式地通过组件的上下文对象传递 Store。但当组件树变得复杂时,需要编写的重复代码就会越来越多。又若把 Store 置于全局变量中,那数据的流向就会变得混乱。为此,react-redux 为我们提供了 Provider 组件和 connect 高阶函数解决这一问题。
安装 react-redux:
yarn add react-redux
1、使用 Provider 组件封装 App 组件。Provider 会将 Store 添加到上下文,以便 App 组件树可以访问它。
import React from "react";
import {render} from "react-dom";
import {Provider} from "react-redux";
import App from "./App";
import configureStore from "./store";
const store = configureStore();
render(
<Provider store={store}>
<App/>
</Provider>
, document.getElementById('react-container')
);
值得注意,Provider 接收单个组件作为参数,并把 Store 作为一个属性传入。
2、使用 react-redux 的 connect 高阶函数创建容器组件,以保持它下面的 UI 组件是无状态的、纯粹的。
import {connect} from "react-redux";
import {ClearUI} from "../components/clear/";
import {clear, resetClear} from "../actionCreator/clear";
export const ClearContainer = connect(
state => ({
isFetching: state.clear.isFetching,
clearSuccess: state.clear.clearSuccess
}), dispatch => ({
onClear() {
dispatch(clear());
},
onResetClear() {
dispatch(resetClear());
}
})
)(ClearUI);
connect 高阶函数的内层函数接收两个函数作为参数。第一个函数接收 state 作为参数,返回属性名称映射 state 数据节点值的对象。第二个函数接收 store 的 dispatch 方法作为参数,返回回调函数属性映射分发相关 Action 的函数的对象。就这样,react-redux 帮助我们快速地将 Store 和 UI 组件连接到一起了。这些状态属性和回调函数属性将以 props 的形式传递给相应的子组件。
在实践中,使用了 react-redux 后,在编写 React 组件时有一些问题需要注意:
查看类组件各生命周期函数及注意问题,可以参阅React类组件生命周期一览。