sorrycc / blog

💡
4.48k stars 324 forks source link

React + Redux 最佳实践 #1

Open sorrycc opened 8 years ago

sorrycc commented 8 years ago

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

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

在 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' });
}

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

异步请求。

方案

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 commented 8 years ago

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

xujihui1985 commented 8 years ago

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

unclay commented 8 years ago

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

AllenFang commented 8 years ago

Nice introduction!

ziluo commented 8 years ago

赞,急需

sskyy commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

superRaytin commented 8 years ago

:+1:

fengmk2 commented 8 years ago

看着 saga 就觉得好熟悉。

shepherdwind commented 8 years ago

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

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

soda-x commented 8 years ago

看成了 Soga 😄

Justin-lu commented 8 years ago

赞,正在学习~

cyy0523xc commented 8 years ago

赞!

mengxingshike2012 commented 8 years ago

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

jerexyz commented 8 years ago

👍,正在学习

rendongsc commented 8 years ago

好文!

oConnerCooper commented 8 years ago

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

sorrycc commented 8 years ago

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

oConnerCooper commented 8 years ago

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

carlos121493 commented 8 years ago

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

sorrycc commented 8 years ago

@carlos121493 https://github.com/dvajs/dva/blob/3438810b8a6eabe9d11eaf7bb62405a07c0ac6f5/test/app.model-test.js#L64-L75

clarkhan commented 8 years ago

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

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

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

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

sorrycc commented 8 years ago

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

clarkhan commented 8 years ago

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

kpaxqin commented 8 years ago

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

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

sorrycc commented 8 years ago

@clarkhan 我们是基于 dva 来做的,公共的部分包含 component 和 model,ajax(effect) 和 reducer 都封到一起。

@kpaxqin 可以看下 dva,和 elm 参考了很多概念过来。

clarkhan commented 8 years ago

@sorrycc 一直在关注。等着 1.0 release 会在部分项目尝试使用。不过其中的 saga 我们之前没有在用,可能会是迁移的一个障碍?

总之很期待。加油!

kpaxqin commented 8 years ago

@sorrycc 我的意思是,即使是elm也很难较好地处理这类情况 elm分形的基础是组件没有对全局的依赖。而事实上涉及到副作用,对全局的依赖(比如token)是不可避免的。

sorrycc commented 8 years ago

全局依赖基于约定? 比如 token 就约定从 cookie 里获取。

kpaxqin commented 8 years ago

@sorrycc 工程上可以这么做,有异步行为的通常是针对一定业务领域的复用了,使用范围有限定的话加这些约定没啥问题。

但从架构的角度这种约定其实还是比较弱的,这也是我对elm架构的一个concern,所谓的完全分形在实际场景下是受限制的。

还有个concern是leaf节点问题,同样的问题cycle.js也有,虽然他们宣称是分形架构,但最终的叶子节点(比如一个div)还是基于属性 + 事件、不可分形的。

这种差异会带来分形的边界问题,比如cycle的作者最近就支持Web Component,因为基于某个架构创建组件的成本更高、通用性更低(只能适配一个架构),而基于WC的组件大家都可以用。理论上答案可以概括为"无业务的通用组件做成WC,有业务的按项目架构来",实际工程中这个边界的把握就完全看团队水平了。

不过上面这个concern是针对elm/cycle类强制分形流的,react/redux本来就不用管这个

qiaolb commented 8 years ago

好文,学习

ascoders commented 7 years ago

redux-saga 的替代方案:redux-observable

原因:

  1. 同样解决了异步 action 处理。
  2. 其实是利用了 rxjs 对数据流处理的优势,解决了交互产生的数据流复杂处理的痛点,顺便解决了异步问题。

劣势:

  1. star 比 redux-saga 少,因为推出的晚,加上 rxjs 的学习成本,可能不会成为主流,但不代表不好用。
magicdawn commented 7 years ago

关于 redux-saga

可以看出,调整之后的代码有几个优点: 所有业务代码都存于 saga 中,不再散落在各处 全同步执行,就算逻辑再复杂,看起来也不会乱

全同步执行

  1. 首先 callback hell 是不好的, 然后不一定要用 generator 啊, async/await/promise 方案也不错

看下 redux thunk 的新版介绍

Any return value from the inner function will be available as the return value of dispatch itself. This is convenient for orchestrating an asynchronous control flow with thunk action creators dispatching each other and returning Promises to wait for each other’s completion:

就是说一个 action creator 返回一个 async function 的话, dispatch 这个 async function 的返回值是一个 Promise, 又可以 await 了

拿 redux-saga readme 例子来说 https://redux-saga.github.io/redux-saga/index.html

