Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React + Redux 最佳实践 #1

Open
sorrycc opened this issue Feb 29, 2016 · 64 comments
Open

React + Redux 最佳实践 #1

sorrycc opened this issue Feb 29, 2016 · 64 comments
Labels

Comments

@sorrycc
Copy link
Owner

sorrycc commented Feb 29, 2016

更新:我们基于此最佳实践做了一个封装方案:dva,可以简化使用 redux 和 redux-saga 时很多繁杂的操作。

前端变化虽快,但其实一直都围绕这几个概念在转:

  • URL - 访问什么页面
  • Data - 显示什么信息
  • View - 页面长成什么样
  • Action - 对页面做了什么操作
  • API Server - Data 数据的来源

在 redux 的生态圈内,每个环节有多种方案,比如 Data 可以是 immutable 或者 plain object,在你选了 immutable 之后,用 immutable.js 还是 seamless-immutable,以及是否用 redux-immutable 来辅助数据修改,都需要选择。

本文总结目前 react + redux 的最佳实践,解释原因,并提供可选方案。

心急的朋友可以直接看代码:https://github.com/sorrycc/github-stars

一、URL > Data

需求

routing

选择

react-router + react-router-redux: 前者是业界标准,后者可以同步 route 信息到 state,这样你可以在 view 根据 route 信息调整展现,以及通过 action 来修改 route 。

可选

二、Data

需求

为 redux 提供数据源,修改容易。

方案

plain object: 配合 combineReducer 已经可以满足需求。

同时在组织 Store 的时候,层次不要太深,尽量保持在 2 - 3 层。如果层次深,可以考虑用 updeep 来辅助修改数据。

可选

immutable.js: 通过自定义的 api 来操作数据,需要额外的学习成本。不熟悉 immutable.js 的可以先尝试用 seamless-immutable,JavaScript 原生接口,无学习门槛。

另外,不推荐用 redux-immutable 以及 redux-immutablejs,一是没啥必要,具体看他们的实现就知道了,都比较简单;更重要的是他们都改写了 combineReducer,会带来潜在的一些兼容问题。

三、Data > View

需求

数据的过滤和筛选。

方案

reselect: store 的 select 方案,用于提取数据的筛选逻辑,让 Component 保持简单。选 reselct 看重的是 可组合特性缓存机制

可选

四、View 之 CSS 方案

需求

合理的 CSS 方案,考虑团队协作。

方案

css-modules: 配合 webpack 的 css-loader 进行打包,会为所有的 class name 和 animation name 加 local scope,避免潜在冲突。

直接看代码:

Header.jsx

import style from './Header.less';
export default () => <div className={style.normal} />;

Header.less

.normal { color: red; }

编译后,文件中的 style.normal.normal 在会被重命名为类似 Header__normal___VI1de

可选

bem, rscss ,这两个都是基于约定的方案。但基于约定会带来额外的学习成本和不遍,比如 rscss 要求所有的 Component 都是两个词的连接,比如 Header 就必须换成类似 HeaderBox 这样。

radium,inline css 方案,没研究。

五、Action <> Store,业务逻辑处理

需求

统一处理业务逻辑,尤其是异步的处理。

方案

redux-saga: 用于管理 action,处理异步逻辑。可测试、可 mock、声明式的指令。

可选

redux-loop: 适用于相对简单点的场景,可以组合异步和同步的 action 。但他有个问题是改写了 combineReducer,会导致一些意想不到的兼容问题,比如我在特定场景下用不了 redux-devtool 。

redux-thunk, redux-promise 等: 相对原始的异步方案,适用于更简单的场景。在 action 需要组合、取消等操作时,会不好处理。

saga 入门

在 saga 之前,你可能会在 action creator 里处理业务逻辑,虽然能跑通,但是难以测试。比如:

// action creator with thunking
function createRequest () {
  return (dispatch, getState) => {
    dispatch({ type: 'REQUEST_STUFF' });
    someApiCall(function(response) {
      // some processing
      dispatch({ type: 'RECEIVE_STUFF' });
    });
  };
}

