React+Redux技术栈核心要点解析(中篇)

感谢作者郭永峰的授权发布。
作者:郭永峰,前端架构师,现用友网络 FED团队负责人,目前主要负责企业级应用前端技术平台建设工作,在前端工程化实现、Node 应用开发、React技术、移动开发等方向有丰富实践经验。Github地址:https://github.com/GuoYongfeng。
责编:陈秋歌,关注前端开发等领域,寻求报道或者投稿请发邮件至chenqg#csdn.net。

使用 React + Redux 这个技术栈开发应用已经有很长一段时间了,我的一些使用经验也许会有些主观,但我觉得写出来也许对你开始学习或是进阶使用 React + Redux 会有些帮助。Redux 并不是只和 React 结合使用的,它也可以和其他的很多类库结合起来一起使用,即使你还未开始深入使用,你也可以阅读文中的部分内容。同时,如果你有一些建议或是疑惑,可以在 Github 给我提交 Issue,很乐意与你一起交流。

本次分享共有三篇文章,本文为第二篇。

对于学习 Redux 的一些建议

React 和 Redux 经常结合在一起使用,Redux 是 flux 架构模式的一种优秀实现,并且在 React 社区被广泛使用,但也不是完全和 React 耦合在一起的。

全局 state

并不是所有的全局state都需要被存储起来,一些组件可以使用 setState 来管理组件的内部状态,这也是为什么在学习 Redux 前要掌握 React 中的 setState ,否则你将习惯式的把所有的global state都存储在store里面。所以思考一下,在大型开发团队里面开发的复杂应用,你更不能将应用的所有 state 都切换成全局状态。

项目目录如何组织

这篇文章organizing-redux-application 给出了三种建议方式来组织项目结构。

第一种方式是按功能划分

React + Redux 的一些教程经常给我们展示按功能划分的目录,这也是一种很好的 React + Redux 学习方式,不过,将应用的所有 reducers 和 actions 都放在专门的文件夹维护的方案,并不是所有人都能赞同。

src/
--actions/
--reducers/
--components/

经常听到的有建设性的想法是,目录划分应该以组件为核心,每个目录应该有组件本身以及它所对应的 reducers、actions,那么一个示例的目录结构应该是这样的:

message/
--components
--reducer.js
--actions.js

一个包含 container component 、presenter component以及测试相关的详细的组件目录会是这样的:

message/
--components/
----messageItem/
------presenter.js
------spec.js
----messageList/
------container.js
------presenter.js
------spec.js
--reducer/
----index.js
----spec.js
--actions/
----index.js
----spec.js

当然了,也并不是大家都会喜欢这种方式。(其实,我个人是很赞同这样的就近维护组件的原则的,因为将各个功能性的reducer和action都丢到对应的目录,这以后维护起来会更加困难,文件也不好找,这可不像是MVC那样的分层结构。)尤其是将reducer隐藏在各个功能目录中,这也不利于全局性的来理解使用 redux 的架构意图。所以建议是适当的在最初就抽取一些 reducers 来共享他们所包含的功能。

但在现实场景中,尤其是多个团队在同一个应用项目中协作的时候,在开发进度的压力之下,并没有那么多机会来正确的抽象出一些 reducers。反而通常是一口气的封装所有的功能模块,只为了感觉把活给干完了,让需求按时上线。

第二种方式是对功能模块划分清晰的界限

给每个模块都设置一个 index.js 文件作为入口,这个文件只是用于导出一些API给其他的模块使用。在基于 React + Redux 的应用中,index.js 文件可以用于导出一个 container components ,或是一个presenter components、action creators、能用于其他地方的 reducer(但不是最终的reducer)。那么,基于这样的思考,我们的目录就可以变成这样了:

message/
--index.js
--components/
----messageItem/
------index.js
------presenter.js
------spec.js
----messageList/
------index.js
------container.js
------presenter.js
------spec.js
--reducer/
----index.js
----spec.js
--actions/
----index.js
----spec.js

那么,在当前功能模块下的 index.js 文件应该包含这些代码:

import MessageList from './messageList';

export default MessageList;

export MessageItem from './messageItem';
export reducer from './reducer';
export actions from './actions';

