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

最终

(完)

JackGit commented 7 years ago

thumbup

tcstory commented 7 years ago

为啥ajax库不选择superagent?

slogeor commented 6 years ago

各位大佬:有几个疑问。

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

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

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

suxu commented 6 years ago

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

xuqinggang commented 6 years ago

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

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

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

sorrycc commented 6 years ago

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

DiamondYuan commented 6 years ago

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

peterguo2017 commented 4 years ago

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

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

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

shandamengcheng commented 3 years ago

最后的图结构很清晰了!

MoxyNJ commented 2 years ago

这两张图太赞了!

jayguojianhai commented 2 years ago

这是来自QQ邮箱的假期自动回复邮件。   您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。