探索Redux的最佳实践

一、 前言

广发证券金钥匙是一个连接用户和投资顾问、为用户提供专业投资咨询服务的的产品。基于Angular 1.x和Ionic,我们为用户和投顾分别提供了覆盖PC Web、Mobile Web和Android/iOS客户端的系列产品。

前端的发展日新月异,React Native/Weex/微信小程序等技术方案进一步扩展了前端技术的应用范围。在金钥匙项目中,我们相继推出了小程序版金钥匙有问必答服务,同时采用React Native替代Ioinc,重构金钥匙项目客户端。如何在前端项目中优雅地管理应用中的数据状态?我们在开发过程中进行了深入的思考,选择了Redux作为应用状态管理工具,从多个角度探索其最佳实践。本文将分享我们对探索过程的一些总结。

二、探索Redux的最佳实践

在以往基于Angular 1.x的开发经验中,有几个问题总是出现在我脑海中:

  • 应用的状态数据在哪里?

  • 应用当前状态数据是什么?

  • 怎样优雅的在组件间共享数据?

在Angular 1.x项目中,我们没能很好地解决上述问题。所以,在开始使用React Native 重构项目时,我们迫切地希望解决上述问题。经过调研后,我们选择了Redux。

2.1 什么是Redux?

Redux是前端应用的状态容器,提供可预测的状态管理,其基本定义可以用下列公式表示:

(state, action) => newState

借用一张经典图示(如下图),可以进一步理解Redux主要元素和数据流向。

2.2 Redux异步方案选型

Redux自身action结构简单,没有定义异步方法部分的支持内容。然而异步请求是前端应用中重要部分,如何管理异步请求,怎样在社区的各式异步相关中间件中选择,是首先需要解决的问题。

 2.2.1 Without Middleware

最初使用Redux时会有疑问——必须借助中间件才能完成异步请求吗?实际上并不是这样,可以像下例中进行异步操作:

//action creator
function loadData(dispatch,userId){
    dispatch({type:'LOAD_START'})
    asyncRequest(userId).then(resp=>{
        dispatch({type:'LOAD_SUCCESS',resp})
    }).catch(error=>{
        dispatch({type:'LOAD_FAIL',error})
    })
}

//component
componentDidMount(){
    loadData(this.props.dispatch,this.props.userId)
}

虽然上述代码示例可以完成工作,但是有下面几个问题:

  • 组件需将dispatch/getState这种业务无关的参数,根据需要传入到action creator

  • 对包含异步逻辑的creator和对普通action的调用方法不一致

  • 带来组件和action creator在一定程度上的耦合

 2.2.2 Redux Thunk

Redux提供了中间件机制,而Redux Thunk是Redux官方文档中用到的异步组件,使用Redux-Thunk完成上述异步请求:

//action creator
function loadData(userId){
    return  dispatch => {
        dispatch({type:'LOAD_START'})
        asyncRequest(userId).then(resp=>{
            dispatch({type:'LOAD_SUCCESS',resp})
        }).catch(error=>{
            dispatch({type:'LOAD_FAIL',error})
        })
    }
}

//component
componentDidMount(){
    this.props.dispatch(loadData(this.props.userId));
}

相比不使用之前,中间件在使用Redux Thunk后,在组件中不再关注action creator中是否需要dispatch/getState参数,不再关注dispatch的是异步还是同步的方法。

当使用中间件完成异步请求时,action在应用中流程如下图所示:

 2.2.3 Redux Promise Middleware

上文中采用Redux Thunk进行异步请求,只是一个简单的请求过程,而我们需要主动的触发请求的开始、成功和失败状态,当应用中有大量这类简单请求时,项目中会充满这种重复代码。

针对这一问题,可以采用Redux Promise Middleware来简化代码:

//action creator
function loadData(userId){
    return {
        type:types.LOAD_DATA,
        payload:asyncRequest(userId)
    }
}

//component
componentDidMount(){
    this.props.dispatch(loadData(this.props.userId));
}

Redux Promise Middleware中间件会帮助我们处理异步请求的状态,为当前action type添加PEDNGING/FULFILLED/REJECTED三种状态,根据异步请求的结果触发不同状态。

Redux Promise Middleware中间件适用于简化简单请求的代码,开发中推荐混合使用Redux Promise Middleware中间件和Redux Thunk。

 2.2.4 Redux Saga

Redux Saga可以理解为一个和系统交互的常驻进程,其中,Saga可简单定义如下:

Saga = Worker + Watcher

采用Redux Saga完成异步请求,示例如下:

//saga
function* loadUserOnClick(){
    yield* takeLatest('LOAD_DATA',fetchUser); 
} 

function* fetchUser(action){
    try{
        yield put({type:'LOAD_START'});
        const user = yield call(asyncRequest,action.payload);
        yield put({type:'LOAD_SUCCESS',user});
    }catch(err){
        yield put({type:'LOAD_FAIL',error})
    }
}

//component
<div onclick={e=>dispatch({type:'LOAD_DATA',payload:'001'})}>load data</div>

相比Redux Thunk,使用Redux Saga有几处明显的变化:

  • 在组件中,不再dispatch(action creator),而是dispatch(pure action)

  • 组件中不再关注由谁来处理当前action,action经由root saga分发

  • 具体业务处理方法中,通过提供的call/put等帮助方法,声明式的进行方法调用

  • 使用ES6 Generator语法,简化异步代码语法

除开上述这些不同点,Redux Saga真正的威力,在于其提供了一系列帮助方法,使得对于各类事件可以进行更细粒度的控制,从而完成更加复杂的操作

这里简单列举如下:

  • 提供takeLatest/takeEvery/throttle方法,可以便利的实现对事件的仅关注最近事件、关注每一次、事件限频

  • 提供cancel/delay方法,可以便利的取消、延迟异步请求

  • 提供race(effects),[…effects]方法来支持竞态和并行场景

  • 提供channel机制支持外部事件

 2.2.5 Redux Observable

Redux Observable是基于RxJS的用于处理异步请求的中间件,可简单定义如下:

Redux Observable = Epic( Type + Operators )

Redux Observable关注Redux中的action,理念是action in ,action out。用Redux Observable完成异步请求示例如下:

//epic
const loadUserEpic = action$ => 
    action$.ofType('LOAD_DATA')
        .map(()=>({type:'LOAD_START'}))
        .mergeMap(action =>
          ajax.getJSON(`/api/users/${action.payload}`)
            .map(user => {type:'LOAD_SUCCESS',user})
            .catch(error => Observable.of({
                type: 'LOAD_FAIL',error
            }))
        );

//component
<div onclick={e=>dispatch({type:'LOAD_DATA',payload:'001'})}>load data</div>

借助RxJS的各种操作符和帮助方法,Redux Observable也能实现对各类事件的细粒度操作,比如取消、限频、延迟请求等。

Redux Observable与Redux Saga适用于对事件操作有细粒度需求的场景,同时他们也提供了更好的可测试性,当你的应用逐渐复杂需要更加强大的工具时,他们会成为很好的帮手。由于Redux Observable基于RxJS,相对来说学习曲线更高。

2.3 Redux应用状态划分

如何设计应用状态的数据结构是一个值得思考的问题,在实践中,我们总结了两点数据划分的指导性原则,应用状态扁平化和抽离公共状态。

 2.3.1 应用状态扁平化

在我们的项目中,有联系人、聊天消息和当前联系人对象。在Angular 1.x 项目中,数据结构如下:

{
contacts:[
    {
        id:'001',
        name:'zhangsan',
        messages:[
            {
                id:1,
                content:{
                    text:'hello'
                },
                status:'succ'
            }
        ]
    },
    {
        id:'002',
        name:'lisi',
        messages:[
            {
                id:2,
                content:{
                    text:'world'
                },
                status:'fail'   
            }
        ]
    }
],
selectedContact:{
        id:'001',
        name:'zhangsan',
        messages:[
            {
                id:1,
                content:{
                    text:'hello'
                },
                status:'succ'
            }
        ]
    }
}

采用上述数据机构,带来几个问题:

  • 消息对象与联系人对象耦合,消息对象的变更操作引发联系人对象的变更操作

  • 联系人集合和当前联系人对象数据冗余,当数据更新时需要多处修改来保持数据一致性

  • 数据结构嵌套过深,不便于数据更新,一定程度上导致更新时的耗时增加

将数据扁平化、解除耦合,得到如下数据结构:

{
contacts:[
    {
        id:'001',
        name:'zhangsan'
    },
    {
        id:'002',
        name:'lisi'
    }
],
messages:{
    '001':[
        {
           id:1,
           content:{
               text:'hello'
           },
           status:'succ'
       },
       ...
    ],
    '002':[
        {
           id:3,
           content:{
               text:'haha'
           },
           status:'succ'
       }
    ]
},
selectedContactId:'001'
}

相对于之前的问题,上述数据结构具有以下优点:

  • 细粒度的更新数据,进而精细控制视图的渲染

  • 结构清晰,避免更新数据时,复杂的数据操作

  • 去除冗余数据,避免数据不一致

在开发过程中,我们可以主动将数据扁平化,或者使用normalizr工具,依据定义的schema设计应用的数据结构。

 2.3.2 抽离公共状态

在领域对象之外,往往还有另外一些与请求过程相关的状态数据,如下所示

{
  user: {
    isError: false, // 加载用户信息失败
    isLoading: false, // 加载用户中
    ...
    entity: { ... },
  },
  messages: {
    isLoading: true, // 加载消息中
    nextHref: '/api/messages?offset=200&size=100', // 消息分页数据
    ...
    entities: { ... },
  },
  authors: {
    isError: false, // 加载作者失败
    isLoading: false, // 加载作者中
    nextHref: '/api/authors?offset=50&size=25', // 作者分页数据
    ...
    entities: { ... },
  },
}

上述数据结构中,我们按照功能模块将状态数据内聚。

采用上述结构,会导致我们需要写很多基本重复的action,如下所示:

{
  type: 'USER_FETCH_ERROR',
  payload: {
    isError,
  },
}

{
  type: 'USER_IS_LOADING',
  payload: {
    isLoading,
  },
}

{
  type: 'MESSAGES_IS_LOADING',
  payload: {
    isLoading,
  },
}

{
  type: 'MESSAGES_NEXT_HREF',
  payload: {
    nextHref,
  },
}

{
  type: 'AUTHORS_FETCH_ERROR',
  payload: {
    isError,
  },
}

{
  type: 'AUTHORS_IS_LOADING',
  payload: {
    isLoading,
  },
}
...

我们分别为usermessageauthor定义了一系列action,而他们作用类似,代码重复。为解决这一问题,我们可以将这类状态数据抽离,不再简单的按照功能模块内聚,抽离后的状态数据如下所示:

{
  isLoading: {
    user: false,
    messages: true,
    authors: false,
    ...
  },
  isError: {
    userEdit: false,
    authorsFetch: false,
    ...
  },
  nextHref: {
    messages: '/api/messages?offset=200&size=100',
    authors: '/api/authors?offset=50&size=25',
    ...
  },
  user: {
    ...
    entity: { ... },
  },
  messages: {
    ...
    entities: { ... },
  },
  authors: {
    ...
    entities: { ... },
  },
}

采用这一结构,可以避免定义大量相似的action type,编写重复的action

2.4 如何修改应用状态

将应用状态数据不可变化是使用Redux的一般范式,有多种方式可以实现不可变数据的效果,这里我们分别尝试了Object.assign、Immutable.js和Seamless-Immutable.js。

 2.4.1 Object.assign/Spread Operator

最初我们使用Object.assign或者Spread Operator来修改数据,在Reducer中使用Spread Operator修改数据的简单示例如下:

function todoApp(state = initialState ,action){
    switch (action.type){
        case SET_VISIBILITY_FILTER:
            return {...state,visibilityFilter: action.filter}
        default:
            return state;
    }
}

随着使用的深入,我们发现这一方式有如下问题:

  • 不能方便的进行嵌套数据的更新

  • 引用类型数据的浅复制可能带来意外的问题

  • 非强制,你仍然有机会直接修改状态数据

 2.4.2 Immutable.js

带着上述问题,我们了解到Immutable.js并开始使用它进行应用状态数据的修改。

Immutable.js为人称道的是它的基于共享数据结构、而非深度复制所带来的数据修改时的高性能,但是在我们的使用过程中,发现其易用性不够友好,使用体验并不美好。

  • 首先,Immutable.js实现的是shallowly immutable,在如下示例中,notFullyImmutable中的对象属性仍然是可变的:

var obj = {foo: "original"};
var notFullyImmutable = Immutable.List.of(obj);

notFullyImmutable.get(0) // { foo: 'original' }

obj.foo = "mutated!";

notFullyImmutable.get(0) // { foo: 'mutated!' }
  • 另外,Immutable.js使用了自定义的数据结构,这意味着贯穿我们的应用都需要明确当前使用的是Immutable.js的数据结构。需要获取数据时,需要使用提供的get方法,而不能使用obj.prop或者obj[prop]。而需要将数据同外部交互,如存储或者请求时,需要将特有数据结构转换成原生JavasScript对象。

  • 最后,以state.set('key',obj)形式更新状态时,obj不能自动的immutable化。

 2.4.3 Seamless-Immutable.js

前面提到的使用Immutable.js过程中的问题,使得我们在开发中不断的需要停下来思考当前写法是否正确,于是我们继续尝试,最后选择使用Seamless-Immutable.js来帮助实现不可变数据。

Seamless-Immutable.js意为无缝的Immutable,与Immutable.js不同,他没有自定义新的数据结构,其基本使用如下所示:

var array = Immutable(["totally", "immutable", {hammer: "Can’t Touch This"}]);

array[1] = "I'm going to mutate you!"
array[1] // "immutable"

array[2].hammer = "hm, surely I can mutate this nested object..."
array[2].hammer // "Can’t Touch This"

for (var index in array) { console.log(array[index]); }
// "totally"
// "immutable"
// { hammer: 'Can’t Touch This' }

JSON.stringify(array) // '["totally","immutable",{"hammer":"Can’t Touch This"}]'

根据我们的使用体验,Seamless-Immutable.js易用性优于Immutable.js。但是在选择使用他之前,有一点需要了解的是,在数据修改时,Seamless-Immutable.js性能低于Immutable.js,当数据嵌套层级越深,数据量越大,性能差异越明显。所以这里需要根据业务特点来做选择,我们的业务没有大批量的深度数据修改需求,所以易用性比性能更重要。

2.5 组织Redux代码结构

学习使用Redux过程中,通常我们会将Redux几个主要元素按类型划分文件目录,通常我们按照如下方式组织代码文件:

|--components/
|--constants/
 ----userTypes.js
|--reducers/
 ----userReducer.js
|--actions/
 ----userAction.js

严格遵循这一模式并无不可,不过在有些场景下,使用其他模式组织代码结构可能更加灵活、便利。

 2.5.1 Redux Ducks

通常我们的actionreducer都是一一对应,同时也会共用一个action type,于是会有一个很自然的想法,与其将actionreducertype分离在各自目标的单独文件,为什么不将他们合并到一起呢?

|--components
 |--redux
  ----userRedux

合并后的userRedux被称为Redux Duck,这是经典的鸭子类型的应用。

合并后的代码示例如下:

//types
const LOAD   = 'LOAD';
const CREATE = 'CREATE';
const UPDATE = 'UPDATE';
const REMOVE = 'REMOVE';

//reducer
export default function reducer(state = {}, action = {}) {
    switch (action.type) {
        // do reducer stuff
        default: return state;
    }
}

//action
export function loadUser() {
    return { type: LOAD };
}

export function createUser(data) {
    return { type: CREATE, data };
}

export function updateUser(data) {
    return { type: UPDATE, data };
}

export function removeUser(data) {
    return { type: REMOVE, data };
}

 2.5.2 按模块组织文件

随着项目规模的增长,代码文件逐渐增多,当actions目录下文件越来越多时,找到目标文件变成了一个稍显麻烦的事情。在这种场景下,按模块组织代码文件,将模块相关的代码聚合在一起,更加适合大型项目的开发。

|--modules/
 ----users/
 ------userComponent.js
 ------userRedux.js
 ----messages/
 ------messageComponent.js
 ------messageRedux.js

与此同时,根据我们的使用经验,鸭子模式与传统模式应当灵活的混合使用。当业务逻辑复杂,actionreducer各自代码量较多时,按照传统模式拆分可能是更好的选择。此时可以如下混合使用两种模式:

|--modules/
 ----users/
 ------userComponent.js
 ------userConstant.js
 ------userAction.js
 ------userReducer.js
 ----messages/
 ------messageComponent.js
 ------messageRedux.js

三、 总结

随着对Redux使用的逐渐深入,我们对Redux几个主要内容的最佳实践进行了一番探索,最终形成了以下几点经验,作为我们现在的指导性原则。

  • 一般项目中,使用Redux Thunk处理异步请求

  • 混合使用Redux Promise Middleware和Redux Thunk简化代码

  • 对于需要细粒度处理事件操作的业务,使用Redux Saga

  • 将应用状态数据扁平化

  • 抽离应用状态中公共的状态数据

  • 在性能不是最高优先级的场景下,使用Seamless-Immutable.js

  • 在大型项目中,按模块组织代码文件

  • 混合使用Redux Duck模式和传统模式

  • 原文:http://www.10tiao.com/html/184/201704/2247485137/1.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Redux是一个用于在React应用中管理状态的库。它结合了React和Redux,提供了一种可预测的状态管理解决方案。在React Redux中,我们使用Provider组件将Redux的store传递给整个应用程序,以便在应用程序的任何地方都可以访问到Redux的状态。\[1\]\[3\] 在React Redux中,我们可以使用connect函数将组件连接到Redux的store,并将store中的状态映射到组件的props上。这样,组件就可以通过props访问和更新Redux的状态。同时,我们还可以使用dispatch函数来触发Redux中的action,从而更新状态。\[2\] 通过React Redux,我们可以更方便地管理React应用的状态,使得应用的状态变化更加可控和可预测。同时,React Redux还提供了一些中间件,如Redux-thunk,可以帮助我们处理异步操作,使得应用的状态管理更加灵活和强大。\[2\] 总之,React Redux是一个强大的状态管理库,可以帮助我们更好地管理React应用的状态,并提供了一些工具和中间件来简化状态管理的过程。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* *3* [React中的Redux](https://blog.csdn.net/yrqlyq/article/details/119118182)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值