Skip to content

支付宝前端应用架构的发展和选择 #6

@sorrycc

Description

@sorrycc
Owner

对 Roof 不感兴趣的同学可以直接从 Redux 段落读起。

下文说说我理解的支付宝前端应用架构发展史,从 roof 到 redux,再到 dva

Roof 应该是从 0.4 开始在项目里大范围推广的。

Roof 0.4

Roof 0.4 接触不多,时间久了已经没有太多印象了,记忆中很多概念是从 baobab 里来的,通过 cursor 订阅数据,并基于此设计了很多针对复杂场景的解决方案。

这种方式灵活且强大,现在想想如果这条路一走到底,或许比现在要好一些。但由于概念比较多,当时大家都比较难理解 cursor 这类的概念。并且 redux 越来越流行。。

Roof 0.5

然后有了 Roof 0.5,提供 createRootContainer 和 createContainer,实现类似 react-redux 里 Provider 和 connect 的功能,并隐藏了 cursor 的概念。

// 定义 state
createRootContainer({
  user: { name: 'chris', age: 30 }
})(App);

// 绑定 state
createContainer({
  myUser: 'user',
})(UserInfo);

这在一定程度上迎合了 redux 用户的习惯。但 redux 用户却并不满足,就算不能用 redux,也希望能在 roof 上使用上更多 redux 相关的特性。

还有个在这一阶段讨论较多的另一个问题是没有最佳实践,大家针对同一个问题通常有不同的解法。最典型的是异步请求的处理,有些人直接写从 Component 生命周期里,有些好一点的提取成 service/api,但还是在 Component 里调,还有些提取成 Controller 。

这是 library 相对于 framework 的略势,Roof 本质上是一个 library,要求他去解决所有开发中能想到的问题其实是不公平的。那么如何做的? 目前看起来有两种方案,1) boilerplate 2) framework 。这在之后会继续探讨。

Roof 0.5.5

在经历了几个 bugfix 版本之后,Roof 0.5.5 却是个有新 feature 的更新。感觉从这个版本起已经不是原作者的本意了,而是对于用户的妥协。

这个版本引入了一个新的概念:action

这也是从 redux (或者说 flux) 里而来的,所有用户操作都可以被理解成是一个 action,这样在 Component 里就不用直接调 Controller 或者 api/service 里的接口了,一定程度上做了解耦。

createActionContainer({
  myUser: 'user',
}, {
  // 绑定 actions
  userActions,
})(UserInfo);

这让 Roof 越来越像 redux,但由于没有引入 dispatch,在实际项目中遇到了不少坑。比较典型的是 action 之间的互相调用。

function actionA() {
  actionB();
}
function actionB() {}

还有 action 里更新数据之前必须重新从 state 里拉最新的进行更新之类的问题,记得当时还写过 issue 来记录踩过的坑。这是想引入 redux,但却只引入一半的结果。

Roof 0.5.6@beta

然后是 Roof 0.5.6@beta,这个版本的内核已经换成了 redux,引入 reducerdispatch 来解决上个版本遇到的问题。所以本质上他等同于 react-redux,看下 import 语句应该就能明白。

import { createStore, combineReducers } from 'redux';
import { createDispatchContainer, createRootContainer } from 'roof';

大家可能注意到这个版本有个 @beta,这也是目前 Roof 的最终版本。因为大家意识到既然已经这样了,为啥不用 redux 呢?

Redux

然后就有不少项目开始用 redux,但是 redux 是一个 library,要在团队中使用,就需要有最佳实践。那么最佳实践是什么呢?

理解 Redux

Redux 本身是一个很轻的库,解决 component -> action -> reducer -> state 的单向数据流转问题。

按我理解,他有两个非常突出的特点是:

  1. predictable,可预测性
  2. 可扩展性

可预测性是由于他大量使用 pure function 和 plain object 等概念(reducer 和 action creator 是 pure function,state 和 action 是 plain object),并且 state 是 immutable 的。这对于项目的稳定性会是非常好的保证。

可扩展性则让我们可以通过 middleware 定制 action 的处理,通过 reducer enhancer 扩展 reducer 等等。从而有了丰富的社区扩展和支持,比如异步处理、Form、router 同步、redu/undo、性能问题(selector)、工具支持。

Library 选择

但是那么多的社区扩展,我们应该如何选才能组成我们的最佳实践? 以异步处理为例。(这也是我觉得最重要的一个问题)

用地比较多的通用解决方案有这些:

redux-thunk 是支持函数形式的 action,这样在 action 里就可以 dispatch 其他的 action 了。这是最简单应该也是用地最广的方案吧,对于简单项目应该是够的。

redux-promise 和上面的类似,支持 promise 形式的 action,这样 action 里就可以通过看似同步的方式来组织代码。

但 thunk 和 promise 都有的问题是,他们改变了 action 的含义,使得 action 变得不那么纯粹了。

然后出现的 redux-saga 让我眼前一亮,具体不多说了,可以看他的文档。总之给我的感觉是优雅而强大,通过他可以把所有的业务逻辑都放到 saga 里,这样可以让 reducer, action 和 component 都很纯粹,干他们原本需要干的事情。

所以在异步处理这一环节,我们选择了 redux-saga

最终通过一系列的选择,我们形成了基于 redux 的最佳实践

新的问题

但就像之前所有的 Roof 版本一样,每个时代的应用架构都有自己的问题。Redux 这套虽然已经比较不错,但仍避免不了在项目中暴露自己的问题。

  1. 文件切换问题

    redux 的项目通常要分 reducer, action, saga, component 等等,我们需要在这些文件之间来回切换。并且这些文件通常是分目录存放的:

    + src
      + sagas
        - user.js
      + reducers
        - user.js
      + actions
        - user.js
    

    所以通常我们需要在这三个 user.js 中来回切换。(真实项目中通常还有 services/user.js 等) 不知大家是否有感觉,这样的频繁切换很容易打断编码思路?

  2. saga 创建麻烦

    我们在 saga 里监听一个 action 通常需要这样写:

    function *userCreate() {
      try {
        // Your logic here
      } catch(e) {}
    }
    function *userCreateWatcher() {
      takeEvery('user/create', userCreate);
    }
    function *rootSaga() {
      yield fork(userCreateWatcher);
    }

    对于 redux-saga 来说,这样设计可以让实现更灵活,但对于我们的项目而言,大部分场景只需要用到 takeEvery 和 takeLatest 就足够,每个 action 的监听都需要这么写就显得非常冗余。

  3. entry 创建麻烦

    可以看下这个 redux entry 的例子,除了 redux store 的创建,中间件的配置,路由的初始化,Provider 的 store 的绑定,saga 的初始化,还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子,看起来比较复杂。

dva

基于上面的这些问题,我们封装了 dva 。dva 是基于 redux 最佳实践 实现的 framework,api 参考了 choo,概念来自于 elm 。详见 dva 简介

并且除了上面这些问题,dva 还能解决 domain model 组织和团队协作的问题。

来看个简单的例子:(这个例子没有异步逻辑,所以并没有包含 effects 和 subscriptions 的使用,感兴趣的可以看 Popular Products 的 Demo)

import React from 'react';
import dva, { connect } from 'dva';
import { Route } from 'dva/router';

// 1. Initialize
const app = dva();

// 2. Model
app.model({
  namespace: 'count',
  state: 0,
  reducers: {
    ['count/add'  ](count) { return count + 1 },
    ['count/minus'](count) { return count - 1 },
  },
});

// 3. View
const App = connect(({ count }) => ({
  count
}))(function(props) {
  return (
    <div>
      <h2>{ props.count }</h2>
      <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
      <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
    </div>
  );
});

// 4. Router
app.router(
  <Route path="/" component={App} />
);

// 5. Start
app.start(document.getElementById('root'));

5 步 4 个接口完成单页应用的编码,不需要配 middleware,不需要初始化 saga runner,不需要 fork, watch saga,不需要创建 store,不需要写 createStore,然后和 Provider 绑定,等等。但却能拥有 redux + redux-saga + ... 的所有功能。

更多 dva 的详解,后面会逐步补充。

最后

从 Roof 到 Redux 再到 dva 一路走来,每个方案都有自己的优点和缺陷,后一个总是为了解决前一个方案的问题而生,感觉上是在逐步变好的过程中,这让我觉得踏实。

另外,感叹坚持走自己的路是件很困难的事情,尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。

Activity

jaredleechn

jaredleechn commented on Jul 2, 2016

@jaredleechn

另外,感叹坚持走自己的路是件很困难的事情,尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。

👏

soda-x

soda-x commented on Jul 2, 2016

@soda-x

我来点赞的

bobodeng

bobodeng commented on Jul 2, 2016

@bobodeng

好文点赞

codering

codering commented on Jul 3, 2016

@codering

感谢分享

SMbey0nd

SMbey0nd commented on Jul 4, 2016

@SMbey0nd

点赞

dqaria

dqaria commented on Jul 4, 2016

@dqaria

ziluo

ziluo commented on Jul 5, 2016

@ziluo

赞啊

concefly

concefly commented on Jul 5, 2016

@concefly

wsw

wsw commented on Jul 5, 2016

@wsw

厉害

ghost

ghost commented on Jul 7, 2016

@ghost

这也太imba了!

fengzhu1131

fengzhu1131 commented on Jul 7, 2016

@fengzhu1131

值得学习,一步一步的前进

dont-see-big-shark

dont-see-big-shark commented on Jul 11, 2016

@dont-see-big-shark

d.v.a都来了,怎么跟redux其他的插件redux-form配合

sorrycc

sorrycc commented on Jul 11, 2016

@sorrycc
OwnerAuthor

@wee911 目前还不行,正式发布会支持,通过配置额外的 reducers 。详见:dvajs/dva#7

dont-see-big-shark

dont-see-big-shark commented on Jul 11, 2016

@dont-see-big-shark

@sorrycc 不错,支持

86 remaining items

nanfs

nanfs commented on Feb 1, 2019

@nanfs

在异步处理上面 当时没有考虑redux-observable吗 为什么没有采用这个呢

cllemon

cllemon commented on Mar 9, 2019

@cllemon

文章质量高

xiaobinwu

xiaobinwu commented on May 28, 2019

@xiaobinwu

学习了

running-snail-sfs

running-snail-sfs commented on Jul 9, 2019

@running-snail-sfs

大牛就是大牛

snowman

snowman commented on Aug 15, 2019

@snowman

Do not send useless comments

Felix-Indoing

Felix-Indoing commented on Nov 27, 2019

@Felix-Indoing

请问能否实现async redux store的方式异步加载reducers?

mvpdream

mvpdream commented on Nov 27, 2019

@mvpdream

现在用redux的多还是用dva的多呢?

Felix-Indoing

Felix-Indoing commented on Nov 27, 2019

@Felix-Indoing

现在是redux用的比较多

mvpdream

mvpdream commented on Nov 27, 2019

@mvpdream

现在是redux用的比较多

为什么不用dva呢? 还有个mbox..

lulongwen

lulongwen commented on Jun 26, 2021

@lulongwen

dva2.x的主要缺点

  1. react-router,不支持 hooks,hooks重构项目要注意;不是最新的 react-router-dom5.x
  • hooks memo 和dva connect使用 bug
  • hooks 不能使用 useHistory
  1. history路由报错
  2. less报错,less版本必须是2.x, less-loader必须是 4.x,否则报 less编译的错误
lulongwen

lulongwen commented on Jun 26, 2021

@lulongwen

和 dva类似的框架 rematch,model写法和 dva类似
Rematch数据流
https://www.yuque.com/lulongwen/react/xbggz1

rematch的异步用的是 async & await,这点比 yield写起来更流畅

DayDayUpDYP

DayDayUpDYP commented on Feb 27, 2024

@DayDayUpDYP

good good

lulongwen

lulongwen commented on Feb 27, 2024

@lulongwen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @sorrycc@c2pig@ianchanning@SMbey0nd@nick3

        Issue actions

          支付宝前端应用架构的发展和选择 · Issue #6 · sorrycc/blog