好了,这样外部的其他模块就可以这样在他的 index.js 文件中调用 message 模块了。

// bad
import { reducer } from ./message/reducer;

// good
import { reducer } from ./message;

收获:按功能模块以及清晰的界限可以帮助我们很好的组织代码和目录。

命名约定

在软件编程中命名可真是一件令人头疼的事情,这跟给孩子取名一样费劲,哈哈。合适的命名是实现可维护性、易于理解的代码的最好实践,React + Redux 的应用中提供了大量的约束来帮助我们组织代码,而且不会在命名上固执己见。无论你的函数封装在 reducer 还是 component 中,在action creator 或是 selector 中,你都应该有一个命名约束,并且在扩展应用之前就确定如何命名,否则经常会让我们陷入难以捉摸的回调和重构当中。

而我习惯为每个类型的函数都加上一个前缀。

  • 在组件的callback中,为每个函数都加上 on 作为前缀,比如 onCreateRplay
  • 在改变 state 的 reducer 中加上 applay 作为前缀,比如 applyCreateReply
  • 在 selector 中 加上 get 作为前缀,比如 getReply
  • 在 action creator 中加上 do 作为前缀,比如 doCreateReply

也许你不一定习惯这种加上前缀的方式,不过我还是推荐给你,同时也建议找到自己喜欢的命名约束规则。

追踪状态的改变

在持续迭代中的应用免不了定义大量的 action,而且还需要追溯 state 是如何改变的,redux-logger 可以帮助你看到所有的 state change。每条日志都会显示出 previous state、执行的 action、next state。

不过你得确保 actions 是可被设备的,因此我建议为不同类型的 action 都加上一个前缀,比如这样:

const MESSAGE_CREATE_REPLY = 'message/CREATE_REPLY';

这样的话,无论你在何时触发了信息回复这个动作,你都能看到 message/CREATE_REPLY 这一条日志,如果出现 state 异常,便能迅速查到是那条错误的 state 改变而导致的。

尽可能让 state tree 扁平化

在 Redux 中,扁平化的 state tree可以让你的 reducers 更加的简单,这样你就不需要在整个 store 的状态树中深层的查找到某个 state 后再将其修改,而是可以很轻松的就能实现。不过,在 Redux 中却不能做这么做,因为 state 是不可变的。

如果你正在开发一个博客应用,需要维护一个类似这样的列表对象,列表中包含 author 和 comment 字段:

{
  post: {
    author: {},
    comments: [],
  }
}

不过实际情况是每个对象都需要有对应的 id 来进行维护:

{
  post: {
    id: '1',
    author: {
      id: 'a',
      ...
    },
    comments: [
      {
        id: 'z',
        ...
      },
      ...
    ],
  }
}

这个时候,我们将数据序列化之后将会变得更有意义,数据解构变得更加扁平化了。序列化之后的数据通过 id 关联其他字段,之后,你就可以通过实体对象来将其报酬,通过 id 来进行关联数据的查找。

{
  posts: {
    1: {
      authorId: 'a',
      commentIds: ['z', ...]
    }
  },
  authors: {
    a: {
      ...
    }
  },
  comments: {
    z: {
      ...
    }
  },
}

这样,数据结构看起来就不在那么深层嵌套了,当你需要改变数据的时候,就可以轻松的实现数据的不可变性了。

normalizr 是个强大的 library,可以帮助我们进行数据格式化,噢耶~!

单一数据源原则

格式化之后的数据可以帮助你按同步的方式来管理 state ,而假如请求后端接口后返回的是深层嵌套的 blog 的 posts 数据结构呢,是不是欲哭无泪啊?! post 字段依然包含 author 和 comments字段,不过这次,comments 是一个数组,数组中的每个对象都有 author 字段:

{
  post: {
    author: { id: 'a' },
    comments: [
      {
        author: { id: 'b' },
        reply: {},
      },
      {
        author: { id: 'a' },
        reply: {},
      },
    ],
  }
}

我们可以看到数据结构中 author 字段在 post 和 comments 中都有维护,这就导致嵌套的数据结构中出现了两次,这就不是单一数据源,当你改变了author 字段的时候就会变得很困难了。

这个时候当你将数据格式化之后, author 这个字段就只有一个了。

{
  authors: {
    a: {},
    b: {},
  }
}

