Open sorrycc opened 8 years ago
对 Roof 不感兴趣的同学可以直接从 Redux 段落读起。
下文说说我理解的支付宝前端应用架构发展史,从 roof 到 redux,再到 dva。
Roof 应该是从 0.4 开始在项目里大范围推广的。
Roof 0.4 接触不多,时间久了已经没有太多印象了,记忆中很多概念是从 baobab 里来的,通过 cursor 订阅数据,并基于此设计了很多针对复杂场景的解决方案。
这种方式灵活且强大,现在想想如果这条路一走到底,或许比现在要好一些。但由于概念比较多,当时大家都比较难理解 cursor 这类的概念。并且 redux 越来越流行。。
然后有了 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 。这在之后会继续探讨。
在经历了几个 bugfix 版本之后,Roof 0.5.5 却是个有新 feature 的更新。感觉从这个版本起已经不是原作者的本意了,而是对于用户的妥协。
这个版本引入了一个新的概念:action 。
action
这也是从 redux (或者说 flux) 里而来的,所有用户操作都可以被理解成是一个 action,这样在 Component 里就不用直接调 Controller 或者 api/service 里的接口了,一定程度上做了解耦。
createActionContainer({ myUser: 'user', }, { // 绑定 actions userActions, })(UserInfo);
这让 Roof 越来越像 redux,但由于没有引入 dispatch,在实际项目中遇到了不少坑。比较典型的是 action 之间的互相调用。
dispatch
function actionA() { actionB(); } function actionB() {}
还有 action 里更新数据之前必须重新从 state 里拉最新的进行更新之类的问题,记得当时还写过 issue 来记录踩过的坑。这是想引入 redux,但却只引入一半的结果。
然后是 Roof 0.5.6@beta,这个版本的内核已经换成了 redux,引入 reducer 和 dispatch 来解决上个版本遇到的问题。所以本质上他等同于 react-redux,看下 import 语句应该就能明白。
reducer
import
import { createStore, combineReducers } from 'redux'; import { createDispatchContainer, createRootContainer } from 'roof';
大家可能注意到这个版本有个 @beta,这也是目前 Roof 的最终版本。因为大家意识到既然已经这样了,为啥不用 redux 呢?
@beta
然后就有不少项目开始用 redux,但是 redux 是一个 library,要在团队中使用,就需要有最佳实践。那么最佳实践是什么呢?
Redux 本身是一个很轻的库,解决 component -> action -> reducer -> state 的单向数据流转问题。
按我理解,他有两个非常突出的特点是:
可预测性是由于他大量使用 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)、工具支持。
但是那么多的社区扩展,我们应该如何选才能组成我们的最佳实践? 以异步处理为例。(这也是我觉得最重要的一个问题)
用地比较多的通用解决方案有这些:
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 这套虽然已经比较不错,但仍避免不了在项目中暴露自己的问题。
文件切换问题
redux 的项目通常要分 reducer, action, saga, component 等等,我们需要在这些文件之间来回切换。并且这些文件通常是分目录存放的:
+ src + sagas - user.js + reducers - user.js + actions - user.js
所以通常我们需要在这三个 user.js 中来回切换。(真实项目中通常还有 services/user.js 等) 不知大家是否有感觉,这样的频繁切换很容易打断编码思路?
services/user.js
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 的监听都需要这么写就显得非常冗余。
entry 创建麻烦
可以看下这个 redux entry 的例子,除了 redux store 的创建,中间件的配置,路由的初始化,Provider 的 store 的绑定,saga 的初始化,还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子,看起来比较复杂。
基于上面的这些问题,我们封装了 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 一路走来,每个方案都有自己的优点和缺陷,后一个总是为了解决前一个方案的问题而生,感觉上是在逐步变好的过程中,这让我觉得踏实。
另外,感叹坚持走自己的路是件很困难的事情,尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。
good good
这是来自卢珑文的假期自动回复邮件。 您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
下文说说我理解的支付宝前端应用架构发展史,从 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 的概念。
这在一定程度上迎合了 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 里的接口了,一定程度上做了解耦。
这让 Roof 越来越像 redux,但由于没有引入
dispatch
,在实际项目中遇到了不少坑。比较典型的是 action 之间的互相调用。还有 action 里更新数据之前必须重新从 state 里拉最新的进行更新之类的问题,记得当时还写过 issue 来记录踩过的坑。这是想引入 redux,但却只引入一半的结果。
Roof 0.5.6@beta
然后是 Roof 0.5.6@beta,这个版本的内核已经换成了 redux,引入
reducer
和dispatch
来解决上个版本遇到的问题。所以本质上他等同于 react-redux,看下import
语句应该就能明白。大家可能注意到这个版本有个
@beta
,这也是目前 Roof 的最终版本。因为大家意识到既然已经这样了,为啥不用 redux 呢?Redux
然后就有不少项目开始用 redux,但是 redux 是一个 library,要在团队中使用,就需要有最佳实践。那么最佳实践是什么呢?
理解 Redux
Redux 本身是一个很轻的库,解决 component -> action -> reducer -> state 的单向数据流转问题。
按我理解,他有两个非常突出的特点是:
可预测性是由于他大量使用 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 这套虽然已经比较不错,但仍避免不了在项目中暴露自己的问题。
文件切换问题
redux 的项目通常要分 reducer, action, saga, component 等等,我们需要在这些文件之间来回切换。并且这些文件通常是分目录存放的:
所以通常我们需要在这三个 user.js 中来回切换。(真实项目中通常还有
services/user.js
等) 不知大家是否有感觉,这样的频繁切换很容易打断编码思路?saga 创建麻烦
我们在 saga 里监听一个 action 通常需要这样写:
对于 redux-saga 来说,这样设计可以让实现更灵活,但对于我们的项目而言,大部分场景只需要用到 takeEvery 和 takeLatest 就足够,每个 action 的监听都需要这么写就显得非常冗余。
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)
5 步 4 个接口完成单页应用的编码,不需要配 middleware,不需要初始化 saga runner,不需要 fork, watch saga,不需要创建 store,不需要写 createStore,然后和 Provider 绑定,等等。但却能拥有 redux + redux-saga + ... 的所有功能。
更多 dva 的详解,后面会逐步补充。
最后
从 Roof 到 Redux 再到 dva 一路走来,每个方案都有自己的优点和缺陷,后一个总是为了解决前一个方案的问题而生,感觉上是在逐步变好的过程中,这让我觉得踏实。
另外,感叹坚持走自己的路是件很困难的事情,尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。