什么是Redux
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux将应用程序所有的状态 State 都保存在 Store 中,因此每个应用程序也只能有一个 Store。State 不能直接改变,必须通过 Reducer 来触发。Reducer 是一个无副作用的纯函数,形式为 (state, action) => state,意思通过传入变化前状态及动作,返回执行该动作后的变化后状态。Action 即描述了如何改变 State。
整体架构及流程图1.在 View 层,用户通过点击等动作触发事件。
2.将事件转化为对应的 Action 传递给处理的 Middleware。
3.Middleware处理对应的副作用后,再调用对应的 Reducer ,根据原始 State 和 传递过来的 Action 合成新的 State,此时可以触发异步调用。
4.将合成后的 State 返回给 View。
Redux目录构成
创建如下目录结构来实现Redux状态管理
src
└─ src/
└─ actions 定义所有的Action
└─ components 展示组件
└─ containers 容器组件
└─ services 管理api
└─ reducers 定义所有的Reducer
└─ store 管理Store
└─ types 管理ts定义的类型
我们预计会使用到以下的包:
- redux Redux 状态管理核心包
- react-redux Redux 与 React 的绑定库
- redux-actions 以FSA标准实现 Action
- redux-thunk 实现异步 Action 的中间件
运行安装命令
yarn add redux react-redux redux-actions redux-thunk
及类型定义
yarn add -D @types/react-redux @types/redux-actions
接着我们来实现一个简单的列表显示功能
Store设计
{
...
novel: {
isLoading: false,
data: [
{
id: 1,
title: 'Learning Python',
author: 'wang3',
comment: 'aaaaaaaaaa'
},
{
id: 2,
title: 'Learning Java',
author: 'wang4',
comment: 'bbbbbbbbbb'
},
]
}
}
- isLoading 表示请求状态,true 请求中,false 请求完成。
- data是一个列表,表示一个域数据,是我们主要的业务数据。
- 实际状态树中可能包含路由,浏览器历史等非业务状态。
Action设计
我们采用标准的FSA来实现 Action。FSA全称为Flux Standard Action,是一个实现 Action 的标准化规范。简单来讲,一个 Action 必须只能是一个简单的
JavaScript 对象,它必须拥有一个字符串常量属性 type 来标识动作类型,它可以拥有 error,payload,meta属性,这3个属性可以是任意类型,除此之外的任何属性都不符合FSA的规范。
- type 标识动作类型的字符串常量。
- payload 任何不是 type或 status 等属性的值,都应该属于 payload,如果 error 等于 true的话,那 payload 就应该为一个 error 对象。
- error 如果 Action 出错,那 error 可以设置为 true,如果 error 被设置成 true 以外的任意值,那该 Action 就不能被认为出错。
- meta 它包括了不属于 payload 的其他任意属性。
理解了FSA的概念,我们选择 redux-actions 来辅助我们实现 Action,这有助于我们在项目中维持统一的规范。
一次异步请求API的过程都需要dispatch至少3种
Action,首先 constants.ts 中增加 Action 定义:
// 定义ACTION类型
export const ACTION_TYPES = {
// 发起请求
FETCH_NOVELS: 'FETCH_NOVELS',
// 请求成功
FETCH_NOVELS_OK: 'FETCH_NOVELS_OK',
// 请求失败
FETCH_NOVELS_NG: 'FETCH_NOVELS_NG',
}
新增actions/nobel.ts
import { ACTION_TYPES } from '../constants'
import { createActions } from "redux-actions";
// 普通 Action
const { fetchNovels, fetchNovelsOK, fetchNovelsNG } = createActions(
ACTION_TYPES.FETCH_NOVELS,
ACTION_TYPES.FETCH_NOVELS_OK,
ACTION_TYPES.FETCH_NOVELS_NG
)
// 异步 Action
export const searchNovels = (url: string) => (
(dispatch: any) => {
dispatch(fetchNovels()); // {type: 'FETCH_NOVELS'}
callApi(url).then(
json => dispatch(fetchNovelsOK(json)), // {type: 'FETCH_NOVELS_OK', payload: json}
error => dispatch(fetchNovelsNG(error)) // {type: 'FETCH_NOVELS_NG', payload: error, error: true}
)
}
);
services/api.ts使用fetch api添加通用api调用
const callApi = (url: string) => {
return fetch(url)
.then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json)
}
return json
})
)
}
Reducer设计
新建reducers/novel.ts
import { ACTION_TYPES } from "../constants";
import { Novel } from "src/types/novel";
import { handleActions, Action } from "redux-actions";
// 定义管理状态树的结构类型
type NovelStateType = {
isLoading: boolean;
data: Array<Novel>;
}
// 初始化状态
export const initialState: NovelStateType = {
isLoading: false,
data: [],
};
// 根据不同的Action生成Reducer
const novel = handleActions({
[ACTION_TYPES.FETCH_NOVELS]: (state: NovelStateType) => {
return { ...state, isLoading: true };
},
[ACTION_TYPES.FETCH_NOVELS_OK]: (state: NovelStateType, action: Action<Array<Novel>>) => {
return {
...state,
data: action.payload,
loadingFlag: false,
};
},
[ACTION_TYPES.FETCH_NOVELS_NG]: (state: NovelStateType, action: Action<any>) => {
return { ...state, loadingFlag: false };
},
}, initialState);
export default novel;
Reducer 也和 Action 一样,不包括复杂的处理,这里使用了 redux-actions包的 handleActions方法,省略了case 判断的写法,看起来更加优雅。由此可知,每个 Action 都会对应创建 Reducer,而Reducer 可以更新所管理的状态树的一部分或者全部,这都是根据业务需要来定。
创建 Store
新建store/configureStore.ts
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import { createBrowserHistory } from 'history'
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
import { NovelStateType, novel } from 'src/reducers/novel';
// 浏览器history对象
export const history = createBrowserHistory();
// 定义应用程序状态树的结构类型
export type AppStateType = {
// 路由状态
router: RouterState;
novel: NovelStateType;
}
// 将各种reducer合并为一个根reducer
const rootReducer = combineReducers({
novel,
// 将路由与浏览器历史关联
router: connectRouter(history),
})
// 创建store
export const store = createStore(
// 跟reducer
rootReducer,
// 应用中间件
applyMiddleware(
thunk,
routerMiddleware(history)
)
)
- 通过 connected-react-router 来完成 redux的绑定路由
- 通过 combineReducers 工具函数合并reducer。
- 通过 applyMiddleware 来增强 store,比如增加异步处理及路由管理能力。
applyMiddleware 完成了Action,Reducer不承担的一些额外的副作用,简单理解applyMiddleware其实就是链式的装饰器模式,通过一步步加入对应的 applyMiddleware 来增强store.dispatch。比如状态变化前后的日志功能等。
可以参照以下格式来自定义 applyMiddleware,next 参数是store的dispatch方法,可以看到本质上就是在保证原有调用的基础上,增加了别的功能。
/**
* 记录所有被发起的 action 以及产生的新的 state。
*/
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
实际使用时
// 创建store
export const store = createStore(
// 跟reducer
rootReducer,
// 应用中间件
applyMiddleware(
thunk,
routerMiddleware(history),
logger
)
)
有许多现成的中间件可以使用,这里推荐一个记录状态变化日志的中间件 redux-logger。
执行以下命令
yarn add redux-logger
yarn add -D @types/redux-logger
修改configureStore.ts
// 状态变化记录中间件
const loggerMiddleware = createLogger()
// 创建store
export const store = createStore(
// 跟reducer
rootReducer,
// 应用中间件
applyMiddleware(
thunk,
routerMiddleware(history),
loggerMiddleware
)
)
创建 React 组件
实现组件前需要简单了解下展示组件和容器组件的区别。
- 展示组件:描述页面呈现,不与Redux直接作用,数据来源自props,修改数据调用props里的回调函数
- 容器组件:描述如何运行(数据获取、状态更新),直接使用Redux,监听 Redux state,向 Redux 派发 actions
展示组件
首先创建components/NovelList.tsx的展示组件,根据传入的novels数组显示小说列表。
import * as React from 'react'
import { Novel } from '../types/novel';
export interface NovelListProps {
novels: Novel[];
}
const NovelList: React.SFC<NovelListProps> = (props) => {
return (<ul>
{
props.novels && props.novels.map((novel, index) => {
return (
<li key={novel.id}>
<span>{index}</span>
<span>{novel.title}</span>
<span>{novel.author}</span>
<span>{novel.summary}</span>
</li>
);
})
}
</ul>);
}
export default NovelList;
这里有个需要注意的关于 key 属性,这个涉及到UI更新渲染问题,React 依靠 Diff 算法,会根据这个值来判断是否渲染UI,尽可能减少性能开销。用能够唯一标识数据的属性来做 key 是推荐的做法。
容器组件
import * as React from 'react'
import NovelList from '../components/NovelList';
import { useEffect } from "react";
import { AppStateType } from '../store/configureStore';
import { searchNovels } from '../actions/novel';
import { Novel } from '../types/novel';
import { connect } from 'react-redux';
export interface NovelContainerProps {
isLoading: boolean;
data: Array<Novel>;
// 通过中间件自动注入
dispatch: any;
}
const NovelContainer: React.SFC<NovelContainerProps> = (props) => {
// componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
// 保持render的纯净,副作用操作都放到渲染之后执行
useEffect(() => {
let { dispatch } = props;
dispatch(searchNovels('test'))
}, [])
// 加载中UI
if (!props.isLoading) {
return (<div>loading...</div>);
}
if (props.data && props.data.length == 0) {
return <div>No Data...</div>
}
// 加载完成后UI
return (<div>
<NovelList novels={props.data}></NovelList>
</div>);
}
// connect生成容器组件
export default connect(
// 将state绑定到容器组件props上
({ novel }: AppStateType) => novel,
)(NovelContainer);
我们这里创建的是属于混合控件,它包含了UI定义及Redux操作。这里使用了React16.8 的新增特性Hook,功能类似于componentDidMount,componentDidUpdate 和 componentWillUnmount,只不过它每次重渲染时都会执行。
App.tsx更新
class App extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Header />
<Route exact path={PATHS.TOP} component={Top}></Route>
<Suspense fallback={<div>Loading...</div>}>
<Route exact path={PATHS.ABOUT} component={About} ></Route>
<Route exact path={PATHS.NOVELS} component={NovelContainer} ></Route>
</Suspense>
</ConnectedRouter>
</Provider>
);
}
}
通过Provider标签将store注入到所有组件属性中,ConnectedRouter将浏览器历史及路由注入到组件属性中。
至此,基于 Redux 状态管理的 React 程序可以顺利运行了。
网友评论