当你想 follow 一个 author 的时候,就可以轻松的更新一个字段了 — 数据源是单一的:

{
  authors: {
    a: { isFollowed: true },
    b: {},
  }
}

应用中所有依赖了 author 这个字段的地方都能得到更新。

Selectors

你还没使用 selectors 吗?没关系,在 redux 中依然可以通过 mapStateToProps 来计算 props:

function mapStateToProps(state) {
  return {
    isShown: state.list.length > 0,
  };
};

而如何你一旦使用了 selectors 之后的话,你就可以将这部分计算的工作放到 selectors,从而让 mapStateToProps 更加的简洁:

function mapStateToProps(state) {
  return {
    isShown: getIsShown(state),
  };
};

你可以使用 reselect 来帮助你完成这些事情,它可以帮助你从 state 中计算得到衍生的数据,并且让你的应用的性能得到提升:

  • Selectors 可以推导出衍生数据,并传递所需数据的最小集,不用一次把所有数据都给组件,解决性能问题;
  • Selectors是可组合的,它可以作为其他 Selectors 的输入;
  • Reselect所提供的 selector 是非常高效,除非它的参数改变了,否则 selector 不会重新计算,这在复杂应用中对性能提升是非常有帮助的。

不断的重构

随着时间得推移,你会想要重构你的代码,无论是你在应用中使用了 React 、React + Redux 或者其他前端框架,你总会不断的掌握更加高效的代码组织方式,或者是一些很好的设计模式。

如果你的应用中的组件非常的多,你可以找到一个更好的方式来分离和组织木偶组件和容器组件,你会发现他们之间的关系并做一些公共的抽取;如果你还没有使用合适的命名约束,你也可以在重构的时候去做这些事情。

Generators, Sagas, Observables, Epics, …

Redux 是一个非常优秀的 library,让我们可以体验不同的编程范式和技术。而大家又常常需要不构建不同的类库来实现 async action,这里有几种不同的方式来处理这些 side effects:

  • Redux Thunk - (Delayed) Functions
  • Redux Promise - Promises
  • Redux Saga - Generators
  • Redux Observable - Observables
  • Redux Loop - Elm Effects

新手的话建议使用 redux thunk 来处理一些异步操作;等你慢慢的熟悉整个生态及其相关的应用的时候,可以看看其他的相关类库。Redux Saga 是目前被广泛采用的一种实现方式。不过,Redux Observables 目前也被越来越多的人所接受,这可是需要掌握不少关于 rxjs 及其响应式编程的概念及其使用方式。

其实,整体看来,redux 生态圈的本身就产生了非常多的前端类库,真是让人应接不暇啊。但也别烦恼,那些你不需要用到的东西,自然也不需要都去掌握,对吧。

多阅读一下 Redux 的实现源码

Redux 本身的源码并不多,总共也才五六个关键文件,不超千行代码。如果你想对 Redux 更加熟悉,那么强烈建议你要抽些时间多分析一下他的源码。

在开始学习的时候,也推荐部分学习视频给你:

  • Redux 的作者 Dan Abramov 自己录制的入门级视频 《getting-started-with-redux》 ,大家都说录制的很棒,不过说实话,这个对理解实现原理是很有帮助的。javascript-redux-implementing-store-from-scratch 和 javascript-redux-implementing-combinereducers-from-scratch 两个视频可以帮助你理解 store 和 combineReducer 的实现原理。
  • 第二个系列的视频是《building-react-applications-with-idiomatic-redux》,你可以从中学习到如何实现你自己的 middleware 中间件,学完后就可以学习如何在 store 中使用它们。然后,你就能掌握到如何使用 applayMiddleware 将中间件应用到 store 中

这些视频内容不仅可以教你快速掌握如何使用 Redux,还可以让你理解 Redux 的实现原理。最后,你就可以啃一啃 Redux 的源码了,可以学习到很多有意思的编程思想和函数式的运用。

相关阅读:

React+Redux技术栈核心要点解析(上篇)

React+Redux技术栈核心要点解析(下篇)

欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码加群主微信,申请入群,务必注明「姓名+公司+职位」
图片描述

你可能感兴趣的:(React+Redux技术栈核心要点解析(中篇))