{"action":"created","issue":{"url":"https://api.github.com/repos/sorrycc/blog/issues/6","repository_url":"https://api.github.com/repos/sorrycc/blog","labels_url":"https://api.github.com/repos/sorrycc/blog/issues/6/labels{/name}","comments_url":"https://api.github.com/repos/sorrycc/blog/issues/6/comments","events_url":"https://api.github.com/repos/sorrycc/blog/issues/6/events","html_url":"https://github.com/sorrycc/blog/issues/6","id":163501772,"number":6,"title":"支付宝前端应用架构的发展和选择","user":{"login":"sorrycc","id":35128,"avatar_url":"https://avatars.githubusercontent.com/u/35128?v=3","gravatar_id":"","url":"https://api.github.com/users/sorrycc","html_url":"https://github.com/sorrycc","followers_url":"https://api.github.com/users/sorrycc/followers","following_url":"https://api.github.com/users/sorrycc/following{/other_user}","gists_url":"https://api.github.com/users/sorrycc/gists{/gist_id}","starred_url":"https://api.github.com/users/sorrycc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sorrycc/subscriptions","organizations_url":"https://api.github.com/users/sorrycc/orgs","repos_url":"https://api.github.com/users/sorrycc/repos","events_url":"https://api.github.com/users/sorrycc/events{/privacy}","received_events_url":"https://api.github.com/users/sorrycc/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":23,"created_at":"2016-07-02T04:46:54Z","updated_at":"2016-09-21T10:11:43Z","closed_at":null,"body":"> 对 Roof 不感兴趣的同学可以直接从 Redux 段落读起。\r\n\r\n下文说说我理解的支付宝前端应用架构发展史，从 roof 到 redux，再到 [dva][dva]。\r\n\r\nRoof 应该是从 0.4 开始在项目里大范围推广的。\r\n\r\n## Roof 0.4\r\n\r\nRoof 0.4 接触不多，时间久了已经没有太多印象了，记忆中很多概念是从 [baobab][baobab] 里来的，通过 cursor 订阅数据，并基于此设计了很多针对复杂场景的解决方案。\r\n\r\n这种方式灵活且强大，现在想想如果这条路一走到底，或许比现在要好一些。但由于概念比较多，当时大家都比较难理解 cursor 这类的概念。并且 redux 越来越流行。。\r\n\r\n## Roof 0.5\r\n\r\n然后有了 Roof 0.5，提供 createRootContainer 和 createContainer，实现类似 react-redux 里 Provider 和 connect 的功能，并隐藏了 cursor 的概念。\r\n\r\n```javascript\r\n// 定义 state\r\ncreateRootContainer({\r\n  user: { name: 'chris', age: 30 }\r\n})(App);\r\n\r\n// 绑定 state\r\ncreateContainer({\r\n  myUser: 'user',\r\n})(UserInfo);\r\n```\r\n\r\n这在一定程度上迎合了 redux 用户的习惯。但 redux 用户却并不满足，就算不能用 redux，也希望能在 roof 上使用上更多 redux 相关的特性。\r\n\r\n还有个在这一阶段讨论较多的另一个问题是没有最佳实践，大家针对同一个问题通常有不同的解法。最典型的是异步请求的处理，有些人直接写从 Component 生命周期里，有些好一点的提取成 service/api，但还是在 Component 里调，还有些提取成 Controller 。\r\n\r\n这是 library 相对于 framework 的略势，Roof 本质上是一个 library，要求他去解决所有开发中能想到的问题其实是不公平的。那么如何做的? 目前看起来有两种方案，1) boilerplate 2) framework 。这在之后会继续探讨。\r\n\r\n## Roof 0.5.5\r\n\r\n在经历了几个 bugfix 版本之后，Roof 0.5.5 却是个有新 feature 的更新。感觉从这个版本起已经不是原作者的本意了，而是对于用户的妥协。\r\n\r\n这个版本引入了一个新的概念：`action` 。\r\n\r\n这也是从 redux (或者说 flux) 里而来的，所有用户操作都可以被理解成是一个 action，这样在 Component 里就不用直接调 Controller 或者 api/service 里的接口了，一定程度上做了解耦。\r\n\r\n```javascript\r\ncreateActionContainer({\r\n  myUser: 'user',\r\n}, {\r\n  // 绑定 actions\r\n  userActions,\r\n})(UserInfo);\r\n```\r\n\r\n这让 Roof 越来越像 redux，但由于没有引入 `dispatch`，在实际项目中遇到了不少坑。比较典型的是 action 之间的互相调用。\r\n\r\n```javascript\r\nfunction actionA() {\r\n  actionB();\r\n}\r\nfunction actionB() {}\r\n```\r\n\r\n还有 action 里更新数据之前必须重新从 state 里拉最新的进行更新之类的问题，记得当时还写过 issue 来记录踩过的坑。这是想引入 redux，但却只引入一半的结果。\r\n\r\n## Roof 0.5.6@beta\r\n\r\n然后是 Roof 0.5.6@beta，这个版本的内核已经换成了 redux，引入 `reducer` 和 `dispatch` 来解决上个版本遇到的问题。所以本质上他等同于 [react-redux][react-redux]，看下 `import` 语句应该就能明白。\r\n\r\n```javascript\r\nimport { createStore, combineReducers } from 'redux';\r\nimport { createDispatchContainer, createRootContainer } from 'roof';\r\n```\r\n\r\n大家可能注意到这个版本有个 `@beta`，这也是目前 Roof 的最终版本。因为大家意识到既然已经这样了，为啥不用 redux 呢? \r\n\r\n## Redux\r\n\r\n然后就有不少项目开始用 redux，但是 redux 是一个 library，要在团队中使用，就需要有最佳实践。那么最佳实践是什么呢？\r\n\r\n### 理解 Redux\r\n\r\n[Redux][redux] 本身是一个很轻的库，解决 component -> action -> reducer -> state 的单向数据流转问题。\r\n\r\n按我理解，他有两个非常突出的特点是：\r\n\r\n1. predictable，可预测性\r\n2. 可扩展性\r\n\r\n可预测性是由于他大量使用 pure function 和 plain object 等概念(reducer 和 action creator 是 pure function，state 和 action 是 plain object)，并且 state 是 immutable 的。这对于项目的稳定性会是非常好的保证。\r\n\r\n可扩展性则让我们可以通过 middleware 定制 action 的处理，通过 reducer enhancer 扩展 reducer 等等。从而有了丰富的社区扩展和支持，比如异步处理、Form、router 同步、redu/undo、性能问题(selector)、工具支持。\r\n\r\n### Library 选择\r\n\r\n但是那么多的社区扩展，我们应该如何选才能组成我们的最佳实践? 以异步处理为例。(这也是我觉得最重要的一个问题)\r\n\r\n用地比较多的通用解决方案有这些：\r\n\r\n- [redux-thunk][redux-thunk]\r\n- [redux-promise][redux-promise]\r\n- [redux-saga][redux-saga]\r\n\r\nredux-thunk 是支持函数形式的 action，这样在 action 里就可以 dispatch 其他的 action 了。这是最简单应该也是用地最广的方案吧，对于简单项目应该是够的。\r\n\r\nredux-promise 和上面的类似，支持 promise 形式的 action，这样 action 里就可以通过看似同步的方式来组织代码。\r\n\r\n但 thunk 和 promise 都有的问题是，他们改变了 action 的含义，使得 action 变得不那么纯粹了。\r\n\r\n然后出现的 redux-saga 让我眼前一亮，具体不多说了，可以看他的文档。总之给我的感觉是优雅而强大，通过他可以把所有的业务逻辑都放到 saga 里，这样可以让 reducer, action 和 component 都很纯粹，干他们原本需要干的事情。\r\n\r\n所以在异步处理这一环节，我们选择了 [redux-saga][redux-saga]。\r\n\r\n最终通过一系列的选择，我们形成了[基于 redux 的最佳实践][redux-best-practice]。\r\n\r\n### 新的问题\r\n\r\n但就像之前所有的 Roof 版本一样，每个时代的应用架构都有自己的问题。Redux 这套虽然已经比较不错，但仍避免不了在项目中暴露自己的问题。\r\n    \r\n1. 文件切换问题\r\n\r\n  redux 的项目通常要分 reducer, action, saga, component 等等，我们需要在这些文件之间来回切换。并且这些文件通常是分目录存放的：\r\n  \r\n  ```\r\n  + src\r\n    + sagas\r\n      - user.js\r\n    + reducers\r\n      - user.js\r\n    + actions\r\n      - user.js\r\n  ```\r\n  \r\n  所以通常我们需要在这三个 user.js 中来回切换。(真实项目中通常还有 `services/user.js` 等) 不知大家是否有感觉，这样的频繁切换很容易打断编码思路？\r\n\r\n1. saga 创建麻烦\r\n\r\n  我们在 saga 里监听一个 action 通常需要这样写：\r\n  \r\n  ```javascript\r\n  function *userCreate() {\r\n    try {\r\n      // Your logic here\r\n    } catch(e) {}\r\n  }\r\n  function *userCreateWatcher() {\r\n    takeEvery('user/create', userCreate);\r\n  }\r\n  function *rootSaga() {\r\n    yield fork(userCreateWatcher);\r\n  }\r\n  ```\r\n  \r\n  对于 redux-saga 来说，这样设计可以让实现更灵活，但对于我们的项目而言，大部分场景只需要用到 takeEvery 和 takeLatest 就足够，每个 action 的监听都需要这么写就显得非常冗余。\r\n\r\n1. entry 创建麻烦\r\n\r\n  可以看下这个 [redux entry][redux-entry] 的例子，除了 redux store 的创建，中间件的配置，路由的初始化，Provider 的 store 的绑定，saga 的初始化，还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子，看起来比较复杂。\r\n\r\n## dva\r\n\r\n基于上面的这些问题，我们封装了 [dva][dva] 。dva 是基于 [redux 最佳实践][redux-best-practice] 实现的 framework，api 参考了 choo，概念来自于 elm 。详见 [dva 简介][dva-intro]。\r\n\r\n并且除了上面这些问题，dva 还能解决 domain model 组织和团队协作的问题。\r\n\r\n来看个简单的例子：(这个例子没有异步逻辑，所以并没有包含 effects 和 subscriptions 的使用，感兴趣的可以看 [Popular Products 的 Demo][popular-products])\r\n\r\n```javascript\r\nimport React from 'react';\r\nimport dva, { connect } from 'dva';\r\nimport { Route } from 'dva/router';\r\n\r\n// 1. Initialize\r\nconst app = dva();\r\n\r\n// 2. Model\r\napp.model({\r\n  namespace: 'count',\r\n  state: 0,\r\n  reducers: {\r\n    ['count/add'  ](count) { return count + 1 },\r\n    ['count/minus'](count) { return count - 1 },\r\n  },\r\n});\r\n\r\n// 3. View\r\nconst App = connect(({ count }) => ({\r\n  count\r\n}))(function(props) {\r\n  return (\r\n    <div>\r\n      <h2>{ props.count }</h2>\r\n      <button key=\"add\" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>\r\n      <button key=\"minus\" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>\r\n    </div>\r\n  );\r\n});\r\n\r\n// 4. Router\r\napp.router(\r\n  <Route path=\"/\" component={App} />\r\n);\r\n\r\n// 5. Start\r\napp.start(document.getElementById('root'));\r\n```\r\n\r\n5 步 4 个接口完成单页应用的编码，不需要配 middleware，不需要初始化 saga runner，不需要 fork, watch saga，不需要创建 store，不需要写 createStore，然后和 Provider 绑定，等等。但却能拥有 [redux + redux-saga + ...](redux-best-practice) 的所有功能。\r\n\r\n更多 dva 的详解，后面会逐步补充。\r\n\r\n## 最后\r\n\r\n从 Roof 到 Redux 再到 dva 一路走来，每个方案都有自己的优点和缺陷，后一个总是为了解决前一个方案的问题而生，感觉上是在逐步变好的过程中，这让我觉得踏实。\r\n\r\n另外，感叹坚持走自己的路是件很困难的事情，尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。\r\n\r\n[baobab]: https://github.com/Yomguithereal/baobab\r\n[dva]: https://github.com/sorrycc/dva\r\n[react-redux]: https://github.com/rackt/react-redux\r\n[redux-best-practice]: https://github.com/sorrycc/blog/issues/1\r\n[redux]: http://redux.js.org/\r\n[redux-thunk]: https://github.com/gaearon/redux-thunk\r\n[redux-promise]: https://github.com/acdlite/redux-promise\r\n[redux-saga]: https://github.com/yelouafi/redux-saga\r\n[dva-intro]: https://github.com/sorrycc/dva/issues/1\r\n[redux-entry]: https://github.com/ant-design/antd-init/blob/master/boilerplates/redux/src/entries/index.js\r\n[popular-products]: https://github.com/sorrycc/dva/blob/master/examples/popular-products\r\n"},"comment":{"url":"https://api.github.com/repos/sorrycc/blog/issues/comments/248568832","html_url":"https://github.com/sorrycc/blog/issues/6#issuecomment-248568832","issue_url":"https://api.github.com/repos/sorrycc/blog/issues/6","id":248568832,"user":{"login":"sorrycc","id":35128,"avatar_url":"https://avatars.githubusercontent.com/u/35128?v=3","gravatar_id":"","url":"https://api.github.com/users/sorrycc","html_url":"https://github.com/sorrycc","followers_url":"https://api.github.com/users/sorrycc/followers","following_url":"https://api.github.com/users/sorrycc/following{/other_user}","gists_url":"https://api.github.com/users/sorrycc/gists{/gist_id}","starred_url":"https://api.github.com/users/sorrycc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sorrycc/subscriptions","organizations_url":"https://api.github.com/users/sorrycc/orgs","repos_url":"https://api.github.com/users/sorrycc/repos","events_url":"https://api.github.com/users/sorrycc/events{/privacy}","received_events_url":"https://api.github.com/users/sorrycc/received_events","type":"User","site_admin":false},"created_at":"2016-09-21T10:11:43Z","updated_at":"2016-09-21T10:11:43Z","body":"@u0x01 https://github.com/sorrycc/dva-example-react-native"}}
{"actor":{"display_login":"sorrycc"}}