export function USER_FETCH_REQUESTED(){
  return async (dispatch, getState) => {
    try{
      const user = await Api.fetchUser(action.payload.userId)
    } catch(e) {
      dispatch({type: "USER_FETCH_FAILED", message: e.message})
      return
    }

    dispatch({type: "USER_FETCH_SUCCEEDED", user: user})
  }
}

// 然后在 View里 
diapatch(USER_FETCH_REQUESTED())

所有业务代码都存于 saga 中

redux-thunk + promise 这个业务也都在 action creator 中, 而且显式的 dispatch 一个 thunk, 比 redux-saga 使用 takeEvery/takeLatest 将一个操作与 action type 关联的做法要好, 俨然一个黑箱. 如果是引用的话编辑器或IDE可以直接点击跳过去

redux-saga takeEvery / takeLatest / 数据相关的可能比较好用...但文中这两点不那么有力... 对于公司项目, 我还是倾向简单一些, 有时候入口 action creator 都不写了, 直接写在 event handler 里

btw Vuex 的 action 也是可以使用 async/await. 方便多了

tangkunyin commented 7 years ago

guozimo commented 7 years ago

云谦师兄我现在也在阿里的技术栈中 在使用dva 但是现在遇到很多不懂得问题 想请教你

albafica2015 commented 7 years ago

您好,我在使用dva roadhog,现在每次build都是index.css index.js 请问怎么能自动加上版本号 比如 index323232.css index232323.js 谢谢。

sorrycc commented 7 years ago

@albafica2015 还不支持,请关注这个 issue,https://github.com/sorrycc/roadhog/issues/69

albafica2015 commented 7 years ago
        好的,非常感谢,我去roadhog文件包里面找到了webpack.prod.js,里面把js css加上了[hash],但是生成的html没有自动换路径。发自网易邮箱大师
        在2017年03月08日 08:17,chencheng (云谦) 写道: @albafica2015 还不支持,请关注这个 issue,sorrycc/roadhog#69

—You are receiving this because you were mentioned.Reply to this email directly, view it on GitHub, or mute the thread.

{"api_version":"1.0","publisher":{"api_key":"05dde50f1d1a384dd78767c55493e4bb","name":"GitHub"},"entity":{"external_key":"github/sorrycc/blog","title":"sorrycc/blog","subtitle":"GitHub repository","main_image_url":"https://cloud.githubusercontent.com/assets/143418/17495839/a5054eac-5d88-11e6-95fc-7290892c7bb5.png","avatar_image_url":"https://cloud.githubusercontent.com/assets/143418/15842166/7c72db34-2c0b-11e6-9aed-b52498112777.png","action":{"name":"Open in GitHub","url":"https://github.com/sorrycc/blog"}},"updates":{"snippets":[{"icon":"PERSON","message":"@sorrycc in #1: @albafica2015 还不支持,请关注这个 issue,https://github.com/sorrycc/roadhog/issues/69"}],"action":{"name":"View Issue","url":"https://github.com/sorrycc/blog/issues/1#issuecomment-284904380"}}}

sorrycc commented 7 years ago

@albafica2015 html 需要通过 htmlwebpackplugin 生成,现在是直接 copy 的。

guozimo commented 7 years ago

云谦师兄 dva的思想不好懂啊

sunopar commented 7 years ago

我想问一下,如果我的项目有一千多个页面,那么我该以什么粒度去区分modal?我觉得modal以route页面来区分,可能开发过程modal会拆分的比较好,但是在redux的store下面会存一千多个平级的state;而如果以一类相似对route页面来区分,大概有两百多个modal,感觉这样的话每一个modal会略复杂,多个route共用一个modal。

xufei commented 7 years ago

@sunOpar 我觉得平级state不是问题,本身你这些页面之间没什么关系的话,最好是分开。如果要做一定程度的复用,可以从业务实体的角度出发,但最好还是跟state隔离开

xiemeilong commented 7 years ago

请问考虑过mobxjs吗

Zane-XZ commented 7 years ago

搞c++的路过,原来我不会看代码。。。。

djyde commented 7 years ago

@xiemeilong 我用 MobX 实现了一套 https://github.com/djyde/cans

1gehunzi commented 7 years ago

大神,借宝地一用。 想请教一下:在项目中使用了redux,知道智能组件和展示组件的区别。可不可以在智能组件中使用react原始的this.state来控制自身内容展示和隐藏。即操作this.setState()的方法。

Pines-Cheng commented 7 years ago

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

kpaxqin commented 7 years ago

@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 commented 7 years ago

请问dva 与mobx 区别有什么?