本文将介绍如何把 Redux 中优秀的设计模式和 TypeScript 这门语言结合起来,全文会围绕 Redux store 设计、Redux reducer 设计、Redux action 的设计以及常用异步中间件(redux-thunk 和 redux-promise-middleware)的使用展开。
接着实践二讲,本文要实现的功能是,在页面加载完成之前,通过 Redux 请求 /user/detail 接口获取用户基本信息并把信息写入到对应的 store 中。
页面组件 User 的代码如下:
|
|
Redux store设计
基于 Ducks 架构,我们会根据每个页面状态划分出一个独立的 state,将这些页面的 state 组合在一起形成一个完整的 store 状态树。以 User 页面为例,根据上述 User 页面组件的代码可以看出 User 组件依赖以下四个属性:
| name | type | description |
|---|---|---|
| loading | boolean | 页面是否处于加载 |
| name | string | 用户姓名 |
| age | string | 用户年龄 |
| department | string | 用户所属部门 |
其中 loading 属于页面 UI 状态,name、age 和 department 属于请求 /user/detail 接口获取到的一部分数据,我们可以简单地把没有业务属性的 UI 状态划分为一类数据,其他数据再按照各自的业务属性划分成不同类:
|
|
Redux action设计
先看 Redux 本身对于 action 的接口定义:
|
|
Redux action 的原始接口定义很简单,必须要有一个任意类型的 type 属性,而其他属性可以让用户随意定制,在实际应用中,这种接口定义不够严格和规范,会出现各式各样的 action,因此,我们将采用 Flux action 标准来规范 Redux action 的使用。
Flux action 的属性有:
| name | type | required | description |
|---|---|---|---|
| type | string | √ | action 标识 |
| payload | any | action 数据载荷 |
|
| error | any | 标志 action 是否是异常的,如果为真,则 payload 应该为 Error 对象 |
|
| meta | any | action 中不属于 payload 的额外数据信息 |
根据 Flux action 的属性可以定义 FluxAction 接口:
|
|
在 Redux 中,action 会由 action creator 函数生成,Redux 对于 action creator 的原始接口定义为:
|
|
基于 ActionCreator,我们可以构造出专门生成 FluxAction 类型 action 的 action creator:
|
|
假设我们现在需要切换 User 组件上的 loading 状态,要求通过 action creator 发出对应的 FluxAction 类型的 action,那么我们可以这么定义这个 action creator:
|
|
当然,FluxActionCreator 类型定义稍显复杂,而复杂的类型定义会带来一定的理解成本,因此,我们也可以将 toggleLoading 简单定义为:
|
|
再比如修改 IUserState.user:
|
|
Redux异步action设计
Redux 本身不支持异步 action,需要引入额外的中间件来支持,这里举两个常见的异步中间件 redux-thunk 和 redux-promise-middleware,我们将分别讲解如何通过这两个中间件从 /user/detail 接口获取用户基本信息。
I. redux-thunk
通常情况下,action creator 函数会返回一个对象,而在引入 redux-thunk 中间件后,action creator 会返回一个函数,该函数参数如下表:
| name | type | description |
|---|---|---|
| dispatch | ThunkDispatch | action creator 触发函数 |
| getState | function(): any | 获取整个 store tree 函数 |
| extraArgument | any | 额外的参数 |
|
|
基于以上源码,我们可以构造出 FluxAction 相关的 ThunkDispatch 和 ThunkAction
|
|
在 FluxThunkDispatch 和 FluxThunkAction 中,我们调整了泛型类型的定义顺序,并对 M、R、E 提供了默认的类型 any,这是因为在大多数情况下,我们更关心代表 store tree 类型的 S 和代表 action payload 类型的 P。来看个使用 FluxThunkAction 的例子:
|
|
通常情况下,一个应用只会配一个 store tree,因此 FluxThunkAction 和 FluxThunkDispatch 可以简化为:
|
|
上述例子就可以简化为:
|
|
现在再看从 /user/detail 接口获取用户信息功能,要求服务端在响应过程中,前端要展示 loading 状态,等到服务端返回后前端再取消 loading 状态,通过 redux-thunk 可以这么实现 action creator:
|
|
II. redux-promise-middleware
redux-thunk 在 action creator 内部处理不同的异步状态,而 redux-promise-middleware 则把这些处理逻辑移至 reducer 中处理,action creator 应该尽量保持代码简洁,减少衍生计算逻辑,从这点看,redux-promise-middleware 要优于 redux-thunk 并且 redux-promise-middleware 对于 Promise 异步状态划分的更为清晰,代码可读性更强。
redux-promise-middleware 要求 action payload 类型为 Promise 类型或是包含以下两个属性的对象
| name | type | description |
|---|---|---|
| promise | Promise | Promise 对象 |
| data | any | 乐观更新的数据 |
|
|
从 /user/detail 接口获取用户信息的 action creator 接口可以写成:
|
|
相比于 redux-thunk,redux-promise-middleware 的 action creator 代码要简洁很多。
Redux reducer设计
设计好 Redux store 后,还需要定义对应的 Redux reducer 来接收 Redux action 并对 Redux store 进行衍生计算。
先看 Redux 对 Reducer 类型的定义:
|
|
结合 FluxAction 和 IStoreTree 可以定义我们自己想要的 Reducer 和 ReducersMapObject:
|
|
当涉及到组件状态时候,不可变数据总是绕不开的话题,社区里常用的不可变数据函数库有 immutable 和 immer 等等,由于 immutable 对于数据结构处理较为繁琐,需要在它提供的对象和原始 javascript 对象间来回切换,故打算采用更为轻量的 immer 库,为了让开发者对不可变数据无感知,需要对每个 reducer 做些额外处理:
|
|
构建好不可变状态后,可以开始实现 user reducer 的功能:
|
|
上述代码中 reducer 函数是标准的 Redux reducer,这种方式存在函数过长、功能划分不够细致、 action payload 类型不确定等弊端,可以尝试使用 type-to-reducer 库对其进行简化:
|
|
至此还剩最后三个步骤:
把不可变
reducer的创建和普通reducer结合起来12345678// 文件: client/store/root-reducer.ts// reducer 集合import createImReducers from './create-immutable-reducers'import user from '../pages/user/user'export default createImReducers({user})创建
store并引入中间件1234567891011121314// 文件: client/store/create-store.tsimport Thunk from 'redux-thunk'import promiseMiddleware from 'redux-promise-middleware'import { createStore, combineReducers, applyMiddleware } from 'redux'import reducers from './root-reducer'export default function(initialState:IStoreTree):any {return createStore(combineReducers(reducers),initialState,applyMiddleware(promiseMiddleware(), Thunk))}User页面组件连接redux1234567891011121314151617181920212223242526272829303132333435363738394041// 文件: client/pages/user/index.tsximport {connect} from 'react-redux'import {bindActionCreators, Dispatch} from 'redux'import {Spin, Input} from 'antd'import {getUserDetail} from './user'// User 组件属性接口interface IProps extends IUserState {actions: any}// User 组件class User extends React.Component<IProps> {componentDidMount() {this.props.actions.getUserDetail('xxxx')}render() {const {ui, user} = this.propsreturn (<Spin spinning={ui.loading}><strong>用户姓名:</strong><Input value={user.name} /><strong>用户年龄:</strong><Input value={user.age} /><strong>所属部门:</strong><Input value={user.department} /></Spin>)}}export default connect((store: IStoreTree): IUserState => store.user,(dispatch: Dispatch<FluxAction>) => ({actions: bindActionCreators({getUserDetail}, dispatch)}))