然后组件里可能这样:

function onHandlePress () {
  this.props.dispatch({ type: 'SHOW_WAITING_MODAL' });
  this.props.dispatch(createRequest());
}

这样通过 redux state 和 reducer 把所有的事情串联到起来。

但问题是:

Code is everywhere.

通过 saga,你只需要触发一个 action 。

function onHandlePress () {
  // createRequest 触发 action `BEGIN_REQUEST`
  this.props.dispatch(createRequest());
}

然后所有后续的操作都通过 saga 来管理。

function *hello() {
  // 等待 action `BEGIN_REQUEST`
  yield take('BEGIN_REQUEST');
  // dispatch action `SHOW_WAITING_MODAL`
  yield put({ type: 'SHOW_WAITING_MODAL' });
  // 发布异步请求
  const response = yield call(myApiFunctionThatWrapsFetch);
  // dispatch action `PRELOAD_IMAGES`, 附上 response 信息
  yield put({ type: 'PRELOAD_IMAGES', response.images });
  // dispatch action `HIDE_WAITING_MODAL`
  yield put({ type: 'HIDE_WAITING_MODAL' });
}

可以看出,调整之后的代码有几个优点:

  • 所有业务代码都存于 saga 中,不再散落在各处
  • 全同步执行,就算逻辑再复杂,看起来也不会乱

六、Data <> API Server

需求

异步请求。

方案

isomorphic-fetch: 便于在同构应用中使用,另外同时要写 node 和 web 的同学可以用一个库,学一套 api 。

然后通过 async + await 组织代码。

示例代码:

import fetch from 'isomorphic-fetch';
export async function fetchUser(uid) {
  return await fetch(`/users/${uid}`).then(res => res.json());
};

可选

reqwest

最终

(完)

@yiminghe
Copy link

updeep 也说了,对于大数据量效率没有 immutable.js 高效,不如推荐 immutable.js

@xujihui1985
Copy link

saga这个词是cqrs来的,是用来监听多个事件,同步事件的,比如创建订单会锁定库存,创建订单对象,当锁定库存和创建订单对象都成功时会处理的方法,所有的action都放里面感觉不是很合适

@unclay
Copy link

unclay commented Feb 29, 2016

虽然没用过react,但看看前边说围绕着几个概念在转比较赞同,开发思维感觉又清晰了点

@AllenFang
Copy link

Nice introduction!

@ziluo
Copy link

ziluo commented Mar 2, 2016

赞,急需

@sskyy
Copy link

sskyy commented Mar 4, 2016

saga 的用途在这个例子里面没有讲清楚。

function createRequest () {
  return (dispatch, getState) => {
    dispatch({ type: 'REQUEST_STUFF' });
    someApiCall(function(response) {
      // some processing
      dispatch({ type: 'RECEIVE_STUFF' });
    });
  };
}

这段代码和下面 saga 的代码区别只是 dispatch 变成了 put。someApiCall 变成了 generator 。

@sskyy
Copy link

sskyy commented Mar 4, 2016

saga 的作用最主要还是解决复杂的异步交互情况,特别是竞争状态。参见 http://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es7-async/34933395 saga 作者自己的回答。不过感觉对我们目前的业务来说 overkill 了。

@sorrycc
Copy link
Owner Author

sorrycc commented Mar 4, 2016

saga 是通用方案,不管是简单还是复杂,有些业务看起来简单,但说不定有一个点的异步逻辑比较复杂呢。竞争状态是其中的一个场景,我觉得他最重要的点是可以统一管理业务代码,并且只需要接收一个 action 来触发。

@superRaytin
Copy link

👍

@fengmk2
Copy link

fengmk2 commented Mar 5, 2016

看着 saga 就觉得好熟悉。

@shepherdwind
Copy link

上面例子,saga 在前端用,使用 generator 似乎区别只是异步改为同步写法而已。

