探索Redux的最佳实践_鸭子类型 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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39939012/article/details/82622016

智能推荐

利用单片机做手机连点器(附别踩白块自动点击程序)_diy电容屏点击器_星追光梦的博客-程序员宅基地

原理手机目前主流都是电容屏,所以可以利用软件控制电流以控制屏幕的触摸效果(屏幕工作原理自行百度),所以定时定点控制屏幕连点就很容易了,所以为了文章不至于太水,文末会给出连点的进阶玩法。基本电路分析及要点都在图里。再说明一下,IO口高电平时断开触屏,低电平时持续触屏,点击频率由IO口高低电平转换频率及屏幕灵敏度决定。实物图这是用来做别踩白块实验用的就是基础电路乘4,由于临近开学没时间买光敏电阻检测黑块位置,所以只写了程序,但连点以及四个流水点击都可以实现了,程序#include._diy电容屏点击器

正则表达式各类字符索引(1)基本元字符_快乐虫的博客-程序员宅基地

基本元字符元字符说明.匹配任意单个字符1逻辑或操作符[ ]匹配该字符集合中的一个字符[ ^ ]排除该字符集合-定义一个范围(例如[A-Z])\对下一个字符转义

机器学习试题_以下哪些方法不可以直接来对文本进行编辑_Panpan Wei的博客-程序员宅基地

1. 习题1(SPSS基础)SPSS的界面中,以下是主窗口是( )A.语法编辑窗B.数据编辑窗口C.结果输出窗口D.脚本编辑窗口正确答案:B解析:SPSS是属于数据分析软件,当然主窗口是在数据编辑上。2. 习题2(分类与聚类)以下哪些方法不可以直接来对文本分类?A.KmeansB.决策树C.支持向量机D.KNN正确答案..._以下哪些方法不可以直接来对文本进行编辑

js 操作select和option常见用法_js select option_安果移不动的博客-程序员宅基地

1、获取选中select的value和text,html&lt;select id="mySelect"&gt;&lt;option value="1"&gt;one&lt;/option&gt;&lt;option value="2"&gt;two&lt;/option&gt;&lt;option value="3"&g_js select option

机器学习该怎么入门?_GarfieldEr007的博客-程序员宅基地

阿猫Knight ,Perfekt张逸萌 等 432 人赞同我也谈谈自己的经验。机器学习说简单就简单,说难就难,但如果一个人不够聪明的话,他大概很难知道机器学习哪里难。基本上要学习机器学习,先修课程是algebra, calculus, probability theory, linear regression。这几门科学好了再学Machine learning是事

(Android入门)Android四大组件_利用android四大组件做个简单程序_我不修电脑的博客-程序员宅基地

最近在做一个Android的小项目,由于没有太系统学过安卓,只是跟了一门MOOC实现了几个例子,所以在使用的过程中一直会出现一些意想不到的错误。就从这两天来说,需要实现一个将自己的APP替换掉锁屏的功能,才发现自己连Activity与Service之间通讯这样简单的东西都不了解,甚至于连四大组件各自功能是什么都不太清楚。为了打牢自己的基础,所以决定写下一些自己的理解。-------_利用android四大组件做个简单程序

随便推点

如何使用bert_thinking慢羊羊的博客-程序员宅基地

1.什么是bertbert原文中文翻译版,论文地址。腾讯一篇对bert解读的文章,从零构建BERT,文章地址。2.bert的使用uer-pyUER-py全称是Universal Encoder Representations,UER-py是一个在通用语料预训练以及对下游任务进行微调的工具包。github项目地址。uer的一些教程,知乎教程(bert做情感分类、序列标注)。...

使用oshi获取系统信息_com.github.oshi_ZZZZVSS的博客-程序员宅基地

pox引入:<!-- 获取系统信息 --> <dependency> <groupId>com.github.oshi</groupId> <artifactId>oshi-core</artifactId> <version>3.13.2</version> </de..._com.github.oshi

numpy 多维数组转为一维数组_numpy多维转一维_默默前行的旅者的博客-程序员宅基地

flatten和ravel方法:两个方法都是将多维数组转换为一维数组,但是有以下不同:flatten是将数组转换为一维数组后,然后将这个拷贝返回回去,所以后续对这个返回值进行修改不会影响之前的数组。ravel是将数组转换为一维数组后,将这个视图(可以理解为引用)返回回去,所以后续对这个返回值进行修改会影响之前的数组。比如以下代码:x = np.array([[1, 2], [3, 4]])x.flatten()[1] = 100 #此时的x[0]的位置元素还是1x.ravel()[1] = 1_numpy多维转一维

分享一下我是如何用Proteus做微机原理实验的_proteus为什么8086interal memory size改不了_nimingzhe2008的博客-程序员宅基地

我用的Proteus的版本是7.8,如果版本太低是找不到8086芯片的。Proteus的8086不含操作系统,因此汇编程序不再支持BIOS和DOS的调用。把生成的exe文件放入8086芯片后,要把芯片的Internal Memory Size设置为0x10000,否则仿真时会报错invalid internal memory size == NULL(internal memory m_proteus为什么8086interal memory size改不了

vue.js学习笔记九_为什么vue中没有生成bundle.js文件_-恰饭第一名-的博客-程序员宅基地

一、什么是Webpack?webpack是一个现代的Javascript应用的静态模块打包工具二、前端模块化2.1、打包是如何理解呢?就是将webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)并且在打包的过程中的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成Javascript等等操作但是打包的操作似乎grunt/gulp也可以帮助我们完成,它们有什么不同呢?2.2、和grunt/gulp的对比_为什么vue中没有生成bundle.js文件

黑马程序员----【javaSE基础】总结整理--泛型_av棋牌_鑫鑫酱的博客-程序员宅基地

java泛型什么是泛型?泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。可以在集合框架(Collection framework)中看到泛型的动机。例如,Map 类允许您向一个 Map_av棋牌

推荐文章

热门文章

相关标签