generator 最大的问题,如果是高级浏览器还好,要兼容低版本的浏览器,需要一堆转换代码,感觉不是很好。在我们的业务中,异步请求是很小的一部分操作,如果后台是自己控制,页面中的数据,基本上一次请求就都拿过来了。同样,可以在前端操作页面,最终完成后,进行一次提交,完成所有的修改。这种情况下,异步操作,用最简单的 thunk 就够了。

@soda-x
Copy link

soda-x commented Mar 7, 2016

看成了 Soga 😄

@Justin-lu
Copy link

赞,正在学习~

@cyy0523xc
Copy link

赞!

@mengxingshike2012
Copy link

saga, 一开始还以为是日文, 这前台的概念真是越来越多了...

@jerexyz
Copy link

jerexyz commented Jul 11, 2016

👍,正在学习

@rendongsc
Copy link

好文!

@oConnerCooper
Copy link

前人栽树后人乘凉,很棒,公司内部项目准备就这么玩

@sorrycc
Copy link
Owner Author

sorrycc commented Aug 2, 2016

@oConnerCooper 推荐用 dva 搭建 react 项目,是基于这套最佳实践的封装。 #8

@oConnerCooper
Copy link

@sorrycc 可以可以,先看看内部实现,这样用dva相对思路更清晰些,直接用框架,有点黑盒的感觉。喜欢看源码=。=

@carlos121493
Copy link

dva中saga的takeLatest可以在哪里设置?

@clarkhan
Copy link

多个应用整合的场景不知道大家有没有考虑过,有没有一些好的实践方式?

比如,后台管理类系统,非常庞大,是由N多个业务领域相对独立的管理系统组成的。
虽然应用开发与部署相对独立,但是我们肯定希望对于用户来讲,能提供较为统一的体验:将各个系统整合起来,提供统一的导航、菜单、页面布局等等。

以往传统的开发形式,可能有 iframe、后端渲染 import 等等方法。
但在 React 这种 SPA 的应用中,不知道有啥好的处理方式。尤其是公共的部分,是可能会包含业务逻辑,不是纯粹的“展示组件”。
即使,在不考虑与老系统的兼容情况下,只是多个React 的 SPA 整合,路由的处理之类的也还没想到很顺畅的方法。

我说的这种场景,有点类似于阿里云的管理控制台(从使用上来看,觉得相似)。阿里的管理控制台应该是 angularjs 实现。其细节不太清楚。

@sorrycc
Copy link
Owner Author

sorrycc commented Aug 31, 2016

@clarkhan 我们是把公共部分提取成 npm 包。

@clarkhan
Copy link

@sorrycc 包含业务逻辑么。比如单应用中可能用 action -> reducer 处理的部分,甚至是 ajax 等会封装到 有“业务状态和处理逻辑”的组件中?

@kpaxqin
Copy link

kpaxqin commented Aug 31, 2016

@clarkhan
这个问题可以参考Elm的架构,如果每个公共组件都分别提供reducer/action/view/model,使用者将这些碎片自由组合的话就没什么问题,elm中ajax是在Reducer中触发的,所以ajax也可以复用。这种程度的复用即使是redux-saga也做不到(因为依赖顶层的middleware)

但是,个人认为这种针对副作用的复用是非常非常极端的情况,比如ajax,即使组件拆成了对全局无依赖的碎片,ajax本身通常也会依赖到全局的token

@Pines-Cheng
Copy link

@clarkhan
随着业务越来越多,也遇到了同样的问题。感觉还是得在原有的基础上再拆一层。

@kpaxqin
Copy link

kpaxqin commented Apr 27, 2017

@sunopar @xufei

我觉得按route拆model(modal一般指模态窗吧?)是对的,elm的状态树本身就是随着组件树组合的,根结点自然就是route。redux里需要预定义state tree才引申出了怎么拆的问题。

按这个思路平级state是【不必要】存在的,每个页面都单独初始化store就行了,单独拥有自己的root reducer,这也更接近redux的模仿对象——elm的做法

至于复用,创建reducer所需要的函数本来就是可复用的,创建的过程也可以进行抽象,所以页面间逻辑复用不会有问题。

跨页面之间数据共享的需求,不应该走redux store,而应该由localstorage/api等手段来解决,因为store是存在内存中的,一刷新就没了。严格区分页面的external resource我认为是更好的实践

@zpp-gp
Copy link

zpp-gp commented Aug 21, 2017

请问dva 与mobx 区别有什么?

@JackGit
Copy link

JackGit commented Oct 19, 2017

thumbup

@tcstory
Copy link

tcstory commented Nov 7, 2017

为啥ajax库不选择superagent?

@slogeor
Copy link

slogeor commented Dec 17, 2017

各位大佬:有几个疑问。

  1. state 和 props 都能触发 render,这两者如何选择?
  2. 接口请求,封装到 action 还是单独页面,单独请求?
  3. props 传递的层级一般控制在几层比较合适

@sunyongjian
Copy link

@slogeor

  1. 看你的 state 需不需要共享了,如果要共享,状态提升肯定涉及到 props 传递,子组件就是根据 props 触发 re-render。选择肯定是看场景啊,简单的 ui 组件,自己有个 state 就够了。业务中大部分都是 smart + dumb 组件,props 多一些,尤其是引入 antd 等 ui 库。
  2. 如果这个页面所在的项目,有 redux 这样的状态管理,那数据流交给 redux 处理比较好一些,项目内一般有封装好的 fetch,saga 等方法,把 effects -> reducer -> store 。不过这个数据可能不是共享的,放 redux 唯一的 store 不太好。用 mobx 的话就容易解决这个问题了,起码比 state 好,我最不喜欢用 state 去处理 effects ,保存接口数据了。
  3. 两层。毕竟现在都是 smart + dumb,只要 store,组件拆分的好。

奥,忘了说 dva 也可以很好的解决你的问题 😆

@HuangHongRui
Copy link

项目要使用Dva...前来学习... 🐤

@suxu
Copy link

suxu commented Mar 29, 2018

😢 dva generate is disabled since we don't have enough time currently.

@xuqinggang
Copy link

xuqinggang commented May 26, 2018

各位大神们,看看下面两张图给点意见呗~~

  1. 参照MVC,设计的。
    1
    2.一个详细一点的数据流动图
    2
    下面这篇文章是有关react项目重构的思考

@sabrinaluo
Copy link

请教一下data部分如果使用了normalizr,应该如何于immutable配合呢?谢谢

@sorrycc
Copy link
Owner Author

sorrycc commented Jun 7, 2018

不要用 normalizr 了,用 dva + dva-immer 就好。

@DiamondYuan
Copy link

根据上面的流程图,view 是 通过 action 改变 data。然后data再渲染 view。
那么页面的 未加载/加载中/加载完成/请求失败 等状态放在哪里呢?
如果是使用 saga ,页面调用 action 后,除非外部传入,是不知道请求状态的。

@peterguo2017
Copy link

上面例子,saga 在前端用,使用 generator 似乎区别只是异步改为同步写法而已。

generator 最大的问题,如果是高级浏览器还好,要兼容低版本的浏览器,需要一堆转换代码,感觉不是很好。在我们的业务中,异步请求是很小的一部分操作,如果后台是自己控制,页面中的数据,基本上一次请求就都拿过来了。同样,可以在前端操作页面,最终完成后,进行一次提交,完成所有的修改。这种情况下,异步操作,用最简单的 thunk 就够了。

我觉得saga文档中login flow的例子就很好。相当于实现了简易的有限状态机来处理logIn和loginOut flow。可以少很多validation。比如在触发loginOut的时候,不必检查是否loginIn了,因为只有loginIn之后才能触发loginOut。应用中这种类似的状态转换其实挺常见的。

@shandamengcheng
Copy link

最后的图结构很清晰了!

@MoxyNJ
Copy link

MoxyNJ commented Jan 14, 2022

这两张图太赞了!

@jayguojianhai
Copy link

jayguojianhai commented Jan 14, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests