[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2.1\nexecutors:\n  node:\n    docker:\n      - image: circleci/node:10.13-browsers\n    working_directory: ~/dva\n\nenvironment:\n  NODE_ENV: test\n  NODE_OPTIONS: --max_old_space_size=4096\n  NPM_CONFIG_LOGLEVEL: error\n  JOBS: max # https://gist.github.com/ralphtheninja/f7c45bdee00784b41fed\n\njobs:\n  yarn_build:\n    executor: node\n    steps:\n      - checkout\n      - run: yarn install\n      - run: yarn bootstrap\n      - run: yarn build\n      - run:\n          command: yarn test -- --forceExit --detectOpenHandles --runInBand --maxWorkers=2\n          no_output_timeout: 300m\n      - run: bash <(curl -s https://codecov.io/bash)\n  cnpm_build:\n    executor: node\n    steps:\n      - checkout\n      - run: sudo npm install -g cnpm\n      - run: cnpm install --registry=https://registry.npmjs.org\n      - run: cnpm run bootstrap -- --npm-client=cnpm\n      - run: cnpm run build\n      - run:\n          command: npm run test -- --forceExit --detectOpenHandles --runInBand --maxWorkers=2\n          no_output_timeout: 300m\n      - run: bash <(curl -s https://codecov.io/bash)\nworkflows:\n  version: 2\n  build-test:\n    jobs:\n      - yarn_build:\n          filters:\n            branches:\n              ignore:\n                - gh-pages\n                - /release\\/.*/\n      - cnpm_build:\n          filters:\n            branches:\n              ignore:\n                - gh-pages\n                - /release\\/.*/\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules/\nlib/\nes/\ndist/\npackages/dva-example/\npackages/dva-example-react-router-3/\npackages/dva-example-nextjs//\npackages/dva-example-user-dashboard/\npackages/dva/*.js\npackages/dva-react-router-3/*.js\npackages/dva-no-router/*.js\nscripts/\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"plugins\": [\n    \"prettier\"\n  ],\n  \"extends\": [\n    \"airbnb\",\n    \"prettier\"\n  ],\n  \"env\": {\n    \"browser\": true,\n    \"jest\": true\n  },\n  \"rules\": {\n    \"prettier/prettier\": \"error\",\n    \"jsx-a11y/href-no-hash\": [\n      0\n    ],\n    \"jsx-a11y/click-events-have-key-events\": [\n      0\n    ],\n    \"jsx-a11y/anchor-is-valid\": [\n      \"error\",\n      {\n        \"components\": [\n          \"Link\"\n        ],\n        \"specialLink\": [\n          \"to\"\n        ]\n      }\n    ],\n    \"generator-star-spacing\": [\n      0\n    ],\n    \"consistent-return\": [\n      0\n    ],\n    \"radix\": [\n      1\n    ],\n    \"react/react-in-jsx-scope\": [\n      0\n    ],\n    \"react/forbid-prop-types\": [\n      0\n    ],\n    \"react/jsx-filename-extension\": [\n      1,\n      {\n        \"extensions\": [\n          \".js\"\n        ]\n      }\n    ],\n    \"global-require\": [\n      0\n    ],\n    \"import/prefer-default-export\": [\n      0\n    ],\n    \"react/jsx-no-bind\": [\n      0\n    ],\n    \"react/prop-types\": [\n      0\n    ],\n    \"react/prefer-stateless-function\": [\n      0\n    ],\n    \"react/jsx-one-expression-per-line\": [\n      0\n    ],\n    \"react/button-has-type\": [\n      0\n    ],\n    \"no-else-return\": [\n      0\n    ],\n    \"no-restricted-syntax\": [\n      0\n    ],\n    \"import/no-extraneous-dependencies\": [\n      0\n    ],\n    \"no-use-before-define\": [\n      0\n    ],\n    \"jsx-a11y/no-static-element-interactions\": [\n      0\n    ],\n    \"no-nested-ternary\": [\n      0\n    ],\n    \"arrow-body-style\": [\n      0\n    ],\n    \"import/extensions\": [\n      0\n    ],\n    \"no-bitwise\": [\n      0\n    ],\n    \"no-cond-assign\": [\n      0\n    ],\n    \"import/no-unresolved\": [\n      0\n    ],\n    \"require-yield\": [\n      1\n    ],\n    \"no-param-reassign\": [\n      0\n    ],\n    \"no-shadow\": [\n      0\n    ],\n    \"no-underscore-dangle\": [\n      0\n    ],\n    \"spaced-comment\": [\n      0\n    ],\n    \"indent\": [\n      0\n    ],\n    \"quotes\": [\n      0\n    ],\n    \"func-names\": [\n      0\n    ],\n    \"arrow-parens\": [\n      0\n    ],\n    \"space-before-function-paren\": [\n      0\n    ],\n    \"no-useless-escape\": [\n      0\n    ],\n    \"object-curly-newline\": [\n      0\n    ],\n    \"function-paren-newline\": [\n      0\n    ],\n    \"class-methods-use-this\": [\n      0\n    ],\n    \"no-new\": [\n      0\n    ],\n    \"import/newline-after-import\": [\n      0\n    ],\n    \"no-console\": [\n      0\n    ]\n  },\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"experimentalObjectRestSpread\": true\n    }\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: 'Bug report'\nabout: 'Report a bug to help us improve'\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## What happens?\nA clear and concise description of what the bug is.\n\n## Mini Showcase Repository(REQUIRED)\n\n> Provide a mini GitHub repository which can reproduce the issue.\n> Use `yarn create umi`, select `app`, choose `dva`, then upload to your GitHub\n\n<!-- https://github.com/YOUR_REPOSITORY_URL -->\n\n## How To Reproduce\n**Steps to reproduce the behavior:**\n1.\n2.\n\n**Expected behavior**\n1.\n2.\n\n## Context\n\n- **Dva Version**:\n- **Node Version**:\n- **Platform**:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_cn.md",
    "content": "---\nname: '缺陷问题反馈'\nabout: '反馈问题以帮助我们改进'\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!--\n感谢您向我们反馈问题，为了高效的解决问题，我们期望你能提供以下信息：\n-->\n\n## What happens?\n\n<!-- 清晰的描述下遇到的问题。-->\n\n## 最小可复现仓库\n\n> 请使用 `yarn create umi` 创建，选择 `app`，然后选上 `dva`，并上传到你的 GitHub 仓库\n\n<!-- https://github.com/YOUR_REPOSITORY_URL -->\n\n## 复现步骤，错误日志以及相关配置\n\n<!-- 请提供复现步骤，错误日志以及相关配置 -->\n<!-- 可以尝试不要锁版本，重新安装依赖试试先 -->\n\n\n## 相关环境信息\n\n- **Umi 版本**：\n- **Node 版本**：\n- **操作系统**：\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: 'Feature request'\nabout: 'Suggest an idea for this project'\ntitle: '[Feature Request] say something'\nlabels: ''\nassignees: ''\n\n---\n\n## Background\n\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n## Proposal\n\nDescribe the solution you'd like, better to provide some pseudo code.\n\n## Additional context\n\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/rfc_cn.md",
    "content": "---\nname: 'RFC Proposals'\nabout: 'Provide a solution for this project'\ntitle: '[RFC] say something'\nlabels: 'type: proposals'\nassignees: ''\n\n---\n\n## 背景\n\n> 描述你希望解决的问题的现状，附上相关的 issue 地址\n\n## 思路\n\n> 描述大概的解决思路，可以包含 API 设计和伪代码等\n\n## 跟进\n\n- [ ] some task\n- [ ] PR URL\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nThank you for your pull request. Please review below requirements.\nBug fixes and new features should include tests.\nContributors guide: https://github.com/dvajs/dva/blob/master/CONTRIBUTING.md\n\n感谢您贡献代码。请确认下列 checklist 的完成情况。\nBug 修复和新功能必须包含测试。\nContributors guide: https://github.com/dvajs/dva/blob/master/CONTRIBUTING.md\n-->\n\n##### Checklist\n\n<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->\n\n- [ ] `npm test` passes\n- [ ] tests are included\n- [ ] documentation is changed or added\n- [ ] commit message follows commit guidelines\n\n\n##### Description of change\n\n<!-- Provide a description of the change below this comment. -->\n\n- any feature?\n- close https://github.com/dvajs/dva/ISSUE_URL\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking an issue as stale\nstaleLabel: wontfix\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Node CI\n\non: [push]\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        node_version: [10.x, 12.x]\n        os: [ubuntu-latest]\n    steps:\n    - uses: actions/checkout@v1\n    - name: Use Node.js ${{ matrix.node_version }}\n      uses: actions/setup-node@v1\n      with:\n        node-version: ${{ matrix.node_version }}\n    - run: npm install\n    - run: npm run bootstrap\n    - run: npm run build\n    - run: npm run test -- --forceExit\n      env:\n        CI: true\n        HEADLESS: false\n        PROGRESS: none\n        NODE_ENV: test\n        NODE_OPTIONS: --max_old_space_size=4096\n\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/coverage\n/.changelog\n/examples/**/.umi\n/examples/**/.umi-production\n/website/dist\n/node_modules\n/packages/**/node_modules\n/packages/**/dist\n/lerna-debug.log\n\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016-present ChenCheng (sorrycc@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "English | [简体中文](./README_zh-CN.md)\n\n# dva\n\n[![codecov](https://codecov.io/gh/dvajs/dva/branch/master/graph/badge.svg)](https://codecov.io/gh/dvajs/dva)\n[![CircleCI](https://circleci.com/gh/dvajs/dva.svg?style=svg)](https://circleci.com/gh/dvajs/dva)\n[![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva)\n[![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva)\n[![Join the chat at https://gitter.im/dvajs/Lobby](https://img.shields.io/gitter/room/dvajs/Lobby.svg?style=flat)](https://gitter.im/dvajs/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link)\n\nLightweight front-end framework based on [redux](https://github.com/reactjs/redux), [redux-saga](https://github.com/redux-saga/redux-saga) and [react-router](https://github.com/ReactTraining/react-router). (Inspired by [elm](http://elm-lang.org/) and [choo](https://github.com/yoshuawuyts/choo))\n\n---\n\n## Features\n\n* **Easy to learn, easy to use**: only 6 apis, very friendly to redux users, and **API reduce to 0 when [use with umi](https://umijs.org/guide/with-dva.html)**\n* **Elm concepts**: organize models with `reducers`, `effects` and `subscriptions`\n* **Support HMR**: support HMR for components, routes and models with [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr)\n* **Plugin system**: e.g. we have [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) plugin to handle loading state automatically\n\n## Demos\n\n* [Count](https://stackblitz.com/edit/dva-example-count): Simple count example\n* [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): User management dashboard\n* [AntDesign Pro](https://github.com/ant-design/ant-design-pro)：([Demo](https://preview.pro.ant.design/))，out-of-box UI solution for enterprise applications\n* [HackerNews](https://github.com/dvajs/dva-hackernews):  ([Demo](https://dvajs.github.io/dva-hackernews/))，HackerNews Clone\n* [antd-admin](https://github.com/zuiidea/antd-admin): ([Demo](http://antd-admin.zuiidea.com/))，A admin dashboard application demo built upon Ant Design and Dva.js\n* [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f))，Github star management application\n* [Account System](https://github.com/yvanwangl/AccountSystem.git): A small inventory management system\n* [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): react-native example integrated dva and react-navigation\n\n## Quick Start\n\nSee the [docs directory](./docs) for guides and API references.\n\n## FAQ\n\n### Why is it called dva?\n\n> D.Va’s mech is nimble and powerful — its twin Fusion Cannons blast away with autofire at short range, and she can use its Boosters to barrel over enemies and obstacles, or deflect attacks with her projectile-dismantling Defense Matrix.\n\n—— From [OverWatch](http://ow.blizzard.cn/heroes/dva)\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/psagSCVHOKQVqqNjjMdf.jpg\" width=\"200\" height=\"200\" />\n\n### Is it production ready?\n\nSure! We have 1000+ projects using dva in Alibaba.\n\n### Does it support IE8?\n\nNo.\n\n## Next\n\nSome basic articles.\n\n* The [8 Concepts](https://github.com/dvajs/dva/blob/master/docs/Concepts.md), and know how they are connected together\n* [dva APIs](https://github.com/dvajs/dva/blob/master/docs/API.md)\n* Checkout [dva knowledgemap](https://github.com/dvajs/dva-knowledgemap), including all the basic knowledge with ES6, React, dva\n* Checkout [more FAQ](https://github.com/dvajs/dva/issues?q=is%3Aissue+is%3Aclosed+label%3Afaq)\n* If your project is created by [dva-cli](https://github.com/dvajs/dva-cli), checkout how to [Configure it](https://github.com/sorrycc/roadhog#configuration)\n\nWant more?\n\n* 看看 dva 的前身 [React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1)，知道 dva 是怎么来的\n* 在 gitc 分享 dva 的 PPT ：[React 应用框架在蚂蚁金服的实践](http://slides.com/sorrycc/dva)\n* 如果还在用 dva@1.x，请尽快 [升级到 2.x](https://github.com/sorrycc/blog/issues/48)\n\n## Community\n\n| Slack Group                                                  | Github Issue                                            | 钉钉群                                                       | 微信群                                                       |\n| ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | <img src=\"https://gw.alipayobjects.com/zos/rmsportal/jPXcQOlGLnylGMfrKdBz.jpg\" width=\"60\" /> | <img src=\"https://img.alicdn.com/tfs/TB13U6aF6DpK1RjSZFrXXa78VXa-752-974.jpg\" width=\"60\" /> |\n\n## License\n\n[MIT](https://tldrlegal.com/license/mit-license)\n"
  },
  {
    "path": "README_zh-CN.md",
    "content": "[English](./README.md) | 简体中文\n\n# dva\n\n[![codecov](https://codecov.io/gh/dvajs/dva/branch/master/graph/badge.svg)](https://codecov.io/gh/dvajs/dva)\n[![CircleCI](https://circleci.com/gh/dvajs/dva.svg?style=svg)](https://circleci.com/gh/dvajs/dva)\n[![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva)\n[![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva)\n[![Join the chat at https://gitter.im/dvajs/Lobby](https://img.shields.io/gitter/room/dvajs/Lobby.svg?style=flat)](https://gitter.im/dvajs/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link)\n\n基于 [redux](https://github.com/reactjs/redux)、[redux-saga](https://github.com/redux-saga/redux-saga) 和 [react-router](https://github.com/ReactTraining/react-router) 的轻量级前端框架。(Inspired by [elm](http://elm-lang.org/) and [choo](https://github.com/yoshuawuyts/choo))\n\n---\n\n## 特性\n\n* **易学易用**，仅有 6 个 api，对 redux 用户尤其友好，**[配合 umi 使用](https://umijs.org/guide/with-dva.html)后更是降低为 0 API**\n* **elm 概念**，通过 reducers, effects 和 subscriptions 组织 model\n* **插件机制**，比如 [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) 可以自动处理 loading 状态，不用一遍遍地写 showLoading 和 hideLoading\n* **支持 HMR**，基于 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 实现 components、routes 和 models 的 HMR\n\n## 快速上手\n请参考 [docs 目录](./docs) 获取指南和 API 参考。\n\n## 他是怎么来的？\n\n* [Why dva and what's dva](https://github.com/dvajs/dva/issues/1)\n* [支付宝前端应用架构的发展和选择](https://www.github.com/sorrycc/blog/issues/6)\n\n## 例子\n\n* [Count](https://stackblitz.com/edit/dva-example-count): 简单计数器\n* [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): 用户管理\n* [AntDesign Pro](https://github.com/ant-design/ant-design-pro)：([Demo](https://preview.pro.ant.design/))，开箱即用的中台前端/设计解决方案\n* [HackerNews](https://github.com/dvajs/dva-hackernews):  ([Demo](https://dvajs.github.io/dva-hackernews/))，HackerNews Clone\n* [antd-admin](https://github.com/zuiidea/antd-admin): ([Demo](http://antd-admin.zuiidea.com/))，基于 antd 和 dva 的后台管理应用\n* [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f))，Github Star 管理应用\n* [Account System](https://github.com/yvanwangl/AccountSystem.git): 小型库存管理系统\n* [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): 集成了 dva 和 react-navigation 典型应用场景的 React Native 实例\n\n## FAQ\n\n### 命名由来？\n\n> D.Va拥有一部强大的机甲，它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。\n\n—— 来自 [守望先锋](http://ow.blizzard.cn/heroes/overwatch-dva) 。\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/psagSCVHOKQVqqNjjMdf.jpg\" width=\"200\" height=\"200\" />\n\n### 是否可用于生产环境？\n\n当然！公司内用于生产环境的项目估计已经有 1000+ 。\n\n### 是否支持 IE8 ？\n\n不支持。\n\n## 下一步\n\n以下能帮你更好地理解和使用 dva ：\n\n* 理解 dva 的 [8 个概念](./docs/Concepts.md) ，以及他们是如何串起来的\n* 掌握 dva 的[所有 API](./docs/API.md)\n* 查看 [dva 知识地图](./docs/knowledgemap/README.md) ，包含 ES6, React, dva 等所有基础知识\n* 查看 [更多 FAQ](https://github.com/dvajs/dva/issues?q=is%3Aissue+is%3Aclosed+label%3Afaq)，看看别人通常会遇到什么问题\n* 如果你基于 dva-cli 创建项目，最好了解他的 [配置方式](https://github.com/sorrycc/roadhog/blob/master/README_zh-cn.md#配置)\n\n还要了解更多?\n\n* 看看 dva 的前身 [React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1)，知道 dva 是怎么来的\n* 在 gitc 分享 dva 的 PPT ：[React 应用框架在蚂蚁金服的实践](http://slides.com/sorrycc/dva)\n* 如果还在用 dva@1.x，请尽快 [升级到 2.x](https://github.com/sorrycc/blog/issues/48)\n\n## 社区\n\n| Slack Group                                                  | Github Issue                                            | 钉钉群                                                       | 微信群                                                       |\n| ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | <img src=\"https://gw.alipayobjects.com/zos/rmsportal/jPXcQOlGLnylGMfrKdBz.jpg\" width=\"60\" /> | <img src=\"https://img.alicdn.com/tfs/TB13U6aF6DpK1RjSZFrXXa78VXa-752-974.jpg\" width=\"60\" /> |\n\n## License\n\n[MIT](https://tldrlegal.com/license/mit-license)\n"
  },
  {
    "path": "docs/.vuepress/config.js",
    "content": "module.exports = {\n  title: 'DvaJS',\n  description: 'React and redux based, lightweight and elm-style framework.',\n  themeConfig: {\n    repo: 'dvajs/dva',\n    lastUpdated: 'Last Updated',\n    editLinks: true,\n    editLinkText: '在 GitHub 上编辑此页',\n    docsDir: 'docs',\n    nav: [\n      { text: '指南', link: '/guide/' },\n      { text: 'API', link: '/api/' },\n      { text: '知识地图', link: '/knowledgemap/' },\n      { text: '发布日志', link: 'https://github.com/dvajs/dva/releases' },\n    ],\n    sidebar: {\n      '/guide/': [\n        {\n          title: '指南',\n          collapsable: false,\n          children: [\n            '',\n            'getting-started',\n            'examples-and-boilerplates',\n            'concepts',\n            'introduce-class',\n          ],\n        },\n        {\n          title: '社区',\n          collapsable: false,\n          children: ['fig-show', 'develop-complex-spa', 'source-code-explore'],\n        },\n      ],\n      '/api/': [''],\n      '/knowledgemap/': [''],\n    },\n  },\n};\n"
  },
  {
    "path": "docs/.vuepress/override.styl",
    "content": "$accentColor = #fc54c3\n$textColor = #2c3e50\n$borderColor = #eaecef\n$codeBgColor = #282c34\n"
  },
  {
    "path": "docs/API.md",
    "content": "# API\n\n## Export Files\n### dva\n\nDefault export file.\n\n### dva/router\n\nExport the api of [react-router@4.x](https://github.com/ReactTraining/react-router), and also export [react-router-redux](https://github.com/reactjs/react-router-redux) with the `routerRedux` key.\n\ne.g.\n\n```js\nimport { Router, Route, routerRedux } from 'dva/router';\n```\n\n### dva/fetch\n\nAsync request library, export the api of [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch). It's just for convenience, you can choose other libraries for free.\n\n### dva/saga\n\nExport the api of [redux-saga](https://github.com/yelouafi/redux-saga).\n\n### dva/dynamic\n\nUtil method to load React Component and dva model dynamically.\n\ne.g.\n\n```js\nimport dynamic from 'dva/dynamic';\n\nconst UserPageComponent = dynamic({\n  app,\n  models: () => [\n    import('./models/users'),\n  ],\n  component: () => import('./routes/UserPage'),\n});\n```\n\n`opts` include:\n\n* app: dva instance\n* models: function which return promise, and the promise return dva model\n* component：function which return promise, and the promise return React Component\n\n## dva API\n### `app = dva(opts)`\n\nCreate app, and return dva instance. (Notice: dva support multiple instances.)\n\n`opts` includes:\n\n* `history`: Specify the history for router, default `hashHistory`\n* `initialState`: Specify the initial state, default `{}`, it's priority is higher then model state\n\ne.g. use `browserHistory`:\n\n```js\nimport createHistory from 'history/createBrowserHistory';\nconst app = dva({\n  history: createHistory(),\n});\n```\n\nBesides, for convenience, we can configure [hooks](#appusehooks) in `opts`, like this:\n\n```js\nconst app = dva({\n  history,\n  initialState,\n  onError,\n  onAction,\n  onStateChange,\n  onReducer,\n  onEffect,\n  onHmr,\n  extraReducers,\n  extraEnhancers,\n});\n```\n\n### `app.use(hooks)`\n\nSpecify hooks or register plugin. (Plugin return hooks finally.)\n\ne.g. register [dva-loading](https://github.com/dvajs/dva-loading) plugin:\n\n```js\nimport createLoading from 'dva-loading';\n...\napp.use(createLoading(opts));\n```\n\n`hooks` includes:\n\n#### `onError((err, dispatch) => {})`\n\nTriggered when `effect` has error or `subscription` throw error with `done`. Used for managing global error.\n\nNotice: `subscription`'s error must be throw with the send argument `done`. e.g.\n\n```js\napp.model({\n  subscriptions: {\n    setup({ dispatch }, done) {\n      done(e);\n    },\n  },\n});\n```\n\nIf we are using antd, the most simple error handle would be like this:\n\n```js\nimport { message } from 'antd';\nconst app = dva({\n  onError(e) {\n    message.error(e.message, /* duration */3);\n  },\n});\n```\n\n#### `onAction(fn | fn[])`\n\nTriggered when action is dispatched. Used for register redux middleware.\n\ne.g. use [redux-logger](https://github.com/evgenyrodionov/redux-logger) to log actions:\n\n```js\nimport createLogger from 'redux-logger';\nconst app = dva({\n  onAction: createLogger(opts),\n});\n```\n\n#### `onStateChange(fn)`\n\nTriggered when `state` changes. Used for sync `state` to localStorage or server and so on.\n\n#### `onReducer(fn)`\n\nWrap reducer execute.\n\ne.g. use [redux-undo](https://github.com/omnidan/redux-undo) to implement redo/undo:\n\n```js\nimport undoable from 'redux-undo';\nconst app = dva({\n  onReducer: reducer => {\n    return (state, action) => {\n      const undoOpts = {};\n      const newState = undoable(reducer, undoOpts)(state, action);\n      // 由于 dva 同步了 routing 数据，所以需要把这部分还原\n      return { ...newState, routing: newState.present.routing };\n    },\n  },\n});\n```\n\n#### `onEffect(fn)`\n\nWrap effect execute.\n\ne.g. [dva-loading](https://github.com/dvajs/dva-loading) has implement auto loading state with this hook.\n\n#### `onHmr(fn)`\n\nHMR(Hot Module Replacement) related, currently used in [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr).\n\n#### `extraReducers`\n\nSpecify extra reducers.\n\ne.g. [redux-form](https://github.com/erikras/redux-form) needs extra `form` reducer:\n\n```js\nimport { reducer as formReducer } from 'redux-form'\nconst app = dva({\n  extraReducers: {\n    form: formReducer,\n  },\n});\n```\n\n#### `extraEnhancers`\n\nSpecify extra [StoreEnhancer](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer)s.\n\ne.g. use dva with [redux-persist](https://github.com/rt2zz/redux-persist):\n\n```js\nimport { persistStore, autoRehydrate } from 'redux-persist';\nconst app = dva({\n  extraEnhancers: [autoRehydrate()],\n});\npersistStore(app._store);\n```\n\n### `app.model(model)`\n\nRegister model, view [#Model](#model)  for details.\n\n### `app.unmodel(namespace)`\n\nUnregister model.\n\n### `app.replaceModel(model)`\n\n> Only available after `app.start()` got called\n\nReplace an existing model with a new one, comparing by the namespace. If no one matches, add the new one. \n\nAfter called, old `reducers`, `effects`, `subscription` will be replaced with the new ones, while original state is kept, which means it's useful for HMR.\n\n### `app.router(({ history, app }) => RouterConfig)`\n\nRegister router config.\n\ne.g.\n\n```js\nimport { Router, Route } from 'dva/router';\napp.router(({ history }) => {\n  return (\n    <Router history={history}>\n      <Route path=\"/\" component={App} />\n    <Router>\n  );\n});\n```\n\nRecommend using separate file to config router. Then we can do hmr with [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr). e.g.\n\n```js\napp.router(require('./router'));\n```\n\nBesides, if don't need router, like multiple-page application, react-native, we can pass in a function which return JSX Element. e.g.\n\n```js\napp.router(() => <App />);\n```\n\n### `app.start(selector?)`\n\nStart application. `selector` is optionally, if no `selector`, it will return a function which return JSX element.\n\n```js\napp.start('#root');\n```\n\ne.g. implement i18n with react-intl:\n\n```js\nimport { IntlProvider } from 'react-intl';\n...\nconst App = app.start();\nReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement);\n```\n\n## Model\nmodel is the most important concept in dva.\n\ne.g.\n\n```js\napp.model({\n  namespace: 'todo',\n\tstate: [],\n  reducers: {\n    add(state, { payload: todo }) {\n      // Save data to state\n      return [...state, todo];\n    },\n  },\n  effects: {\n    *save({ payload: todo }, { put, call }) {\n      // Call saveTodoToServer, then trigger `add` action to save data\n      yield call(saveTodoToServer, todo);\n      yield put({ type: 'add', payload: todo });\n    },\n  },\n  subscriptions: {\n    setup({ history, dispatch }) {\n      // Subscribe history(url) change, trigger `load` action if pathname is `/`\n      return history.listen(({ pathname }) => {\n        if (pathname === '/') {\n          dispatch({ type: 'load' });\n        }\n      });\n    },\n  },\n});\n```\n\nmodel includes 5 properties:\n\n### namespace\n\nmodel's namespace.\n\n### state\n\nmodels's initial state, it's priority is lower than `opts.initialState` in `dva()`.\n\ne.g.\n\n```\nconst app = dva({\n  initialState: { count: 1 },\n});\napp.model({\n  namespace: 'count',\n  state: 0,\n});\n```\n\nThen, state.count is 1 after `app.start()`.\n\n### reducers\n\nStore reducers in key/value Object. reducer is the only place to modify `state`. Triggered by `action`.\n\n`(state, action) => newState` or `[(state, action) => newState, enhancer]`\n\nView https://github.com/dvajs/dva/blob/master/packages/dva-core/test/reducers.test.js for details.\n\n### effects\n\nStore effects in key/value Object. Used for do async operations and biz logic, don't modify `state` directly. Triggered by `action`, could trigger new `action`, communicate with server, select data from global `state` and so on.\n\n`*(action, effects) => void` or `[*(action, effects) => void, { type }]`。\n\ntype includes:\n\n* `takeEvery`\n* `takeLatest`\n* `throttle`\n* `watcher`\n* `poll`\n\nView https://github.com/dvajs/dva/blob/master/packages/dva-core/test/effects.test.js for details.\n\n### subscriptions\n\nStore subscriptions in key/value Object. Subscription is used for subscribing data source, then trigger action by need. It's executed when `app.start()`.\n\n`({ dispatch, history }, done) => unlistenFunction`\n\nNotice: if we want to unregister a model with `app.unmodel()` or `app.replaceModel()`, it's subscriptions must return unsubscribe method.\n"
  },
  {
    "path": "docs/Concepts.md",
    "content": "# Concepts\n\n## Data Flow\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/PPrerEAKbIoDZYr.png\" width=\"807\" />\n\n## Models\n\n### State\n\n`type State = any`\n\nThe state tree of your models. Usually, the state is a JavaScript object (although technically it can be any type) which is immutable data.\n\nIn dva, you can access top state tree data by `_store`.\n\n```javascript\nconst app = dva();\nconsole.log(app._store); // top state\n```\n\n### Action\n\n`type AsyncAction = any`\n\nJust like Redux's Action, in dva, action is a plain object that represents an intention to change the state. Actions are the only way to get data into the store. Any data, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions.action. (PS: dispatch is realized through props by connecting components.)\n\n```javascript\ndispatch({\n  type: 'add',\n});\n```\n\n### dispatch function\n\n`type dispatch = (a: Action) => Action`\n\nA dispatching function (or simply dispatch function) is a function that accepts an action or an async action; it then may or may not dispatch one or more actions to the store.\n\nDispatching function is a function for triggering action, action is the only way to change state, but it just describes an action. while dispatch can be regarded as a way to trigger this action, and Reducer is to describe how to change state.\n\n```javascript\ndispatch({\n  type: 'user/add', // if in model outside, need to add namespace\n  payload: {},\n});\n```\n\n### Reducer\n\n`type Reducer<S, A> = (state: S, action: A) => S`\n\nJust like Redux's Reducer, a reducer (also called a reducing function) is a function that accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of values down to a single value.\n\nReducer's concepts from FP:\n\n```javascript\n[{x:1},{y:2},{z:3}].reduce(function(prev, next){\n    return Object.assign({}, prev, next);\n})\n//return {x:1, y:2, z:3}\n```\n\nIn dva, reducers accumulate current model's state. There are some things need to be notice that reducer must be [pure function](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md) and every calculated data must be [immutable data](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#reasonable).\n\n### Effect\n\nIn dva, we use [redux-sagas](https://redux-saga.js.org/) to control asynchronous flow.\nYou can learn more in [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide).\n\nIn our applications, the most well-known side effect is asynchronous operation, it comes from the conception of functional programing, it is called side effect because it makes our function impure, and the same input may not result in the same output.\n\n### Subscription\n\nSubscriptions is a way to get data from source, it is come from elm.\n\nData source can be: the current time, the websocket connection of server, keyboard input, geolocation change, history router change, etc..\n\n```javascript\nimport key from 'keymaster';\n...\napp.model({\n  namespace: 'count',\n  subscriptions: {\n    keyEvent(dispatch) {\n      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });\n    },\n  }\n});\n```\n\n## Router\n\nHereby router usually means frontend router. Because our current app is single page app, frontend codes are required to control the router logics. Through History API provided by the browser, we can monitor the change of the browser's url, so as to control the router.\n\ndva provide `router` function to control router, based on [react-router](https://github.com/reactjs/react-router)。\n\n```javascript\nimport { Router, Route } from 'dva/router';\napp.router(({history}) =>\n  <Router history={history}>\n    <Route path=\"/\" component={HomePage} />\n  </Router>\n);\n```\n\n## Route Components\n\nIn dva, we restrict container components to route components, because we use page dimension to design container components.\n\ntherefore, almost all connected model components are route components, route components in `/routes/` directory, presentational Components in `/components/` directory.\n\n## References\n- [redux docs](http://redux.js.org/docs/Glossary.html)\n- [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide)\n- [choo docs](https://github.com/yoshuawuyts/choo)\n- [elm](http://elm-lang.org/blog/farewell-to-frp)\n"
  },
  {
    "path": "docs/GettingStarted.md",
    "content": "# Getting Started\n\n> This article will lead you to create [dva](https://github.com/dvajs/dva) app quickly, and learn all new concepts.\n\nFinal App.\n\n<p align=\"center\">\n  <img src=\"https://zos.alipayobjects.com/rmsportal/iYdclktvqzUHGBe.gif\" />\n</p>\n\nThis app is used to test click speed, by collecting click count within 1 second.\n\nSome questions you may ask:\n\n1. How to create the app?\n2. How to organize the code after creating the app?\n3. How to build, deploy and publish after development?\n\nAnd somethings about code organization.\n\n1. How to write the Component?\n1. How to write CSS?\n1. How to write the Model?\n1. How to connect the Model and the Component?\n1. How to update State after user interaction?\n1. How to handle async logic?\n1. How to config the router?\n\nAlso:\n\n1. If I want to use localStorage to save Highest Record, what do I have to do?\n2. If we want to support keyboard click rate tests, what do I have to do?\n\nWe can takes these questions to read this article. But don't worry, all the code we need is about 70 lines.\n\n## Install dva-cli\n\ndva-cli is the cli tool for dva, include `init`, `new`.\n\n```bash\n$ npm install -g dva-cli\n```\n\nAfter installed, you can check the version with `dva -v`, and view help info with `dva -h`.\n\n## Create new App\n\nAfter installing dva-cli, we can create a new app with it, called `myapp`.\n\n```bash\n$ dva new myapp --demo\n```\n\nNotice: the `--demo` option is only used for creating demo level apps. If you want to create a normal project, don't add this option.\n\n`cd` myapp, and start it.\n\n```bash\n$ cd myapp\n$ npm start\n```\n\nAfter a few seconds, you will get the following output:\n\n```bash\nCompiled successfully!\n\nThe app is running at:\n\n  http://localhost:8000/\n\nNote that the development build is not optimized.\nTo create a production build, use npm run build.\n```\n\n(Press `Ctrl-C` if you want to close server)\n\nOpen http://localhost:8000/ in browser. If successful, you will see a page with \"Hello Dva\".\n\n## Define models\n\nWhen you get the task, you should not write code immediately. But it is recommended to do state design in `god mode`.\n\n1. design models\n2. design components\n3. connect models and components\n\nWith this task, we define the model as this:\n\n```javascript\napp.model({\n  namespace: 'count',\n  state: {\n    record : 0,\n    current: 0,\n  },\n});\n```\n\n`namespace` is the key where model state is in global state. `state` is the default data for the model. Then `record` presents `highest record`，and `current` presents current click speed.\n\n## Write components\n\nAfter designing the model, we begin to write the component. We recommend organizing the Component with [stateless functions](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions) because we don't need state almost at all in dva architecture.\n\n```javascript\nimport styles from './index.less';\nconst CountApp = ({count, dispatch}) => {\n  return (\n    <div className={styles.normal}>\n      <div className={styles.record}>Highest Record: {count.record}</div>\n      <div className={styles.current}>{count.current}</div>\n      <div className={styles.button}>\n        <button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>\n      </div>\n    </div>\n  );\n};\n```\n\nNotice:\n\n1. `import styles from './index.less';`, and then use `styles.xxx` to define css classname is the solution of [css-modules](https://github.com/css-modules/css-modules)\n2. Two props are passed in，`count` and `dispatch`. `count` is the state in the model, which is bound with [connect](https://redux.js.org/docs/api/bindActionCreators.html). `dispatch` is used to trigger an action.\n3. `dispatch({type: 'count/add'})` means trigger an action `{type: 'count/add'}`. View [Actions@redux.js.org](http://redux.js.org/basics/Actions.html) on what an action is.\n\n## Update state\n\n`reducer` is the only one that can update state.  This makes our app stable, as all data modifications are traceable. `reducer` is a pure function, and accepts two arguments - `state` and an `action`, and returns a new `state`.\n\n```javascript\n(state, action) => newState\n```\n\nWe need two reducers, `add` and `minus`. Please note that `add` will only be recorded if it is the highest.\n\n> Note: `add` and `minus` don't need to add namespace prefixes in `count` model. But if it is outside the model, the action must prefix the namespace separated with `/`. e.g. `count/add`.\n\n```diff\napp.model({\n  namespace: 'count',\n  state: {\n    record: 0,\n    current: 0,\n  },\n+ reducers: {\n+   add(state) {\n+     const newCurrent = state.current + 1;\n+     return { ...state,\n+       record: newCurrent > state.record ? newCurrent : state.record,\n+       current: newCurrent,\n+     };\n+   },\n+   minus(state) {\n+     return { ...state, current: state.current - 1};\n+   },\n+ },\n});\n```\n\nNote:\n\n1. Confused with the `...` [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator) operator? It's used to extend an Object, similar to `Object.extend`\n2. `add(state) {}` is equal to `add: function(state) {}`\n\n## Bind Data\n\n> Remember `count` and `dispatch` props used in the Component before? Where do they come from?\n\nAfter defining Model and Component, we need to connect them together. After connecting them, the Component can use the data from Model, and Model can receive actions dispatched from Component.\n\nIn this task, we only need to bind `count` .\n\n```javascript\nfunction mapStateToProps(state) {\n  return { count: state.count };\n}\nconst HomePage = connect(mapStateToProps)(CountApp);\n```\n\nNote: `connect` is from [react-redux](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)。\n\n## Define Router\n\n> Which Component should be rendered after receiving a url? It's defined by the router.\n\nThis app has only one page, so we don't need to modify the router part.\n\n```javascript\napp.router(({history}) =>\n  <Router history={history}>\n    <Route path=\"/\" component={HomePage} />\n  </Router>\n);\n```\n\nNote:\n\n1. `history` is default hashHistory with `_k` params. It can be changed to browserHistory, or remove `_k` params with extra configuration.\n\nRefresh page in browser, if success, you will see page below.\n\n<p align=\"center\">\n  <img src=\"https://zos.alipayobjects.com/rmsportal/EJWakirNlogUSSU.gif\" />\n</p>\n\n## Add StyleSheet\n\nWe define stylesheet in `css modules`, which doesn't have many differences from normal css. Because we have already hooked up the className in Component, at this moment, we only need to replace `index.less` with the follow content:\n\n```css\n.normal {\n  width: 200px;\n  margin: 100px auto;\n  padding: 20px;\n  border: 1px solid #ccc;\n  box-shadow: 0 0 20px #ccc;\n}\n\n.record {\n  border-bottom: 1px solid #ccc;\n  padding-bottom: 8px;\n  color: #ccc;\n}\n\n.current {\n  text-align: center;\n  font-size: 40px;\n  padding: 40px 0;\n}\n\n.button {\n  text-align: center;\n  button {\n    width: 100px;\n    height: 40px;\n    background: #aaa;\n    color: #fff;\n  }\n}\n```\n\nResult.\n\n<p align=\"center\">\n  <img width=\"270\" src=\"https://zos.alipayobjects.com/rmsportal/oMiYwVUzcIAgLei.png\" />\n</p>\n\n## Async Logic\n\nPrior to this, all of our operations were synchronous. When clicking on the + button, the value is incremented by 1.\n\nNow we have to deal with async logic. dva processes side effects ( async logic ) with effects on model, which is executed based on [redux-saga](https://github.com/redux-saga/redux-saga), with generator syntax. [MDN documentation for generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators).\n\nIn this app, when the user clicks the + button, the value will increase by 1 and trigger a side effect, that is, subtract (decrease by) 1 after 1 second.\n\n```diff\napp.model({\n  namespace: 'count',\n+ effects: {\n+   *addThenMinus(action, { call, put }) {\n+     yield put({ type: 'add' });\n+     yield call(delay, 1000);\n+     yield put({ type: 'minus' });\n+   },\n+ },\n...\n+function delay(timeout){\n+  return new Promise(resolve => {\n+    setTimeout(resolve, timeout);\n+  });\n+}\n```\n\n```diff\nimport styles from './index.less';\nconst CountApp = ({count, dispatch}) => {\n  return (\n    <div className={styles.normal}>\n      <div className={styles.record}>Highest Record: {count.record}</div>\n      <div className={styles.current}>{count.current}</div>\n      <div className={styles.button}>\n-       <button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>\n+       <button onClick={() => { dispatch({type: 'count/addThenMinus'}); }}>+</button>\n      </div>\n    </div>\n  );\n};\n```\n\nNote:\n\n1. `*add() {}` is equal to `add: function*(){}`\n2. `call` and `put` are effect commands from redux-saga. `call` is for async logic, and `put` is for dispatching actions. Besides these, there are commands such as `select`, `take`, `fork`, `cancel`, and so on. View more on [redux-saga documatation](http://redux-saga.github.io/redux-saga/docs/basics/DeclarativeEffects.html)\n\nRefresh you browser, and if successful, it should have all the effects of the beginning gif.\n\n## Subscribe Keyboard Event\n\n> After implementing the mouse click speed test, how do you implement the keyboard click speed test?\n\nThere is a concept called `Subscription` from dva, which is from [elm 0.17](http://elm-lang.org/blog/farewell-to-frp).\n\nSubscription is used to subscribe to a data source, then dispatch an action if needed. The data source could be current time, a websocket connection from server, a keyboard input, a geolocation change, a history router change, etc.\n\nThe subscription takes place in the model.\n\n```diff\n+import key from 'keymaster';\n...\napp.model({\n  namespace: 'count',\n+ subscriptions: {\n+   keyboardWatcher({ dispatch }) {\n+     key('⌘+up, ctrl+up', () => { dispatch({type:'addThenMinus'}) });\n+   },\n+ },\n});\n```\n\nHere, we don't need to install the `keymaster` dependency manually. When we write `import key from 'keymaster';` and save, dva-cli will install `keymaster` and save to `package.json`. The output looks like this:\n\n```bash\nuse npm: tnpm\nInstalling `keymaster`...\n[keymaster@*] installed at node_modules/.npminstall/keymaster/1.6.2/keymaster (1 packages, use 745ms, speed 24.06kB/s, json 2.98kB, tarball 15.08kB)\nAll packages installed (1 packages installed from npm registry, use 755ms, speed 23.93kB/s, json 1(2.98kB), tarball 15.08kB)\n📦  2/2 build modules\nwebpack: bundle build is now finished.\n```\n\n## All Code Together\n\nindex.js\n\n```javascript\nimport dva, { connect } from 'dva';\nimport { Router, Route } from 'dva/router';\nimport React from 'react';\nimport styles from './index.less';\nimport key from 'keymaster';\n\nconst app = dva();\n\napp.model({\n  namespace: 'count',\n  state: {\n    record: 0,\n    current: 0,\n  },\n  reducers: {\n    add(state) {\n      const newCurrent = state.current + 1;\n      return { ...state,\n        record: newCurrent > state.record ? newCurrent : state.record,\n        current: newCurrent,\n      };\n    },\n    minus(state) {\n      return { ...state, current: state.current - 1};\n    },\n  },\n  effects: {\n    *addThenMinus(action, { call, put }) {\n      yield put({ type: 'add' });\n      yield call(delay, 1000);\n      yield put({ type: 'minus' });\n    },\n  },\n  subscriptions: {\n    keyboardWatcher({ dispatch }) {\n      key('⌘+up, ctrl+up', () => { dispatch({type:'addThenMinus'}) });\n    },\n  },\n});\n\nconst CountApp = ({count, dispatch}) => {\n  return (\n    <div className={styles.normal}>\n      <div className={styles.record}>Highest Record: {count.record}</div>\n      <div className={styles.current}>{count.current}</div>\n      <div className={styles.button}>\n        <button onClick={() => { dispatch({type: 'count/addThenMinus'}); }}>+</button>\n      </div>\n    </div>\n  );\n};\n\nfunction mapStateToProps(state) {\n  return { count: state.count };\n}\nconst HomePage = connect(mapStateToProps)(CountApp);\n\napp.router(({history}) =>\n  <Router history={history}>\n    <Route path=\"/\" component={HomePage} />\n  </Router>\n);\n\napp.start('#root');\n\n\n// ---------\n// Helpers\n\nfunction delay(timeout){\n  return new Promise(resolve => {\n    setTimeout(resolve, timeout);\n  });\n}\n```\n\n## Build\n\nNow that we've written our application and verified that it works in development, it's time to get it ready to deploy to our users. To do so, run the following command:\n\n```bash\n$ npm run build\n```\n\nOutput.\n\n```bash\n> @ build /private/tmp/dva-quickstart\n> atool-build\n\nChild\n    Time: 6891ms\n        Asset       Size  Chunks             Chunk Names\n    common.js    1.18 kB       0  [emitted]  common\n     index.js     281 kB    1, 0  [emitted]  index\n    index.css  353 bytes    1, 0  [emitted]  index\n```\n\nAfter build success, you can find compiled files in `dist` directory.\n\n## What's Next\n\nAfter completing this app, do you have answers to all of the questions in the beginning? Do you understand the concepts in dva, like `model`, `router`, `reducers`, `effects` and `subscriptions` ?\n\nNext, you can view [dva official library](https://github.com/dvajs/dva) for more information.\n"
  },
  {
    "path": "docs/README.md",
    "content": "---\nhome: true\nactionText: 快速上手 →\nactionLink: /guide/\nfeatures:\n- title: 易学易用\n  details: 仅有 6 个 api，对 redux 用户尤其友好，配合 umi 使用后更是降低为 0 API\n- title: elm 概念\n  details: 通过 reducers, effects 和 subscriptions 组织 model，简化 redux 和 redux-saga 引入的概念\n- title: 插件机制\n  details: 比如 dva-loading 可以自动处理 loading 状态，不用一遍遍地写 showLoading 和 hideLoading\nfooter: MIT Licensed | Copyright © 2017-present\n---\n\n## 社区\n\n| Slack Group                                                  | Github Issue                                            | 钉钉群                                                       | 微信群                                                       |\n| ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | <img src=\"https://gw.alipayobjects.com/zos/rmsportal/jPXcQOlGLnylGMfrKdBz.jpg\" width=\"60\" /> | <img src=\"https://img.alicdn.com/tfs/TB13U6aF6DpK1RjSZFrXXa78VXa-752-974.jpg\" width=\"60\" /> |\n[https://t.me/joinchat/G0DdHw9tDZC-_NmdKY2jYg](https://t.me/joinchat/G0DdHw9tDZC-_NmdKY2jYg)\n"
  },
  {
    "path": "docs/api/README.md",
    "content": "---\nsidebarDepth: 2\n---\n\n# API\n\n## 输出文件\n\n### dva\n\n默认输出文件。\n\n### dva/router\n\n默认输出 [react-router](https://github.com/ReactTraining/react-router) 接口， [react-router-redux](https://github.com/reactjs/react-router-redux) 的接口通过属性 routerRedux 输出。\n\n比如：\n\n```js\nimport { Router, Route, routerRedux } from 'dva/router';\n```\n\n### dva/fetch\n\n异步请求库，输出 [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) 的接口。不和 dva 强绑定，可以选择任意的请求库。\n\n### dva/saga\n\n输出 [redux-saga](https://github.com/yelouafi/redux-saga) 的接口，主要用于用例的编写。（用例中需要用到 effects）\n\n### dva/dynamic\n\n解决组件动态加载问题的 util 方法。\n\n比如：\n\n```js\nimport dynamic from 'dva/dynamic';\n\nconst UserPageComponent = dynamic({\n  app,\n  models: () => [\n    import('./models/users'),\n  ],\n  component: () => import('./routes/UserPage'),\n});\n```\n\n`opts` 包含：\n\n* app: dva 实例，加载 models 时需要\n* models: 返回 Promise 数组的函数，Promise 返回 dva model\n* component：返回 Promise 的函数，Promise 返回 React Component\n\n## dva API\n\n### `app = dva(opts)`\n\n创建应用，返回 dva 实例。(注：dva 支持多实例)\n\n`opts` 包含：\n\n* `history`：指定给路由用的 history，默认是 `hashHistory`\n* `initialState`：指定初始数据，优先级高于 model 中的 state，默认是 `{}`\n\n如果要配置 history 为 `browserHistory`，可以这样：\n\n```js\nimport createHistory from 'history/createBrowserHistory';\nconst app = dva({\n  history: createHistory(),\n});\n```\n\n另外，出于易用性的考虑，`opts` 里也可以配所有的 [hooks](#appusehooks) ，下面包含全部的可配属性：\n\n```js\nconst app = dva({\n  history,\n  initialState,\n  onError,\n  onAction,\n  onStateChange,\n  onReducer,\n  onEffect,\n  onHmr,\n  extraReducers,\n  extraEnhancers,\n});\n```\n\n### `app.use(hooks)`\n\n配置 hooks 或者注册插件。（插件最终返回的是 hooks ）\n\n比如注册 [dva-loading](https://github.com/dvajs/dva-loading) 插件的例子：\n\n```js\nimport createLoading from 'dva-loading';\n...\napp.use(createLoading(opts));\n```\n\n`hooks` 包含：\n\n#### `onError((err, dispatch) => {})`\n\n`effect` 执行错误或 `subscription` 通过 `done` 主动抛错时触发，可用于管理全局出错状态。\n\n注意：`subscription` 并没有加 `try...catch`，所以有错误时需通过第二个参数 `done` 主动抛错。例子：\n\n```js\napp.model({\n  subscriptions: {\n    setup({ dispatch }, done) {\n      done(e);\n    },\n  },\n});\n```\n\n如果我们用 antd，那么最简单的全局错误处理通常会这么做：\n\n```js\nimport { message } from 'antd';\nconst app = dva({\n  onError(e) {\n    message.error(e.message, /* duration */3);\n  },\n});\n```\n\n#### `onAction(fn | fn[])`\n\n在 action 被 dispatch 时触发，用于注册 redux 中间件。支持函数或函数数组格式。\n\n例如我们要通过 [redux-logger](https://github.com/evgenyrodionov/redux-logger) 打印日志：\n\n```js\nimport createLogger from 'redux-logger';\nconst app = dva({\n  onAction: createLogger(opts),\n});\n```\n\n#### `onStateChange(fn)`\n\n`state` 改变时触发，可用于同步 `state` 到 localStorage，服务器端等。\n\n#### `onReducer(fn)`\n\n封装 reducer 执行。比如借助 [redux-undo](https://github.com/omnidan/redux-undo) 实现 redo/undo ：\n\n```js\nimport undoable from 'redux-undo';\nconst app = dva({\n  onReducer: reducer => {\n    return (state, action) => {\n      const undoOpts = {};\n      const newState = undoable(reducer, undoOpts)(state, action);\n      // 由于 dva 同步了 routing 数据，所以需要把这部分还原\n      return { ...newState, routing: newState.present.routing };\n    },\n  },\n});\n```\n\n#### `onEffect(fn)`\n\n封装 effect 执行。比如 [dva-loading](https://github.com/dvajs/dva-loading) 基于此实现了自动处理 loading 状态。\n\n#### `onHmr(fn)`\n\n热替换相关，目前用于 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 。\n\n#### `extraReducers`\n\n指定额外的 reducer，比如 [redux-form](https://github.com/erikras/redux-form) 需要指定额外的 `form` reducer：\n\n```js\nimport { reducer as formReducer } from 'redux-form'\nconst app = dva({\n  extraReducers: {\n    form: formReducer,\n  },\n});\n```\n\n#### `extraEnhancers`\n\n指定额外的 [StoreEnhancer](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer) ，比如结合 [redux-persist](https://github.com/rt2zz/redux-persist) 的使用：\n\n```js\nimport { persistStore, autoRehydrate } from 'redux-persist';\nconst app = dva({\n  extraEnhancers: [autoRehydrate()],\n});\npersistStore(app._store);\n```\n\n### `app.model(model)`\n\n注册 model，详见  [#Model](#model)  部分。\n\n### `app.unmodel(namespace)`\n\n取消 model 注册，清理 reducers, effects 和 subscriptions。subscription 如果没有返回 unlisten 函数，使用 `app.unmodel` 会给予警告⚠️。\n\n### `app.replaceModel(model)`\n\n> 只在app.start()之后可用\n\n替换model为新model，清理旧model的reducers, effects 和 subscriptions，但会保留旧的state状态，对于HMR非常有用。subscription 如果没有返回 unlisten 函数，使用 `app.unmodel` 会给予警告⚠️。 \n\n如果原来不存在相同namespace的model，那么执行`app.model`操作\n\n### `app.router(({ history, app }) => RouterConfig)`\n\n注册路由表。\n\n通常是这样的：\n\n```js\nimport { Router, Route } from 'dva/router';\napp.router(({ history }) => {\n  return (\n    <Router history={history}>\n      <Route path=\"/\" component={App} />\n    </Router>\n  );\n});\n```\n\n推荐把路由信息抽成一个单独的文件，这样结合 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 可实现路由和组件的热加载，比如：\n\n```js\napp.router(require('./router'));\n```\n\n而有些场景可能不使用路由，比如多页应用，所以也可以传入返回 JSX 元素的函数。比如：\n\n```js\napp.router(() => <App />);\n```\n\n### `app.start(selector?)`\n\n启动应用。`selector` 可选，如果没有 `selector` 参数，会返回一个返回 JSX 元素的函数。\n\n```js\napp.start('#root');\n```\n\n那么什么时候不加 `selector`？常见场景有测试、node 端、react-native 和 i18n 国际化支持。\n\n比如通过 react-intl 支持国际化的例子：\n\n```js\nimport { IntlProvider } from 'react-intl';\n...\nconst App = app.start();\nReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement);\n```\n\n## Model\nmodel 是 dva 中最重要的概念。以下是典型的例子：\n\n```js\napp.model({\n  namespace: 'todo',\n  state: [],\n  reducers: {\n    add(state, { payload: todo }) {\n      // 保存数据到 state\n      return [...state, todo];\n    },\n  },\n  effects: {\n    *save({ payload: todo }, { put, call }) {\n      // 调用 saveTodoToServer，成功后触发 `add` action 保存到 state\n      yield call(saveTodoToServer, todo);\n      yield put({ type: 'add', payload: todo });\n    },\n  },\n  subscriptions: {\n    setup({ history, dispatch }) {\n      // 监听 history 变化，当进入 `/` 时触发 `load` action\n      return history.listen(({ pathname }) => {\n        if (pathname === '/') {\n          dispatch({ type: 'load' });\n        }\n      });\n    },\n  },\n});\n```\n\nmodel 包含 5 个属性：\n\n### namespace\n\nmodel 的命名空间，同时也是他在全局 state 上的属性，只能用字符串，不支持通过 `.` 的方式创建多层命名空间。\n\n### state\n\n初始值，优先级低于传给 `dva()` 的 `opts.initialState`。\n\n比如：\n\n```js\nconst app = dva({\n  initialState: { count: 1 },\n});\napp.model({\n  namespace: 'count',\n  state: 0,\n});\n```\n\n此时，在 `app.start()` 后 state.count 为 1 。\n\n### reducers\n\n以 key/value 格式定义 reducer。用于处理同步操作，唯一可以修改 `state` 的地方。由 `action` 触发。\n\n格式为 `(state, action) => newState` 或 `[(state, action) => newState, enhancer]`。\n\n详见： https://github.com/dvajs/dva/blob/master/packages/dva-core/test/reducers.test.js\n\n### effects\n\n以 key/value 格式定义 effect。用于处理异步操作和业务逻辑，不直接修改 `state`。由 `action` 触发，可以触发 `action`，可以和服务器交互，可以获取全局 `state` 的数据等等。\n\n格式为 `*(action, effects) => void` 或 `[*(action, effects) => void, { type }]`。\n\ntype 类型有：\n\n* `takeEvery`\n* `takeLatest`\n* `throttle`\n* `watcher`\n* `poll`\n\n详见：https://github.com/dvajs/dva/blob/master/packages/dva-core/test/effects.test.js\n\n### subscriptions\n\n以 key/value 格式定义 subscription。subscription 是订阅，用于订阅一个数据源，然后根据需要 dispatch 相应的 action。在 `app.start()` 时被执行，数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。\n\n格式为 `({ dispatch, history }, done) => unlistenFunction`。\n\n注意：如果要使用 `app.unmodel()`，subscription 必须返回 unlisten 方法，用于取消数据订阅。\n"
  },
  {
    "path": "docs/guide/README.md",
    "content": "# 介绍\n\ndva 首先是一个基于 [redux](https://github.com/reduxjs/redux) 和 [redux-saga](https://github.com/redux-saga/redux-saga) 的数据流方案，然后为了简化开发体验，dva 还额外内置了 [react-router](https://github.com/ReactTraining/react-router) 和 [fetch](https://github.com/github/fetch)，所以也可以理解为一个轻量级的应用框架。\n\n## 特性\n\n* **易学易用**，仅有 6 个 api，对 redux 用户尤其友好，[配合 umi 使用](https://umijs.org/guide/with-dva.html)后更是降低为 0 API\n* **elm 概念**，通过 reducers, effects 和 subscriptions 组织 model\n* **插件机制**，比如 [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) 可以自动处理 loading 状态，不用一遍遍地写 showLoading 和 hideLoading\n* **支持 HMR**，基于 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 实现 components、routes 和 models 的 HMR\n\n## 他是如何工作的？\n\n## 他是怎么来的？\n\n* [Why dva and what's dva](https://github.com/dvajs/dva/issues/1)\n* [支付宝前端应用架构的发展和选择](https://www.github.com/sorrycc/blog/issues/6)\n\n## 谁在用？\n\n## 为什么不是...?\n\n### redux\n### mobx\n\n## 命名由来？\n\n> D.Va拥有一部强大的机甲，它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。\n\n—— 来自 [守望先锋](https://ow.blizzard.cn/heroes/overwatch-dva) 。\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/psagSCVHOKQVqqNjjMdf.jpg\" width=\"200\" height=\"200\" />\n"
  },
  {
    "path": "docs/guide/concepts.md",
    "content": "---\nsidebarDepth: 2\n---\n\n# Dva 概念\n\n## 数据流向\n\n数据的改变发生通常是通过用户交互行为或者浏览器行为（如路由跳转等）触发的，当此类行为会改变数据的时候可以通过 `dispatch` 发起一个 action，如果是同步行为会直接通过 `Reducers` 改变 `State` ，如果是异步行为（副作用）会先触发 `Effects` 然后流向 `Reducers` 最终改变 `State`，所以在 dva 中，数据流向非常清晰简明，并且思路基本跟开源社区保持一致（也是来自于开源社区）。\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/PPrerEAKbIoDZYr.png\" width=\"807\" />\n\n## Models\n\n### State\n\n`type State = any`\n\nState 表示 Model 的状态数据，通常表现为一个 javascript 对象（当然它可以是任何值）；操作的时候每次都要当作不可变数据（immutable data）来对待，保证每次都是全新对象，没有引用关系，这样才能保证 State 的独立性，便于测试和追踪变化。\n\n在 dva 中你可以通过 dva 的实例属性 `_store` 看到顶部的 state 数据，但是通常你很少会用到:\n\n```javascript\nconst app = dva();\nconsole.log(app._store); // 顶部的 state 数据\n```\n\n### Action\n\n`type AsyncAction = any`\n\nAction 是一个普通 javascript 对象，它是改变 State 的唯一途径。无论是从 UI 事件、网络回调，还是 WebSocket 等数据源所获得的数据，最终都会通过 dispatch 函数调用一个 action，从而改变对应的数据。action 必须带有 `type` 属性指明具体的行为，其它字段可以自定义，如果要发起一个 action 需要使用 `dispatch` 函数；需要注意的是 `dispatch` 是在组件 connect Models以后，通过 props 传入的。\n```\ndispatch({\n  type: 'add',\n});\n```\n\n### dispatch 函数\n\n`type dispatch = (a: Action) => Action`\n\ndispatching function 是一个用于触发 action 的函数，action 是改变 State 的唯一途径，但是它只描述了一个行为，而 dipatch 可以看作是触发这个行为的方式，而 Reducer 则是描述如何改变数据的。\n\n在 dva 中，connect Model 的组件通过 props 可以访问到 dispatch，可以调用 Model 中的 Reducer 或者 Effects，常见的形式如：\n\n```javascript\ndispatch({\n  type: 'user/add', // 如果在 model 外调用，需要添加 namespace\n  payload: {}, // 需要传递的信息\n});\n```\n\n### Reducer\n\n`type Reducer<S, A> = (state: S, action: A) => S`\n\nReducer（也称为 reducing function）函数接受两个参数：之前已经累积运算的结果和当前要被累积的值，返回的是一个新的累积结果。该函数把一个集合归并成一个单值。\n\nReducer 的概念来自于是函数式编程，很多语言中都有 reduce API。如在 javascript 中：\n\n```javascript\n[{x:1},{y:2},{z:3}].reduce(function(prev, next){\n    return Object.assign(prev, next);\n})\n//return {x:1, y:2, z:3}\n```\n\n在 dva 中，reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值，与当前 reducers 中的值进行运算获得新的值（也就是新的 state）。需要注意的是 Reducer 必须是[纯函数](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md)，所以同样的输入必然得到同样的输出，它们不应该产生任何副作用。并且，每一次的计算都应该使用[immutable data](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#reasonable)，这种特性简单理解就是每次操作都是返回一个全新的数据（独立，纯净），所以热重载和时间旅行这些功能才能够使用。\n\n### Effect\n\nEffect 被称为副作用，在我们的应用中，最常见的就是异步操作。它来自于函数编程的概念，之所以叫副作用是因为它使得我们的函数变得不纯，同样的输入不一定获得同样的输出。\n\ndva 为了控制副作用的操作，底层引入了[redux-sagas](http://superraytin.github.io/redux-saga-in-chinese)做异步流程控制，由于采用了[generator的相关概念](http://www.ruanyifeng.com/blog/2015/04/generator.html)，所以将异步转成同步写法，从而将effects转为纯函数。至于为什么我们这么纠结于 __纯函数__，如果你想了解更多可以阅读[Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide)，或者它的中文译本[JS函数式编程指南](https://www.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details)。\n\n### Subscription\n\nSubscriptions 是一种从 __源__ 获取数据的方法，它来自于 elm。\n\nSubscription 语义是订阅，用于订阅一个数据源，然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。\n\n```javascript\nimport key from 'keymaster';\n...\napp.model({\n  namespace: 'count',\n  subscriptions: {\n    keyEvent({dispatch}) {\n      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });\n    },\n  }\n});\n```\n\n## Router\n\n这里的路由通常指的是前端路由，由于我们的应用现在通常是单页应用，所以需要前端代码来控制路由逻辑，通过浏览器提供的 [History API](http://mdn.beonex.com/en/DOM/window.history.html) 可以监听浏览器url的变化，从而控制路由相关操作。\n\ndva 实例提供了 router 方法来控制路由，使用的是[react-router](https://github.com/reactjs/react-router)。\n\n```javascript\nimport { Router, Route } from 'dva/router';\napp.router(({history}) =>\n  <Router history={history}>\n    <Route path=\"/\" component={HomePage} />\n  </Router>\n);\n```\n\n## Route Components\n\n在[组件设计方法](https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/tutorial/04-%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95.md)中，我们提到过 Container Components，在 dva 中我们通常将其约束为 Route Components，因为在 dva 中我们通常以页面维度来设计 Container Components。\n\n所以在 dva 中，通常需要 connect Model的组件都是 Route Components，组织在`/routes/`目录下，而`/components/`目录下则是纯组件（Presentational Components）。\n\n## 参考\n\n- [redux docs](http://redux.js.org/docs/Glossary.html)\n- [redux docs 中文](http://cn.redux.js.org/index.html)\n- [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide)\n- [JS函数式编程指南](https://www.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details)\n- [choo docs](https://github.com/yoshuawuyts/choo)\n- [elm](http://elm-lang.org/blog/farewell-to-frp)\n"
  },
  {
    "path": "docs/guide/develop-complex-spa.md",
    "content": "# 使用 Dva 开发复杂 SPA\n\n> 作者：徐飞\n\n在dva的官方仓库里，提供了上手教程，讲述了dva的一些基本概念。到了真实的业务开发过程中，会遇到许许多多不能用那些基本操作覆盖的场景，本文尝试列举一些常见的需求在dva中的实现方式。\n\n## 动态加载model\n\n有不少业务场景下，我们可能会定义出很多个model，但并不需要在应用启动的时候就全部加载，比较典型的是各类管理控制台。如果每个功能页面是通过路由切换，互相之间没有关系的话，通常会使用webpack的require.ensure来做代码模块的懒加载。\n\n我们也可以利用这个特性来做model的动态加载。\n\n```JavaScript\nfunction RouterConfig({ history, app }) {\n  const routes = [\n    {\n      path: '/',\n      name: 'IndexPage',\n      getComponent(nextState, cb) {\n        require.ensure([], (require) => {\n          registerModel(app, require('./models/dashboard'));\n          cb(null, require('./routes/IndexPage'));\n        });\n      },\n    },\n    {\n      path: '/users',\n      name: 'UsersPage',\n      getComponent(nextState, cb) {\n        require.ensure([], (require) => {\n          registerModel(app, require('./models/users'));\n          cb(null, require('./routes/Users'));\n        });\n      },\n    },\n  ];\n\n  return <Router history={history} routes={routes} />;\n}\n```\n\n这样，在视图切换到这个路由的时候，对应的model就会被加载。同理，也可以做model的动态移除，不过，一般情况下是不需要移除的。\n\n## 使用model共享全局信息\n\n在上一节我们提到，可以动态加载model，也可以移除。从这个角度看，model是可以有不同生命周期的，有些可以与功能视图伴随，而有些可以贯穿整个应用的生命周期。\n\n从业务场景来说，有不少场景是可以做全局model的，比如说，我们在路由之间前进后退，model可以用于在路由间共享数据，比较典型的，像列表页和详情页的互相跳转，就可以用同一份model去共享它们的数据。\n\n注意，如果当前应用中加载了不止一个model，在其中一个的effect里面做select操作，是可以获取另外一个model中的state的：\n\n```JavaScript\n*foo(action, { select }) {\n  const { a, b } = yield select();\n}\n```\n\n这里，a，b可以分别是两个不同model的state。所以，借助这个特点，我们就不必非要把model按照视图的结构进行组织，可以适当按照业务分类，把一些数据存在对应业务的model中，分别通过不同的effect去更新，在获取的地方再去组合，这样可以使得model拥有更好的复用性。\n\n## model的复用\n\n有时候，业务上可能遇到期望把一些与外部关联较少的model拆出来的需求，我们可能会拆出这样的一个model，然后用不同的视图容器去connect它。\n\n```JavaScript\nexport default {\n  namespace: 'reusable',\n  state: {},\n  reducers: {},\n  effects: {}\n}\n```\n\n所以，在业务上，可能出现的使用情况就是：\n\n```\n              ContainerA <-- ModelA\n                   |\n    ------------------------------\n    |                            |\nContainerB <-- reusable     ContainerC <-- reusable\n```\n\n这里面，ContainerB和ContainerC是ContainerA的下属，它们的逻辑结构一致，只是展现不同。我们可以让它们分别connect同一个model，注意，这个时候，model的修改会同时影响到两个视图，因为model在state中是直接以namespace作key存放的，实际上只有一份实例。\n\n## 动态扩展model\n\n在上一节中，我们提到可以把model进行分类，以实现在若干视图中的共享，但业务需求是比较多变的，很可能我们又会遇到这种情况：\n\n`几个业务视图长得差不多，model也存在少量差别`\n\n这个情况下，如果我们让它们复用同一个model也可以，但这么做，对维护是一种挑战，很可能改其中一个，对另外一些造成了影响，所以这种情况下，可能会期望能够对model进行扩展。\n\n所谓扩展，通常是要做几个事情：\n\n- 新增一些东西\n- 覆盖一些原有的东西\n- 根据条件动态创建一些东西\n\n注意到dva中的每个model，实际上都是普通的JavaScript对象，包含\n\n- namespace\n- state\n- reducers\n- effects\n- subscriptions\n\n从这个角度看，我们要新增或者覆盖一些东西，都会是比较容易的，比如说，使用Object.assign来进行对象属性复制，就可以把新的内容添加或者覆盖到原有对象上。\n\n注意这里有两级，model结构中的`state`，`reducers`，`effects`，`subscriptions`都是对象结构，需要分别在这一级去做assign。\n\n可以借助dva社区的`dva-model-extend`库来做这件事。\n\n换个角度，也可以通过工厂函数来生成model，比如：\n\n```JavaScript\nfunction createModel(options) {\n  const { namespace, param } = options;\n  return {\n    namespace: `demo${namespace}`,\n    states: {},\n    reducers: {},\n    effects: {\n      *foo() {\n        // 这里可以根据param来确定下面这个call的参数\n        yield call()\n      }\n    }\n  };\n}\n\nconst modelA = createModel({ namespace: 'A', param: { type: 'A' } });\nconst modelB = createModel({ namespace: 'A', param: { type: 'B' } });\n```\n\n这样，也能够实现对model的扩展。\n\n## 长流程的业务逻辑\n\n在业务中，有时候会出现较长的流程，比如说，我们的一个复杂表单的提交，中间会需要去发起多种对视图状态的操作：\n\n**这是一个真实业务**\n```JavaScript\n*submit(action, { put, call, select }) {\n  const formData = yield select(state => {\n    const buyModel = state.buy;\n    const context = state.context;\n    const { stock } = buyModel;\n    return {\n      uuid: context.uuid,\n      market: stock && stock.market,\n      stockCode: stock && stock.code,\n      stockName: stock && stock.name,\n      price: String(buyModel.price),\n      // 委托数量\n      entrustAmount: String(buyModel.count),\n      totalBalance: buyModel.totalBalance,\n      availableTzbBalance: buyModel.availableTzbBalance,\n      availableDepositBalance: buyModel.availableDepositBalance,\n    };\n  });\n  const result = yield call(post, '/h5/ajax/trade/entrust_buy', formData, { loading: true });\n\n  if (result.success) {\n    toast({\n      type: 'success',\n      content: '委托已受理',\n    });\n    // 成功之后再获取一次现价，并填入\n    // yield put({type: 'fetchQuotation', payload: stock});\n\n    yield put({ type: 'entrustNoChange', payload: result.result && result.result.entrustNo });\n    // 清空输入框内容\n    yield put({ type: 'searchQueryChange', value: '' });\n  }\n\n  // 403时，需要验证密码再重新提交\n  if (!result.success && result.resultCode === 403) {\n    yield put({ type: 'checkPassword', payload: {} });\n    return;\n  }\n\n  // 失败之后也需要更新投资宝和保证金金额\n  if (result.result) {\n    yield put({ type: 'balanceChange', payload: result.result });\n  }\n\n  // 重新获取最新可撤单列表\n  yield put({ type: 'fetchRevockList' });\n\n  // 返回的结果里面如果有uuid, 用新的uuid替换\n  if (result.uuid) {\n    yield put({ type: 'context/updateUuid', payload: result.uuid });\n  }\n},\n```\n\n在一个effect中，可以使用多个put来分别调用reducer来更新状态。\n\n存在另外一些流程，在effect中可能会存在多个异步的服务调用，比如说，要调用一次服务端的验证，成功之后再去提交数据，这时候，在一个effect中就会存在多个call操作了。\n\n## 使用take操作进行事件监听\n\n与上一节提到的情况相比，我们还可能遇到另外一些场景，比如：\n\n`一个流程的变动，需要扩散到若干个其他model中`\n\n这个需求其实也覆盖了上一节这种，但在这一节中，我们侧重讨论比较通用的这类需求的处理方式。\n\n在redux-saga中，提供了take和takeLatest这两个操作，dva是redux-saga的封装，也是可以使用这种操作的。\n\n要理解take操作的语义，可以参见这两种示例的对比：\n\n假设我们有一个事件处理的代码：\n\n```JavaScript\nsomeSource.on('click', event => doSomething(event))\n```\n\n这段代码转成用generator来表达，就是下面这个形式：\n\n```JavaScript\nfunction* saga() {\n  while(true) {\n     const event = yield take('click');\n     doSomething(event);\n  }\n}\n```\n\n所以，我们也可以在dva中使用take操作来监听action。\n\n## 多任务调度\n\n上一节我们提到的是多个任务的串行执行方式，这是业务中最常见的多任务执行方式，只需逐个yield call就可以了。\n\n有的时候，我们可能会希望多个任务以另外一些方式执行，比如：\n\n- 并行，若干个任务之间不存在依赖关系，并且后续操作对它们的结果无依赖\n- 竞争，若干个任务之间，只要有一个执行完成，就进入下一个环节\n- 子任务，若干个任务，并行执行，但必须全部做完之后，下一个环节才继续执行\n\n### 任务的并行执行\n\n如果想要让任务并行执行，可以通过下面这种方式：\n\n```JavaScript\nconst [result1, result2]  = yield all([\n  call(service1, param1),\n  call(service2, param2)\n])\n```\n\n把多个要并行执行的东西放在一个数组里，就可以并行执行，等所有的都结束之后，进入下个环节，类似promise.all的操作。一般有一些集成界面，比如dashboard，其中各组件之间业务关联较小，就可以用这种方式去分别加载数据，此时，整体加载时间只取决于时间最长的那个。\n\n注意：上面代码中的那个：\n\n```JavaScript\nyield [];\n```\n\n不要写成：\n\n```JavaScript\nyield* [];\n```\n\n这两者含义是不同的，后者会顺序执行。\n\n### 任务的竞争\n\n如果多个任务之间存在竞争关系，可以通过下面这种方式：\n\n```JavaScript\nconst { data, timeout } = yield race({\n  data: call(service, 'some data'),\n  timeout: call(delay, 1000)\n});\n\nif (data)\n  put({type: 'DATA_RECEIVED', data});\nelse\n  put({type: 'TIMEOUT_ERROR'});\n```\n\n这个例子比较巧妙地用一个延时一秒的空操作来跟一个网络请求竞争，如果到了一秒，请求还没结束，就让它超时。\n\n这个类似于Promise.race的作用。\n\n## 跨model的通信\n\n当业务复杂的情况下，我们可能会对model进行拆分，但在这种情况下，往往又会遇到一些比较复杂的事情，比如：\n\n`一个流程贯穿多个model`\n\n对这个事情，我们可能有若干中不同的解决办法。假设有如下场景：\n\n- 父容器A，子容器B，二者各自connect了不同的model A和B\n- 父容器中有一个操作，分三个步骤：\n  - model A中某个effect处理第一步\n  - call model B中的某个effect去处理第二步\n  - 第二步结束后，再返回model A中做第三步\n\n在dva中，可以用namespace去指定接受action的model，所以可以通过类似这样的方式去组合：\n\n```JavaScript\nyield call({ type: 'a/foo' });\nyield call({ type: 'b/foo' });\nyield call({ type: 'a/bar' });\n```\n\n甚至，还可以利用take命令，在另外一个model的某个effect中插入逻辑：\n\n```JavaScript\n*effectA() {\n  yield call(service1);\n  yield put({ type: 'service1Success' });\n  // 如果我们复用这个effect，但要在这里加一件事，怎么办？\n  yield call(service2);\n  yield put({ type: 'service2Success' });\n}\n```\n\n可以利用之前我们说的take命令：\n\n```JavaScript\nyield take('a/service1Success');\n```\n\n这样，可以在外部往里面添加一个并行操作，通过这样的组合可以处理一些组合流程。但实际情况下，我们可能要处理的不仅仅是effect，很可能视图组件中还存在后续逻辑，在某个action执行之后，还需要再做某些事情。\n\n比如：\n\n```JavaScript\nyield call({ type: 'a/foo' });\nyield call({ type: 'b/foo' });\n// 如果这里是要在组件里面做某些事情，怎么办？\n```\n\n可以利用一些特殊手段把流程延伸出来到组件里。比如说，我们通常在组件中dispatch一个action的时候，不会处理后续事情，但可以修改这个过程：\n\n```JavaScript\nnew Promise((resolve, reject) => {\n  dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } });\n})\n.then((data) => {\n  console.log(`after a long time, ${data} returns`);\n});\n```\n\n注意这里，我们是把resolve和reject传到action里面了，所以，只需在effect里面这样处理：\n\n```JavaScript\ntry {\n  const result = yield call(service1);\n  yield put({ type: 'service1Success', payload: result });\n  resolve(result);\n}\ncatch (error) {\n  yield put({ type: 'service1Fail', error });\n  reject(ex);\n}\n```\n\n这样，就实现了跨越组件、模型的复杂的长流程的调用。\n\n## 为DVA应用编写测试\n\n在比较追求稳定性的工程中，应当使用单元测试来保证代码质量。在Redux的各类中间件中，redux-saga应当是测试最简单的了，原因如下：\n\n在一个应用中，除了视图组件之外，可能存在逻辑的地方主要是两种：reducer、effect。这两者中，reducer是普通函数，并且是纯函数，职责单一，对于固定输入，就有固定输出，所以很容易测试。而在effect中，我们所要测试的东西是什么呢？如何确保测试能够覆盖某个effect，是全部真实执行一遍吗？\n\n所谓的单元测试，其实要测试的是某个函数自身的逻辑是否全被覆盖，像在一个effect中对外部服务（比如网络请求）的调用，这些外部服务的执行过程其实与本模块的单元测试无关，因此，我们只需要验证这件事：\n\n`是否发起了对某个服务的调用`\n\n至于说，这个服务是否在执行，无关于本模块的正确性，那是这个服务的单元测试要做的事。所以这么一来，一个effect实际上是转化为同步逻辑的测试，因为它是一个generator函数，只需对这个effect一路next，就能跑完整个逻辑。\n\n对redux-saga的测试是这样的原理，而dva是对redux-saga的封装，这块的机制是一致的，所以我们可以用同样的方式，从model对象中获取reducer和effect，分别编写测试用例。 \n"
  },
  {
    "path": "docs/guide/examples-and-boilerplates.md",
    "content": "# 例子和脚手架\n\n## 官方\n\n* [Count](https://stackblitz.com/edit/dva-example-count): 简单计数器\n* [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): 用户管理\n* [AntDesign Pro](https://github.com/ant-design/ant-design-pro)：([Demo](https://preview.pro.ant.design/))，开箱即用的中台前端/设计解决方案\n* [HackerNews](https://github.com/dvajs/dva-hackernews):  ([Demo](https://dvajs.github.io/dva-hackernews/))，HackerNews Clone\n* [antd-admin](https://github.com/zuiidea/antd-admin): ([Demo](http://antd-admin.zuiidea.com/))，基于 antd 和 dva 的后台管理应用\n* [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f))，Github Star 管理应用\n\n## 社区\n\n* [umi-dva-antd-mobile](https://github.com/hqwlkj/umi-dva-antd-mobile)，来自 @Yanghc 的 umi + dva + antd-mobile 的 mobile 版本脚手架，支持 TypeScript。\n* [Account System](https://github.com/yvanwangl/AccountSystem.git): 小型库存管理系统\n* [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): 集成了 dva 和 react-navigation 典型应用场景的 React Native 实例\n"
  },
  {
    "path": "docs/guide/fig-show.md",
    "content": "# Dva 图解\n\n> 作者：至正<br />\n> 原文链接：[https://yuque.com/flying.ni/the-tower/tvzasn](https://yuque.com/flying.ni/the-tower/tvzasn)\n\n## 示例背景\n\n最常见的 Web 类示例之一: TodoList = Todo list + Add todo button\n\n## 图解一: React 表示法\n\n![图片.png | left | 747x518](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436560812-2586a0b5-7a6a-4a07-895c-f822fa85d5de.png \"\")\n\n按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是 `<App/>`\n\n`<TodoList/> <Todo/>` 以及`<AddTodoBtn/>` 本身不维持任何 state, 完全由父节点`<App/>` 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: `Pure Component`\n\n## 图解二: Redux 表示法\n\nReact 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store\n\n![图片.png | left | 747x558](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436134375-4c15f63d-72f1-4c73-94a6-55b220d2547c.png \"\")\n\n与图一相比, 几个明显的改进点:\n\n1. 状态及页面逻辑从 `<App/>`里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer\n2. `<TodoList/> ` 及`<AddTodoBtn/>`都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新\n3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging\n\n这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好\n\n## 图解三: 加入 Saga\n\n![图片.png | left | 747x504](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436167824-7fa834ea-aa6c-4f9f-bab5-b8c5312bcf7e.png \"\")\n\n上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:\n\n1. 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action\n2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可\n\n## 图解四: Dva 表示法\n\n![图片.png | left | 747x490](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436195004-cd3800f2-f13d-40ba-bb1f-4efba99cfe0d.png \"\")\n\n有了前面的三步铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:\n\n1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面\n2. 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作\n3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️\n\n`约定优于配置, 总是好的`😆\n\n```js\napp.model({\n  namespace: 'count',\n  state: {\n    record: 0,\n    current: 0,\n  },\n  reducers: {\n    add(state) {\n      const newCurrent = state.current + 1;\n      return { ...state,\n        record: newCurrent > state.record ? newCurrent : state.record,\n        current: newCurrent,\n      };\n    },\n    minus(state) {\n      return { ...state, current: state.current - 1};\n    },\n  },\n  effects: {\n    *add(action, { call, put }) {\n      yield call(delay, 1000);\n      yield put({ type: 'minus' });\n    },\n  },\n  subscriptions: {\n    keyboardWatcher({ dispatch }) {\n      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });\n    },\n  },\n});\n```\n"
  },
  {
    "path": "docs/guide/getting-started.md",
    "content": "# 快速上手\n\n## 安装 dva-cli\n\n通过 npm 安装 dva-cli 并确保版本是 `0.9.1` 或以上。\n\n```bash\n$ npm install dva-cli -g\n$ dva -v\ndva-cli version 0.9.1\n```\n\n## 创建新应用\n\n安装完 dva-cli 之后，就可以在命令行里访问到 `dva` 命令（[不能访问？](http://stackoverflow.com/questions/15054388/global-node-modules-not-installing-correctly-command-not-found)）。现在，你可以通过 `dva new` 创建新应用。\n\n```bash\n$ dva new dva-quickstart\n```\n\n这会创建 `dva-quickstart` 目录，包含项目初始化目录和文件，并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。\n\n然后我们 `cd` 进入 `dva-quickstart` 目录，并启动开发服务器：\n\n```bash\n$ cd dva-quickstart\n$ npm start\n```\n\n几秒钟后，你会看到以下输出：\n\n```bash\nCompiled successfully!\n\nThe app is running at:\n\n  http://localhost:8000/\n\nNote that the development build is not optimized.\nTo create a production build, use npm run build.\n```\n\n在浏览器里打开 http://localhost:8000 ，你会看到 dva 的欢迎界面。\n\n## 使用 antd\n\n通过 npm 安装 `antd` 和 `babel-plugin-import` 。`babel-plugin-import` 是用来按需加载 antd 的脚本和样式的，详见 [repo](https://github.com/ant-design/babel-plugin-import) 。\n\n```bash\n$ npm install antd babel-plugin-import --save\n```\n\n编辑 `.webpackrc`，使 `babel-plugin-import` 插件生效。\n\n```diff\n{\n+  \"extraBabelPlugins\": [\n+    [\"import\", { \"libraryName\": \"antd\", \"libraryDirectory\": \"es\", \"style\": \"css\" }]\n+  ]\n}\n```\n\n> 注：dva-cli 基于 roadhog 实现 build 和 dev，更多 `.webpackrc` 的配置详见 [roadhog#配置](https://github.com/sorrycc/roadhog#配置)\n\n## 定义路由\n\n我们要写个应用来先显示产品列表。首先第一步是创建路由，路由可以想象成是组成应用的不同页面。\n\n新建 route component `routes/Products.js`，内容如下：\n\n```javascript\nimport React from 'react';\n\nconst Products = (props) => (\n  <h2>List of Products</h2>\n);\n\nexport default Products;\n```\n\n添加路由信息到路由表，编辑 `router.js` :\n\n```diff\n+ import Products from './routes/Products';\n...\n+ <Route path=\"/products\" exact component={Products} />\n```\n\n然后在浏览器里打开 http://localhost:8000/#/products ，你应该能看到前面定义的 `<h2>` 标签。\n\n## 编写 UI Component\n\n随着应用的发展，你会需要在多个页面分享 UI 元素 (或在一个页面使用多次)，在 dva 里你可以把这部分抽成 component 。\n\n我们来编写一个 `ProductList` component，这样就能在不同的地方显示产品列表了。\n\n新建 `components/ProductList.js` 文件：\n\n```javascript\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { Table, Popconfirm, Button } from 'antd';\n\nconst ProductList = ({ onDelete, products }) => {\n  const columns = [{\n    title: 'Name',\n    dataIndex: 'name',\n  }, {\n    title: 'Actions',\n    render: (text, record) => {\n      return (\n        <Popconfirm title=\"Delete?\" onConfirm={() => onDelete(record.id)}>\n          <Button>Delete</Button>\n        </Popconfirm>\n      );\n    },\n  }];\n  return (\n    <Table\n      dataSource={products}\n      columns={columns}\n    />\n  );\n};\n\nProductList.propTypes = {\n  onDelete: PropTypes.func.isRequired,\n  products: PropTypes.array.isRequired,\n};\n\nexport default ProductList;\n```\n\n## 定义 Model\n\n完成 UI 后，现在开始处理数据和逻辑。\n\ndva 通过 model 的概念把一个领域的模型管理起来，包含同步更新 state 的 reducers，处理异步逻辑的 effects，订阅数据源的 subscriptions 。\n\n新建 model `models/products.js` ：\n\n```javascript\nexport default {\n  namespace: 'products',\n  state: [],\n  reducers: {\n    'delete'(state, { payload: id }) {\n      return state.filter(item => item.id !== id);\n    },\n  },\n};\n```\n\n这个 model 里：\n\n- `namespace` 表示在全局 state 上的 key\n- `state` 是初始值，在这里是空数组\n- `reducers` 等同于 redux 里的 reducer，接收 action，同步更新 state\n\n然后别忘记在 `index.js` 里载入他：\n\n```diff\n// 3. Model\n+ app.model(require('./models/products').default);\n```\n\n## connect 起来\n\n到这里，我们已经单独完成了 model 和 component，那么他们如何串联起来呢?\n\ndva 提供了 connect 方法。如果你熟悉 redux，这个 connect 就是 react-redux 的 connect 。\n\n编辑 `routes/Products.js`，替换为以下内容：\n\n```javascript\nimport React from 'react';\nimport { connect } from 'dva';\nimport ProductList from '../components/ProductList';\n\nconst Products = ({ dispatch, products }) => {\n  function handleDelete(id) {\n    dispatch({\n      type: 'products/delete',\n      payload: id,\n    });\n  }\n  return (\n    <div>\n      <h2>List of Products</h2>\n      <ProductList onDelete={handleDelete} products={products} />\n    </div>\n  );\n};\n\n// export default Products;\nexport default connect(({ products }) => ({\n  products,\n}))(Products);\n```\n\n最后，我们还需要一些初始数据让这个应用 run 起来。编辑 `index.js`：\n\n```diff\n- const app = dva();\n+ const app = dva({\n+   initialState: {\n+     products: [\n+       { name: 'dva', id: 1 },\n+       { name: 'antd', id: 2 },\n+     ],\n+   },\n+ });\n```\n\n刷新浏览器，应该能看到以下效果：\n\n<p style=\"text-align: center\">\n  <img src=\"https://zos.alipayobjects.com/rmsportal/GQJeDDeUCSTRMMg.gif\" />\n</p>\n\n## 构建应用\n\n完成开发并且在开发环境验证之后，就需要部署给我们的用户了。先执行下面的命令：\n\n```bash\n$ npm run build\n```\n\n几秒后，输出应该如下：\n\n```bash\n> @ build /private/tmp/myapp\n> roadhog build\n\nCreating an optimized production build...\nCompiled successfully.\n\nFile sizes after gzip:\n\n  82.98 KB  dist/index.js\n  270 B     dist/index.css\n```\n\n`build` 命令会打包所有的资源，包含 JavaScript, CSS, web fonts, images, html 等。然后你可以在 `dist/` 目录下找到这些文件。\n"
  },
  {
    "path": "docs/guide/introduce-class.md",
    "content": "# 入门课\n\n::: tip\n内容来自之前为内部同学准备的入门课。\n:::\n\n## React 没有解决的问题\n\nReact 本身只是一个 DOM 的抽象层，使用组件构建虚拟 DOM。\n\n如果开发大应用，还需要解决一个问题。\n\n* 通信：组件之间如何通信？\n* 数据流：数据如何和视图串联起来？路由和数据如何绑定？如何编写异步逻辑？等等\n\n## 通信问题\n组件会发生三种通信。\n\n* 向子组件发消息\n* 向父组件发消息\n* 向其他组件发消息\n\nReact 只提供了一种通信手段：传参。对于大应用，很不方便。\n\n## 组件通信的例子\n\n### 步骤1\n\n```js\nclass Son extends React.Component {\n  render() {\n    return <input/>;\n  }\n}\n\nclass Father extends React.Component {\n  render() {\n    return <div>\n      <Son/>\n      <p>这里显示 Son 组件的内容</p>\n    </div>;\n  }\n}\n\nReactDOM.render(<Father/>, mountNode);\n```\n\n看这个例子，想一想父组件如何拿到子组件的值。\n\n### 步骤2\n\n```js\nclass Son extends React.Component {\n  render() {\n    return <input onChange={this.props.onChange}/>;\n  }\n}\n\nclass Father extends React.Component {\n  constructor() {\n    super();\n    this.state = {\n      son: \"\"\n    }\n  }\n  changeHandler(e) {\n    this.setState({\n      son: e.target.value\n    });\n  }\n  render() {\n    return <div>\n      <Son onChange={this.changeHandler.bind(this)}/>\n      <p>这里显示 Son 组件的内容：{this.state.son}</p>\n    </div>;\n  }\n}\n\nReactDOM.render(<Father/>, mountNode);\n```\n\n看下这个例子，看懂源码，理解子组件如何通过父组件传入的函数，将自己的值再传回父组件。 \n\n## 数据流问题\n\n目前流行的数据流方案有：\n\n* Flux，单向数据流方案，以 [Redux](https://github.com/reactjs/redux) 为代表\n* Reactive，响应式数据流方案，以 [Mobx](https://github.com/mobxjs/mobx) 为代表\n* 其他，比如 rxjs 等\n\n到底哪一种架构最合适 React ？\n\n## 目前最流行的数据流方案\n\n截止 2017.1，最流行的社区 React 应用架构方案如下。\n\n* 路由： [React-Router](https://github.com/ReactTraining/react-router/tree/v2.8.1)\n* 架构： [Redux](https://github.com/reactjs/redux)\n* 异步操作： [Redux-saga](https://github.com/yelouafi/redux-saga)\n\n缺点：要引入多个库，项目结构复杂。\n\n## dva 是什么\n\ndva 是体验技术部开发的 React 应用框架，将上面三个 React 工具库包装在一起，简化了 API，让开发 React 应用更加方便和快捷。\n\ndva = React-Router + Redux + Redux-saga\n\n## dva 应用的最简结构\n```js\nimport dva from 'dva';\nconst App = () => <div>Hello dva</div>;\n\n// 创建应用\nconst app = dva();\n// 注册视图\napp.router(() => <App />);\n// 启动应用\napp.start('#root');\n```\n\n## 数据流图\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/hUFIivoOFjVmwNXjjfPE.png\" width=\"460\" height=\"290\" /> \n\n## 核心概念 \n* State：一个对象，保存整个应用状态\n* View：React 组件构成的视图层\n* Action：一个对象，描述事件 \n* connect 方法：一个函数，绑定 State 到 View\n* dispatch 方法：一个函数，发送 Action 到 State\n\n## State 和 View\nState 是储存数据的地方，收到 Action 以后，会更新数据。\n\nView 就是 React 组件构成的 UI 层，从 State 取数据后，渲染成 HTML 代码。只要 State 有变化，View 就会自动更新。\n\n## Action\nAction 是用来描述 UI 层事件的一个对象。\n\n```js\n{\n  type: 'click-submit-button',\n  payload: this.form.data\n}\n```\n\n## connect 方法\n\nconnect 是一个函数，绑定 State 到 View。\n\n```js\nimport { connect } from 'dva';\n\nfunction mapStateToProps(state) {\n  return { todos: state.todos };\n}\nconnect(mapStateToProps)(App);\n```\n\nconnect 方法返回的也是一个 React 组件，通常称为容器组件。因为它是原始 UI 组件的容器，即在外面包了一层 State。\n\nconnect 方法传入的第一个参数是 mapStateToProps  函数，mapStateToProps 函数会返回一个对象，用于建立 State 到 Props 的映射关系。\n\n## dispatch 方法\ndispatch 是一个函数方法，用来将 Action 发送给 State。\n\n```js\ndispatch({\n  type: 'click-submit-button',\n  payload: this.form.data\n})\n```\n\ndispatch 方法从哪里来？被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。\n\n> connect 的数据从哪里来? \n\n## dva 应用的最简结构（带 model)\n```js\n// 创建应用\nconst app = dva();\n\n// 注册 Model\napp.model({\n  namespace: 'count',\n  state: 0,\n  reducers: {\n    add(state) { return state + 1 },\n  },\n  effects: {\n    *addAfter1Second(action, { call, put }) {\n      yield call(delay, 1000);\n      yield put({ type: 'add' });\n    },\n  },\n});\n\n// 注册视图\napp.router(() => <ConnectedApp />);\n\n// 启动应用\napp.start('#root');\n```\n\n## 数据流图 1\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/cyzvnIrRhJGOiLliwhcZ.png\" width=\"450\" height=\"380\" />\n\n## 数据流图 2\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/pHTYrKJxQHPyJGAYOzMu.png\" width=\"607\" height=\"464\" />\n\n## app.model\n\ndva 提供 app.model 这个对象，所有的应用逻辑都定义在它上面。\n\n```js\nconst app = dva();\n\n// 新增这一行\napp.model({ /**/ });\n\napp.router(() => <App />);\napp.start('#root');\n```\n\n## Model 对象的例子\n\n```js\n{\n  namespace: 'count',\n  state: 0,\n  reducers: {\n    add(state) { return state + 1 },\n  },\n  effects: {\n    *addAfter1Second(action, { call, put }) {\n      yield call(delay, 1000);\n      yield put({ type: 'add' });\n    },\n  },\n}\n```\n\n## Model 对象的属性\n\n* namespace: 当前 Model 的名称。整个应用的 State，由多个小的 Model 的 State 以 namespace 为 key 合成\n* state: 该 Model 当前的状态。数据保存在这里，直接决定了视图层的输出\n* reducers: Action 处理器，处理同步动作，用来算出最新的 State\n* effects：Action 处理器，处理异步动作\n\n## Reducer\n\nReducer 是 Action 处理器，用来处理同步操作，可以看做是 state 的计算器。它的作用是根据 Action，从上一个 State 算出当前 State。\n\n一些例子：\n\n```js\n// count +1\nfunction add(state) { return state + 1; }\n\n// 往 [] 里添加一个新 todo\nfunction addTodo(state, action) { return [...state, action.payload]; }\n\n// 往 { todos: [], loading: true } 里添加一个新 todo，并标记 loading 为 false\nfunction addTodo(state, action) {\n  return {\n    ...state,\n    todos: state.todos.concat(action.payload),\n    loading: false\n  };\n}\n```\n\n## Effect\n\nAction 处理器，处理异步动作，基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程，计算以外的操作都属于 Effect，典型的就是 I/O 操作、数据库读写。\n\n```js\nfunction *addAfter1Second(action, { put, call }) {\n  yield call(delay, 1000);\n  yield put({ type: 'add' });\n}\n```\n\n## Generator 函数\n\nEffect 是一个 Generator 函数，内部使用 yield 关键字，标识每一步的操作（不管是异步或同步）。\n\n## call 和 put\n\ndva 提供多个 effect 函数内部的处理函数，比较常用的是 `call` 和 `put`。\n\n* call：执行异步函数\n* put：发出一个 Action，类似于 dispatch\n\n## 课堂实战\n写一个列表，包含删除按钮，点删除按钮后延迟 1 秒执行删除。\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/qEVcuGVcKMGjlUNghHel.gif\" />\n"
  },
  {
    "path": "docs/guide/source-code-explore.md",
    "content": "# Dva 源码解析\n\n> 作者：杨光\n\n## 隐藏在 package.json 里的秘密\n\n随便哪个 dva 的项目，只要敲入 npm start 就可以运行启动。之前敲了无数次我都没有在意，直到我准备研究源码的时候才意识到：**在敲下这行命令的时候，到底发生了什么呢？**\n\n答案要去 package.json 里去寻找。\n\n>有位技术大牛曾经告诉过我：看源码之前，先去看 package.json 。看看项目的入口文件，翻翻它用了哪些依赖，对项目便有了大致的概念。\n\npackage.json 里是这么写的：\n\n```json\n \"scripts\": {\n    \"start\": \"roadhog server\"\n  },\n```\n\n翻翻依赖，`\"roadhog\": \"^0.5.2\"`。\n\n\n既然能在 devDependencies 找到，那么肯定也能在 [npm](https://www.npmjs.com/package/roadhog) 上找到。原来是个和 webpack 相似的库，而且作者看着有点眼熟...\n\n如果说 dva 是亲女儿，那 [roadhog](https://github.com/sorrycc/roadhog.git) 就是亲哥哥了，起的是 webpack 自动打包和热更替的作用。\n\n在 roadhog 的默认配置里有这么一条信息：\n\n```json\n{\n  \"entry\": \"src/index.js\",\n}\n```\n\n后转了一圈，启动的入口回到了 `src/index.js`。\n\n## `src/index.js`\n\n在 `src/index.js` 里，dva 一共做了这么几件事：\n\n0. 从 'dva' 依赖中引入 dva ：`import dva from 'dva'`; \n\n1. 通过函数生成一个 app 对象：`const app = dva()`; \n\n2. 加载插件：`app.use({})`;\n\n3. 注入 model：`app.model(require('./models/example'))`;\n\n4. 添加路由：`app.router(require('./routes/indexAnother'))`;\n\n5. 启动：app.start('#root');\n\n在这 6 步当中，dva 完成了 `使用 React 解决 view 层`、`redux 管理 model `、`saga 解决异步`的主要功能。事实上在我查阅资料以及回忆用过的脚手架时，发现目前端框架之所以被称为“框架”也就是解决了这些事情。前端工程师至今所做的事情都是在 **分离动态的 data 和静态的 view** ，只不过侧重点和实现方式也不同。\n\n至今为止出了这么多框架，但是前端 MVX 的思想一直都没有改变。\n\n# dva \n\n## 寻找 “dva”\n\n既然 dva 是来自于 `dva`，那么 dva 是什么这个问题自然要去 dva 的[源码](https://github.com/dvajs/dva)中寻找了。\n\n> 剧透：dva 是个函数，返回一了个 app 的对象。\n\n> 剧透2：目前 dva 的源码核心部分包含两部分，`dva` 和 `dva-core`。前者用高阶组件 React-redux 实现了 view 层，后者是用 redux-saga 解决了 model 层。\n\n老规矩，还是先翻 package.json 。\n\n引用依赖很好的说明了 dva 的功能：统一 view 层。\n\n```json\n// dva 使用的依赖如下：\n\n    \"babel-runtime\": \"^6.26.0\", // 一个编译后文件引用的公共库，可以有效减少编译后的文件体积\n    \"dva-core\": \"^1.1.0\", // dva 另一个核心，用于处理数据层\n    \"global\": \"^4.3.2\", // 用于提供全局函数的引用\n    \"history\": \"^4.6.3\", // browserHistory 或者 hashHistory\n    \"invariant\": \"^2.2.2\", // 一个有趣的断言库\n    \"isomorphic-fetch\": \"^2.2.1\", // 方便请求异步的函数，dva 中的 fetch 来源\n    \"react-async-component\": \"^1.0.0-beta.3\", // 组件懒加载\n    \"react-redux\": \"^5.0.5\", // 提供了一个高阶组件，方便在各处调用 store\n    \"react-router-dom\": \"^4.1.2\", // router4，终于可以像写组件一样写 router 了\n    \"react-router-redux\": \"5.0.0-alpha.6\",// redux 的中间件，在 provider 里可以嵌套 router\n    \"redux\": \"^3.7.2\" // 提供了 store、dispatch、reducer \n\t\n```\n不过 script 没有给太多有用的信息，因为 `ruban build` 中的 `ruban` 显然是个私人库(虽然在 tnpm 上可以查到但是也是私人库)。但根据惯例，应该是 dva 包下的 `index.js` 文件提供了对外调用：\n```js\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\n\nexports.default = require('./lib');\nexports.connect = require('react-redux').connect;\n```\n\n显然这个 `exports.default` 就是我们要找的 dva，但是源码中没有 `./lib` 文件夹。当然直接看也应该看不懂，因为一般都是使用 babel 的命令 `babel src -d libs` 进行编译后生成的，所以直接去看 `src/index.js` 文件。\n\n\n## `src/index.js`\n\n`src/index.js`[在此](https://github.com/dvajs/dva/blob/master/packages/dva/src/index.js) ：\n\n在这里，dva 做了三件比较重要的事情：\n\n1. 使用 call 给 dva-core 实例化的 app(这个时候还只有数据层) 的 start 方法增加了一些新功能（或者说，通过代理模式给 model 层增加了 view 层）。\n2. 使用 react-redux 完成了 react 到 redux 的连接。\n3. 添加了 redux 的中间件 react-redux-router，强化了 history 对象的功能。\n\n### 使用 call 方法实现代理模式\n\ndva 中实现代理模式的方式如下：\n\n**1. 新建 function ，函数内实例化一个 app 对象。**\n**2. 新建变量指向该对象希望代理的方法， `oldStart = app.start`。**\n**3. 新建同名方法 start，在其中使用 call，指定 oldStart 的调用者为 app。**\n**4. 令 app.start = start，完成对 app 对象的 start 方法的代理。**\n\n上代码:\n\n```js\nexport default function(opts = {}) {\n\n  // ...初始化 route ，和添加 route 中间件的方法。\n\n  /**\n   * 1. 新建 function ，函数内实例化一个 app 对象。\n   * \n   */\n  const app = core.create(opts, createOpts);\n  /**\n   * 2. 新建变量指向该对象希望代理的方法\n   * \n   */\n  const oldAppStart = app.start;\n  app.router = router;\n  /**\n   * 4. 令 app.start = start，完成对 app 对象的 start 方法的代理。\n   * @type {[type]}\n   */\n  app.start = start;\n  return app;\n\n  // router 赋值\n\n  /**\n   * 3.1 新建同名方法 start，\n   * \n   */\n  function start(container) {\n    // 合法性检测代码\n\n    /**\n     * 3.2 在其中使用 call，指定 oldStart 的调用者为 app。\n     */\n    oldAppStart.call(app);\n\t\n\t// 因为有 3.2 的执行才有现在的 store\n    const store = app._store;\n\n\t// 使用高阶组件创建视图\n  }\n}\n```  \n\n> 为什么不直接在 start 方式中 oldAppStart ?\n- 因为 dva-core 的 start 方法里有用到 this，不用 call 指定调用者为 app 的话，oldAppStart() 会找错对象。\n\n> 实现代理模式一定要用到 call 吗？\n- 不一定，看有没有 使用 this 或者代理的函数是不是箭头函数。从另一个角度来说，如果使用了 function 关键字又在内部使用了 this，那么一定要用 call/apply/bind 指定 this。\n\n> 前端还有那里会用到 call ？\n- 就实际开发来讲，因为已经使用了 es6 标准，基本和 this 没什么打交道的机会。使用 class 类型的组件中偶尔还会用到 this.xxx.bind(this)，stateless 组件就洗洗睡吧(因为压根没有 this)。如果实现代理，可以使用继承/反向继承的方法 —— 比如高阶组件。\n\n\n### 使用 react-redux 的高阶组件传递 store\n\n经过 call 代理后的 start 方法的主要作用，便是使用 react-redux 的 provider 组件将数据与视图联系了起来，生成 React 元素呈现给使用者。\n\n不多说，上代码。\n\n```js\n// 使用 querySelector 获得 dom\nif (isString(container)) {\n  container = document.querySelector(container);\n  invariant(\n    container,\n    `[app.start] container ${container} not found`,\n  );\n}\n\n// 其他代码\n\n// 实例化 store\noldAppStart.call(app); \nconst store = app._store;\n\n// export _getProvider for HMR\n// ref: https://github.com/dvajs/dva/issues/469\napp._getProvider = getProvider.bind(null, store, app);\n\n// If has container, render; else, return react component\n// 如果有真实的 dom 对象就把 react 拍进去\nif (container) {\n  render(container, store, app, app._router);\n  // 热加载在这里\n  app._plugin.apply('onHmr')(render.bind(null, container, store, app));\n} else {\n  // 否则就生成一个 react ，供外界调用\n  return getProvider(store, this, this._router);\n}\n  \n // 使用高阶组件包裹组件\nfunction getProvider(store, app, router) {\n  return extraProps => (\n    <Provider store={store}>\n      { router({ app, history: app._history, ...extraProps }) }\n    </Provider>\n  );\n}\n\n// 真正的 react 在这里\nfunction render(container, store, app, router) {\n  const ReactDOM = require('react-dom/client') // eslint-disable-line\n  ReactDOM.createRoot(container).render(React.createElement(getProvider(store, app, router)));\n}\n```\n\n> React.createElement(getProvider(store, app, router)) 怎么理解？\n- getProvider 实际上返回的不单纯是函数，而是一个无状态的 React 组件。从这个角度理解的话，ReactElement.createElement(string/ReactClass type,[object props],[children ...]) 是可以这么写的。\n\n> 怎么理解 React 的 stateless 组件和 class 组件？\n- 你猜猜？\n```\nJavaScript 并不存在 class 这个东西，即便是 es6 引入了以后经过 babel 编译也会转换成函数。因此直接使用无状态组件，省去了将 class 实例化再调用 render 函数的过程，有效的加快了渲染速度。\n\n即便是 class 组件，React.createElement 最终调用的也是 render 函数。不过这个目前只是我的推论，没有代码证据的证明。\n```\n\n#### react-redux 与 provider \n\n> provider 是个什么东西？\n\n本质上是个高阶组件，也是代理模式的一种实践方式。接收 redux 生成的 store 做参数后，通过上下文 context 将 store 传递进被代理组件。在保留原组件的功能不变的同时，增加了 store 的 dispatch 等方法。\n\n> connect 是个什么东西？\n\nconnect 也是一个代理模式实现的高阶组件，为被代理的组件实现了从 context 中获得 store 的方法。\n\n> connect()(MyComponent) 时发生了什么？\n\n只放关键部分代码，因为我也只看懂了关键部分(捂脸跑)：\n\n```js\nimport connectAdvanced from '../components/connectAdvanced' \nexport function createConnect({\n  connectHOC = connectAdvanced,\n.... 其他初始值\n} = {}) {\n\t\n  return function connect( { // 0 号 connnect\n    mapStateToProps,\n    mapDispatchToProps,\n   \t... 其他初始值\n    } = {}\n  ) {\n\t....其他逻辑\n    return connectHOC(selectorFactory, {//  1号 connect\n\t\t.... 默认参数\n\t\tselectorFactory 也是个默认参数\n      })\n  }\n}\n\nexport default createConnect() // 这是 connect 的本体，导出时即生成 connect 0\n\n```\n```js\n// hoist-non-react-statics，会自动把所有绑定在对象上的非React方法都绑定到新的对象上\nimport hoistStatics from 'hoist-non-react-statics'\n// 1号 connect 的本体\nexport default function connectAdvanced() {\n\t// 逻辑处理\n\n\t// 1 号 connect 调用时生成 2 号 connect\n  return function wrapWithConnect(WrappedComponent) {\n   \t// ... 逻辑处理\n\n\t// 在函数内定义了一个可以拿到上下文对象中 store 的组件\n    class Connect extends Component {\n      \n      getChildContext() {\n\t\t// 上下文对象中获得 store\n        const subscription = this.propsMode ? null : this.subscription\n        return { [subscriptionKey]: subscription || this.context[subscriptionKey] }\n      }\n\t\t\n\t\t// 逻辑处理\n\n      render() {\n\n\t\t  \t// \t最终生成了新的 react 元素，并添加了新属性\n          return createElement(WrappedComponent, this.addExtraProps(selector.props))\n\n      }\n    }\n\n\t// 逻辑处理\n\t\n\t// 最后用定义的 class 和 被代理的组件生成新的 react 组件\n    return hoistStatics(Connect, WrappedComponent)  // 2 号函数调用后生成的对象是组件\n  }\n}\n\n\n```\n结论：对于 connect()(MyComponent)\n\n1. connect 调用时生成 0 号 connect\n2. connect()  0 号 connect 调用，返回 1 号 connect 的调用 `connectHOC()` ，生成 2 号 connect(也是个函数) 。\n3. connect()(MyComponent) 等价于 connect2(MyComponent)，返回值是一个新的组件\n\n\n### redux 与 router\n\nredux 是状态管理的库，router 是(唯一)控制页面跳转的库。两者都很美好，但是不美好的是两者无法协同工作。换句话说，当路由变化以后，store 无法感知到。\n\n于是便有了 `react-router-redux`。\n\n`react-router-redux` 是 redux 的一个中间件(中间件：JavaScript 代理模式的另一种实践 针对 dispatch 实现了方法的代理，在 dispatch action 的时候增加或者修改) ，主要作用是：\n\n> 加强了React Router库中history这个实例，以允许将history中接受到的变化反应到state中去。\n\n[github 在此](https://github.com/reactjs/react-router-redux)\n\n从代码上讲，主要是监听了 history 的变化：\n\n`history.listen(location => analyticsService.track(location.pathname))`\n\ndva 在此基础上又进行了一层代理，把代理后的对象当作初始值传递给了 dva-core，方便其在 model 的 \nsubscriptions 中监听 router 变化。\n\n看看 `index.js` 里 router 的实现：\n\n1.在 createOpts 中初始化了添加 react-router-redux 中间件的方法和其 reducer ，方便 dva-core 在创建 store 的时候直接调用。\n\n2. 使用 patchHistory 函数代理 history.linsten，增加了一个回调函数的做参数(也就是订阅)。\n\n> subscriptions 的东西可以放在 dva-core 里再说，\n\n```js\nimport createHashHistory from 'history/createHashHistory';\nimport {\n  routerMiddleware,\n  routerReducer as routing,\n} from 'react-router-redux';\nimport * as core from 'dva-core';\n\nexport default function (opts = {}) {\n  const history = opts.history || createHashHistory();\n  const createOpts = {\n  \t// \t初始化 react-router-redux 的 router\n    initialReducer: {\n      routing,\n    },\n\t// 初始化 react-router-redux 添加中间件的方法，放在所有中间件最前面\n    setupMiddlewares(middlewares) {\n      return [\n        routerMiddleware(history),\n        ...middlewares,\n      ];\n    },\n\t// 使用代理模式为 history 对象增加新功能，并赋给 app\n    setupApp(app) {\n      app._history = patchHistory(history);\n    },\n  };\n\n  const app = core.create(opts, createOpts);\n  const oldAppStart = app.start;\n  app.router = router;\n  app.start = start;\n  return app;\n\n  function router(router) {\n    invariant(\n      isFunction(router),\n      `[app.router] router should be function, but got ${typeof router}`,\n    );\n    app._router = router;\n  }\n\n\n}\n\n// 使用代理模式扩展 history 对象的 listen 方法，添加了一个回调函数做参数并在路由变化是主动调用\nfunction patchHistory(history) {\n  const oldListen = history.listen;\n  history.listen = (callback) => {\n    callback(history.location);\n    return oldListen.call(history, callback);\n  };\n  return history;\n}\n```\n\n> 剧透：redux 中创建 store 的方法为：\n\n```js\n// combineReducers 接收的参数是对象\n// 所以 initialReducer 的类型是对象\n// 作用：将对象中所有的 reducer 组合成一个大的 reducer\nconst reducers = {}; \n// applyMiddleware 接收的参数是可变参数\n// 所以 middleware 是数组\n// 作用：将所有中间件组成一个数组，依次执行\nconst middleware = []; \nconst store = createStore(\n  combineReducers(reducers),\n  initial_state, // 设置 state 的初始值\n  applyMiddleware(...middleware)\n);\n```\n\n## 视图与数据(上)\n\n`src/index.js` 主要实现了 dva 的 view 层，同时传递了一些初始化数据到 dva-core 所实现的 model 层。当然，还提供了一些 dva 中常用的方法函数：\n\n- `dynamic` 动态加载(2.0 以后官方提供 1.x 自己手动实现吧)\n- `fetch` 请求方法(其实 dva 只是做了一把搬运工)\n- `saga`(数据层处理异步的方法)。\n\n这么看 dva 真的是很薄的一层封装。\n\n而 dva-core 主要解决了 model 的问题，包括 state 管理、数据的异步加载、订阅-发布模式的实现，可以作为数据层在别处使用(看 2.0 更新也确实是作者的意图)。使用的状体啊管理库还是 redux，异步加载的解决方案是 saga。当然，一切也都写在 index.js 和 package.json 里。\n\n## 视图与数据(下)\n\n处理 React 的 model 层问题有很多种办法，比如状态管理就不一定要用 Redux，也可以使用 Mobx(写法会更有 MVX 框架的感觉)；异步数据流也未必使用 redux-saga，redux-thunk 或者 redux-promise 的解决方式也可以(不过目前看来 saga 是相对更优雅的)。\n\n放两篇个人感觉比较全面的技术文档：\n\n- 阮一峰前辈的 [redux 三部曲](http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html)。\n- redux-saga 的[中文文档](http://leonshi.com/redux-saga-in-chinese/docs/api/index.html)。\n\n以及两者的 github：\n\n- [redux](https://github.com/reactjs/redux)\n- [redux-saga](https://github.com/redux-saga/redux-saga)\n\n然后继续深扒 `dva-core`，还是先从 `package.json` 扒起。\n\n## package.json\n\n`dva-core` 的 `package.json` 中依赖包如下：\n\n```json\n    \"babel-runtime\": \"^6.26.0\",  // 一个编译后文件引用的公共库，可以有效减少编译后的文件体积\n    \"flatten\": \"^1.0.2\", // 一个将多个数组值合并成一个数组的库\n    \"global\": \"^4.3.2\",// 用于提供全局函数比如 document 的引用\n    \"invariant\": \"^2.2.1\",// 一个有趣的断言库\n    \"is-plain-object\": \"^2.0.3\", // 判断是否是一个对象\n    \"redux\": \"^3.7.1\", // redux ，管理 react 状态的库\n    \"redux-saga\": \"^0.15.4\", // 处理异步数据流\n    \"warning\": \"^3.0.0\" // 同样是个断言库，不过输出的是警告\n```\n\n当然因为打包还是用的 `ruban`，script 里没有什么太多有用的东西。继续依循惯例，去翻 `src/index.js`。\n\n## `src/index.js`\n\n`src/index` 的源码在[这里](https://github.com/dvajs/dva/blob/master/packages/dva-core/src/index.js)\n\n在 `dva` 的 `src/index.js` 里，通过传递 2 个变量 `opts` 和 `createOpts` 并调用 `core.create`，`dva` 创建了一个 app 对象。其中 `opts` 是使用者添加的控制选项，`createOpts` 则是初始化了 reducer 与 redux 的中间件。\n\n`dva-core` 的 `src/index.js` 里便是这个 app 对象的具体创建过程以及包含的方法：\n\n```js\nexport function create(hooksAndOpts = {}, createOpts = {}) {\n  const {\n    initialReducer,\n    setupApp = noop,\n  } = createOpts;\n\n  const plugin = new Plugin();\n  plugin.use(filterHooks(hooksAndOpts));\n\n  const app = {\n    _models: [\n      prefixNamespace({ ...dvaModel }),\n    ],\n    _store: null,\n    _plugin: plugin,\n    use: plugin.use.bind(plugin),\n    model,\n    start,\n  };\n  return app;\n  \t// .... 方法的实现\n\t\n\tfunction model(){\n\t\t// model 方法\n\t}\n\t\n\tfunctoin start(){\n\t\t// Start 方法\n\t}\n  }\n  ```\n\n> 我最开始很不习惯 JavaScript 就是因为 JavaScript 还是一个函数向的编程语言，也就是函数里可以定义函数，返回值也可以是函数，class 最后也是被解释成函数。在 dva-core 里创建了 app 对象，但是把 model 和 start 的定义放在了后面。一开始对这种简写没看懂，后来熟悉了以后发现确实好理解。一眼就可以看到 app 所包含的方法，如果需要研究具体方法的话才需要向后看。\n\n[Plugin](https://github.com/dvajs/dva/blob/master/packages/dva-core/src/Plugin.js) 是作者设置的一堆**钩子**性监听函数——即是在符合某些条件的情况下下(dva 作者)进行手动调用。这样使用者只要按照作者设定过的关键词传递回调函数，在这些条件下便会自动触发。\n\n> 有趣的是，我最初理解**钩子**的概念是在 Angular 里。为了能像 React 一样优雅的控制组件的生命周期，Angular 设置了一堆接口(因为使用的是 ts，所以 Angular 里有类和接口的区分)。只要组件实现(implements)对应的接口————或者称生命周期钩子，在对应的条件下就会运行接口的方法。 \n\n#### Plugin 与 plugin.use\n\nPlugin 与 plugin.use 都有使用数组的 reduce 方法的行为：\n```js\nconst hooks = [\n  'onError',\n  'onStateChange',\n  'onAction',\n  'onHmr',\n  'onReducer',\n  'onEffect',\n  'extraReducers',\n  'extraEnhancers',\n];\n\nexport function filterHooks(obj) {\n  return Object.keys(obj).reduce((memo, key) => {\n  // 如果对象的 key 在 hooks 数组中\n  // 为 memo 对象添加新的 key，值为 obj 对应 key 的值\n    if (hooks.indexOf(key) > -1) {\n      memo[key] = obj[key];\n    }\n    return memo;\n  }, {});\n}\n\nexport default class Plugin {\n  constructor() {\n    this.hooks = hooks.reduce((memo, key) => {\n      memo[key] = [];\n      return memo;\n    }, {});\n\t/*\n\t\t等同于\n\t\t\n\t\tthis.hooks = {\n\t\t\tonError: [],\n\t\t\tonStateChange:[],\n\t\t\t....\n\t\t\textraEnhancers: []\n\t\t}\n\t*/\n  }\n\n  use(plugin) {\n    invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');\n    const hooks = this.hooks;\n    for (const key in plugin) {\n      if (Object.prototype.hasOwnProperty.call(plugin, key)) {\n        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);\n        if (key === 'extraEnhancers') {\n          hooks[key] = plugin[key];\n        } else {\n          hooks[key].push(plugin[key]);\n        }\n      }\n    }\n  }\n\n  // 其他方法\n}\n```\n- 构造器中的 `reduce` 初始化了一个以 `hooks` 数组所有元素为 key，值为空数组的对象，并赋给了 class 的私有变量 `this.hooks`。\n\n- `filterHooks` 通过 `reduce` 过滤了 `hooks` 数组以外的钩子。\n\n- `use` 中使用 `hasOwnProperty` 判断 `key` 是 `plugin` 的自身属性还是继承属性，使用原型链调用而不是 `plugin.hasOwnProperty()` 是防止使用者故意捣乱在 `plugin` 自己写一个 `hasOwnProperty = () => false // 这样无论如何调用 plugin.hasOwnProperty() 返回值都是 false`。\n\n- `use` 中使用 `reduce` 为 `this.hooks` 添加了 `plugin[key]` 。 \n\n## model 方法\n\n`model` 是 app 添加 model 的方法，在 **dva 项目** 的 index.js 是这么用的。\n\n> app.model(require('./models/example'));\n\n在 `dva` 中没对 model 做任何处理，所以 `dva-core` 中的 model 就是 **dva 项目** 里调用的 model。\n\n```js\n  function model(m) {\n    if (process.env.NODE_ENV !== 'production') {\n      checkModel(m, app._models);\n    }\n    app._models.push(prefixNamespace(m));\n  }\n  \n```\n\n- `checkModel` 主要是用 `invariant` 对传入的 model 进行了合法性检查。\n\n- `prefixNamespace` 又使用 reduce 对每一个 model 做处理，为 model 的 reducers 和 effects 中的方法添加了 `${namespace}/` 的前缀。\n\n> Ever wonder why we dispatch the action like this in dva ? `dispatch({type: 'example/loadDashboard'` \n\n## start 方法\n\n`start` 方法是 `dva-core` 的核心，在 `start` 方法里，dva 完成了 **`store` 初始化** 以及 **`redux-saga` 的调用**。比起 `dva` 的 `start`，它引入了更多的调用方式。\n\n一步一步分析：\n\n### `onError`\n\n```js\n    const onError = (err) => {\n      if (err) {\n        if (typeof err === 'string') err = new Error(err);\n        err.preventDefault = () => {\n          err._dontReject = true;\n        };\n        plugin.apply('onError', (err) => {\n          throw new Error(err.stack || err);\n        })(err, app._store.dispatch);\n      }\n    };\n```\n这是一个全局错误处理，返回了一个接收错误并处理的函数，并以 `err` 和 `app._store.dispatch` 为参数执行调用。\n\n看一下 `plugin.apply` 的实现：\n\n```js\n  apply(key, defaultHandler) {\n    const hooks = this.hooks;\n\t/* 通过 validApplyHooks 进行过滤， apply 方法只能应用在全局报错或者热更替上 */ \n    const  validApplyHooks = ['onError', 'onHmr'];\n    invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);\n\t/* 从钩子中拿出挂载的回调函数 ，挂载动作见 use 部分*/\n    const fns = hooks[key];\n\n    return (...args) => {\n\t\t// 如果有回调执行回调\n      if (fns.length) {\n        for (const fn of fns) {\n          fn(...args);\n        }\n\t\t// 没有回调直接抛出错误\n      } else if (defaultHandler) {\n        defaultHandler(...args);\n\t\t\n\t\t/*\n\t\t这里 defaultHandler 为 (err) => {\n          throw new Error(err.stack || err);\n        }\n\t\t*/\n      }\n    };\n  }\n  ```\n\n###  `sagaMiddleware`\n\n下一行代码是：    \n\n> `const sagaMiddleware = createSagaMiddleware();`\n\n和 `redux-sagas` 的入门教程有点差异，因为正统的教程上添加 sagas 中间件的方法是： `createSagaMiddleware(...sagas)`\n\n> sagas 为含有 saga 方法的 generator 函数数组。\n\n但是 api 里确实还提到，还有一~~~招从天而降的掌法~~~种动态调用的方式：\n\n>  `const task = sagaMiddleware.run(dynamicSaga)`\n\n于是：\n\n```js\n\t  const sagaMiddleware = createSagaMiddleware();\n\t  // ...\n      const sagas = [];\n      const reducers = {...initialReducer\n      };\n      for (const m of app._models) {\n      \treducers[m.namespace] = getReducer(m.reducers, m.state);\n      \tif (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));\n      }\n      // ....\n\n      store.runSaga = sagaMiddleware.run;\n      // Run sagas\n      sagas.forEach(sagaMiddleware.run);\n```\n\n### `sagas`\n\n那么 sagas 是什么呢？\n\n```js\n    const {\n      middleware: promiseMiddleware,\n      resolve,\n      reject,\n    } = createPromiseMiddleware(app);\n    app._getSaga = getSaga.bind(null, resolve, reject);\n\n    const sagas = [];\n \n    for (const m of app._models) {\n      if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));\n    }\n```\n\n显然，sagas 是一个数组，里面的元素是用 `app._getSaga` 处理后的返回结果，而 `app._getSaga` 又和上面 createPromiseMiddleware 代理 app 后返回的对象有很大关系。\n\n#### `createPromiseMiddleware`\n\ncreatePromiseMiddleware 的代码[在此](https://github.com/dvajs/dva/blob/master/packages/dva-core/src/createPromiseMiddleware.js)。\n\n如果看着觉得眼熟，那肯定不是因为看过 redux-promise 源码的缘故，:-p。\n\n##### `middleware`\n\n`middleware` 是一个 redux 的中间件，即在不影响 redux 本身功能的情况下为其添加了新特性的代码。redux 的中间件通过拦截 action 来实现其作用的。\n\n```js\n  const middleware = () => next => (action) => {\n    const { type } = action;\n    if (isEffect(type)) {\n      return new Promise((resolve, reject) => {\n\t\t// .... resolve ,reject\n      });\n    } else {\n      return next(action);\n    }\n  };\n  \n    function isEffect(type) {\n\t\t// dva 里 action 的 type 有固定格式： model.namespace/model.effects\n\t\t// const [namespace] = type.split(NAMESPACE_SEP); 是 es6 解构的写法\n\t\t// 等同于 const namespace = type.split(NAMESPACE_SEP)[0];\n\t\t// NAMESPACE_SEP 的值是 `/`\n    \tconst [namespace] = type.split(NAMESPACE_SEP);\n\t\t// 根据 namespace 过滤出对应的 model\n    \tconst model = app._models.filter(m => m.namespace === namespace)[0];\n\t\t// 如果 model 存在并且 model.effects[type] 也存在，那必然是 effects\n    \tif (model) {\n    \t\tif (model.effects && model.effects[type]) {\n    \t\t\treturn true;\n    \t\t}\n    \t}\n\n    \treturn false;\n    }\n  ```\n\n>  const middleware = ({dispatch}) => next => (action) => {... return next(action)} 基本上是一个标准的中间件写法。在 return next(action) 之前可以对 action 做各种各样的操作。因为此中间件没用到 dispatch 方法，所以省略了。\n\n本段代码的意思是，如果 dispatch 的 action 指向的是 model 里的 effects，那么返回一个 Promise 对象。此 Promise 的对象的解决( resolve )或者驳回方法 ( reject ) 放在 map 对象中。如果是非 effects (那就是 action 了)，放行。\n\n换句话说，middleware 拦截了指向 effects 的 action。\n\n##### 神奇的 bind\n\nbind 的作用是绑定新的对象，生成新函数是大家都知道概念。但是 bind 也可以提前设定好函数的某些参数生成新函数，等到最后一个参数确定时直接调用。\n\n> JavaScript 的参数是怎么被调用的？[JavaScript 专题之函数柯里化](https://juejin.im/post/598d0b7ff265da3e1727c491)。作者：[冴羽](https://juejin.im/user/58e4b9b261ff4b006b3227f4)。文章来源：[掘金](https://juejin.im/timeline)\n\n这段代码恰好就是 bind 的一种实践方式。\n\n```js\n  const map = {};\n\n  const middleware = () => next => (action) => {\n    const { type } = action;\n    // ...\n      return new Promise((resolve, reject) => {\n        map[type] = {\n          resolve: wrapped.bind(null, type, resolve),\n          reject: wrapped.bind(null, type, reject),\n        };\n      });\n\t// ....\n  };\n  \n  function wrapped(type, fn, args) {\n    if (map[type]) delete map[type];\n    fn(args);\n  }\n\n  function resolve(type, args) {\n    if (map[type]) {\n      map[type].resolve(args);\n    }\n  }\n\n  function reject(type, args) {\n    if (map[type]) {\n      map[type].reject(args);\n    }\n  }\n  \n   return {\n    middleware,\n    resolve,\n    reject,\n  };\n```\n分析这段代码，dva 是这样做的：\n\n1. 通过 `wrapped.bind(null, type, resolve)` 产生了一个新函数，并且赋值给匿名对象的 resolve 属性(reject 同理)。\n\n> 1.1 wrap 接收三个参数，通过 bind 已经设定好了两个。`wrapped.bind(null, type, resolve)` 等同于 `wrap(type, resolve, xxx)`（**此处  `resolve` 是 Promise 对象中的**）。 \n\n> 1.2 通过 bind 赋给匿名对象的 resolve 属性后，匿名对象.resolve(xxxx) 等同于 wrap(type, resolve, xxx)，即 reslove(xxx)。\n\n2. 使用 type 在 map 对象中保存此匿名对象，而 type 是 action 的 type，即 namespace/effects 的形式，方便之后进行调用。\n\n3. return 出的 resolve 接收 type 和 args 两个参数。type 用来在 map 中寻找 1 里的匿名函数，args 用来像 1.2 里那样执行。\n\n> 这样做的作用是：分离了 promise 与 promise 的执行。在函数的作用域外依然可以访问到函数的内部变量，换言之：闭包。\n\n#### `getSaga`\n\n导出的 `resolve` 与 `reject` 方法，通过 bind 先设置进了 `getSaga` (同时也赋给了 `app._getSaga`)，sagas 最终也将 `getSaga` 的返回值放入了数组。\n\n[getSaga 源码](https://github.com/dvajs/dva/blob/master/packages/dva-core/src/getSaga.js)\n\n```js\nexport default function getSaga(resolve, reject, effects, model, onError, onEffect) {\n  return function *() {\n    for (const key in effects) {\n      if (Object.prototype.hasOwnProperty.call(effects, key)) {\n        const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);\n\t\t// 将 watcher 分离到另一个线程去执行\n        const task = yield sagaEffects.fork(watcher);\n\t\t// 同时 fork 了一个线程，用于在 model 卸载后取消正在进行中的 task\n\t\t// `${model.namespace}/@@CANCEL_EFFECTS` 的发出动作在 index.js 的 start 方法中，unmodel 方法里。\n        yield sagaEffects.fork(function *() {\n          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);\n          yield sagaEffects.cancel(task);\n        });\n      }\n    }\n  };\n}\n```\n可以看到，`getSaga` 最终返回了一个 [generator 函数](http://www.ruanyifeng.com/blog/2015/04/generator.html)。\n\n在该函数遍历了 **model 中 effects 属性** 的所有方法（注：同样是 generator 函数）。结合 `index.js` 里的 ` for (const m of app._models)`，该遍历针对所有的 model。\n\n对于每一个 effect，getSaga 生成了一个 watcher ，并使用 saga 函数的 **fork** 将该函数切分到另一个单独的线程中去（生成了一个 task 对象）。同时为了方便对该线程进行控制，在此 fork 了一个 generator 函数。在该函数中拦截了取消 effect 的 action（事实上，应该是卸载effect 所在 model 的 action），一旦监听到则立刻取消分出去的 task 线程。\n\n##### getWatcher\n\n```js\nfunction getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {\n  let effect = _effect;\n  let type = 'takeEvery';\n  let ms;\n\n  if (Array.isArray(_effect)) {\n\t// effect 是数组而不是函数的情况下暂不考虑\n  }\n\n  function *sagaWithCatch(...args) {\n\t\t// .... sagaWithCatch 的逻辑\n  }\n\n  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);\n\n  switch (type) {\n    case 'watcher':\n      return sagaWithCatch;\n    case 'takeLatest':\n      return function*() {\n        yield takeLatest(key, sagaWithOnEffect);\n      };\n    case 'throttle':\n      return function*() {\n        yield throttle(ms, key, sagaWithOnEffect);\n      };\n    default:\n      return function*() {\n        yield takeEvery(key, sagaWithOnEffect);\n      };\n  }\n}\n\nfunction createEffects(model) {\n\t// createEffects(model) 的逻辑\n}\n\nfunction applyOnEffect(fns, effect, model, key) {\n  for (const fn of fns) {\n    effect = fn(effect, sagaEffects, model, key);\n  }\n  return effect;\n}\n```\n\n先不考虑 effect 的属性是数组而不是方法的情况。\n\n`getWatcher` 接收六个参数：\n- `resolve/reject`: 中间件 `middleware` 的 res 和 rej 方法。\n- `key`:经过 prefixNamespace 转义后的 effect 方法名，namespace/effect（也是调用 action 时的 type）。\n-` _effect`:effects 中 key 属性所指向的 generator 函数。\n- `model`： model\n- `onError`： 之前定义过的捕获全局错误的方法\n- `onEffect`：plugin.use 中传入的在触发 effect 时执行的回调函数（钩子函数）\n\n\n`applyOnEffect` 对 effect 进行了动态代理，在保证 effect （即 `_effect`）正常调用的情况下，为期添加了 fns 的回调函数数组(即 `onEffect`)。使得在 effect 执行时， `onEffect` 内的每一个回调函数都可以被触发。\n\n因为没有经过 effects 的属性是数组的情况，所以 `type` 的值是 `takeEvery`，也就是监听每一个发出的 action ，即 `getWatcher` 的返回值最终走的是 switch 的 default 选项:\n\n```js\nfunction*() {\n        yield takeEvery(key, sagaWithOnEffect);\n      };\n\t  \n```\n换句话说，每次发出指向 effects 的函数都会调用 `sagaWithOnEffect`。\n\n根据 `const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);` 的执行情况，如果 onEffect 的插件为空的情况下，`sagaWithOnEffect` 的值为 `sagaWithCatch`。\n\n```js\n  function *sagaWithCatch(...args) {\n    try {\n      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });\n      const ret = yield effect(...args.concat(createEffects(model)));\n      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });\n      resolve(key, ret);\n    } catch (e) {\n      onError(e);\n      if (!e._dontReject) {\n        reject(key, e);\n      }\n    }\n  }\n\n```\n\n在 `sagaWithOnEffect` 函数中，sagas 使用传入的参数(也就是 action)执行了对应的 model 中 对应的 effect 方法，同时将返回值使用之前保存在 map 里的 resolve 返回了其返回值。同时在执行 effect 方法的时候，将 saga 本身的所有方法(put、call、fork 等等)作为第二个参数，使用 `concat` 拼接在 action 的后面。在执行 effect 方法前，又发出了 start 和 end 两个 action，方便 onEffect 的插件进行拦截和调用。\n\n因此，对于 `if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));`。\n\n1. dva 通过 `app._getSaga(m.effects, m, onError, plugin.get('onEffect'))` 返回了一个 genenrator 函数。\n2. 在 genenrator 函数中手动 fork 出一个 watcher 函数的监听线程(当然也 fork 了取消线程的功能)。\n3. 该函数(在普通状态下)是一个 takeEvery 的阻塞是线程，接收 2 个参数。第一个参数为监听的 action，第二个参数为监听到 action 后的回调函数。\n4. (普通状态下)的回调函数，就是手动调用了 model 里 effects 中对应属性的函数。在此之前之后发出了 `start` 和 `end` 的 action，同时用之前 promise 中间件保存在 map 中的 resolve 方法返回了值。\n5. 最后使用 sagas.forEach(sagaMiddleware.run) 启动了 watcher 的监听。\n\n### store\n\n现在已经有了针对异步数据流的解决办法，那么该创建 store 了。\n\n正常情况的 redux 的 createStore 接收三个参数 reducer, initState,applyMiddleware(middlewares)。\n\n不过 dva 提供了自己的 `createStore` 方法，用来组织一系列自己创建的参数。\n```js\n    // Create store\n    const store = app._store = createStore({ // eslint-disable-line\n      reducers: createReducer(),\n      initialState: hooksAndOpts.initialState || {},\n      plugin,\n      createOpts,\n      sagaMiddleware,\n      promiseMiddleware,\n    });\n```\n\n#### createReducer\n\n```js\n    function createReducer() {\n      return reducerEnhancer(combineReducers({\n        ...reducers,\n        ...extraReducers,\n        ...(app._store ? app._store.asyncReducers : {}),\n      }));\n    }\n```\n\n`createReducer` 实际上是用 plugin 里的 onReducer (如果有)扩展了 reducer 功能，对于 `const reducerEnhancer = plugin.get('onReducer');`，plugin 里的相关代码为：\n\n```js\nfunction getOnReducer(hook) {\n  return function (reducer) {\n    for (const reducerEnhancer of hook) {\n      reducer = reducerEnhancer(reducer);\n    }\n    return reducer;\n  };\n}\n\n```\n\n> 如果有 onReducer 的插件，那么用 reducer 的插件扩展 reducer；否则直接返回 reducer。\n\ncombineReducers 中：\n- 第一个 `...reducers` 是从 dva 里传入的 historyReducer，以及通过 ` reducers[m.namespace] = getReducer(m.reducers, m.state);` 剥离出的 model 中的 reducer\n- 第二个参数为手动在 plugin 里添加的 extraReducers；\n- 第三个参数为异步 reducer，主要是用于在 dva 运行以后动态加载 model 里的 reducer。\n\n\n#### createStore\n\n\n现在我们有了一个 combine 过的 reducer，有了 core 中创建的 sagaMiddleware 和 promiseMiddleware，还有了从 dva 中传入的 createOpts，现在可以正式创建 store 了。\n\n> 从 dva 中传入的 createOpts 为 \n```js\n    setupMiddlewares(middlewares) {\n      return [\n        routerMiddleware(history),\n        ...middlewares,\n      ];\n    },\n```\n> 用与把 redux-router 的中间件排在中间件的第一个。\n\n\n虽然看起来很长，但是对于大多数普通用户来说，在未开启 redux 的调试插件，未传入额外的 onAction 以及 extraEnhancers 的情况下，上面的代码等价于:\n\n```js\nimport { createStore, applyMiddleware, compose } from 'redux';\nimport flatten from 'flatten';\nimport invariant from 'invariant';\nimport window from 'global/window';\nimport { returnSelf, isArray } from './utils';\n\nexport default function ({\n  reducers,\n  initialState,\n  plugin,\n  sagaMiddleware,\n  promiseMiddleware,\n  createOpts: {\n    setupMiddlewares = returnSelf,\n  },\n}) {\n\n  const middlewares = setupMiddlewares([\n    sagaMiddleware,\n    promiseMiddleware\n  ]);\n\n  const enhancers = [\n    applyMiddleware(...middlewares)\n  ];\n\n  return createStore(reducers, initialState, compose(...enhancers));\n  // 对于 redux 中 的 compose 函数，在数组长度为 1  的情况下返回第一个元素。\n  // compose(...enhancers) 等同于 applyMiddleware(...middlewares)\n}\n\n```\n\n### 订阅\n\n现在 dva 已经创建了 store，有了异步数据流加载方案，并且又做了一些其他的事情：\n\n```js\n    // Extend store\n    store.runSaga = sagaMiddleware.run;\n    store.asyncReducers = {};\n\n    // Execute listeners when state is changed\n    const listeners = plugin.get('onStateChange');\n    for (const listener of listeners) {\n      store.subscribe(() => {\n        listener(store.getState());\n      });\n    }\n\n    // Run sagas\n    sagas.forEach(sagaMiddleware.run);\n```\n\n- 手动运行 getSaga 里返回的 watcer 函数。\n- 判断如果有 onStateChange 的 plugin 也手动运行一下。\n\nmodel 里的 state、effect、reducer 已经实现了，就缺最后的订阅 subscription 部分。\n\n```js\n    // Setup app\n    setupApp(app);\n\n    // Run subscriptions\n    const unlisteners = {};\n    for (const model of this._models) {\n      if (model.subscriptions) {\n        unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);\n      }\n    }\n```\n\nsetupApp(app) 是从 dva 里传过来的，主要是使用 patchHistory 函数代理 history.linsten，即强化了 redux 和 router 的联系，是的路径变化可以引起 state 的变化，进而听过监听 state 的变化来触发回调。\n> 这也是 core 中唯一使用 this 的地方，逼得 dva 中必须使用 oldStart.call(app) 来进行调用。\n\n#### runSubscription\n\n这是 runSubscription 的代码\n\n```js\nexport function run(subs, model, app, onError) {\n  const funcs = [];\n  const nonFuncs = [];\n  for (const key in subs) {\n    if (Object.prototype.hasOwnProperty.call(subs, key)) {\n      const sub = subs[key];\n      const unlistener = sub({\n        dispatch: prefixedDispatch(app._store.dispatch, model),\n        history: app._history,\n      }, onError);\n      if (isFunction(unlistener)) {\n        funcs.push(unlistener);\n      } else {\n        nonFuncs.push(key);\n      }\n    }\n  }\n  return { funcs, nonFuncs };\n}\n```\n- 第一个参数为 model 中的 subscription 对象。\n- 第二个参数为对应的 model\n- 第三个参数为 core 里创建的 app\n- 第四个参数为全局异常捕获的 onError\n\n1. `Object.prototype.hasOwnProperty.call(subs, key)` \n还是使用原型方法判断 key 是不是 subs 的自有属性。\n\n2. 如果是自由属性，那么拿到属性对应的值(是一个 function)\n\n3. 调用该 function，传入 dispatch 和 history 属性。history 就是经过 redux-router 强化过的 history，而 dispatch，也就是 `prefixedDispatch(app._store.dispatch, model)`\n\n```js\nexport default function prefixedDispatch(dispatch, model) {\n  return (action) => {\n\t// 断言检测\n    return dispatch({ ...action, type: prefixType(type, model) });\n  };\n}\n\n```\n\n实际上是用将 action 里的 type 添加了 `${model.namespance}/` 的前缀。\n\n自此，model 中的四大组件全部完毕，完成了 dva 的数据层处理。\n"
  },
  {
    "path": "docs/knowledgemap/README.md",
    "content": "---\nsidebarDepth: 2\n---\n\n# 知识地图\n\n- [Read \"the dva.js Knowledgemap\" in English](https://github.com/dvajs/dva-knowledgemap/blob/master/README_en.md)\n- [\"the dva.js Knowledgemap\" 日本語版](https://github.com/dvajs/dva-knowledgemap/blob/master/README_ja.md)\n\n---\n\n不知大家学 react 或 dva 时会不会有这样的疑惑：\n\n- es6 特性那么多，我需要全部学会吗?\n- react component 有 3 种写法，我需要全部学会吗?\n- reducer 的增删改应该怎么写?\n- 怎么做全局/局部的错误处理?\n- 怎么发异步请求?\n- 怎么处理复杂的异步业务逻辑?\n- 怎么配置路由?\n- ...\n\n这篇文档梳理了基于 [dva-cli](https://github.com/dvajs/dva-cli) 使用 [dva](https://github.com/dvajs/dva) 的最小知识集，让你可以用最少的时间掌握创建类似 [dva-hackernews](https://github.com/dvajs/dva-hackernews) 应用的全部知识，并且不需要掌握额外的冗余知识。\n\n## JavaScript 语言\n\n### 变量声明\n\n#### const 和 let\n\n不要用 `var`，而是用 `const` 和 `let`，分别表示常量和变量。不同于 `var` 的函数作用域，`const` 和 `let` 都是块级作用域。\n\n```javascript\nconst DELAY = 1000;\n\nlet count = 0;\ncount = count + 1;\n```\n\n#### 模板字符串\n\n模板字符串提供了另一种做字符串组合的方法。\n\n```javascript\nconst user = 'world';\nconsole.log(`hello ${user}`);  // hello world\n\n// 多行\nconst content = `\n  Hello ${firstName},\n  Thanks for ordering ${qty} tickets to ${event}.\n`;\n```\n\n#### 默认参数\n\n```javascript\nfunction logActivity(activity = 'skiing') {\n  console.log(activity);\n}\n\nlogActivity();  // skiing\n```\n\n### 箭头函数\n\n函数的快捷写法，不需要通过 `function` 关键字创建函数，并且还可以省略 `return` 关键字。\n\n同时，箭头函数还会继承当前上下文的 `this` 关键字。\n\n比如：\n\n```javascript\n[1, 2, 3].map(x => x + 1);  // [2, 3, 4]\n```\n\n等同于：\n\n```javascript\n[1, 2, 3].map((function(x) {\n  return x + 1;\n}).bind(this));\n```\n\n### 模块的 Import 和 Export\n\n`import` 用于引入模块，`export` 用于导出模块。\n\n比如：\n\n```javascript\n// 引入全部\nimport dva from 'dva';\n\n// 引入部分\nimport { connect } from 'dva';\nimport { Link, Route } from 'dva/router';\n\n// 引入全部并作为 github 对象\nimport * as github from './services/github';\n\n// 导出默认\nexport default App;\n// 部分导出，需 import { App } from './file'; 引入\nexport class App extend Component {};\n```\n\n### ES6 对象和数组\n\n#### 析构赋值\n\n析构赋值让我们从 Object 或 Array 里取部分数据存为变量。\n\n```javascript\n// 对象\nconst user = { name: 'guanguan', age: 2 };\nconst { name, age } = user;\nconsole.log(`${name} : ${age}`);  // guanguan : 2\n\n// 数组\nconst arr = [1, 2];\nconst [foo, bar] = arr;\nconsole.log(foo);  // 1\n```\n\n我们也可以析构传入的函数参数。\n\n```javascript\nconst add = (state, { payload }) => {\n  return state.concat(payload);\n};\n```\n\n析构时还可以配 alias，让代码更具有语义。\n\n```javascript\nconst add = (state, { payload: todo }) => {\n  return state.concat(todo);\n};\n```\n\n#### 对象字面量改进\n\n这是析构的反向操作，用于重新组织一个 Object 。\n\n```javascript\nconst name = 'duoduo';\nconst age = 8;\n\nconst user = { name, age };  // { name: 'duoduo', age: 8 }\n```\n\n定义对象方法时，还可以省去 `function` 关键字。\n\n```javascript\napp.model({\n  reducers: {\n    add() {}  // 等同于 add: function() {}\n  },\n  effects: {\n    *addRemote() {}  // 等同于 addRemote: function*() {}\n  },\n});\n```\n\n#### Spread Operator\n\nSpread Operator 即 3 个点 `...`，有几种不同的使用方法。\n\n可用于组装数组。\n\n```javascript\nconst todos = ['Learn dva'];\n[...todos, 'Learn antd'];  // ['Learn dva', 'Learn antd']\n```\n\n也可用于获取数组的部分项。\n\n```javascript\nconst arr = ['a', 'b', 'c'];\nconst [first, ...rest] = arr;\nrest;  // ['b', 'c']\n\n// With ignore\nconst [first, , ...rest] = arr;\nrest;  // ['c']\n```\n\n还可收集函数参数为数组。\n\n```javascript\nfunction directions(first, ...rest) {\n  console.log(rest);\n}\ndirections('a', 'b', 'c');  // ['b', 'c'];\n```\n\n代替 apply。\n\n```javascript\nfunction foo(x, y, z) {}\nconst args = [1,2,3];\n\n// 下面两句效果相同\nfoo.apply(null, args);\nfoo(...args);\n```\n\n对于 Object 而言，用于组合成新的 Object 。(ES2017 stage-2 proposal)\n\n```javascript\nconst foo = {\n  a: 1,\n  b: 2,\n};\nconst bar = {\n  b: 3,\n  c: 2,\n};\nconst d = 4;\n\nconst ret = { ...foo, ...bar, d };  // { a:1, b:3, c:2, d:4 }\n```\n\n此外，在 JSX 中 Spread Operator 还可用于扩展 props，详见 [Spread Attributes](#spread-attributes)。\n\n### Promises\n\nPromise 用于更优雅地处理异步请求。比如发起异步请求：\n\n```javascript\nfetch('/api/todos')\n  .then(res => res.json())\n  .then(data => ({ data }))\n  .catch(err => ({ err }));\n```\n\n定义 Promise 。\n\n```javascript\nconst delay = (timeout) => {\n  return new Promise(resolve => {\n    setTimeout(resolve, timeout);\n  });\n};\n\ndelay(1000).then(_ => {\n  console.log('executed');\n});\n```\n\n### Generators\n\ndva 的 effects 是通过 generator 组织的。Generator 返回的是迭代器，通过 `yield` 关键字实现暂停功能。\n\n这是一个典型的 dva effect，通过 `yield` 把异步逻辑通过同步的方式组织起来。\n\n```javascript\napp.model({\n  namespace: 'todos',\n  effects: {\n    *addRemote({ payload: todo }, { put, call }) {\n      yield call(addTodo, todo);\n      yield put({ type: 'add', payload: todo });\n    },\n  },\n});\n```\n\n## React Component\n\n###  Stateless Functional Components\n\nReact Component 有 3 种定义方式，分别是 `React.createClass`, `class` 和 `Stateless Functional Component`。推荐尽量使用最后一种，保持简洁和无状态。这是函数，不是 Object，没有 `this` 作用域，是 pure function。\n\n比如定义 App Component 。\n\n```javascript\nfunction App(props) {\n  function handleClick() {\n    props.dispatch({ type: 'app/create' });\n  }\n  return <div onClick={handleClick}>${props.name}</div>\n}\n```\n\n等同于：\n\n```javascript\nclass App extends React.Component {\n  handleClick() {\n    this.props.dispatch({ type: 'app/create' });\n  }\n  render() {\n    return <div onClick={this.handleClick.bind(this)}>${this.props.name}</div>\n  }\n}\n```\n\n### JSX\n\n#### Component 嵌套\n\n类似 HTML，JSX 里可以给组件添加子组件。\n\n```html\n<App>\n  <Header />\n  <MainContent />\n  <Footer />\n</App>\n```\n\n#### className\n\n`class` 是保留词，所以添加样式时，需用 `className` 代替 `class` 。\n\n```html\n<h1 className=\"fancy\">Hello dva</h1>\n```\n\n#### JavaScript 表达式\n\nJavaScript 表达式需要用 `{}` 括起来，会执行并返回结果。\n\n比如：\n\n```javascript\n<h1>{ this.props.title }</h1>\n```\n\n#### Mapping Arrays to JSX\n\n可以把数组映射为 JSX 元素列表。\n\n```javascript\n<ul>\n  { this.props.todos.map((todo, i) => <li key={i}>{todo}</li>) }\n</ul>\n```\n\n#### 注释\n\n尽量别用 `//` 做单行注释。\n\n```javascript\n<h1>\n  {/* multiline comment */}\n  {/*\n    multi\n    line\n    comment\n    */}\n  {\n    // single line\n  }\n  Hello\n</h1>\n```\n\n#### Spread Attributes\n\n这是 JSX 从 ECMAScript6 借鉴过来的很有用的特性，用于扩充组件 props 。\n\n比如：\n\n```javascript\nconst attrs = {\n  href: 'http://example.org',\n  target: '_blank',\n};\n<a {...attrs}>Hello</a>\n```\n\n等同于\n\n```javascript\nconst attrs = {\n  href: 'http://example.org',\n  target: '_blank',\n};\n<a href={attrs.href} target={attrs.target}>Hello</a>\n```\n\n### Props\n\n数据处理在 React 中是非常重要的概念之一，分别可以通过 props, state 和 context 来处理数据。而在 dva 应用里，你只需关心 props 。\n\n#### propTypes\n\nJavaScript 是弱类型语言，所以请尽量声明 propTypes 对 props 进行校验，以减少不必要的问题。\n\n```javascript\nfunction App(props) {\n  return <div>{props.name}</div>;\n}\nApp.propTypes = {\n  name: React.PropTypes.string.isRequired,\n};\n```\n\n内置的 prop type 有：\n\n- PropTypes.array\n- PropTypes.bool\n- PropTypes.func\n- PropTypes.number\n- PropTypes.object\n- PropTypes.string\n\n#### 往下传数据\n\n![](https://zos.alipayobjects.com/rmsportal/NAzeMyUoPMqxfRv.png)\n\n#### 往上传数据\n\n![](https://zos.alipayobjects.com/rmsportal/fiKKgDGuEJfSvxv.png)\n\n### CSS Modules\n\n<img src=\"https://zos.alipayobjects.com/rmsportal/mHVRpjNYhVuFdsS.png\" width=\"150\" style=\"background:#fff;\" />\n\n#### 理解 CSS Modules\n\n一张图理解 CSS Modules 的工作原理：\n\n![](https://zos.alipayobjects.com/rmsportal/SWBwWTbZKqxwEPq.png)\n\n`button` class 在构建之后会被重命名为 `ProductList_button_1FU0u` 。`button` 是 local name，而 `ProductList_button_1FU0u` 是 global name 。**你可以用简短的描述性名字，而不需要关心命名冲突问题。**\n\n然后你要做的全部事情就是在 css/less 文件里写 `.button {...}`，并在组件里通过 `styles.button` 来引用他。\n\n#### 定义全局 CSS\n\nCSS Modules 默认是局部作用域的，想要声明一个全局规则，可用 `:global` 语法。\n\n比如：\n\n```css\n.title {\n  color: red;\n}\n:global(.title) {\n  color: green;\n}\n```\n\n然后在引用的时候：\n\n```javascript\n<App className={styles.title} /> // red\n<App className=\"title\" />        // green\n```\n\n#### `classnames` Package\n\n在一些复杂的场景中，一个元素可能对应多个 className，而每个 className 又基于一些条件来决定是否出现。这时，[classnames](https://github.com/JedWatson/classnames) 这个库就非常有用。\n\n```javascript\nimport classnames from 'classnames';\nconst App = (props) => {\n  const cls = classnames({\n    btn: true,\n    btnLarge: props.type === 'submit',\n    btnSmall: props.type === 'edit',\n  });\n  return <div className={ cls } />;\n}\n```\n\n这样，传入不同的 type 给 App 组件，就会返回不同的 className 组合：\n\n```javascript\n<App type=\"submit\" /> // btn btnLarge\n<App type=\"edit\" />   // btn btnSmall\n```\n\n## Reducer\n\nreducer 是一个函数，接受 state 和 action，返回老的或新的 state 。即：`(state, action) => state`\n\n### 增删改\n\n以 todos 为例。\n\n```javascript\napp.model({\n  namespace: 'todos',\n  state: [],\n  reducers: {\n    add(state, { payload: todo }) {\n      return state.concat(todo);\n    },\n    remove(state, { payload: id }) {\n      return state.filter(todo => todo.id !== id);\n    },\n    update(state, { payload: updatedTodo }) {\n      return state.map(todo => {\n        if (todo.id === updatedTodo.id) {\n          return { ...todo, ...updatedTodo };\n        } else {\n          return todo;\n        }\n      });\n    },\n  },\n};\n```\n\n### 嵌套数据的增删改\n\n建议最多一层嵌套，以保持 state 的扁平化，深层嵌套会让 reducer 很难写和难以维护。\n\n```javascript\napp.model({\n  namespace: 'app',\n  state: {\n    todos: [],\n    loading: false,\n  },\n  reducers: {\n    add(state, { payload: todo }) {\n      const todos = state.todos.concat(todo);\n      return { ...state, todos };\n    },\n  },\n});\n```\n\n下面是深层嵌套的例子，应尽量避免。\n\n```javascript\napp.model({\n  namespace: 'app',\n  state: {\n    a: {\n      b: {\n        todos: [],\n        loading: false,\n      },\n    },\n  },\n  reducers: {\n    add(state, { payload: todo }) {\n      const todos = state.a.b.todos.concat(todo);\n      const b = { ...state.a.b, todos };\n      const a = { ...state.a, b };\n      return { ...state, a };\n    },\n  },\n});\n```\n\n## Effect\n\n示例：\n\n```javascript\napp.model({\n  namespace: 'todos',\n  effects: {\n    *addRemote({ payload: todo }, { put, call }) {\n      yield call(addTodo, todo);\n      yield put({ type: 'add', payload: todo });\n    },\n  },\n});\n```\n\n### Effects\n\n#### put\n\n用于触发 action 。\n\n```javascript\nyield put({ type: 'todos/add', payload: 'Learn Dva' });\n```\n\n#### call\n\n用于调用异步逻辑，支持 promise 。\n\n```javascript\nconst result = yield call(fetch, '/todos');\n```\n\n#### select\n\n用于从 state 里获取数据。\n\n```javascript\nconst todos = yield select(state => state.todos);\n```\n\n### 错误处理\n\n#### 全局错误处理\n\ndva 里，effects 和 subscriptions 的抛错全部会走 `onError` hook，所以可以在 `onError` 里统一处理错误。\n\n```javascript\nconst app = dva({\n  onError(e, dispatch) {\n    console.log(e.message);\n  },\n});\n```\n\n然后 effects 里的抛错和 reject 的 promise 就都会被捕获到了。\n\n#### 本地错误处理\n\n如果需要对某些 effects 的错误进行特殊处理，需要在 effect 内部加 `try catch` 。\n\n```javascript\napp.model({\n  effects: {\n    *addRemote() {\n      try {\n        // Your Code Here\n      } catch(e) {\n        console.log(e.message);\n      }\n    },\n  },\n});\n```\n\n### 异步请求\n\n异步请求基于 whatwg-fetch，API 详见：https://github.com/github/fetch\n\n#### GET 和 POST\n\n```javascript\nimport request from '../util/request';\n\n// GET\nrequest('/api/todos');\n\n// POST\nrequest('/api/todos', {\n  method: 'POST',\n  body: JSON.stringify({ a: 1 }),\n});\n```\n\n#### 统一错误处理\n\n假如约定后台返回以下格式时，做统一的错误处理。\n\n```javascript\n{\n  status: 'error',\n  message: '',\n}\n```\n\n编辑 `utils/request.js`，加入以下中间件：\n\n```javascript\nfunction parseErrorMessage({ data }) {\n  const { status, message } = data;\n  if (status === 'error') {\n    throw new Error(message);\n  }\n  return { data };\n}\n```\n\n然后，这类错误就会走到 `onError` hook 里。\n\n## Subscription\n\n`subscriptions` 是订阅，用于订阅一个数据源，然后根据需要 dispatch 相应的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。格式为 `({ dispatch, history }) => unsubscribe` 。\n\n### 异步数据初始化\n\n比如：当用户进入 `/users` 页面时，触发 action `users/fetch` 加载用户数据。\n\n```javascript\napp.model({\n  subscriptions: {\n    setup({ dispatch, history }) {\n      history.listen(({ pathname }) => {\n        if (pathname === '/users') {\n          dispatch({\n            type: 'users/fetch',\n          });\n        }\n      });\n    },\n  },\n});\n```\n\n#### `path-to-regexp` Package\n\n如果 url 规则比较复杂，比如 `/users/:userId/search`，那么匹配和 userId 的获取都会比较麻烦。这时推荐用 [path-to-regexp](https://github.com/pillarjs/path-to-regexp) 简化这部分逻辑。\n\n```javascript\nimport pathToRegexp from 'path-to-regexp';\n\n// in subscription\nconst match = pathToRegexp('/users/:userId/search').exec(pathname);\nif (match) {\n  const userId = match[1];\n  // dispatch action with userId\n}\n```\n\n## Router\n\n### Route Components\n\nRoute Components 是指 `./src/routes/` 目录下的文件，他们是 `./src/router.js` 里匹配的 Component。\n\n#### 通过 connect 绑定数据\n\n比如：\n\n```javascript\nimport { connect } from 'dva';\nfunction App() {}\n\nfunction mapStateToProps(state, ownProps) {\n  return {\n    users: state.users,\n  };\n}\nexport default connect(mapStateToProps)(App);\n```\n\n然后在 App 里就有了 `dispatch` 和 `users` 两个属性。\n\n#### Injected Props (e.g. location)\n\nRoute Component 会有额外的 props 用以获取路由信息。\n\n- location\n- params\n- children\n\n更多详见：[react-router](https://github.com/reactjs/react-router/blob/master/docs/API.md#injected-props)\n\n### 基于 action 进行页面跳转\n\n```javascript\nimport { routerRedux } from 'dva/router';\n\n// Inside Effects\nyield put(routerRedux.push('/logout'));\n\n// Outside Effects\ndispatch(routerRedux.push('/logout'));\n\n// With query\nrouterRedux.push({\n  pathname: '/logout',\n  query: {\n    page: 2,\n  },\n});\n```\n\n除 `push(location)` 外还有更多方法，详见 [react-router-redux](https://github.com/reactjs/react-router-redux#pushlocation-replacelocation-gonumber-goback-goforward)\n\n## dva 配置\n\n### Redux Middleware\n\n比如要添加 redux-logger 中间件：\n\n```javascript\nimport createLogger from 'redux-logger';\nconst app = dva({\n  onAction: createLogger(),\n});\n```\n\n注：onAction 支持数组，可同时传入多个中间件。\n\n### 切换 history 为 browserHistory\n\n先安装 history 依赖，\n\n```bash\n$ npm install --save history\n```\n\n然后修改入口文件， \n\n```javascript\nimport createHistory from 'history/createBrowserHistory';\nconst app = dva({\n  history: createHistory(),\n});\n```\n\n## 工具\n\n### 通过 dva-cli 创建项目\n\n先安装 dva-cli 。\n\n```bash\n$ npm install dva-cli -g\n```\n\n然后创建项目。\n\n```bash\n$ dva new myapp\n```\n\n最后，进入目录并启动。\n\n```bash\n$ cd myapp\n$ npm start\n```\n\n### 通过 umi 使用 dva\n\n先安装 dva-cli@next 。\n\n```bash\n$ npm install dva-cli@next -g\n```\n\n然后创建项目。\n\n```bash\n$ dva new myapp\n```\n\n最后，进入目录并启动。\n\n```bash\n$ cd myapp\n$ npm start\n```\n\n详见[和 dva 一起用@umijs.org](https://umijs.org/guide/with-dva.html)。\n"
  },
  {
    "path": "examples/func-test/.eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"airbnb\",\n  \"rules\": {\n    \"generator-star-spacing\": [0],\n    \"consistent-return\": [0],\n    \"react/forbid-prop-types\": [0],\n    \"react/jsx-filename-extension\": [1, { \"extensions\": [\".js\"] }],\n    \"global-require\": [1],\n    \"import/prefer-default-export\": [0],\n    \"react/jsx-no-bind\": [0],\n    \"react/prop-types\": [0],\n    \"react/prefer-stateless-function\": [0],\n    \"no-else-return\": [0],\n    \"no-restricted-syntax\": [0],\n    \"import/no-extraneous-dependencies\": [0],\n    \"no-use-before-define\": [0],\n    \"jsx-a11y/no-static-element-interactions\": [0],\n    \"no-nested-ternary\": [0],\n    \"arrow-body-style\": [0],\n    \"import/extensions\": [0],\n    \"no-bitwise\": [0],\n    \"no-cond-assign\": [0],\n    \"import/no-unresolved\": [0],\n    \"require-yield\": [1]\n  },\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"experimentalObjectRestSpread\": true\n    }\n  }\n}\n"
  },
  {
    "path": "examples/func-test/.roadhogrc",
    "content": "{\n  \"entry\": \"src/index.js\",\n  \"env\": {\n    \"development\": {\n      \"extraBabelPlugins\": [\n        \"dva-hmr\",\n        \"transform-runtime\"\n      ]\n    },\n    \"production\": {\n      \"extraBabelPlugins\": [\n        \"transform-runtime\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/func-test/.roadhogrc.mock.js",
    "content": "export default {};\n"
  },
  {
    "path": "examples/func-test/package.json",
    "content": "{\n  \"name\": \"dva-example\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"roadhog server\",\n    \"build\": \"roadhog build\",\n    \"lint\": \"eslint --ext .js src test\"\n  },\n  \"engines\": {\n    \"install-node\": \"6.9.2\"\n  },\n  \"dependencies\": {\n    \"babel-runtime\": \"^6.9.2\",\n    \"dva\": \"^2.0.0-0\",\n    \"react\": \"^16.0.0\",\n    \"react-dom\": \"^16.0.0\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"^7.1.1\",\n    \"babel-plugin-dva-hmr\": \"^0.3.2\",\n    \"babel-plugin-module-alias\": \"^1.6.0\",\n    \"babel-plugin-transform-runtime\": \"^6.9.0\",\n    \"eslint\": \"^3.12.2\",\n    \"eslint-config-airbnb\": \"^13.0.0\",\n    \"eslint-plugin-import\": \"^2.2.0\",\n    \"eslint-plugin-jsx-a11y\": \"^2.2.3\",\n    \"eslint-plugin-react\": \"^6.8.0\",\n    \"expect\": \"^1.20.2\",\n    \"redbox-react\": \"^1.3.2\",\n    \"roadhog\": \"^1.2.0\"\n  }\n}\n"
  },
  {
    "path": "examples/func-test/src/components/Example.js",
    "content": "import React from 'react';\n\nconst Example = () => {\n  return <div>Example</div>;\n};\n\nExample.propTypes = {};\n\nexport default Example;\n"
  },
  {
    "path": "examples/func-test/src/index.css",
    "content": "\nhtml, body, :global(#root) {\n  height: 100%;\n}\n\n"
  },
  {
    "path": "examples/func-test/src/index.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Dva Demo</title>\n  <link rel=\"stylesheet\" href=\"index.css\" />\n</head>\n<body>\n\n<div id=\"root\"></div>\n\n<script src=\"index.js\"></script>\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/func-test/src/index.js",
    "content": "import dva from 'dva';\nimport './index.css';\n\n// 1. Initialize\nconst app = dva();\n\n// 2. Plugins\n// app.use({});\n\n// 3. Model\napp.model(require('./models/example'));\n\n// 4. Router\napp.router(require('./router'));\n\n// 5. Start\napp.start('#root');\n"
  },
  {
    "path": "examples/func-test/src/models/example.js",
    "content": "export default {\n  namespace: 'example',\n\n  state: {},\n\n  subscriptions: {\n    setup({ dispatch, history }) {\n      // eslint-disable-line\n      history.listen(location => {\n        console.log(1, location);\n      });\n    },\n  },\n\n  effects: {\n    *fetch({ payload }, { call, put }) {\n      // eslint-disable-line\n      yield put({ type: 'save' });\n    },\n  },\n\n  reducers: {\n    save(state, action) {\n      return { ...state, ...action.payload };\n    },\n  },\n};\n"
  },
  {
    "path": "examples/func-test/src/router.js",
    "content": "import React from 'react';\nimport { routerRedux, Route, Switch } from 'dva/router';\nimport IndexPage from './routes/IndexPage';\n\nconst { ConnectedRouter } = routerRedux;\n\nfunction RouterConfig({ history }) {\n  return (\n    <ConnectedRouter history={history}>\n      <Route path=\"/\" exact component={IndexPage} />\n    </ConnectedRouter>\n  );\n}\n\nexport default RouterConfig;\n"
  },
  {
    "path": "examples/func-test/src/routes/IndexPage.css",
    "content": "\n.normal {\n  font-family: Georgia, sans-serif;\n  margin-top: 3em;\n  text-align: center;\n}\n\n.title {\n  font-size: 2.5rem;\n  font-weight: normal;\n  letter-spacing: -1px;\n}\n\n.welcome {\n  height: 328px;\n  background: url(../assets/yay.jpg) no-repeat center 0;\n  background-size: 388px 328px;\n}\n\n.list {\n  font-size: 1.2em;\n  margin-top: 1.8em;\n  list-style: none;\n  line-height: 1.5em;\n}\n\n.list code {\n  background: #f7f7f7;\n}\n"
  },
  {
    "path": "examples/func-test/src/routes/IndexPage.js",
    "content": "import React from 'react';\nimport { connect } from 'dva';\nimport styles from './IndexPage.css';\n\nfunction IndexPage() {\n  return (\n    <div className={styles.normal}>\n      <h1 className={styles.title}>Yay! Welcome to dva!</h1>\n      <div className={styles.welcome} />\n      <ul className={styles.list}>\n        <li>\n          To get started, edit <code>src/index.js</code> and save to reload.\n        </li>\n        <li>\n          <a href=\"https://github.com/dvajs/dva-docs/blob/master/v1/en-us/getting-started.md\">\n            Getting Started\n          </a>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nIndexPage.propTypes = {};\n\nexport default connect()(IndexPage);\n"
  },
  {
    "path": "examples/func-test/src/services/example.js",
    "content": "import request from '../utils/request';\n\nexport async function query() {\n  return request('/api/users');\n}\n"
  },
  {
    "path": "examples/func-test/src/utils/request.js",
    "content": "import fetch from 'dva/fetch';\n\nfunction parseJSON(response) {\n  return response.json();\n}\n\nfunction checkStatus(response) {\n  if (response.status >= 200 && response.status < 300) {\n    return response;\n  }\n\n  const error = new Error(response.statusText);\n  error.response = response;\n  throw error;\n}\n\n/**\n * Requests a URL, returning a promise.\n *\n * @param  {string} url       The URL we want to request\n * @param  {object} [options] The options we want to pass to \"fetch\"\n * @return {object}           An object containing either \"data\" or \"err\"\n */\nexport default function request(url, options) {\n  return fetch(url, options)\n    .then(checkStatus)\n    .then(parseJSON)\n    .then(data => ({ data }))\n    .catch(err => ({ err }));\n}\n"
  },
  {
    "path": "examples/user-dashboard/.editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": "examples/user-dashboard/.eslintrc",
    "content": "{\n  \"extends\": \"umi\"\n}"
  },
  {
    "path": "examples/user-dashboard/.gitignore",
    "content": "dist\nnode_modules\n.DS_Store\n\n.idea/\n"
  },
  {
    "path": "examples/user-dashboard/.umirc.js",
    "content": "export default {\n  plugins: ['umi-plugin-dva'],\n};\n"
  },
  {
    "path": "examples/user-dashboard/.webpackrc",
    "content": "{\n  \"theme\": {\n    \"@primary-color\": \"#dc6aac\",\n    \"@link-color\": \"#dc6aac\",\n    \"@border-radius-base\": \"2px\",\n    \"@font-size-base\": \"16px\",\n    \"@line-height-base\": \"1.2\"\n  },\n  \"proxy\": {\n    \"/api\": {\n      \"target\": \"http://jsonplaceholder.typicode.com/\",\n      \"changeOrigin\": true,\n      \"pathRewrite\": { \"^/api\" : \"\" }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/user-dashboard/README.md",
    "content": "# dva-example-user-dashboard\n\n详见[《12 步 30 分钟，完成用户管理的 CURD 应用 (react+dva+antd)》](https://github.com/sorrycc/blog/issues/18)。\n\n---\n\n<p align=\"center\">\n  <img src=\"https://zos.alipayobjects.com/rmsportal/bmkNCEoluwGaeGjYjInf.png\" />\n</p>\n\n## Getting Started\nInstall dependencies.\n\n```bash\n$ npm install\n```\n\nStart server.\n\n```bash\n$ npm start\n```\n\nIf success, app will be open in your default browser automatically.\n"
  },
  {
    "path": "examples/user-dashboard/package.json",
    "content": "{\n  \"name\": \"dva-example-user-dashboard\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"umi dev\",\n    \"build\": \"umi build\",\n    \"test\": \"umi test\",\n    \"lint\": \"eslint --ext .js src test\",\n    \"precommit\": \"npm run lint\"\n  },\n  \"dependencies\": {\n    \"umi\": \"^1.0.0-0\",\n    \"umi-plugin-dva\": \"^0.1.0\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^4.17.0\",\n    \"eslint-config-umi\": \"^0.1.2\",\n    \"eslint-plugin-flowtype\": \"^2.42.0\",\n    \"eslint-plugin-import\": \"^2.8.0\",\n    \"eslint-plugin-jsx-a11y\": \"^5.1.1\",\n    \"eslint-plugin-react\": \"^7.6.1\",\n    \"husky\": \"^0.13.0\"\n  }\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/constants.js",
    "content": "export const PAGE_SIZE = 3;\n"
  },
  {
    "path": "examples/user-dashboard/src/global.css",
    "content": "\nhtml, body, :global(#root) {\n  height: 100%;\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/layouts/Header.js",
    "content": "import React from 'react';\nimport { Menu, Icon } from 'antd';\nimport Link from 'umi/link';\nimport withRouter from 'umi/withRouter';\n\nfunction Header({ location }) {\n  return (\n    <Menu selectedKeys={[location.pathname]} mode=\"horizontal\" theme=\"dark\">\n      <Menu.Item key=\"/users\">\n        <Link to=\"/users\">\n          <Icon type=\"bars\" />Users\n        </Link>\n      </Menu.Item>\n      <Menu.Item key=\"/\">\n        <Link to=\"/\">\n          <Icon type=\"home\" />Home\n        </Link>\n      </Menu.Item>\n      <Menu.Item key=\"/404\">\n        <Link to=\"/page-you-dont-know\">\n          <Icon type=\"frown-circle\" />404\n        </Link>\n      </Menu.Item>\n      <Menu.Item key=\"/antd\">\n        <a href=\"https://github.com/dvajs/dva\">dva</a>\n      </Menu.Item>\n    </Menu>\n  );\n}\n\nexport default withRouter(Header);\n"
  },
  {
    "path": "examples/user-dashboard/src/layouts/index.css",
    "content": "\n.normal {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.content {\n  flex: 1;\n  display: flex;\n}\n\n.main {\n  padding: 0 8px;\n  flex: 1 0 auto;\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/layouts/index.js",
    "content": "import React from 'react';\nimport styles from './index.css';\nimport Header from './Header';\n\nfunction MainLayout({ children, location }) {\n  return (\n    <div className={styles.normal}>\n      <Header location={location} />\n      <div className={styles.content}>\n        <div className={styles.main}>{children}</div>\n      </div>\n    </div>\n  );\n}\n\nexport default MainLayout;\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/index.css",
    "content": "\n.normal {\n  font-family: Georgia, sans-serif;\n  margin-top: 3em;\n  text-align: center;\n}\n\n.title {\n  font-size: 2.5rem;\n  font-weight: normal;\n  letter-spacing: -1px;\n}\n\n.welcome {\n  height: 328px;\n  background: url(../assets/yay.jpg) no-repeat center 0;\n  background-size: 388px 328px;\n}\n\n.list {\n  font-size: 1.2em;\n  margin-top: 1.8em;\n  list-style: none;\n  line-height: 1.5em;\n}\n\n.list code {\n  background: #f7f7f7;\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/index.js",
    "content": "import React from 'react';\nimport styles from './index.css';\n\nfunction IndexPage() {\n  return (\n    <div className={styles.normal}>\n      <h1 className={styles.title}>Yay! Welcome to dva!</h1>\n      <div className={styles.welcome} />\n      <ul className={styles.list}>\n        <li>\n          To get started, edit <code>src/index.js</code> and save to reload.\n        </li>\n        <li>\n          <a href=\"https://github.com/dvajs/dva-docs/blob/master/v1/en-us/getting-started.md\">\n            Getting Started\n          </a>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nexport default IndexPage;\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/components/Users/UserModal.js",
    "content": "import React, { Component } from 'react';\nimport { Modal, Form, Input } from 'antd';\n\nconst FormItem = Form.Item;\n\nclass UserEditModal extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      visible: false,\n    };\n  }\n\n  showModelHandler = e => {\n    if (e) e.stopPropagation();\n    this.setState({\n      visible: true,\n    });\n  };\n\n  hideModelHandler = () => {\n    this.setState({\n      visible: false,\n    });\n  };\n\n  okHandler = () => {\n    const { onOk } = this.props;\n    this.props.form.validateFields((err, values) => {\n      if (!err) {\n        onOk(values);\n        this.hideModelHandler();\n      }\n    });\n  };\n\n  render() {\n    const { children } = this.props;\n    const { getFieldDecorator } = this.props.form;\n    const { name, email, website } = this.props.record;\n    const formItemLayout = {\n      labelCol: { span: 6 },\n      wrapperCol: { span: 14 },\n    };\n\n    return (\n      <span>\n        <span onClick={this.showModelHandler}>{children}</span>\n        <Modal\n          title=\"Edit User\"\n          visible={this.state.visible}\n          onOk={this.okHandler}\n          onCancel={this.hideModelHandler}\n        >\n          <Form layout=\"horizontal\" onSubmit={this.okHandler}>\n            <FormItem {...formItemLayout} label=\"Name\">\n              {getFieldDecorator('name', {\n                initialValue: name,\n              })(<Input />)}\n            </FormItem>\n            <FormItem {...formItemLayout} label=\"Email\">\n              {getFieldDecorator('email', {\n                initialValue: email,\n              })(<Input />)}\n            </FormItem>\n            <FormItem {...formItemLayout} label=\"Website\">\n              {getFieldDecorator('website', {\n                initialValue: website,\n              })(<Input />)}\n            </FormItem>\n          </Form>\n        </Modal>\n      </span>\n    );\n  }\n}\n\nexport default Form.create()(UserEditModal);\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/components/Users/Users.css",
    "content": "\n.normal {\n}\n\n.create {\n  margin-bottom: 1.5em;\n}\n\n.operation a {\n  margin: 0 .5em;\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/components/Users/Users.js",
    "content": "import React from 'react';\nimport { connect } from 'dva';\nimport { Table, Pagination, Popconfirm, Button } from 'antd';\nimport { routerRedux } from 'dva/router';\nimport styles from './Users.css';\nimport { PAGE_SIZE } from '../../../../constants';\nimport UserModal from './UserModal';\n\nfunction Users({ dispatch, list: dataSource, loading, total, page: current }) {\n  function deleteHandler(id) {\n    dispatch({\n      type: 'users/remove',\n      payload: id,\n    });\n  }\n\n  function pageChangeHandler(page) {\n    dispatch(\n      routerRedux.push({\n        pathname: '/users',\n        query: { page },\n      })\n    );\n  }\n\n  function editHandler(id, values) {\n    dispatch({\n      type: 'users/patch',\n      payload: { id, values },\n    });\n  }\n\n  function createHandler(values) {\n    dispatch({\n      type: 'users/create',\n      payload: values,\n    });\n  }\n\n  const columns = [\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      key: 'name',\n      render: text => <a href=\"\">{text}</a>,\n    },\n    {\n      title: 'Email',\n      dataIndex: 'email',\n      key: 'email',\n    },\n    {\n      title: 'Website',\n      dataIndex: 'website',\n      key: 'website',\n    },\n    {\n      title: 'Operation',\n      key: 'operation',\n      render: (text, record) => (\n        <span className={styles.operation}>\n          <UserModal record={record} onOk={editHandler.bind(null, record.id)}>\n            <a>Edit</a>\n          </UserModal>\n          <Popconfirm\n            title=\"Confirm to delete?\"\n            onConfirm={deleteHandler.bind(null, record.id)}\n          >\n            <a href=\"\">Delete</a>\n          </Popconfirm>\n        </span>\n      ),\n    },\n  ];\n\n  return (\n    <div className={styles.normal}>\n      <div>\n        <div className={styles.create}>\n          <UserModal record={{}} onOk={createHandler}>\n            <Button type=\"primary\">Create User</Button>\n          </UserModal>\n        </div>\n        <Table\n          columns={columns}\n          dataSource={dataSource}\n          loading={loading}\n          rowKey={record => record.id}\n          pagination={false}\n        />\n        <Pagination\n          className=\"ant-table-pagination\"\n          total={total}\n          current={current}\n          pageSize={PAGE_SIZE}\n          onChange={pageChangeHandler}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction mapStateToProps(state) {\n  const { list, total, page } = state.users;\n  return {\n    loading: state.loading.models.users,\n    list,\n    total,\n    page,\n  };\n}\n\nexport default connect(mapStateToProps)(Users);\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/models/users.js",
    "content": "import * as usersService from '../services/users';\n\nexport default {\n  namespace: 'users',\n  state: {\n    list: [],\n    total: null,\n    page: null,\n  },\n  reducers: {\n    save(state, { payload: { data: list, total, page } }) {\n      return { ...state, list, total, page };\n    },\n  },\n  effects: {\n    *fetch({ payload: { page = 1 } }, { call, put }) {\n      const { data, headers } = yield call(usersService.fetch, { page });\n      yield put({\n        type: 'save',\n        payload: {\n          data,\n          total: parseInt(headers['x-total-count'], 10),\n          page: parseInt(page, 10),\n        },\n      });\n    },\n    *remove({ payload: id }, { call, put }) {\n      yield call(usersService.remove, id);\n      yield put({ type: 'reload' });\n    },\n    *patch({ payload: { id, values } }, { call, put }) {\n      yield call(usersService.patch, id, values);\n      yield put({ type: 'reload' });\n    },\n    *create({ payload: values }, { call, put }) {\n      yield call(usersService.create, values);\n      yield put({ type: 'reload' });\n    },\n    *reload(action, { put, select }) {\n      const page = yield select(state => state.users.page);\n      yield put({ type: 'fetch', payload: { page } });\n    },\n  },\n  subscriptions: {\n    setup({ dispatch, history }) {\n      return history.listen(({ pathname, query }) => {\n        if (pathname === '/users') {\n          dispatch({ type: 'fetch', payload: query });\n        }\n      });\n    },\n  },\n};\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/page.css",
    "content": "\n.normal {\n  width: 900px;\n  margin: 3em auto 0;\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/page.js",
    "content": "import React from 'react';\nimport styles from './page.css';\nimport UsersComponent from './components/Users/Users';\n\nfunction Users() {\n  return (\n    <div className={styles.normal}>\n      <UsersComponent />\n    </div>\n  );\n}\n\nexport default Users;\n"
  },
  {
    "path": "examples/user-dashboard/src/pages/users/services/users.js",
    "content": "import request from '../../../utils/request';\nimport { PAGE_SIZE } from '../../../constants';\n\nexport function fetch({ page }) {\n  return request(`/api/users?_page=${page}&_limit=${PAGE_SIZE}`);\n}\n\nexport function remove(id) {\n  return request(`/api/users/${id}`, {\n    method: 'DELETE',\n  });\n}\n\nexport function patch(id, values) {\n  return request(`/api/users/${id}`, {\n    method: 'PATCH',\n    body: JSON.stringify(values),\n  });\n}\n\nexport function create(values) {\n  return request('/api/users', {\n    method: 'POST',\n    body: JSON.stringify(values),\n  });\n}\n"
  },
  {
    "path": "examples/user-dashboard/src/plugins/onError.js",
    "content": "import { message } from 'antd';\n\nconst ERROR_MSG_DURATION = 3; // 3 秒\n\nexport default {\n  onError(e) {\n    message.error(e.message, ERROR_MSG_DURATION);\n  },\n};\n"
  },
  {
    "path": "examples/user-dashboard/src/utils/request.js",
    "content": "import fetch from 'dva/fetch';\n\nfunction checkStatus(response) {\n  if (response.status >= 200 && response.status < 300) {\n    return response;\n  }\n\n  const error = new Error(response.statusText);\n  error.response = response;\n  throw error;\n}\n\n/**\n * Requests a URL, returning a promise.\n *\n * @param  {string} url       The URL we want to request\n * @param  {object} [options] The options we want to pass to \"fetch\"\n * @return {object}           An object containing either \"data\" or \"err\"\n */\nasync function request(url, options) {\n  const response = await fetch(url, options);\n\n  checkStatus(response);\n\n  const data = await response.json();\n\n  const ret = {\n    data,\n    headers: {},\n  };\n\n  if (response.headers.get('x-total-count')) {\n    ret.headers['x-total-count'] = response.headers.get('x-total-count');\n  }\n\n  return ret;\n}\n\nexport default request;\n"
  },
  {
    "path": "examples/with-immer/.umirc.js",
    "content": "export default {\n  plugins: ['umi-plugin-dva'],\n};\n"
  },
  {
    "path": "examples/with-immer/dva.js",
    "content": "import useImmer from 'dva-immer';\n\nexport function config() {\n  return {\n    ...useImmer(),\n  };\n}\n"
  },
  {
    "path": "examples/with-immer/model.js",
    "content": "export default {\n  namespace: 'count',\n  state: {\n    a: {\n      b: {\n        c: {\n          count: 0,\n        },\n      },\n    },\n  },\n  reducers: {\n    add(state) {\n      state.a.b.c.count += 1;\n    },\n    setNewProp(state) {\n      state.newProp = 'hi new prop';\n    },\n  },\n};\n"
  },
  {
    "path": "examples/with-immer/package.json",
    "content": "{\n  \"dependencies\": {\n    \"umi\": \"*\",\n    \"umi-plugin-dva\": \"*\",\n    \"dva-immer\": \"*\"\n  }\n}\n"
  },
  {
    "path": "examples/with-immer/pages/index.js",
    "content": "import { connect } from 'dva';\nimport { Button } from 'antd-mobile';\n\nfunction App({ count, newProp, dispatch }) {\n  return (\n    <div>\n      <h1>Count: {count}</h1>\n      <h1>state.newProp: {newProp || 'not setted'}</h1>\n      <Button\n        onClick={() => {\n          dispatch({\n            type: 'count/add',\n          });\n        }}\n      >\n        Add\n      </Button>\n      <Button\n        onClick={() => {\n          dispatch({\n            type: 'count/setNewProp',\n          });\n        }}\n      >\n        Set New Prop\n      </Button>\n    </div>\n  );\n}\n\nfunction mapStateToProps(state) {\n  return {\n    count: state.count.a.b.c.count,\n    newProp: state.count.newProp,\n  };\n}\n\nexport default connect(mapStateToProps)(App);\n"
  },
  {
    "path": "examples/with-nextjs/.babelrc",
    "content": "{\n    \"presets\": [\n      \"next/babel\"\n    ],\n    \"plugins\": [\n      [\"module-resolver\", {\n        \"alias\": {\n          \"dva\": \"dva-no-router\"\n        }\n      }]\n    ]\n  }"
  },
  {
    "path": "examples/with-nextjs/.eslintignore",
    "content": ".next/*\nnode_modules/*\n\n\n"
  },
  {
    "path": "examples/with-nextjs/.eslintrc",
    "content": "{\n    \"parser\": \"babel-eslint\",\n    \"extends\": \"airbnb\",\n    \"settings\": {\n      \"import/resolver\": {\n        \"babel-module\": {\n          \"alias\": {\n            \"test\": \"./test\",\n            \"underscore\": \"lodash\"\n          }\n        }\n      }\n    },\n    \"rules\": {\n      \"arrow-body-style\": [0],\n      \"consistent-return\": [0],\n      \"generator-star-spacing\": [0],\n      \"global-require\": [1],\n      \"import/extensions\": [0],\n      \"import/no-extraneous-dependencies\": [0],\n      \"import/no-unresolved\": [0],\n      \"import/prefer-default-export\": [0],\n      \"jsx-a11y/no-static-element-interactions\": [0],\n      \"no-bitwise\": [0],\n      \"no-cond-assign\": [0],\n      \"no-else-return\": [0],\n      \"no-nested-ternary\": [0],\n      \"no-restricted-syntax\": [0],\n      \"no-use-before-define\": [0],\n      \"react/forbid-prop-types\": [0],\n      \"react/jsx-filename-extension\": [1, { \"extensions\": [\".js\"] }],\n      \"react/jsx-no-bind\": [0],\n      \"react/prefer-stateless-function\": [0],\n      \"react/prop-types\": [0],\n      \"require-yield\": [1],\n      \"react/react-in-jsx-scope\": [0],\n      \"jsx-a11y/anchor-is-valid\": [0]\n    },\n    \"parserOptions\": {\n      \"ecmaFeatures\": {\n        \"experimentalObjectRestSpread\": true\n      }\n    }\n  }"
  },
  {
    "path": "examples/with-nextjs/.gitignore",
    "content": "/node_modules/\n/.next/"
  },
  {
    "path": "examples/with-nextjs/README.md",
    "content": "# dva_next\ncompose dva.js with next.js\n\n1.install\n\n```\nnpm install\n```\n\n2.start\n\n```\nnpm start\n```\n"
  },
  {
    "path": "examples/with-nextjs/model/homepage.js",
    "content": "const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));\n\nconst model = {\n  namespace: 'index',\n  state: {\n    name: 'hopperhuang',\n    count: 0,\n    init: false,\n  },\n  reducers: {\n    caculate(state, payload) {\n      const { count } = state;\n      const { delta } = payload;\n      return { ...state, count: count + delta };\n    },\n  },\n  effects: {\n    *init(action, { put }) {\n      yield delay(2000);\n      yield put({ type: 'caculate', delta: 1 });\n    },\n  },\n};\n\nexport default model;\n\n"
  },
  {
    "path": "examples/with-nextjs/model/index.js",
    "content": "import homepage from './homepage';\n\nconst model = [\n  homepage,\n];\n\nexport default model;\n"
  },
  {
    "path": "examples/with-nextjs/package.json",
    "content": "{\n  \"name\": \"dva-next.js\",\n  \"version\": \"1.0.0\",\n  \"description\": \"dva-next.js-example\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"start\": \"next\"\n  },\n  \"author\": \"hopperhuang\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"dva-no-router\": \"^1.0.3\",\n    \"next\": \"^5.0.0\",\n    \"react\": \"^16.2.0\",\n    \"react-dom\": \"^16.2.0\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"^8.2.2\",\n    \"babel-plugin-module-resolver\": \"^3.1.0\",\n    \"eslint\": \"^4.19.1\",\n    \"eslint-config-airbnb\": \"^16.1.0\",\n    \"eslint-import-resolver-babel-module\": \"^4.0.0\",\n    \"eslint-plugin-import\": \"^2.9.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.0.3\",\n    \"eslint-plugin-react\": \"^7.7.0\"\n  }\n}\n"
  },
  {
    "path": "examples/with-nextjs/pages/index.js",
    "content": "\nimport Link from 'next/link';\nimport React from 'react';\nimport WithDva from '../utils/store';\n\nclass Page extends React.Component {\n  static async getInitialProps(props) {\n    // first time run in server side\n    // other times run in client side ( client side init with default props\n    // console.log('get init props');\n    const {\n      pathname, query, isServer, store,\n    } = props;\n    // dispatch effects to fetch data here\n    await props.store.dispatch({ type: 'index/init' });\n    return {\n      // dont use store as property name, it will confilct with initial store\n      pathname, query, isServer, dvaStore: store,\n    };\n  }\n\n  render() {\n    const { index } = this.props;\n    const { name, count } = index;\n    // console.log('rendered!!');\n    return (\n      <div>\n      Hi,{name}!! &nbsp;\n        <p>count:&nbsp; {count}</p>\n        <p>\n          <button onClick={() => { this.props.dispatch({ type: 'index/caculate', delta: 1 }); }} >\n        plus\n          </button>\n        </p>\n        <p>\n          <button onClick={() => { this.props.dispatch({ type: 'index/caculate', delta: -1 }); }} >\n          minus\n          </button>\n        </p>\n        <p>\n          <Link href=\"/users\">\n            <a>Go to /users</a>\n          </Link>\n        </p>\n      </div>\n    );\n  }\n}\n\nexport default WithDva((state) => { return { index: state.index }; })(Page);\n"
  },
  {
    "path": "examples/with-nextjs/pages/users.js",
    "content": "\nimport Link from 'next/link';\n\nexport default function () {\n  return (\n    <div>\n      Users\n      <br />\n      <Link href=\"/\">\n        <a>\n          Back\n        </a>\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/with-nextjs/utils/store.js",
    "content": "import React from 'react';\nimport dva, { connect } from 'dva-no-router';\nimport { Provider } from 'react-redux';\nimport model from '../model/index';\n\nconst checkServer = () => Object.prototype.toString.call(global.process) === '[object process]';\n\n// eslint-disable-next-line\nconst __NEXT_DVA_STORE__ =  '__NEXT_DVA_STORE__'\n\nfunction createDvaStore(initialState) {\n  let app;\n  if (initialState) {\n    app = dva({\n      initialState,\n    });\n  } else {\n    app = dva({});\n  }\n  const isArray = Array.isArray(model);\n  if (isArray) {\n    model.forEach((m) => {\n      app.model(m);\n    });\n  } else {\n    app.model(model);\n  }\n  app.router(() => {});\n  app.start();\n  // console.log(app);\n  // eslint-disable-next-line\n  const store = app._store\n  return store;\n}\n\nfunction getOrCreateStore(initialState) {\n  const isServer = checkServer();\n  if (isServer) { // run in server\n    // console.log('server');\n    return createDvaStore(initialState);\n  }\n  // eslint-disable-next-line\n  if (!window[__NEXT_DVA_STORE__]) {\n    // console.log('client');\n    // eslint-disable-next-line\n    window[__NEXT_DVA_STORE__] = createDvaStore(initialState);\n  }\n  // eslint-disable-next-line\n  return window[__NEXT_DVA_STORE__];\n}\n\nexport default function withDva(...args) {\n  return function CreateNextPage(Component) {\n    const ComponentWithDva = (props = {}) => {\n      const { store, initialProps, initialState } = props;\n      const ConnectedComponent = connect(...args)(Component);\n      return React.createElement(\n        Provider,\n        // in client side, it will init store with the initial state tranfer from server side\n        { store: store && store.dispatch ? store : getOrCreateStore(initialState) },\n        // transfer next.js's props to the page\n        React.createElement(ConnectedComponent, initialProps),\n      );\n    };\n    ComponentWithDva.getInitialProps = async (props = {}) => {\n      // console.log('get......');\n      const isServer = checkServer();\n      const store = getOrCreateStore(props.req);\n      // call children's getInitialProps\n      // get initProps and transfer in to the page\n      const initialProps = Component.getInitialProps\n        ? await Component.getInitialProps({ ...props, isServer, store })\n        : {};\n      return {\n        store,\n        initialProps,\n        initialState: store.getState(),\n      };\n    };\n    return ComponentWithDva;\n  };\n}\n"
  },
  {
    "path": "examples/with-react-router-3/.eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"airbnb\",\n  \"rules\": {\n    \"generator-star-spacing\": [0],\n    \"consistent-return\": [0],\n    \"react/forbid-prop-types\": [0],\n    \"react/jsx-filename-extension\": [1, { \"extensions\": [\".js\"] }],\n    \"global-require\": [1],\n    \"import/prefer-default-export\": [0],\n    \"react/jsx-no-bind\": [0],\n    \"react/prop-types\": [0],\n    \"react/prefer-stateless-function\": [0],\n    \"no-else-return\": [0],\n    \"no-restricted-syntax\": [0],\n    \"import/no-extraneous-dependencies\": [0],\n    \"no-use-before-define\": [0],\n    \"jsx-a11y/no-static-element-interactions\": [0],\n    \"no-nested-ternary\": [0],\n    \"arrow-body-style\": [0],\n    \"import/extensions\": [0],\n    \"no-bitwise\": [0],\n    \"no-cond-assign\": [0],\n    \"import/no-unresolved\": [0],\n    \"require-yield\": [1]\n  },\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"experimentalObjectRestSpread\": true\n    }\n  }\n}\n"
  },
  {
    "path": "examples/with-react-router-3/.roadhogrc",
    "content": "{\n  \"entry\": \"src/index.js\",\n  \"env\": {\n    \"development\": {\n      \"extraBabelPlugins\": [\n        \"dva-hmr\",\n        \"transform-runtime\",\n        [\"module-resolver\", {\n          \"alias\": {\n            \"dva\": \"dva-react-router-3\"\n          }\n        }]\n      ]\n    },\n    \"production\": {\n      \"extraBabelPlugins\": [\n        \"transform-runtime\",\n        [\"module-resolver\", {\n          \"alias\": {\n            \"dva\": \"dva-react-router-3\"\n          }\n        }]\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/with-react-router-3/.roadhogrc.mock.js",
    "content": "export default {};\n"
  },
  {
    "path": "examples/with-react-router-3/package.json",
    "content": "{\n  \"name\": \"dva-example-react-router-3\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"roadhog server\",\n    \"build\": \"roadhog build\",\n    \"lint\": \"eslint --ext .js src test\"\n  },\n  \"engines\": {\n    \"install-node\": \"6.9.2\"\n  },\n  \"dependencies\": {\n    \"babel-runtime\": \"^6.9.2\",\n    \"dva-react-router-3\": \"^0.3.0\",\n    \"react\": \"^16.0.0\",\n    \"react-dom\": \"^16.0.0\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"^7.1.1\",\n    \"babel-plugin-dva-hmr\": \"^0.3.2\",\n    \"babel-plugin-module-resolver\": \"^2.7.1\",\n    \"babel-plugin-transform-runtime\": \"^6.9.0\",\n    \"eslint\": \"^3.12.2\",\n    \"eslint-config-airbnb\": \"^13.0.0\",\n    \"eslint-plugin-import\": \"^2.2.0\",\n    \"eslint-plugin-jsx-a11y\": \"^2.2.3\",\n    \"eslint-plugin-react\": \"^6.8.0\",\n    \"expect\": \"^1.20.2\",\n    \"redbox-react\": \"^1.3.2\",\n    \"roadhog\": \"^1.2.0\"\n  }\n}\n"
  },
  {
    "path": "examples/with-react-router-3/src/components/Example.js",
    "content": "import React from 'react';\n\nconst Example = () => {\n  return <div>Example</div>;\n};\n\nExample.propTypes = {};\n\nexport default Example;\n"
  },
  {
    "path": "examples/with-react-router-3/src/index.css",
    "content": "\nhtml, body, :global(#root) {\n  height: 100%;\n}\n\n"
  },
  {
    "path": "examples/with-react-router-3/src/index.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Dva Demo</title>\n</head>\n<body>\n\n<div id=\"root\"></div>\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/with-react-router-3/src/index.js",
    "content": "import dva from 'dva';\nimport './index.css';\n\n// 1. Initialize\nconst app = dva();\n\n// 2. Plugins\n// app.use({});\n\n// 3. Model\napp.model(require('./models/example'));\n\n// 4. Router\napp.router(require('./router'));\n\n// 5. Start\napp.start('#root');\n"
  },
  {
    "path": "examples/with-react-router-3/src/models/example.js",
    "content": "export default {\n  namespace: 'example',\n\n  state: {},\n\n  subscriptions: {\n    setup({ dispatch, history }) {\n      // eslint-disable-line\n    },\n  },\n\n  effects: {\n    *fetch({ payload }, { call, put }) {\n      // eslint-disable-line\n      yield put({ type: 'save' });\n    },\n  },\n\n  reducers: {\n    save(state, action) {\n      return { ...state, ...action.payload };\n    },\n  },\n};\n"
  },
  {
    "path": "examples/with-react-router-3/src/router.js",
    "content": "import React from 'react';\nimport { Router, Route } from 'dva/router';\nimport IndexPage from './routes/IndexPage';\n\nfunction RouterConfig({ history }) {\n  return (\n    <Router history={history}>\n      <Route path=\"/\" component={IndexPage} />\n    </Router>\n  );\n}\n\nexport default RouterConfig;\n"
  },
  {
    "path": "examples/with-react-router-3/src/routes/IndexPage.css",
    "content": "\n.normal {\n  font-family: Georgia, sans-serif;\n  margin-top: 3em;\n  text-align: center;\n}\n\n.title {\n  font-size: 2.5rem;\n  font-weight: normal;\n  letter-spacing: -1px;\n}\n\n.welcome {\n  height: 328px;\n  background: url(../assets/yay.jpg) no-repeat center 0;\n  background-size: 388px 328px;\n}\n\n.list {\n  font-size: 1.2em;\n  margin-top: 1.8em;\n  list-style: none;\n  line-height: 1.5em;\n}\n\n.list code {\n  background: #f7f7f7;\n}\n"
  },
  {
    "path": "examples/with-react-router-3/src/routes/IndexPage.js",
    "content": "import React from 'react';\nimport { connect } from 'dva';\nimport styles from './IndexPage.css';\n\nfunction IndexPage() {\n  return (\n    <div className={styles.normal}>\n      <h1 className={styles.title}>Yay! Welcome to dva!</h1>\n      <div className={styles.welcome} />\n      <ul className={styles.list}>\n        <li>\n          To get started, edit <code>src/index.js</code> and save to reload.\n        </li>\n        <li>\n          <a href=\"https://github.com/dvajs/dva-docs/blob/master/v1/en-us/getting-started.md\">\n            Getting Started\n          </a>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nIndexPage.propTypes = {};\n\nexport default connect()(IndexPage);\n"
  },
  {
    "path": "examples/with-react-router-3/src/services/example.js",
    "content": "import request from '../utils/request';\n\nexport async function query() {\n  return request('/api/users');\n}\n"
  },
  {
    "path": "examples/with-react-router-3/src/utils/request.js",
    "content": "import fetch from 'dva/fetch';\n\nfunction parseJSON(response) {\n  return response.json();\n}\n\nfunction checkStatus(response) {\n  if (response.status >= 200 && response.status < 300) {\n    return response;\n  }\n\n  const error = new Error(response.statusText);\n  error.response = response;\n  throw error;\n}\n\n/**\n * Requests a URL, returning a promise.\n *\n * @param  {string} url       The URL we want to request\n * @param  {object} [options] The options we want to pass to \"fetch\"\n * @return {object}           An object containing either \"data\" or \"err\"\n */\nexport default function request(url, options) {\n  return fetch(url, options)\n    .then(checkStatus)\n    .then(parseJSON)\n    .then(data => ({ data }))\n    .catch(err => ({ err }));\n}\n"
  },
  {
    "path": "examples/with-redux-undo/.babelrc",
    "content": "{\n  \"presets\": [\"env\", \"react\"],\n  \"plugins\": [\"transform-object-rest-spread\"]\n}\n"
  },
  {
    "path": "examples/with-redux-undo/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# development\n.cache\n*.map"
  },
  {
    "path": "examples/with-redux-undo/.npmrc",
    "content": "package-lock=false"
  },
  {
    "path": "examples/with-redux-undo/README.md",
    "content": "# Redux Undo Sample\n\n```\nnpm install\nnpm start\n```\n\nor\n\n```\nyarn\nyarn start\n```\n\nOpen http://localhost:1234 to view Counter App\n---\n\nSupport\nhttps://github.com/omnidan/redux-undo\n"
  },
  {
    "path": "examples/with-redux-undo/package.json",
    "content": "{\n  \"name\": \"dva-example-redux-redu\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"parcel ./src/index.html \"\n  },\n  \"author\": \"sjy <shijianyue47@gmail.com>\",\n  \"devDependencies\": {\n    \"babel-preset-env\": \"^1.6.1\",\n    \"babel-preset-react\": \"^6.24.1\",\n    \"babel-plugin-transform-object-rest-spread\": \"^6.26.0\",\n    \"parcel-bundler\": \"^1.4.1\"\n  },\n  \"dependencies\": {\n    \"dva\": \"^2.2.3\",\n    \"react\": \"^16.2.0\",\n    \"react-dom\": \"^16.2.0\",\n    \"redux-undo\": \"^0.6.1\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "examples/with-redux-undo/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\" />\n  <title>dva-example-redux-redu</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n</head>\n\n<body>\n  <div id=\"root\"></div>\n  <script src=\"./index.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/with-redux-undo/src/index.js",
    "content": "import React from 'react';\nimport dva, { connect } from 'dva';\nimport undoable, { ActionCreators } from 'redux-undo';\n\nimport counter from './models/counter';\n\n// 1. Initialize\nconst app = dva({\n  onReducer: reducer => (state, action) => {\n    const newState = undoable(reducer, {})(state, action);\n    return { ...newState };\n  },\n});\n\n// 2. Model\napp.model(counter);\n\n// 3. View\nconst App = connect(({ present: { counter } }) => ({\n  counter,\n}))(props => {\n  return (\n    <div style={{ textAlignLast: 'center' }}>\n      <h2>Count: {props.counter}</h2>\n      <button\n        onClick={() => {\n          props.dispatch({ type: 'counter/minus' });\n        }}\n      >\n        -\n      </button>\n      <button\n        onClick={() => {\n          props.dispatch({ type: 'counter/add' });\n        }}\n      >\n        +\n      </button>\n      <button\n        style={{ marginLeft: 20 }}\n        onClick={() => {\n          props.dispatch(ActionCreators.undo());\n        }}\n      >\n        Undo\n      </button>\n      <button\n        onClick={() => {\n          props.dispatch(ActionCreators.redo());\n        }}\n      >\n        Redo\n      </button>\n    </div>\n  );\n});\n\n// 4. Router\napp.router(() => <App />);\n\n// 5. Start\napp.start('#root');\n"
  },
  {
    "path": "examples/with-redux-undo/src/models/counter.js",
    "content": "export default {\n  namespace: 'counter',\n  state: 0,\n  reducers: {\n    add(state) {\n      return state + 1;\n    },\n    minus(state) {\n      return state - 1;\n    },\n  },\n};\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  collectCoverageFrom: ['packages/**/src/*.{ts,tsx,js,jsx}'],\n};\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"changelog\": {\n    \"repo\": \"dvajs/dva\",\n    \"labels\": {\n      \"pr(enhancement)\": \":rocket: Enhancement\",\n      \"pr(bug)\": \":bug: Bug Fix\",\n      \"pr(documentation)\": \":book: Documentation\",\n      \"pr(dependency)\": \":deciduous_tree: Dependency\",\n      \"pr(chore)\": \":turtle: Chore\"\n    },\n    \"cacheDir\": \".changelog\"\n  },\n  \"packages\": [\n    \"packages/*\"\n  ],\n  \"command\": {\n    \"version\": {\n      \"exact\": true\n    }\n  },\n  \"npmClient\": \"yarn\",\n  \"version\": \"independent\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"father-build\",\n    \"doc:dev\": \"./website/node_modules/.bin/vuepress dev ./docs\",\n    \"doc:deploy\": \"rm -rf ./website/yarn.lock && cd ./website && npm run deploy && cd -\",\n    \"changelog\": \"lerna-changelog\",\n    \"test\": \"npm run debug -- --coverage\",\n    \"debug\": \"umi-test\",\n    \"coveralls\": \"cat ./coverage/lcov.info | coveralls\",\n    \"lint\": \"eslint --ext .js packages\",\n    \"precommit\": \"lint-staged\",\n    \"release\": \"./scripts/publish.js\",\n    \"bootstrap\": \"lerna bootstrap\"\n  },\n  \"devDependencies\": {\n    \"@types/jest\": \"^24.0.22\",\n    \"babel-eslint\": \"^9.0.0\",\n    \"chalk\": \"^2.3.2\",\n    \"coveralls\": \"^3.0.0\",\n    \"eslint\": \"^5.6.0\",\n    \"eslint-config-airbnb\": \"^17.1.0\",\n    \"eslint-config-prettier\": \"^4.3.0\",\n    \"eslint-plugin-import\": \"^2.14.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.0.2\",\n    \"eslint-plugin-prettier\": \"^3.1.0\",\n    \"eslint-plugin-react\": \"^7.11.1\",\n    \"father-build\": \"^1.14.0\",\n    \"husky\": \"^0.14.3\",\n    \"lerna\": \"^3.4.0\",\n    \"lerna-changelog\": \"^0.8.0\",\n    \"lint-staged\": \"^7.2.2\",\n    \"prettier\": \"^1.14.3\",\n    \"react\": \"^18.0.0\",\n    \"react-dom\": \"^18.0.0\",\n    \"react-testing-library\": \"^6.0.0\",\n    \"shelljs\": \"^0.8.1\",\n    \"umi-test\": \"^1.5.2\"\n  },\n  \"lint-staged\": {\n    \"*.js\": [\n      \"prettier --trailing-comma all --single-quote --write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/dva/.fatherrc.js",
    "content": "export default {\n  entry: ['src/index.js', 'src/dynamic.js'],\n  cjs: 'rollup',\n  esm: 'rollup',\n  runtimeHelpers: true,\n};\n"
  },
  {
    "path": "packages/dva/README.md",
    "content": "# dva\n\n[![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva)\n[![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva)\n[![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva)\n\nOfficial React bindings for dva, with react-router@4.\n\n## LICENSE\n\nMIT\n"
  },
  {
    "path": "packages/dva/dynamic.d.ts",
    "content": "declare const dynamic: (resolve: (value?: PromiseLike<any>) => void) => void;\nexport default dynamic;\n"
  },
  {
    "path": "packages/dva/dynamic.js",
    "content": "require('./warnAboutDeprecatedCJSRequire.js')('dynamic');\nmodule.exports = require('./dist/dynamic');\n"
  },
  {
    "path": "packages/dva/fetch.d.ts",
    "content": "import * as isomorphicFetch from 'isomorphic-fetch';\n\nexport = isomorphicFetch;\n"
  },
  {
    "path": "packages/dva/fetch.js",
    "content": "require('./warnAboutDeprecatedCJSRequire.js')('fetch');\nmodule.exports = require('isomorphic-fetch');\n"
  },
  {
    "path": "packages/dva/index.d.ts",
    "content": "import {\n  Reducer,\n  Action,\n  AnyAction,\n  ReducersMapObject,\n  MiddlewareAPI,\n  StoreEnhancer,\n  bindActionCreators\n} from 'redux';\n\nimport { History } from \"history\";\n\nexport interface Dispatch<A extends Action = AnyAction> {\n  <T extends A>(action: T): Promise<any> | T;\n}\n\nexport interface onActionFunc {\n  (api: MiddlewareAPI<any>): void,\n}\n\nexport interface ReducerEnhancer {\n  (reducer: Reducer<any>): void,\n}\n\nexport interface Hooks {\n  onError?: (e: Error, dispatch: Dispatch<any>) => void,\n  onAction?: onActionFunc | onActionFunc[],\n  onStateChange?: () => void,\n  onReducer?: ReducerEnhancer,\n  onEffect?: () => void,\n  onHmr?: () => void,\n  extraReducers?: ReducersMapObject,\n  extraEnhancers?: StoreEnhancer<any>[],\n}\n\nexport type DvaOption = Hooks & {\n  namespacePrefixWarning?: boolean,\n  initialState?: Object,\n  history?: Object,\n}\n\nexport interface EffectsCommandMap {\n  put: <A extends AnyAction>(action: A) => any,\n  call: Function,\n  select: Function,\n  take: Function,\n  cancel: Function,\n  [key: string]: any,\n}\n\nexport type Effect = (action: AnyAction, effects: EffectsCommandMap) => void;\nexport type EffectType = 'takeEvery' | 'takeLatest' | 'watcher' | 'throttle';\nexport type EffectWithType = [Effect, { type: EffectType }];\nexport type Subscription = (api: SubscriptionAPI, done: Function) => void;\nexport type ReducersMapObjectWithEnhancer = [ReducersMapObject, ReducerEnhancer];\n\nexport interface EffectsMapObject {\n  [key: string]: Effect | EffectWithType,\n}\n\nexport interface SubscriptionAPI {\n  history: History,\n  dispatch: Dispatch<any>,\n}\n\nexport interface SubscriptionsMapObject {\n  [key: string]: Subscription,\n}\n\nexport interface Model {\n  namespace: string,\n  state?: any,\n  reducers?: ReducersMapObject | ReducersMapObjectWithEnhancer,\n  effects?: EffectsMapObject,\n  subscriptions?: SubscriptionsMapObject,\n}\n\nexport interface RouterAPI {\n  history: History,\n  app: DvaInstance,\n}\n\nexport interface Router {\n  (api?: RouterAPI): JSX.Element | Object,\n}\n\nexport interface DvaInstance {\n  /**\n   * Register an object of hooks on the application.\n   *\n   * @param hooks\n   */\n  use: (hooks: Hooks) => void,\n\n  /**\n   * Register a model.\n   *\n   * @param model\n   */\n  model: (model: Model) => void,\n\n  /**\n   * Unregister a model.\n   *\n   * @param namespace\n   */\n  unmodel: (namespace: string) => void,\n\n  /**\n   * Config router. Takes a function with arguments { history, dispatch },\n   * and expects router config. It use the same api as react-router,\n   * return jsx elements or JavaScript Object for dynamic routing.\n   *\n   * @param router\n   */\n  router: (router: Router) => void,\n\n  /**\n   * Start the application. Selector is optional. If no selector\n   * arguments, it will return a function that return JSX elements.\n   *\n   * @param selector\n   */\n  start: (selector?: HTMLElement | string) => any,\n}\n\nexport default function dva(opts?: DvaOption): DvaInstance;\n\nexport { bindActionCreators };\n\nexport {\n  connect, connectAdvanced, useSelector, useDispatch, useStore,\n  DispatchProp, shallowEqual\n} from 'react-redux';\n\nimport * as routerRedux from 'connected-react-router';\nexport { routerRedux };\n\nimport * as fetch from 'isomorphic-fetch';\nexport { fetch };\n\nimport * as router from 'react-router-dom';\nexport { router };\nexport { useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom';\n"
  },
  {
    "path": "packages/dva/package.json",
    "content": "{\n  \"name\": \"dva\",\n  \"version\": \"3.0.0-alpha.1\",\n  \"description\": \"React and redux based, lightweight and elm-style framework.\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"typings\": \"index.d.ts\",\n  \"sideEffects\": false,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dvajs/dva\"\n  },\n  \"homepage\": \"https://github.com/dvajs/dva\",\n  \"keywords\": [\n    \"dva\",\n    \"alibaba\",\n    \"react\",\n    \"react-native\",\n    \"redux\",\n    \"redux-saga\",\n    \"elm\",\n    \"framework\",\n    \"frontend\"\n  ],\n  \"authors\": [\n    \"chencheng <sorrycc@gmail.com> (https://github.com/sorrycc)\"\n  ],\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/dvajs/dva/issues\"\n  },\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.0.0\",\n    \"@types/isomorphic-fetch\": \"^0.0.35\",\n    \"@types/react-redux\": \"^7.1.0\",\n    \"@types/react-router-dom\": \"^5.1.2\",\n    \"connected-react-router\": \"6.5.2\",\n    \"dva-core\": \"2.0.4\",\n    \"global\": \"^4.3.2\",\n    \"history\": \"^4.7.2\",\n    \"invariant\": \"^2.2.4\",\n    \"isomorphic-fetch\": \"^2.2.1\",\n    \"react-redux\": \"^7.1.0\",\n    \"react-router-dom\": \"^5.1.2\",\n    \"redux\": \"^4.0.1\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=18\",\n    \"react-dom\": \">=18\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"dynamic.js\",\n    \"fetch.js\",\n    \"index.js\",\n    \"router.js\",\n    \"saga.js\",\n    \"warnAboutDeprecatedCJSRequire.js\",\n    \"*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/dva/router.d.ts",
    "content": "import * as routerRedux from 'connected-react-router';\n\nexport * from 'react-router-dom';\nexport { routerRedux };\n"
  },
  {
    "path": "packages/dva/router.js",
    "content": "require('./warnAboutDeprecatedCJSRequire.js')('router');\nmodule.exports = require('react-router-dom');\nmodule.exports.routerRedux = require('connected-react-router');\n"
  },
  {
    "path": "packages/dva/saga.js",
    "content": "require('./warnAboutDeprecatedCJSRequire.js')('saga');\nmodule.exports = require('dva-core/saga');\n"
  },
  {
    "path": "packages/dva/src/dynamic.js",
    "content": "import React, { Component } from 'react';\n\nconst cached = {};\nfunction registerModel(app, model) {\n  model = model.default || model;\n  if (!cached[model.namespace]) {\n    app.model(model);\n    cached[model.namespace] = 1;\n  }\n}\n\nlet defaultLoadingComponent = () => null;\n\nfunction asyncComponent(config) {\n  const { resolve } = config;\n\n  return class DynamicComponent extends Component {\n    constructor(...args) {\n      super(...args);\n      this.LoadingComponent = config.LoadingComponent || defaultLoadingComponent;\n      this.state = {\n        AsyncComponent: null,\n      };\n      this.load();\n    }\n\n    componentDidMount() {\n      this.mounted = true;\n    }\n\n    componentWillUnmount() {\n      this.mounted = false;\n    }\n\n    load() {\n      resolve().then(m => {\n        const AsyncComponent = m.default || m;\n        if (this.mounted) {\n          this.setState({ AsyncComponent });\n        } else {\n          this.state.AsyncComponent = AsyncComponent; // eslint-disable-line\n        }\n      });\n    }\n\n    render() {\n      const { AsyncComponent } = this.state;\n      const { LoadingComponent } = this;\n      if (AsyncComponent) return <AsyncComponent {...this.props} />;\n\n      return <LoadingComponent {...this.props} />;\n    }\n  };\n}\n\nexport default function dynamic(config) {\n  const { app, models: resolveModels, component: resolveComponent } = config;\n  return asyncComponent({\n    resolve:\n      config.resolve ||\n      function() {\n        const models = typeof resolveModels === 'function' ? resolveModels() : [];\n        const component = resolveComponent();\n        return new Promise(resolve => {\n          Promise.all([...models, component]).then(ret => {\n            if (!models || !models.length) {\n              return resolve(ret[0]);\n            } else {\n              const len = models.length;\n              ret.slice(0, len).forEach(m => {\n                m = m.default || m;\n                if (!Array.isArray(m)) {\n                  m = [m];\n                }\n                m.map(_ => registerModel(app, _));\n              });\n              resolve(ret[len]);\n            }\n          });\n        });\n      },\n    ...config,\n  });\n}\n\ndynamic.setDefaultLoadingComponent = LoadingComponent => {\n  defaultLoadingComponent = LoadingComponent;\n};\n"
  },
  {
    "path": "packages/dva/src/index.js",
    "content": "import React from 'react';\nimport invariant from 'invariant';\nimport { createBrowserHistory, createMemoryHistory, createHashHistory } from 'history';\nimport document from 'global/document';\nimport {\n  Provider,\n  connect,\n  connectAdvanced,\n  useSelector,\n  useDispatch,\n  useStore,\n  shallowEqual,\n} from 'react-redux';\nimport { bindActionCreators } from 'redux';\nimport { utils, create, saga } from 'dva-core';\nimport * as router from 'react-router-dom';\nimport * as routerRedux from 'connected-react-router';\n\nconst { connectRouter, routerMiddleware } = routerRedux;\nconst { isFunction } = utils;\nconst { useHistory, useLocation, useParams, useRouteMatch } = router;\n\nexport default function(opts = {}) {\n  const history = opts.history || createHashHistory();\n  const createOpts = {\n    initialReducer: {\n      router: connectRouter(history),\n    },\n    setupMiddlewares(middlewares) {\n      return [routerMiddleware(history), ...middlewares];\n    },\n    setupApp(app) {\n      app._history = patchHistory(history);\n    },\n  };\n\n  const app = create(opts, createOpts);\n  const oldAppStart = app.start;\n  app.router = router;\n  app.start = start;\n  return app;\n\n  function router(router) {\n    invariant(\n      isFunction(router),\n      `[app.router] router should be function, but got ${typeof router}`,\n    );\n    app._router = router;\n  }\n\n  function start(container) {\n    // 允许 container 是字符串，然后用 querySelector 找元素\n    if (isString(container)) {\n      container = document.querySelector(container);\n      invariant(container, `[app.start] container ${container} not found`);\n    }\n\n    // 并且是 HTMLElement\n    invariant(\n      !container || isHTMLElement(container),\n      `[app.start] container should be HTMLElement`,\n    );\n\n    // 路由必须提前注册\n    invariant(app._router, `[app.start] router must be registered before app.start()`);\n\n    if (!app._store) {\n      oldAppStart.call(app);\n    }\n    const store = app._store;\n\n    // export _getProvider for HMR\n    // ref: https://github.com/dvajs/dva/issues/469\n    app._getProvider = getProvider.bind(null, store, app);\n\n    // If has container, render; else, return react component\n    if (container) {\n      render(container, store, app, app._router);\n      app._plugin.apply('onHmr')(render.bind(null, container, store, app));\n    } else {\n      return getProvider(store, this, this._router);\n    }\n  }\n}\n\nfunction isHTMLElement(node) {\n  return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;\n}\n\nfunction isString(str) {\n  return typeof str === 'string';\n}\n\nfunction getProvider(store, app, router) {\n  const DvaRoot = extraProps => (\n    <Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>\n  );\n  return DvaRoot;\n}\n\nfunction render(container, store, app, router) {\n  const ReactDOM = require('react-dom/client'); // eslint-disable-line\n  ReactDOM.createRoot(container).render(React.createElement(getProvider(store, app, router)));\n}\n\nfunction patchHistory(history) {\n  const oldListen = history.listen;\n  history.listen = callback => {\n    // TODO: refact this with modified ConnectedRouter\n    // Let ConnectedRouter to sync history to store first\n    // connected-react-router's version is locked since the check function may be broken\n    // min version of connected-react-router\n    // e.g.\n    // function (e, t) {\n    //   var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2];\n    //   r.inTimeTravelling ? r.inTimeTravelling = !1 : a(e, t, n)\n    // }\n    // ref: https://github.com/umijs/umi/issues/2693\n    const cbStr = callback.toString();\n    const isConnectedRouterHandler =\n      (callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||\n      (cbStr.indexOf('.inTimeTravelling') > -1 &&\n        cbStr.indexOf('.inTimeTravelling') > -1 &&\n        cbStr.indexOf('arguments[2]') > -1);\n    // why add __isDvaPatch: true\n    // since it's a patch from dva, we need to identify it in the listen handlers\n    callback(history.location, history.action, { __isDvaPatch: true });\n    return oldListen.call(history, (...args) => {\n      if (isConnectedRouterHandler) {\n        callback(...args);\n      } else {\n        // Delay all listeners besides ConnectedRouter\n        setTimeout(() => {\n          callback(...args);\n        });\n      }\n    });\n  };\n  return history;\n}\n\nexport fetch from 'isomorphic-fetch';\nexport dynamic from './dynamic';\nexport { connect, connectAdvanced, useSelector, useDispatch, useStore, shallowEqual };\nexport { bindActionCreators };\nexport { router };\nexport { saga };\nexport { routerRedux };\nexport { createBrowserHistory, createMemoryHistory, createHashHistory };\nexport { useHistory, useLocation, useParams, useRouteMatch };\n"
  },
  {
    "path": "packages/dva/test/index.e2e.js",
    "content": "import React from 'react';\nimport { render, fireEvent, cleanup } from 'react-testing-library';\nimport dva, {\n  connect,\n  useDispatch,\n  useSelector,\n  useStore,\n  createMemoryHistory,\n  router,\n  routerRedux,\n  shallowEqual,\n} from '../dist/index';\n\nconst { Link, Switch, Route, Router } = router;\n\nafterEach(cleanup);\n\nconst delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));\n\ntest('normal', () => {\n  const app = dva();\n  app.model({\n    namespace: 'count',\n    state: 0,\n    reducers: {\n      add(state) {\n        return state + 1;\n      },\n    },\n  });\n  app.router(() => <div />);\n  app.start();\n\n  expect(app._store.getState().count).toEqual(0);\n  app._store.dispatch({ type: 'count/add' });\n  expect(app._store.getState().count).toEqual(1);\n});\n\ntest('subscription execute multiple times', async () => {\n  const app = dva();\n  app.model({\n    namespace: 'count',\n    state: 0,\n    subscriptions: {\n      setup({ history, dispatch }) {\n        return history.listen(() => {\n          dispatch({\n            type: 'add',\n          });\n        });\n      },\n    },\n    reducers: {\n      add(state) {\n        return state + 1;\n      },\n    },\n  });\n\n  const Count = connect(state => ({ count: state.count }))(function(props) {\n    return <div data-testid=\"count\">{props.count}</div>;\n  });\n\n  function Home() {\n    return <div />;\n  }\n\n  function Users() {\n    return <div />;\n  }\n\n  app.router(({ history }) => {\n    return (\n      <Router history={history}>\n        <>\n          <Link to=\"/\">Home</Link>\n          <Link to=\"/users\">Users</Link>\n          <Count />\n          <Switch>\n            <Route path=\"/\" exact component={Home} />\n            <Route path=\"/users\" component={Users} />\n          </Switch>\n        </>\n      </Router>\n    );\n  });\n\n  const { getByTestId, getByText } = render(React.createElement(app.start()));\n  expect(getByTestId('count').innerHTML).toEqual('1');\n  fireEvent.click(getByText('Users'));\n  await delay(100);\n  expect(getByTestId('count').innerHTML).toEqual('2');\n  fireEvent.click(getByText('Home'));\n  await delay(100);\n  expect(getByTestId('count').innerHTML).toEqual('3');\n});\n\ntest('connect', () => {\n  const app = dva();\n  app.model({\n    namespace: 'count',\n    state: 0,\n    reducers: {\n      add(state) {\n        return state + 1;\n      },\n    },\n  });\n  const App = connect(state => ({ count: state.count }))(({ count, dispatch }) => {\n    return (\n      <>\n        <div data-testid=\"count\">{count}</div>\n        <button\n          onClick={() => {\n            dispatch({ type: 'count/add' });\n          }}\n        >\n          add\n        </button>\n      </>\n    );\n  });\n  app.router(() => <App />);\n\n  const { getByTestId, getByText } = render(React.createElement(app.start()));\n  expect(getByTestId('count').innerHTML).toEqual('0');\n  fireEvent.click(getByText('add'));\n  expect(getByTestId('count').innerHTML).toEqual('1');\n});\n\ntest('hooks api: useDispatch, useSelector shallowEqual, and useStore', () => {\n  const app = dva();\n  app.model({\n    namespace: 'count',\n    state: 0,\n    reducers: {\n      add(state) {\n        return state + 1;\n      },\n    },\n  });\n\n  const useShallowEqualSelector = selector => useSelector(selector, shallowEqual);\n\n  const App = () => {\n    const dispatch = useDispatch();\n    const store = useStore();\n    const { count } = useSelector(state => ({ count: state.count }));\n    const { shallowEqualCount } = useShallowEqualSelector(state => ({\n      shallowEqualCount: state.count,\n    }));\n\n    return (\n      <>\n        <div data-testid=\"count\">{count}</div>\n        <div data-testid=\"shallowEqualCount\">{shallowEqualCount}</div>\n        <div data-testid=\"state\">{store.getState().count}</div>\n        <button\n          onClick={() => {\n            dispatch({ type: 'count/add' });\n          }}\n        >\n          add\n        </button>\n      </>\n    );\n  };\n  app.router(() => <App />);\n\n  const { getByTestId, getByText } = render(React.createElement(app.start()));\n  expect(getByTestId('count').innerHTML).toEqual('0');\n  expect(getByTestId('shallowEqualCount').innerHTML).toEqual('0');\n  fireEvent.click(getByText('add'));\n  expect(getByTestId('count').innerHTML).toEqual('1');\n  expect(getByTestId('shallowEqualCount').innerHTML).toEqual('1');\n  expect(getByTestId('state').innerHTML).toEqual('1');\n});\n\ntest('navigate', async () => {\n  const history = createMemoryHistory({\n    initialEntries: ['/'],\n  });\n  const app = dva({\n    history,\n  });\n\n  function Home() {\n    return <h1 data-testid=\"title\">You are on Home</h1>;\n  }\n  function Users() {\n    return <h1 data-testid=\"title\">You are on Users</h1>;\n  }\n  app.router(({ history }) => {\n    return (\n      <Router history={history}>\n        <>\n          <Link to=\"/\">Home</Link>\n          <Link to=\"/users\">Users</Link>\n          <button\n            onClick={() => {\n              app._store.dispatch(routerRedux.push('/'));\n            }}\n          >\n            RouterRedux to Home\n          </button>\n          <Switch>\n            <Route path=\"/\" exact component={Home} />\n            <Route path=\"/users\" component={Users} />\n          </Switch>\n        </>\n      </Router>\n    );\n  });\n\n  const { getByTestId, getByText } = render(React.createElement(app.start()));\n  expect(getByTestId('title').innerHTML).toEqual('You are on Home');\n  fireEvent.click(getByText('Users'));\n  await delay(100);\n  expect(getByTestId('title').innerHTML).toEqual('You are on Users');\n  fireEvent.click(getByText('RouterRedux to Home'));\n  await delay(100);\n  expect(getByTestId('title').innerHTML).toEqual('You are on Home');\n});\n"
  },
  {
    "path": "packages/dva/test/index.test.js",
    "content": "import expect from 'expect';\nimport React from 'react';\nimport dva, {\n  useDispatch,\n  useSelector,\n  useStore,\n  useHistory,\n  useLocation,\n  useParams,\n  useRouteMatch,\n} from '../src/index';\n\nconst countModel = {\n  namespace: 'count',\n  state: 0,\n  reducers: {\n    add(state, { payload }) {\n      return state + payload || 1;\n    },\n    minus(state, { payload }) {\n      return state - payload || 1;\n    },\n  },\n};\n\ndescribe('index', () => {\n  xit('normal', () => {\n    const app = dva();\n    app.model({ ...countModel });\n    app.router(() => <div />);\n    app.start('#root');\n  });\n\n  it('start without container', () => {\n    const app = dva();\n    app.model({ ...countModel });\n    app.router(() => <div />);\n    app.start();\n  });\n\n  it('throw error if no routes defined', () => {\n    const app = dva();\n    expect(() => {\n      app.start();\n    }).toThrow(/router must be registered before app.start/);\n  });\n\n  it('opts.initialState', () => {\n    const app = dva({\n      initialState: { count: 1 },\n    });\n    app.model({ ...countModel });\n    app.router(() => <div />);\n    app.start();\n    expect(app._store.getState().count).toEqual(1);\n  });\n\n  it('opts.onAction', () => {\n    let count;\n    const countMiddleware = () => () => () => {\n      count += 1;\n    };\n\n    const app = dva({\n      onAction: countMiddleware,\n    });\n    app.router(() => <div />);\n    app.start();\n\n    count = 0;\n    app._store.dispatch({ type: 'test' });\n    expect(count).toEqual(1);\n  });\n\n  it('opts.onAction with array', () => {\n    let count;\n    const countMiddleware = () => next => action => {\n      count += 1;\n      next(action);\n    };\n    const count2Middleware = () => next => action => {\n      count += 2;\n      next(action);\n    };\n\n    const app = dva({\n      onAction: [countMiddleware, count2Middleware],\n    });\n    app.router(() => <div />);\n    app.start();\n\n    count = 0;\n    app._store.dispatch({ type: 'test' });\n    expect(count).toEqual(3);\n  });\n\n  it('opts.extraEnhancers', () => {\n    let count = 0;\n    const countEnhancer = storeCreator => (reducer, preloadedState, enhancer) => {\n      const store = storeCreator(reducer, preloadedState, enhancer);\n      const oldDispatch = store.dispatch;\n      store.dispatch = action => {\n        count += 1;\n        oldDispatch(action);\n      };\n      return store;\n    };\n    const app = dva({\n      extraEnhancers: [countEnhancer],\n    });\n    app.router(() => <div />);\n    app.start();\n\n    app._store.dispatch({ type: 'test' });\n    expect(count).toEqual(1);\n  });\n\n  it('opts.onStateChange', () => {\n    let savedState = null;\n\n    const app = dva({\n      onStateChange(state) {\n        savedState = state;\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.router(() => <div />);\n    app.start();\n\n    app._store.dispatch({ type: 'count/add' });\n    expect(savedState.count).toEqual(1);\n  });\n\n  it('hooks should not be undifined', () => {\n    [useSelector, useDispatch, useStore, useHistory, useLocation, useParams, useRouteMatch].forEach(\n      hook => {\n        expect(hook !== undefined).toEqual(true);\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "packages/dva/warnAboutDeprecatedCJSRequire.js",
    "content": "var printWarning = function() {};\n\nif (process.env.NODE_ENV !== 'production') {\n  printWarning = function(format, subs) {\n    var index = 0;\n    var message =\n      'Warning: ' +\n      (subs.length > 0\n        ? format.replace(/%s/g, function() {\n            return subs[index++];\n          })\n        : format);\n\n    if (typeof console !== 'undefined') {\n      console.error(message);\n    }\n\n    try {\n      // --- Welcome to debugging history ---\n      // This error was thrown as a convenience so that you can use the\n      // stack trace to find the callsite that triggered this warning.\n      throw new Error(message);\n    } catch (e) {}\n  };\n}\n\nmodule.exports = function(member) {\n  printWarning(\n    'Please use `require(\"dva\").%s` instead of `require(\"dva/%s\")`. ' +\n      'Support for the latter will be removed in the next major release.',\n    [member, member]\n  );\n};\n"
  },
  {
    "path": "packages/dva-core/.fatherrc.js",
    "content": "export default {\n  cjs: 'rollup',\n  esm: 'rollup',\n  runtimeHelpers: true,\n};\n"
  },
  {
    "path": "packages/dva-core/README.md",
    "content": "# dva-core\n\n[![NPM version](https://img.shields.io/npm/v/dva-core.svg?style=flat)](https://npmjs.org/package/dva-core)\n[![Build Status](https://img.shields.io/travis/dvajs/dva-core.svg?style=flat)](https://travis-ci.org/dvajs/dva-core)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-core.svg?style=flat)](https://coveralls.io/r/dvajs/dva-core)\n[![NPM downloads](http://img.shields.io/npm/dm/dva-core.svg?style=flat)](https://npmjs.org/package/dva-core)\n[![Dependencies](https://david-dm.org/dvajs/dva-core/status.svg)](https://david-dm.org/dvajs/dva-core)\n\nThe core lightweight library for dva, based on redux and redux-saga.\n\n## LICENSE\n\nMIT\n\n"
  },
  {
    "path": "packages/dva-core/package.json",
    "content": "{\n  \"name\": \"dva-core\",\n  \"version\": \"2.0.4\",\n  \"description\": \"The core lightweight library for dva, based on redux and redux-saga.\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"sideEffects\": false,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dvajs/dva\"\n  },\n  \"homepage\": \"https://github.com/dvajs/dva\",\n  \"keywords\": [\n    \"dva\",\n    \"alibaba\",\n    \"redux\",\n    \"redux-saga\",\n    \"elm\",\n    \"framework\",\n    \"frontend\"\n  ],\n  \"authors\": [\n    \"chencheng <sorrycc@gmail.com> (https://github.com/sorrycc)\"\n  ],\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/dvajs/dva/issues\"\n  },\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.0.0\",\n    \"flatten\": \"^1.0.2\",\n    \"global\": \"^4.3.2\",\n    \"invariant\": \"^2.2.1\",\n    \"is-plain-object\": \"^2.0.3\",\n    \"redux-saga\": \"^0.16.0\",\n    \"warning\": \"^3.0.0\"\n  },\n  \"peerDependencies\": {\n    \"redux\": \"4.x\"\n  },\n  \"devDependencies\": {\n    \"mm\": \"^2.5.0\",\n    \"redux\": \"^4.0.1\"\n  },\n  \"files\": [\n    \"dist\",\n    \"lib\",\n    \"src\",\n    \"index.js\",\n    \"saga.js\"\n  ]\n}\n"
  },
  {
    "path": "packages/dva-core/saga.js",
    "content": "module.exports = require('redux-saga');\n"
  },
  {
    "path": "packages/dva-core/src/Plugin.js",
    "content": "import invariant from 'invariant';\nimport { isPlainObject } from './utils';\n\nconst hooks = [\n  'onError',\n  'onStateChange',\n  'onAction',\n  'onHmr',\n  'onReducer',\n  'onEffect',\n  'extraReducers',\n  'extraEnhancers',\n  '_handleActions',\n];\n\nexport function filterHooks(obj) {\n  return Object.keys(obj).reduce((memo, key) => {\n    if (hooks.indexOf(key) > -1) {\n      memo[key] = obj[key];\n    }\n    return memo;\n  }, {});\n}\n\nexport default class Plugin {\n  constructor() {\n    this._handleActions = null;\n    this.hooks = hooks.reduce((memo, key) => {\n      memo[key] = [];\n      return memo;\n    }, {});\n  }\n\n  use(plugin) {\n    invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');\n    const { hooks } = this;\n    for (const key in plugin) {\n      if (Object.prototype.hasOwnProperty.call(plugin, key)) {\n        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);\n        if (key === '_handleActions') {\n          this._handleActions = plugin[key];\n        } else if (key === 'extraEnhancers') {\n          hooks[key] = plugin[key];\n        } else {\n          hooks[key].push(plugin[key]);\n        }\n      }\n    }\n  }\n\n  apply(key, defaultHandler) {\n    const { hooks } = this;\n    const validApplyHooks = ['onError', 'onHmr'];\n    invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);\n    const fns = hooks[key];\n\n    return (...args) => {\n      if (fns.length) {\n        for (const fn of fns) {\n          fn(...args);\n        }\n      } else if (defaultHandler) {\n        defaultHandler(...args);\n      }\n    };\n  }\n\n  get(key) {\n    const { hooks } = this;\n    invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);\n    if (key === 'extraReducers') {\n      return getExtraReducers(hooks[key]);\n    } else if (key === 'onReducer') {\n      return getOnReducer(hooks[key]);\n    } else {\n      return hooks[key];\n    }\n  }\n}\n\nfunction getExtraReducers(hook) {\n  let ret = {};\n  for (const reducerObj of hook) {\n    ret = { ...ret, ...reducerObj };\n  }\n  return ret;\n}\n\nfunction getOnReducer(hook) {\n  return function(reducer) {\n    for (const reducerEnhancer of hook) {\n      reducer = reducerEnhancer(reducer);\n    }\n    return reducer;\n  };\n}\n"
  },
  {
    "path": "packages/dva-core/src/checkModel.js",
    "content": "import invariant from 'invariant';\nimport { isArray, isFunction, isPlainObject } from './utils';\n\nexport default function checkModel(model, existModels) {\n  const { namespace, reducers, effects, subscriptions } = model;\n\n  // namespace 必须被定义\n  invariant(namespace, `[app.model] namespace should be defined`);\n  // 并且是字符串\n  invariant(\n    typeof namespace === 'string',\n    `[app.model] namespace should be string, but got ${typeof namespace}`,\n  );\n  // 并且唯一\n  invariant(\n    !existModels.some(model => model.namespace === namespace),\n    `[app.model] namespace should be unique`,\n  );\n\n  // state 可以为任意值\n\n  // reducers 可以为空，PlainObject 或者数组\n  if (reducers) {\n    invariant(\n      isPlainObject(reducers) || isArray(reducers),\n      `[app.model] reducers should be plain object or array, but got ${typeof reducers}`,\n    );\n    // 数组的 reducers 必须是 [Object, Function] 的格式\n    invariant(\n      !isArray(reducers) || (isPlainObject(reducers[0]) && isFunction(reducers[1])),\n      `[app.model] reducers with array should be [Object, Function]`,\n    );\n  }\n\n  // effects 可以为空，PlainObject\n  if (effects) {\n    invariant(\n      isPlainObject(effects),\n      `[app.model] effects should be plain object, but got ${typeof effects}`,\n    );\n  }\n\n  if (subscriptions) {\n    // subscriptions 可以为空，PlainObject\n    invariant(\n      isPlainObject(subscriptions),\n      `[app.model] subscriptions should be plain object, but got ${typeof subscriptions}`,\n    );\n\n    // subscription 必须为函数\n    invariant(isAllFunction(subscriptions), `[app.model] subscription should be function`);\n  }\n}\n\nfunction isAllFunction(obj) {\n  return Object.keys(obj).every(key => isFunction(obj[key]));\n}\n"
  },
  {
    "path": "packages/dva-core/src/constants.js",
    "content": "export const NAMESPACE_SEP = '/';\n"
  },
  {
    "path": "packages/dva-core/src/createPromiseMiddleware.js",
    "content": "import { NAMESPACE_SEP } from './constants';\n\nexport default function createPromiseMiddleware(app) {\n  return () => next => action => {\n    const { type } = action;\n    if (isEffect(type)) {\n      return new Promise((resolve, reject) => {\n        next({\n          __dva_resolve: resolve,\n          __dva_reject: reject,\n          ...action,\n        });\n      });\n    } else {\n      return next(action);\n    }\n  };\n\n  function isEffect(type) {\n    if (!type || typeof type !== 'string') return false;\n    const [namespace] = type.split(NAMESPACE_SEP);\n    const model = app._models.filter(m => m.namespace === namespace)[0];\n    if (model) {\n      if (model.effects && model.effects[type]) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/dva-core/src/createStore.js",
    "content": "import { createStore, applyMiddleware, compose } from 'redux';\nimport flatten from 'flatten';\nimport invariant from 'invariant';\nimport win from 'global/window';\nimport { returnSelf, isArray } from './utils';\n\nexport default function({\n  reducers,\n  initialState,\n  plugin,\n  sagaMiddleware,\n  promiseMiddleware,\n  createOpts: { setupMiddlewares = returnSelf },\n}) {\n  // extra enhancers\n  const extraEnhancers = plugin.get('extraEnhancers');\n  invariant(\n    isArray(extraEnhancers),\n    `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`,\n  );\n\n  const extraMiddlewares = plugin.get('onAction');\n  const middlewares = setupMiddlewares([\n    promiseMiddleware,\n    sagaMiddleware,\n    ...flatten(extraMiddlewares),\n  ]);\n\n  const composeEnhancers =\n    process.env.NODE_ENV !== 'production' && win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__\n      ? win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, maxAge: 30 })\n      : compose;\n\n  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];\n\n  return createStore(reducers, initialState, composeEnhancers(...enhancers));\n}\n"
  },
  {
    "path": "packages/dva-core/src/getReducer.js",
    "content": "import defaultHandleActions from './handleActions';\n\nexport default function getReducer(reducers, state, handleActions) {\n  // Support reducer enhancer\n  // e.g. reducers: [realReducers, enhancer]\n  if (Array.isArray(reducers)) {\n    return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state));\n  } else {\n    return (handleActions || defaultHandleActions)(reducers || {}, state);\n  }\n}\n"
  },
  {
    "path": "packages/dva-core/src/getSaga.js",
    "content": "import invariant from 'invariant';\nimport warning from 'warning';\nimport { effects as sagaEffects } from 'redux-saga';\nimport { NAMESPACE_SEP } from './constants';\nimport prefixType from './prefixType';\n\nexport default function getSaga(effects, model, onError, onEffect, opts = {}) {\n  return function*() {\n    for (const key in effects) {\n      if (Object.prototype.hasOwnProperty.call(effects, key)) {\n        const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts);\n        const task = yield sagaEffects.fork(watcher);\n        yield sagaEffects.fork(function*() {\n          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);\n          yield sagaEffects.cancel(task);\n        });\n      }\n    }\n  };\n}\n\nfunction getWatcher(key, _effect, model, onError, onEffect, opts) {\n  let effect = _effect;\n  let type = 'takeEvery';\n  let ms;\n  let delayMs;\n\n  if (Array.isArray(_effect)) {\n    [effect] = _effect;\n    const opts = _effect[1];\n    if (opts && opts.type) {\n      ({ type } = opts);\n      if (type === 'throttle') {\n        invariant(opts.ms, 'app.start: opts.ms should be defined if type is throttle');\n        ({ ms } = opts);\n      }\n      if (type === 'poll') {\n        invariant(opts.delay, 'app.start: opts.delay should be defined if type is poll');\n        ({ delay: delayMs } = opts);\n      }\n    }\n    invariant(\n      ['watcher', 'takeEvery', 'takeLatest', 'throttle', 'poll'].indexOf(type) > -1,\n      'app.start: effect type should be takeEvery, takeLatest, throttle, poll or watcher',\n    );\n  }\n\n  function noop() {}\n\n  function* sagaWithCatch(...args) {\n    const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =\n      args.length > 0 ? args[0] : {};\n    try {\n      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });\n      const ret = yield effect(...args.concat(createEffects(model, opts)));\n      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });\n      resolve(ret);\n    } catch (e) {\n      onError(e, {\n        key,\n        effectArgs: args,\n      });\n      if (!e._dontReject) {\n        reject(e);\n      }\n    }\n  }\n\n  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);\n\n  switch (type) {\n    case 'watcher':\n      return sagaWithCatch;\n    case 'takeLatest':\n      return function*() {\n        yield sagaEffects.takeLatest(key, sagaWithOnEffect);\n      };\n    case 'throttle':\n      return function*() {\n        yield sagaEffects.throttle(ms, key, sagaWithOnEffect);\n      };\n    case 'poll':\n      return function*() {\n        function delay(timeout) {\n          return new Promise(resolve => setTimeout(resolve, timeout));\n        }\n        function* pollSagaWorker(sagaEffects, action) {\n          const { call } = sagaEffects;\n          while (true) {\n            yield call(sagaWithOnEffect, action);\n            yield call(delay, delayMs);\n          }\n        }\n        const { call, take, race } = sagaEffects;\n        while (true) {\n          const action = yield take(`${key}-start`);\n          yield race([call(pollSagaWorker, sagaEffects, action), take(`${key}-stop`)]);\n        }\n      };\n    default:\n      return function*() {\n        yield sagaEffects.takeEvery(key, sagaWithOnEffect);\n      };\n  }\n}\n\nfunction createEffects(model, opts) {\n  function assertAction(type, name) {\n    invariant(type, 'dispatch: action should be a plain Object with type');\n\n    const { namespacePrefixWarning = true } = opts;\n\n    if (namespacePrefixWarning) {\n      warning(\n        type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,\n        `[${name}] ${type} should not be prefixed with namespace ${model.namespace}`,\n      );\n    }\n  }\n  function put(action) {\n    const { type } = action;\n    assertAction(type, 'sagaEffects.put');\n    return sagaEffects.put({ ...action, type: prefixType(type, model) });\n  }\n\n  // The operator `put` doesn't block waiting the returned promise to resolve.\n  // Using `put.resolve` will wait until the promsie resolve/reject before resuming.\n  // It will be helpful to organize multi-effects in order,\n  // and increase the reusability by seperate the effect in stand-alone pieces.\n  // https://github.com/redux-saga/redux-saga/issues/336\n  function putResolve(action) {\n    const { type } = action;\n    assertAction(type, 'sagaEffects.put.resolve');\n    return sagaEffects.put.resolve({\n      ...action,\n      type: prefixType(type, model),\n    });\n  }\n  put.resolve = putResolve;\n\n  function take(type) {\n    if (typeof type === 'string') {\n      assertAction(type, 'sagaEffects.take');\n      return sagaEffects.take(prefixType(type, model));\n    } else if (Array.isArray(type)) {\n      return sagaEffects.take(\n        type.map(t => {\n          if (typeof t === 'string') {\n            assertAction(t, 'sagaEffects.take');\n            return prefixType(t, model);\n          }\n          return t;\n        }),\n      );\n    } else {\n      return sagaEffects.take(type);\n    }\n  }\n  return { ...sagaEffects, put, take };\n}\n\nfunction applyOnEffect(fns, effect, model, key) {\n  for (const fn of fns) {\n    effect = fn(effect, sagaEffects, model, key);\n  }\n  return effect;\n}\n"
  },
  {
    "path": "packages/dva-core/src/handleActions.js",
    "content": "import invariant from 'invariant';\n\nfunction identify(value) {\n  return value;\n}\n\nfunction handleAction(actionType, reducer = identify) {\n  return (state, action) => {\n    const { type } = action;\n    invariant(type, 'dispatch: action should be a plain Object with type');\n    if (actionType === type) {\n      return reducer(state, action);\n    }\n    return state;\n  };\n}\n\nfunction reduceReducers(...reducers) {\n  return (previous, current) => reducers.reduce((p, r) => r(p, current), previous);\n}\n\nfunction handleActions(handlers, defaultState) {\n  const reducers = Object.keys(handlers).map(type => handleAction(type, handlers[type]));\n  const reducer = reduceReducers(...reducers);\n  return (state = defaultState, action) => reducer(state, action);\n}\n\nexport default handleActions;\n"
  },
  {
    "path": "packages/dva-core/src/index.js",
    "content": "import { combineReducers } from 'redux';\nimport createSagaMiddleware, * as saga from 'redux-saga';\nimport invariant from 'invariant';\nimport checkModel from './checkModel';\nimport prefixNamespace from './prefixNamespace';\nimport Plugin, { filterHooks } from './Plugin';\nimport createStore from './createStore';\nimport getSaga from './getSaga';\nimport getReducer from './getReducer';\nimport createPromiseMiddleware from './createPromiseMiddleware';\nimport { run as runSubscription, unlisten as unlistenSubscription } from './subscription';\nimport * as utils from './utils';\n\nconst { noop, findIndex } = utils;\n\n// Internal model to update global state when do unmodel\nconst dvaModel = {\n  namespace: '@@dva',\n  state: 0,\n  reducers: {\n    UPDATE(state) {\n      return state + 1;\n    },\n  },\n};\n\n/**\n * Create dva-core instance.\n *\n * @param hooksAndOpts\n * @param createOpts\n */\nexport function create(hooksAndOpts = {}, createOpts = {}) {\n  const { initialReducer, setupApp = noop } = createOpts;\n\n  const plugin = new Plugin();\n  plugin.use(filterHooks(hooksAndOpts));\n\n  const app = {\n    _models: [prefixNamespace({ ...dvaModel })],\n    _store: null,\n    _plugin: plugin,\n    use: plugin.use.bind(plugin),\n    model,\n    start,\n  };\n  return app;\n\n  /**\n   * Register model before app is started.\n   *\n   * @param m {Object} model to register\n   */\n  function model(m) {\n    if (process.env.NODE_ENV !== 'production') {\n      checkModel(m, app._models);\n    }\n    const prefixedModel = prefixNamespace({ ...m });\n    app._models.push(prefixedModel);\n    return prefixedModel;\n  }\n\n  /**\n   * Inject model after app is started.\n   *\n   * @param createReducer\n   * @param onError\n   * @param unlisteners\n   * @param m\n   */\n  function injectModel(createReducer, onError, unlisteners, m) {\n    m = model(m);\n\n    const store = app._store;\n    store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);\n    store.replaceReducer(createReducer());\n    if (m.effects) {\n      store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));\n    }\n    if (m.subscriptions) {\n      unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);\n    }\n  }\n\n  /**\n   * Unregister model.\n   *\n   * @param createReducer\n   * @param reducers\n   * @param unlisteners\n   * @param namespace\n   *\n   * Unexpected key warn problem:\n   * https://github.com/reactjs/redux/issues/1636\n   */\n  function unmodel(createReducer, reducers, unlisteners, namespace) {\n    const store = app._store;\n\n    // Delete reducers\n    delete store.asyncReducers[namespace];\n    delete reducers[namespace];\n    store.replaceReducer(createReducer());\n    store.dispatch({ type: '@@dva/UPDATE' });\n\n    // Cancel effects\n    store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });\n\n    // Unlisten subscrioptions\n    unlistenSubscription(unlisteners, namespace);\n\n    // Delete model from app._models\n    app._models = app._models.filter(model => model.namespace !== namespace);\n  }\n\n  /**\n   * Replace a model if it exsits, if not, add it to app\n   * Attention:\n   * - Only available after dva.start gets called\n   * - Will not check origin m is strict equal to the new one\n   * Useful for HMR\n   * @param createReducer\n   * @param reducers\n   * @param unlisteners\n   * @param onError\n   * @param m\n   */\n  function replaceModel(createReducer, reducers, unlisteners, onError, m) {\n    const store = app._store;\n    const { namespace } = m;\n    const oldModelIdx = findIndex(app._models, model => model.namespace === namespace);\n\n    if (~oldModelIdx) {\n      // Cancel effects\n      store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });\n\n      // Delete reducers\n      delete store.asyncReducers[namespace];\n      delete reducers[namespace];\n\n      // Unlisten subscrioptions\n      unlistenSubscription(unlisteners, namespace);\n\n      // Delete model from app._models\n      app._models.splice(oldModelIdx, 1);\n    }\n\n    // add new version model to store\n    app.model(m);\n\n    store.dispatch({ type: '@@dva/UPDATE' });\n  }\n\n  /**\n   * Start the app.\n   *\n   * @returns void\n   */\n  function start() {\n    // Global error handler\n    const onError = (err, extension) => {\n      if (err) {\n        if (typeof err === 'string') err = new Error(err);\n        err.preventDefault = () => {\n          err._dontReject = true;\n        };\n        plugin.apply('onError', err => {\n          throw new Error(err.stack || err);\n        })(err, app._store.dispatch, extension);\n      }\n    };\n\n    const sagaMiddleware = createSagaMiddleware();\n    const promiseMiddleware = createPromiseMiddleware(app);\n    app._getSaga = getSaga.bind(null);\n\n    const sagas = [];\n    const reducers = { ...initialReducer };\n    for (const m of app._models) {\n      reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);\n      if (m.effects) {\n        sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));\n      }\n    }\n    const reducerEnhancer = plugin.get('onReducer');\n    const extraReducers = plugin.get('extraReducers');\n    invariant(\n      Object.keys(extraReducers).every(key => !(key in reducers)),\n      `[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(\n        reducers,\n      ).join(', ')}`,\n    );\n\n    // Create store\n    app._store = createStore({\n      reducers: createReducer(),\n      initialState: hooksAndOpts.initialState || {},\n      plugin,\n      createOpts,\n      sagaMiddleware,\n      promiseMiddleware,\n    });\n\n    const store = app._store;\n\n    // Extend store\n    store.runSaga = sagaMiddleware.run;\n    store.asyncReducers = {};\n\n    // Execute listeners when state is changed\n    const listeners = plugin.get('onStateChange');\n    for (const listener of listeners) {\n      store.subscribe(() => {\n        listener(store.getState());\n      });\n    }\n\n    // Run sagas\n    sagas.forEach(sagaMiddleware.run);\n\n    // Setup app\n    setupApp(app);\n\n    // Run subscriptions\n    const unlisteners = {};\n    for (const model of this._models) {\n      if (model.subscriptions) {\n        unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);\n      }\n    }\n\n    // Setup app.model and app.unmodel\n    app.model = injectModel.bind(app, createReducer, onError, unlisteners);\n    app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);\n    app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);\n\n    /**\n     * Create global reducer for redux.\n     *\n     * @returns {Object}\n     */\n    function createReducer() {\n      return reducerEnhancer(\n        combineReducers({\n          ...reducers,\n          ...extraReducers,\n          ...(app._store ? app._store.asyncReducers : {}),\n        }),\n      );\n    }\n  }\n}\n\nexport { saga };\nexport { utils };\n"
  },
  {
    "path": "packages/dva-core/src/prefixNamespace.js",
    "content": "import warning from 'warning';\nimport { isArray } from './utils';\nimport { NAMESPACE_SEP } from './constants';\n\nfunction prefix(obj, namespace, type) {\n  return Object.keys(obj).reduce((memo, key) => {\n    warning(\n      key.indexOf(`${namespace}${NAMESPACE_SEP}`) !== 0,\n      `[prefixNamespace]: ${type} ${key} should not be prefixed with namespace ${namespace}`,\n    );\n    const newKey = `${namespace}${NAMESPACE_SEP}${key}`;\n    memo[newKey] = obj[key];\n    return memo;\n  }, {});\n}\n\nexport default function prefixNamespace(model) {\n  const { namespace, reducers, effects } = model;\n\n  if (reducers) {\n    if (isArray(reducers)) {\n      // 需要复制一份，不能直接修改 model.reducers[0], 会导致微前端场景下，重复添加前缀\n      const [reducer, ...rest] = reducers;\n      model.reducers = [prefix(reducer, namespace, 'reducer'), ...rest];\n    } else {\n      model.reducers = prefix(reducers, namespace, 'reducer');\n    }\n  }\n  if (effects) {\n    model.effects = prefix(effects, namespace, 'effect');\n  }\n  return model;\n}\n"
  },
  {
    "path": "packages/dva-core/src/prefixType.js",
    "content": "import { NAMESPACE_SEP } from './constants';\n\nexport default function prefixType(type, model) {\n  const prefixedType = `${model.namespace}${NAMESPACE_SEP}${type}`;\n  const typeWithoutAffix = prefixedType.replace(/\\/@@[^/]+?$/, '');\n\n  const reducer = Array.isArray(model.reducers)\n    ? model.reducers[0][typeWithoutAffix]\n    : model.reducers && model.reducers[typeWithoutAffix];\n  if (reducer || (model.effects && model.effects[typeWithoutAffix])) {\n    return prefixedType;\n  }\n  return type;\n}\n"
  },
  {
    "path": "packages/dva-core/src/prefixedDispatch.js",
    "content": "import invariant from 'invariant';\nimport warning from 'warning';\nimport { NAMESPACE_SEP } from './constants';\nimport prefixType from './prefixType';\n\nexport default function prefixedDispatch(dispatch, model) {\n  return action => {\n    const { type } = action;\n    invariant(type, 'dispatch: action should be a plain Object with type');\n    warning(\n      type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,\n      `dispatch: ${type} should not be prefixed with namespace ${model.namespace}`,\n    );\n    return dispatch({ ...action, type: prefixType(type, model) });\n  };\n}\n"
  },
  {
    "path": "packages/dva-core/src/subscription.js",
    "content": "import warning from 'warning';\nimport { isFunction } from './utils';\nimport prefixedDispatch from './prefixedDispatch';\n\nexport function run(subs, model, app, onError) {\n  const funcs = [];\n  const nonFuncs = [];\n  for (const key in subs) {\n    if (Object.prototype.hasOwnProperty.call(subs, key)) {\n      const sub = subs[key];\n      const unlistener = sub(\n        {\n          dispatch: prefixedDispatch(app._store.dispatch, model),\n          history: app._history,\n        },\n        onError,\n      );\n      if (isFunction(unlistener)) {\n        funcs.push(unlistener);\n      } else {\n        nonFuncs.push(key);\n      }\n    }\n  }\n  return { funcs, nonFuncs };\n}\n\nexport function unlisten(unlisteners, namespace) {\n  if (!unlisteners[namespace]) return;\n\n  const { funcs, nonFuncs } = unlisteners[namespace];\n  warning(\n    nonFuncs.length === 0,\n    `[app.unmodel] subscription should return unlistener function, check these subscriptions ${nonFuncs.join(\n      ', ',\n    )}`,\n  );\n  for (const unlistener of funcs) {\n    unlistener();\n  }\n  delete unlisteners[namespace];\n}\n"
  },
  {
    "path": "packages/dva-core/src/utils.js",
    "content": "import isPlainObject from 'is-plain-object';\nexport { isPlainObject };\nexport const isArray = Array.isArray.bind(Array);\nexport const isFunction = o => typeof o === 'function';\nexport const returnSelf = m => m;\nexport const noop = () => {};\nexport const findIndex = (array, predicate) => {\n  for (let i = 0, { length } = array; i < length; i += 1) {\n    if (predicate(array[i], i)) return i;\n  }\n\n  return -1;\n};\n"
  },
  {
    "path": "packages/dva-core/test/checkModel.test.js",
    "content": "import { create } from '../src/index';\n\ndescribe('checkModel', () => {\n  it('namespace should be defined', () => {\n    const app = create();\n    expect(() => {\n      app.model({});\n    }).toThrow(/\\[app\\.model\\] namespace should be defined/);\n  });\n\n  it('namespace should be unique', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: 'repeat',\n      });\n      app.model({\n        namespace: 'repeat',\n      });\n    }).toThrow(/\\[app\\.model\\] namespace should be unique/);\n  });\n\n  it('reducers can be specified array', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_array',\n        reducers: [{}, () => {}],\n      });\n    }).not.toThrow();\n  });\n\n  it('reducers can be object', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_object',\n        reducers: {},\n      });\n    }).not.toThrow();\n  });\n\n  it('reducers can not be string', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_neither',\n        reducers: '_',\n      });\n    }).toThrow(/\\[app\\.model\\] reducers should be plain object or array/);\n  });\n\n  it('reducers in array should be [Object, Function]', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_none',\n        reducers: [],\n      });\n    }).toThrow(/\\[app\\.model\\] reducers with array should be \\[Object, Function\\]/);\n  });\n\n  it('subscriptions should be plain object', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_',\n        subscriptions: [],\n      });\n    }).toThrow(/\\[app\\.model\\] subscriptions should be plain object/);\n    expect(() => {\n      app.model({\n        namespace: '_',\n        subscriptions: '_',\n      });\n    }).toThrow(/\\[app\\.model\\] subscriptions should be plain object/);\n  });\n\n  it('subscriptions can be undefined', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_',\n      });\n    }).not.toThrow();\n  });\n\n  it('effects should be plain object', () => {\n    const app = create();\n    expect(() => {\n      app.model({\n        namespace: '_',\n        effects: [],\n      });\n    }).toThrow(/\\[app\\.model\\] effects should be plain object/);\n    expect(() => {\n      app.model({\n        namespace: '_',\n        effects: '_',\n      });\n    }).toThrow(/\\[app\\.model\\] effects should be plain object/);\n    expect(() => {\n      app.model({\n        namespace: '_',\n        effects: {},\n      });\n    }).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/effects.test.js",
    "content": "import expect from 'expect';\nimport mm from 'mm';\nimport { create } from '../src/index';\n\nconst delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));\n\ndescribe('effects', () => {\n  it('put action', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *addDelay({ payload }, { put, call }) {\n          yield call(delay, 100);\n          yield put({ type: 'add', payload });\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'count/addDelay', payload: 2 });\n    expect(app._store.getState().count).toEqual(0);\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(2);\n      done();\n    }, 200);\n  });\n\n  function testAppCreator(opts) {\n    const app = create(opts);\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *putWithNamespace({ payload }, { put }) {\n          yield put({ type: 'count/add', payload });\n        },\n        *putWithoutNamespace({ payload }, { put }) {\n          yield put({ type: 'add', payload });\n        },\n      },\n    });\n    return app;\n  }\n\n  it('put action with namespace will get a warning', () => {\n    const app = testAppCreator();\n    const logs = [];\n    mm(console, 'error', log => {\n      logs.push(log);\n    });\n    app.start();\n    expect(app._store.getState().count).toEqual(0);\n    expect(logs.length).toEqual(0);\n    app._store.dispatch({ type: 'count/putWithNamespace', payload: 2 });\n    expect(logs.length).toEqual(1);\n    expect(logs[0]).toEqual(\n      'Warning: [sagaEffects.put] count/add should not be prefixed with namespace count',\n    );\n    app._store.dispatch({ type: 'count/putWithoutNamespace', payload: 2 });\n    expect(logs.length).toEqual(1);\n    expect(app._store.getState().count).toEqual(4);\n    mm.restore();\n  });\n\n  it('test disable namespacePrefixWarning', () => {\n    const app = testAppCreator({ namespacePrefixWarning: false });\n    const logs = [];\n    mm(console, 'error', log => {\n      logs.push(log);\n    });\n    app.start();\n    expect(app._store.getState().count).toEqual(0);\n    expect(logs.length).toEqual(0);\n    app._store.dispatch({ type: 'count/putWithNamespace', payload: 2 });\n    expect(logs.length).toEqual(0);\n    app._store.dispatch({ type: 'count/putWithoutNamespace', payload: 2 });\n    expect(logs.length).toEqual(0);\n    expect(app._store.getState().count).toEqual(4);\n    mm.restore();\n  });\n\n  it('put multi effects in order', done => {\n    const app = create();\n    app.model({\n      namespace: 'counter',\n      state: {\n        count: 0,\n        resolveCount: 0,\n      },\n      reducers: {\n        dump(state, { payload }) {\n          return {\n            ...state,\n            ...payload,\n          };\n        },\n      },\n      effects: {\n        *changeCountDelay({ payload }, { put, call }) {\n          yield call(delay, 200);\n          yield put({ type: 'dump', payload: { count: payload } });\n        },\n        *process({ payload }, { put, select }) {\n          yield put.resolve({ type: 'changeCountDelay', payload });\n          const count = yield select(state => state.counter.count);\n          yield put({ type: 'dump', payload: { resolveCount: count } });\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'counter/process', payload: 1 }).then(() => {\n      expect(app._store.getState().counter.resolveCount).toEqual(1);\n      done();\n    });\n    expect(app._store.getState().counter.resolveCount).toEqual(0);\n  });\n\n  it('take', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *addDelay({ payload }, { put, call }) {\n          yield call(delay, payload.delay || 100);\n          yield put({ type: 'add', payload: payload.amount });\n        },\n        *test(action, { put, select, take }) {\n          yield put({ type: 'addDelay', payload: { amount: 2 } });\n          yield take('addDelay/@@end');\n          const count = yield select(state => state.count);\n          yield put({ type: 'addDelay', payload: { amount: count, delay: 0 } });\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'count/test' });\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(4);\n      done();\n    }, 300);\n  });\n\n  it('take with array of actions', () => {\n    const app = create();\n    let takenCount = 0;\n    app.model({\n      namespace: 'count',\n      state: null,\n      reducers: {\n        addRequest() {\n          return 1;\n        },\n        addFailure() {\n          return -1;\n        },\n        addSuccess() {\n          return 0;\n        },\n      },\n      effects: {\n        *add(action, { put }) {\n          yield put({ type: 'addRequest' });\n          if (action.amount > 0.5) {\n            yield put({ type: 'addSuccess' });\n          } else {\n            yield put({ type: 'addFailure' });\n          }\n        },\n        *test(action, { put, take }) {\n          yield put({ type: 'add', amount: action.amount });\n          yield take(['addSuccess', 'addFailure']);\n          takenCount += 1;\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'count/test', amount: 0 });\n    expect(app._store.getState().count).toEqual(-1);\n    app._store.dispatch({ type: 'count/test', amount: 1 });\n    expect(app._store.getState().count).toEqual(0);\n    expect(takenCount).toEqual(2);\n  });\n\n  it('dispatch action for other models', () => {\n    const app = create();\n    app.model({\n      namespace: 'loading',\n      state: false,\n      reducers: {\n        show() {\n          return true;\n        },\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *addDelay(_, { put }) {\n          yield put({ type: 'loading/show' });\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'count/addDelay' });\n    expect(app._store.getState().loading).toEqual(true);\n  });\n\n  it('onError', () => {\n    const errors = [];\n    const app = create({\n      onError: (error, dispatch) => {\n        error.preventDefault();\n        errors.push(error.message);\n        dispatch({ type: 'count/add' });\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *addDelay({ payload }, { put }) {\n          if (!payload) {\n            throw new Error('effect error');\n          } else {\n            yield put({ type: 'add', payload });\n          }\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({ type: 'count/addDelay' });\n    expect(errors).toEqual(['effect error']);\n    expect(app._store.getState().count).toEqual(1);\n    app._store.dispatch({ type: 'count/addDelay', payload: 2 });\n    expect(app._store.getState().count).toEqual(3);\n  });\n\n  it('onError: extension', () => {\n    const app = create({\n      onError(err, dispatch, extension) {\n        err.preventDefault();\n        dispatch({\n          type: 'err/append',\n          payload: extension,\n        });\n      },\n    });\n    app.model({\n      namespace: 'err',\n      state: [],\n      reducers: {\n        append(state, action) {\n          return [...state, action.payload];\n        },\n      },\n      effects: {\n        // eslint-disable-next-line\n        *generate() {\n          throw new Error('Effect error');\n        },\n      },\n    });\n    app.start();\n    app._store.dispatch({\n      type: 'err/generate',\n      payload: 'err.payload',\n    });\n    expect(app._store.getState().err.length).toEqual(1);\n    expect(app._store.getState().err[0].key).toEqual('err/generate');\n    expect(app._store.getState().err[0].effectArgs[0].type).toEqual('err/generate');\n    expect(app._store.getState().err[0].effectArgs[0].payload).toEqual('err.payload');\n  });\n\n  it('type: takeLatest', done => {\n    const app = create();\n    const takeLatest = { type: 'takeLatest' };\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        addDelay: [\n          function*({ payload }, { call, put }) {\n            yield call(delay, 100);\n            yield put({ type: 'add', payload });\n          },\n          takeLatest,\n        ],\n      },\n    });\n    app.start();\n\n    // Only catch the last one.\n    app._store.dispatch({ type: 'count/addDelay', payload: 2 });\n    app._store.dispatch({ type: 'count/addDelay', payload: 3 });\n\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(3);\n      done();\n    }, 200);\n  });\n\n  xit('type: throttle throw error if no ms', () => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        addDelay: [\n          function*() {\n            yield console.log(1);\n          },\n          { type: 'throttle' },\n        ],\n      },\n    });\n    expect(() => {\n      app.start();\n    }).toThrow(/app.start: opts.ms should be defined if type is throttle/);\n  });\n\n  it('type: throttle', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        addDelay: [\n          function*({ payload }, { call, put }) {\n            yield call(delay, 120);\n            yield put({ type: 'add', payload });\n          },\n          { type: 'throttle', ms: 100 },\n        ],\n      },\n    });\n    app.start();\n\n    // Only catch the last one.\n    app._store.dispatch({ type: 'count/addDelay', payload: 2 });\n    app._store.dispatch({ type: 'count/addDelay', payload: 3 });\n\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(2);\n      done();\n    }, 200);\n  });\n\n  it('type: watcher', done => {\n    const watcher = { type: 'watcher' };\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        addWatcher: [\n          function*({ take, put, call }) {\n            while (true) {\n              const { payload } = yield take('addWatcher');\n              yield call(delay, 100);\n              yield put({ type: 'add', payload });\n            }\n          },\n          watcher,\n        ],\n      },\n    });\n    app.start();\n\n    // Only catch the first one.\n    app._store.dispatch({ type: 'count/addWatcher', payload: 2 });\n    app._store.dispatch({ type: 'count/addWatcher', payload: 3 });\n\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(2);\n      done();\n    }, 200);\n  });\n\n  it('type: poll', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*(_, { put }) {\n            yield put({ type: 'add', payload: 1 });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n\n    setTimeout(() => {\n      app._store.dispatch({ type: 'count/pollAdd-stop' });\n      expect(app._store.getState().count).toEqual(2);\n      done();\n    }, 2000);\n  });\n\n  it('type: poll and stop', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*(_, { put }) {\n            yield put({ type: 'add', payload: 1 });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n    // should work one time\n    app._store.dispatch({ type: 'count/pollAdd-stop' });\n\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(1);\n      done();\n    }, 200);\n  });\n\n  it('type: poll with payload', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*({ payload }, { put }) {\n            yield put({ type: 'add', payload });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start', payload: 2 });\n\n    setTimeout(() => {\n      app._store.dispatch({ type: 'count/pollAdd-stop' });\n      expect(app._store.getState().count).toEqual(4);\n      done();\n    }, 2000);\n  });\n\n  it('type: poll, start many time', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*({ payload }, { put }) {\n            yield put({ type: 'add', payload });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start', payload: 2 });\n\n    setTimeout(() => {\n      // second start should not work\n      app._store.dispatch({ type: 'count/pollAdd-start', payload: 3 });\n      app._store.dispatch({ type: 'count/pollAdd-stop' });\n      expect(app._store.getState().count).toEqual(6);\n      done();\n    }, 3000);\n  });\n\n  it('type: poll, start many time 2', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*(_, { put }) {\n            yield put({ type: 'add', payload: 1 });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n    // second start should not work\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n\n    setTimeout(() => {\n      app._store.dispatch({ type: 'count/pollAdd-stop' });\n      expect(app._store.getState().count).toEqual(3);\n      done();\n    }, 3000);\n  });\n\n  it('type: poll, start and stop many time', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        pollAdd: [\n          function*(_, { put }) {\n            yield put({ type: 'add', payload: 1 });\n          },\n          { type: 'poll', delay: 1000 },\n        ],\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n    app._store.dispatch({ type: 'count/pollAdd-stop' });\n    app._store.dispatch({ type: 'count/pollAdd-start' });\n\n    setTimeout(() => {\n      app._store.dispatch({ type: 'count/pollAdd-stop' });\n      expect(app._store.getState().count).toEqual(3);\n      done();\n    }, 2000);\n  });\n\n  xit('nonvalid type', () => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        addDelay: [\n          function*() {\n            yield console.log(1);\n          },\n          { type: 'nonvalid' },\n        ],\n      },\n    });\n\n    expect(() => {\n      app.start();\n    }).toThrow(/app.start: effect type should be takeEvery, takeLatest, throttle or watcher/);\n  });\n\n  it('onEffect', done => {\n    const SHOW = '@@LOADING/SHOW';\n    const HIDE = '@@LOADING/HIDE';\n\n    const app = create();\n\n    // Test model should be accessible\n    let modelNamespace = null;\n    // Test onEffect should be run orderly\n    let count = 0;\n    let expectedKey = null;\n\n    app.use({\n      extraReducers: {\n        loading(state = false, action) {\n          switch (action.type) {\n            case SHOW:\n              return true;\n            case HIDE:\n              return false;\n            default:\n              return state;\n          }\n        },\n      },\n      onEffect(effect, { put }, model, key) {\n        expectedKey = key;\n        modelNamespace = model.namespace;\n        return function*(...args) {\n          count *= 2;\n          yield put({ type: SHOW });\n          yield effect(...args);\n          yield put({ type: HIDE });\n        };\n      },\n    });\n\n    app.use({\n      onEffect(effect) {\n        return function*(...args) {\n          count += 2;\n          yield effect(...args);\n          count += 1;\n        };\n      },\n    });\n\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        *addRemote(action, { put }) {\n          yield delay(100);\n          yield put({ type: 'add' });\n        },\n      },\n    });\n\n    app.start();\n\n    expect(app._store.getState().loading).toEqual(false);\n\n    app._store.dispatch({ type: 'count/addRemote' });\n    expect(app._store.getState().loading).toEqual(true);\n    expect(modelNamespace).toEqual('count');\n    expect(expectedKey).toEqual('count/addRemote');\n\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual(false);\n      expect(app._store.getState().count).toEqual(1);\n      expect(count).toEqual(5);\n      done();\n    }, 200);\n  });\n\n  it('return Promise', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *addDelay({ payload }, { put, call, select }) {\n          yield call(delay, payload.delay || 100);\n          yield put({ type: 'add', payload: payload.amount });\n          return yield select(state => state.count);\n        },\n      },\n    });\n    app.start();\n    const p1 = app._store.dispatch({\n      type: 'count/addDelay',\n      payload: { amount: 2 },\n    });\n    const p2 = app._store.dispatch({\n      type: 'count/add',\n      payload: 2,\n    });\n    expect(p1 instanceof Promise).toEqual(true);\n    expect(p2).toEqual({ type: 'count/add', payload: 2 });\n    expect(app._store.getState().count).toEqual(2);\n    p1.then(count => {\n      expect(count).toEqual(4);\n      expect(app._store.getState().count).toEqual(4);\n      done();\n    });\n  });\n\n  it('return Promises when trigger the same effect multiple times', done => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *addDelay({ payload }, { put, call, select }) {\n          yield call(delay, payload.delay || 100);\n          yield put({ type: 'add', payload: payload.amount });\n          return yield select(state => state.count);\n        },\n      },\n    });\n    app.start();\n\n    const p1 = app._store.dispatch({\n      type: 'count/addDelay',\n      payload: { delay: 100, amount: 1 },\n    });\n    const p2 = app._store.dispatch({\n      type: 'count/add',\n      payload: 2,\n    });\n    const p3 = app._store.dispatch({\n      type: 'count/addDelay',\n      payload: { delay: 200, amount: 3 },\n    });\n    expect(p1 instanceof Promise).toEqual(true);\n    expect(p2).toEqual({ type: 'count/add', payload: 2 });\n    expect(app._store.getState().count).toEqual(2);\n    p1.then(count => {\n      expect(count).toEqual(3);\n      expect(app._store.getState().count).toEqual(3);\n      p3.then(count => {\n        expect(count).toEqual(6);\n        expect(app._store.getState().count).toEqual(6);\n        done();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/handleActions.test.js",
    "content": "import expect from 'expect';\nimport handleActions from '../src/handleActions';\n\ndescribe('handleActions', () => {\n  const LOGIN_START = 'user/login/start';\n\n  const LOGIN_END = 'user/login/end';\n\n  const LOGIN_SAVE = 'user/login/save';\n\n  const initialState = {\n    isLoading: false,\n  };\n\n  const reducers = handleActions(\n    {\n      [LOGIN_START](state) {\n        return {\n          ...state,\n          isLoading: true,\n        };\n      },\n\n      [LOGIN_END](state) {\n        return {\n          ...state,\n          isLoading: false,\n        };\n      },\n\n      [LOGIN_SAVE]: undefined,\n    },\n    initialState,\n  );\n\n  it('LOGIN_START', () => {\n    expect(reducers(initialState, { type: LOGIN_START })).toEqual({\n      isLoading: true,\n    });\n  });\n\n  it('LOGIN_END', () => {\n    expect(reducers(initialState, { type: LOGIN_END })).toEqual({\n      isLoading: false,\n    });\n  });\n\n  it('uses the identity if the specified reducer is undefined', () => {\n    expect(reducers(initialState, { type: LOGIN_SAVE })).toBe(initialState);\n  });\n\n  it('dispatch not valid action', () => {\n    expect(() => {\n      reducers(initialState, { type: '' });\n    }).toThrow(/dispatch: action should be a plain Object with type/);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/model.test.js",
    "content": "import EventEmitter from 'events';\nimport { create } from '../src/index';\n\ndescribe('app.model', () => {\n  it('dynamic model', () => {\n    let count = 0;\n\n    const app = create();\n    app.model({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n    });\n    app.start();\n\n    // inject model\n    app.model({\n      namespace: 'tasks',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      effects: {},\n      subscriptions: {\n        setup() {\n          count += 1;\n        },\n      },\n    });\n\n    // subscriptions\n    expect(count).toEqual(1);\n\n    // reducers\n    app._store.dispatch({ type: 'tasks/add', payload: 'foo' });\n    app._store.dispatch({ type: 'users/add', payload: 'foo' });\n    const state = app._store.getState();\n    expect(state.users).toEqual(['foo']);\n    expect(state.tasks).toEqual(['foo']);\n  });\n\n  it(\"don't inject if exists\", () => {\n    const app = create();\n\n    const model = {\n      namespace: 'count',\n      state: 0,\n      subscriptions: {\n        setup() {},\n      },\n    };\n\n    app.model(model);\n    app.start();\n    expect(() => {\n      app.model(model);\n    }).toThrow(/\\[app\\.model\\] namespace should be unique/);\n  });\n\n  it('unmodel', () => {\n    const emitter = new EventEmitter();\n    let emitterCount = 0;\n\n    const app = create();\n    app.model({\n      namespace: 'a',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.model({\n      namespace: 'b',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        *addBoth(action, { put }) {\n          yield put({ type: 'a/add' });\n          yield put({ type: 'add' });\n        },\n      },\n      subscriptions: {\n        setup() {\n          emitter.on('event', () => {\n            emitterCount += 1;\n          });\n          return () => {\n            emitter.removeAllListeners();\n          };\n        },\n      },\n    });\n    app.start();\n\n    emitter.emit('event');\n    app.unmodel('b');\n    emitter.emit('event');\n\n    app._store.dispatch({ type: 'b/addBoth' });\n\n    const { a, b } = app._store.getState();\n    expect(emitterCount).toEqual(1);\n    expect({ a, b }).toEqual({ a: 0, b: undefined });\n  });\n\n  it(\"don't run saga when effects is not provided\", () => {\n    let count = 0;\n\n    const app = create();\n    app.model({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n    });\n    app.start();\n\n    // inject model\n    app.model({\n      namespace: 'tasks',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      effects: null,\n      subscriptions: {\n        setup() {\n          count += 1;\n        },\n      },\n    });\n\n    // subscriptions\n    expect(count).toEqual(1);\n\n    // reducers\n    app._store.dispatch({ type: 'tasks/add', payload: 'foo' });\n    app._store.dispatch({ type: 'users/add', payload: 'foo' });\n    const state = app._store.getState();\n    expect(state.users).toEqual(['foo']);\n    expect(state.tasks).toEqual(['foo']);\n\n    // effects is not taken\n    expect(count).toEqual(1);\n  });\n\n  it('unmodel with asyncReducers', () => {\n    const app = create();\n    app.model({\n      namespace: 'a',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.start();\n\n    app.model({\n      namespace: 'b',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        *addBoth(action, { put }) {\n          yield put({ type: 'a/add' });\n          yield put({ type: 'add' });\n        },\n      },\n    });\n\n    app._store.dispatch({ type: 'b/addBoth' });\n    app.unmodel('b');\n    app._store.dispatch({ type: 'b/addBoth' });\n    const { a, b } = app._store.getState();\n    expect({ a, b }).toEqual({ a: 1, b: undefined });\n  });\n\n  it(\"unmodel, warn user if subscription don't return function\", () => {\n    const app = create();\n    app.model({\n      namespace: 'a',\n      state: 0,\n      subscriptions: {\n        a() {},\n      },\n    });\n    app.start();\n    app.unmodel('a');\n  });\n\n  it('unmodel with other type of effects', () => {\n    const app = create();\n    let countA = 0;\n    let countB = 0;\n    let countC = 0;\n    let countD = 0;\n\n    app.model({\n      namespace: 'a',\n      state: 0,\n      effects: {\n        a: [\n          function*() {\n            yield (countA += 1);\n          },\n          { type: 'throttle', ms: 100 },\n        ],\n        b: [\n          function*() {\n            yield (countB += 1);\n          },\n          { type: 'takeEvery' },\n        ],\n        c: [\n          function*() {\n            yield (countC += 1);\n          },\n          { type: 'takeLatest' },\n        ],\n        d: [\n          function*({ take }) {\n            while (true) {\n              yield take('a/d');\n              countD += 1;\n            }\n          },\n          { type: 'watcher' },\n        ],\n      },\n    });\n\n    app.start();\n\n    app._store.dispatch({ type: 'a/a' });\n    app._store.dispatch({ type: 'a/b' });\n    app._store.dispatch({ type: 'a/c' });\n    app._store.dispatch({ type: 'a/d' });\n\n    expect([countA, countB, countC, countD]).toEqual([1, 1, 1, 1]);\n\n    app.unmodel('a');\n\n    app._store.dispatch({ type: 'a/b' });\n    app._store.dispatch({ type: 'a/c' });\n    app._store.dispatch({ type: 'a/d' });\n\n    expect([countA, countB, countC, countD]).toEqual([1, 1, 1, 1]);\n  });\n\n  it('register the model without affecting itself', () => {\n    const countModel = {\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add() {},\n      },\n    };\n    const app = create();\n    app.model(countModel);\n    app.start();\n    expect(Object.keys(countModel.reducers)).toEqual(['add']);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/optsAndHooks.test.js",
    "content": "import expect from 'expect';\nimport { create } from '../src/index';\n\nfunction delay(timeout) {\n  return new Promise(resolve => setTimeout(resolve, timeout));\n}\n\ndescribe('opts and hooks', () => {\n  it('basic', done => {\n    const app = create();\n\n    app.model({\n      namespace: 'loading',\n      state: false,\n      reducers: {\n        show() {\n          return true;\n        },\n        hide() {\n          return false;\n        },\n      },\n    });\n\n    const nsAction = namespace => action => `${namespace}/${action}`;\n\n    const ADD = 'add';\n    const ADD_DELAY = 'addDelay';\n    const countAction = nsAction('count');\n    const loadingAction = nsAction('loading');\n\n    app.model({\n      namespace: 'count',\n      state: 0,\n      subscriptions: {\n        setup({ dispatch }) {\n          dispatch({ type: ADD });\n        },\n      },\n      reducers: {\n        [ADD](state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      effects: {\n        *[ADD_DELAY]({ payload }, { call, put }) {\n          yield put({ type: loadingAction('show') });\n          yield call(delay, 100);\n          yield put({ type: ADD, payload });\n          yield put({ type: loadingAction('hide') });\n        },\n      },\n    });\n    app.start();\n\n    expect(app._store.getState().count).toEqual(1);\n    expect(app._store.getState().loading).toEqual(false);\n    app._store.dispatch({ type: countAction(ADD_DELAY), payload: 2 });\n    expect(app._store.getState().loading).toEqual(true);\n\n    setTimeout(() => {\n      expect(app._store.getState().count).toEqual(3);\n      expect(app._store.getState().loading).toEqual(false);\n      done();\n    }, 500);\n  });\n\n  it('opts.onError prevent reject error', done => {\n    let rejectCount = 0;\n    const app = create({\n      onError(e) {\n        e.preventDefault();\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        // eslint-disable-next-line require-yield\n        *add() {\n          throw new Error('add failed');\n        },\n      },\n    });\n    app.start();\n    app._store\n      .dispatch({\n        type: 'count/add',\n      })\n      .catch(() => {\n        rejectCount += 1;\n      });\n\n    setTimeout(() => {\n      expect(rejectCount).toEqual(0);\n      done();\n    }, 200);\n  });\n\n  it('opts.initialState', () => {\n    const app = create({\n      initialState: { count: 1 },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n    });\n    app.start();\n    expect(app._store.getState().count).toEqual(1);\n  });\n\n  it('opts.onAction', () => {\n    let count;\n    const countMiddleware = () => () => () => {\n      count += 1;\n    };\n\n    const app = create({\n      onAction: countMiddleware,\n    });\n    app.start();\n\n    count = 0;\n    app._store.dispatch({ type: 'test' });\n    expect(count).toEqual(1);\n  });\n\n  it('opts.onAction with array', () => {\n    let count;\n    const countMiddleware = () => next => action => {\n      count += 1;\n      next(action);\n    };\n    const count2Middleware = () => next => action => {\n      count += 2;\n      next(action);\n    };\n\n    const app = create({\n      onAction: [countMiddleware, count2Middleware],\n    });\n    app.start();\n\n    count = 0;\n    app._store.dispatch({ type: 'test' });\n    expect(count).toEqual(3);\n  });\n\n  it('opts.extraEnhancers', () => {\n    let count = 0;\n    const countEnhancer = storeCreator => (reducer, preloadedState, enhancer) => {\n      const store = storeCreator(reducer, preloadedState, enhancer);\n      const oldDispatch = store.dispatch;\n      store.dispatch = action => {\n        count += 1;\n        oldDispatch(action);\n      };\n      return store;\n    };\n    const app = create({\n      extraEnhancers: [countEnhancer],\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'abc' });\n    expect(count).toEqual(1);\n  });\n\n  it('opts.onStateChange', () => {\n    let savedState = null;\n\n    const app = create({\n      onStateChange(state) {\n        savedState = state;\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/add' });\n    expect(savedState.count).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/plugin.test.js",
    "content": "import expect from 'expect';\nimport Plugin from '../src/Plugin';\n\ndescribe('plugin', () => {\n  it('basic', () => {\n    let hmrCount = 0;\n    let errorMessage = '';\n\n    function onError(err) {\n      errorMessage = err.message;\n    }\n\n    const plugin = new Plugin();\n\n    plugin.use({\n      onHmr: x => {\n        hmrCount += 1 * x;\n      },\n      onStateChange: 2,\n      onAction: 1,\n      extraReducers: { form: 1 },\n      onReducer: r => {\n        return (state, action) => {\n          const res = r(state, action);\n          return res + 1;\n        };\n      },\n    });\n    plugin.use({\n      onHmr: x => {\n        hmrCount += 2 + x;\n      },\n      extraReducers: { user: 2 },\n      onReducer: r => {\n        return (state, action) => {\n          const res = r(state, action);\n          return res * 2;\n        };\n      },\n    });\n\n    plugin.apply('onHmr')(2);\n    plugin.apply('onError', onError)({ message: 'hello dva' });\n\n    expect(hmrCount).toEqual(6);\n    expect(errorMessage).toEqual('hello dva');\n\n    expect(plugin.get('extraReducers')).toEqual({ form: 1, user: 2 });\n    expect(plugin.get('onAction')).toEqual([1]);\n    expect(plugin.get('onStateChange')).toEqual([2]);\n\n    expect(plugin.get('onReducer')(state => state + 1)(0)).toEqual(4);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/reducers.test.js",
    "content": "import expect from 'expect';\nimport { create } from '../src/index';\n\ndescribe('reducers', () => {\n  it('enhancer', () => {\n    function enhancer(reducer) {\n      return (state, action) => {\n        if (action.type === 'square') {\n          return state * state;\n        }\n        return reducer(state, action);\n      };\n    }\n\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 3,\n      reducers: [\n        {\n          add(state, { payload }) {\n            return state + (payload || 1);\n          },\n        },\n        enhancer,\n      ],\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'square' });\n    app._store.dispatch({ type: 'count/add' });\n    expect(app._store.getState().count).toEqual(10);\n  });\n\n  it('extraReducers', () => {\n    const reducers = {\n      count: (state, { type }) => {\n        if (type === 'add') {\n          return state + 1;\n        }\n        // default state\n        return 0;\n      },\n    };\n    const app = create({\n      extraReducers: reducers,\n    });\n    app.start();\n\n    expect(app._store.getState().count).toEqual(0);\n    app._store.dispatch({ type: 'add' });\n    expect(app._store.getState().count).toEqual(1);\n  });\n\n  // core 没有 routing 这个 reducer，所以用例无效了\n  xit('extraReducers: throw error if conflicts', () => {\n    const app = create({\n      extraReducers: { routing() {} },\n    });\n    expect(() => {\n      app.start();\n    }).toThrow(/\\[app\\.start\\] extraReducers is conflict with other reducers/);\n  });\n\n  it('onReducer with saveAndLoad', () => {\n    let savedState = null;\n    const saveAndLoad = r => (state, action) => {\n      const newState = r(state, action);\n      if (action.type === 'save') {\n        savedState = newState;\n      }\n      if (action.type === 'load') {\n        return savedState;\n      }\n      return newState;\n    };\n    const app = create({\n      onReducer: saveAndLoad,\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/add' });\n    expect(app._store.getState().count).toEqual(1);\n    app._store.dispatch({ type: 'save' });\n    expect(app._store.getState().count).toEqual(1);\n    app._store.dispatch({ type: 'count/add' });\n    app._store.dispatch({ type: 'count/add' });\n    expect(app._store.getState().count).toEqual(3);\n    app._store.dispatch({ type: 'load' });\n    expect(app._store.getState().count).toEqual(1);\n  });\n\n  it('onReducer', () => {\n    const undo = r => (state, action) => {\n      const newState = r(state, action);\n      return { present: newState, routing: newState.routing };\n    };\n    const app = create({\n      onReducer: undo,\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        update(state) {\n          return state + 1;\n        },\n      },\n    });\n    app.start();\n\n    expect(app._store.getState().present.count).toEqual(0);\n  });\n\n  it('effects put reducers when reducers is array', () => {\n    const enhancer = r => (state, action) => {\n      const newState = r(state, action);\n      return newState;\n    };\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *putSetState(action, { put }) {\n          yield put({ type: 'setState' });\n        },\n      },\n      reducers: [\n        {\n          setState(state) {\n            return state + 1;\n          },\n        },\n        enhancer,\n      ],\n    });\n    app.start();\n\n    app._store.dispatch({ type: 'count/putSetState' });\n    expect(app._store.getState().count).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/repalceModel.test.js",
    "content": "import expect from 'expect';\nimport EventEmitter from 'events';\nimport { create } from '../src/index';\n\ndescribe('app.replaceModel', () => {\n  it('should not be available before app.start() get called', () => {\n    const app = create();\n\n    expect('replaceModel' in app).toEqual(false);\n  });\n\n  it(\"should add model if it doesn't exist\", () => {\n    const app = create();\n    app.start();\n\n    const oldCount = app._models.length;\n\n    app.replaceModel({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n    });\n\n    expect(app._models.length).toEqual(oldCount + 1);\n\n    app._store.dispatch({ type: 'users/add', payload: 'jack' });\n    const state = app._store.getState();\n    expect(state.users).toEqual(['jack']);\n  });\n\n  it('should run new reducers if model exists', () => {\n    const app = create();\n    app.model({\n      namespace: 'users',\n      state: ['foo'],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n    });\n    app.start();\n\n    const oldCount = app._models.length;\n\n    app.replaceModel({\n      namespace: 'users',\n      state: ['bar'],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, 'world', payload];\n        },\n        clear() {\n          return [];\n        },\n      },\n    });\n\n    expect(app._models.length).toEqual(oldCount);\n    let state = app._store.getState();\n    expect(state.users).toEqual(['foo']);\n\n    app._store.dispatch({ type: 'users/add', payload: 'jack' });\n    state = app._store.getState();\n    expect(state.users).toEqual(['foo', 'world', 'jack']);\n\n    // test new added action\n    app._store.dispatch({ type: 'users/clear' });\n\n    state = app._store.getState();\n    expect(state.users).toEqual([]);\n  });\n\n  it('should run new effects if model exists', () => {\n    const app = create();\n    app.model({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        setter(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      effects: {\n        *add({ payload }, { put }) {\n          yield put({\n            type: 'setter',\n            payload,\n          });\n        },\n      },\n    });\n    app.start();\n\n    app.replaceModel({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        setter(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      effects: {\n        *add(_, { put }) {\n          yield put({\n            type: 'setter',\n            payload: 'mock',\n          });\n        },\n      },\n    });\n\n    app._store.dispatch({ type: 'users/add', payload: 'jack' });\n    const state = app._store.getState();\n    expect(state.users).toEqual(['mock']);\n  });\n\n  it('should run subscriptions after replaceModel', () => {\n    const app = create();\n    app.model({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      subscriptions: {\n        setup({ dispatch }) {\n          // should return unlistener but omitted here\n          dispatch({ type: 'add', payload: 1 });\n        },\n      },\n    });\n    app.start();\n\n    app.replaceModel({\n      namespace: 'users',\n      state: [],\n      reducers: {\n        add(state, { payload }) {\n          return [...state, payload];\n        },\n      },\n      subscriptions: {\n        setup({ dispatch }) {\n          // should return unlistener but omitted here\n          dispatch({ type: 'add', payload: 2 });\n        },\n      },\n    });\n\n    const state = app._store.getState();\n    // This should be an issue but can't be avoided with dva\n    // To avoid, in client code, setup method should be idempotent when running multiple times\n    expect(state.users).toEqual([1, 2]);\n  });\n\n  it('should remove old subscription listeners after replaceModel', () => {\n    const app = create();\n    const emitter = new EventEmitter();\n    let emitterCount = 0;\n\n    app.model({\n      namespace: 'users',\n      state: [],\n      subscriptions: {\n        setup() {\n          emitter.on('event', () => {\n            emitterCount += 1;\n          });\n          return () => {\n            emitter.removeAllListeners();\n          };\n        },\n      },\n    });\n    app.start();\n\n    emitter.emit('event');\n\n    app.replaceModel({\n      namespace: 'users',\n      state: [],\n    });\n\n    emitter.emit('event');\n\n    expect(emitterCount).toEqual(1);\n  });\n\n  it('should trigger onError if error is thown after replaceModel', () => {\n    let triggeredError = false;\n    const app = create({\n      onError() {\n        triggeredError = true;\n      },\n    });\n    app.model({\n      namespace: 'users',\n      state: [],\n    });\n    app.start();\n\n    app.replaceModel({\n      namespace: 'users',\n      state: [],\n      effects: {\n        *add() {\n          yield 'fake';\n\n          throw new Error('fake error');\n        },\n      },\n    });\n\n    app._store.dispatch({\n      type: 'users/add',\n    });\n\n    expect(triggeredError).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/subscriptions.test.js",
    "content": "import expect from 'expect';\nimport { create } from '../src/index';\n\ndescribe('subscriptions', () => {\n  it('dispatch action', () => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      subscriptions: {\n        setup({ dispatch }) {\n          dispatch({ type: 'add', payload: 2 });\n        },\n      },\n    });\n    app.start();\n    expect(app._store.getState().count).toEqual(2);\n  });\n\n  it('dispatch action with namespace will get a warn', () => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state, { payload }) {\n          return state + payload || 1;\n        },\n      },\n      subscriptions: {\n        setup({ dispatch }) {\n          dispatch({ type: 'add', payload: 2 });\n        },\n      },\n    });\n    app.start();\n    expect(app._store.getState().count).toEqual(2);\n  });\n\n  it('dispatch not valid action', () => {\n    const app = create();\n    app.model({\n      namespace: 'count',\n      state: 0,\n      subscriptions: {\n        setup({ dispatch }) {\n          dispatch('add');\n        },\n      },\n    });\n    expect(() => {\n      app.start();\n    }).toThrow(/dispatch: action should be a plain Object with type/);\n  });\n\n  it('dispatch action for other models', () => {\n    const app = create();\n    app.model({\n      namespace: 'loading',\n      state: false,\n      reducers: {\n        show() {\n          return true;\n        },\n      },\n    });\n    app.model({\n      namespace: 'count',\n      state: 0,\n      subscriptions: {\n        setup({ dispatch }) {\n          dispatch({ type: 'loading/show' });\n        },\n      },\n    });\n    app.start();\n    expect(app._store.getState().loading).toEqual(true);\n  });\n\n  it('onError', () => {\n    const errors = [];\n    const app = create({\n      onError: error => {\n        errors.push(error.message);\n      },\n    });\n    app.model({\n      namespace: '-',\n      state: {},\n      subscriptions: {\n        setup(_obj, done) {\n          done('subscription error');\n        },\n      },\n    });\n    app.start();\n    expect(errors).toEqual(['subscription error']);\n  });\n\n  it('onError async', done => {\n    const errors = [];\n    const app = create({\n      onError: error => {\n        errors.push(error.message);\n      },\n    });\n    app.model({\n      namespace: '-',\n      state: {},\n      subscriptions: {\n        setup(_obj, done) {\n          setTimeout(() => {\n            done('subscription error');\n          }, 100);\n        },\n      },\n    });\n    app.start();\n    expect(errors).toEqual([]);\n    setTimeout(() => {\n      expect(errors).toEqual(['subscription error']);\n      done();\n    }, 200);\n  });\n});\n"
  },
  {
    "path": "packages/dva-core/test/utils.test.js",
    "content": "import expect from 'expect';\nimport { findIndex } from '../src/utils';\n\ndescribe('utils', () => {\n  describe('#findIndex', () => {\n    it('should return -1 when no item matches', () => {\n      const array = [1, 2, 3];\n      const action = i => i === 4;\n\n      expect(findIndex(array, action)).toEqual(-1);\n    });\n\n    it('should return index of the match item in array', () => {\n      const array = ['a', 'b', 'c'];\n      const action = i => i === 'b';\n\n      const actualValue = findIndex(array, action);\n      const expectedValue = 1;\n\n      expect(actualValue).toEqual(expectedValue);\n    });\n\n    it('should return the first match if more than one items match', () => {\n      const target = {\n        id: 1,\n      };\n\n      const array = [target, { id: 1 }];\n      const action = i => i.id === 1;\n\n      expect(findIndex(array, action)).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/dva-immer/.fatherrc.js",
    "content": "export default {\n  cjs: 'rollup',\n  esm: 'rollup',\n  runtimeHelpers: true,\n};\n"
  },
  {
    "path": "packages/dva-immer/README.md",
    "content": "# dva-immer\n\n[![NPM version](https://img.shields.io/npm/v/dva-immer.svg?style=flat)](https://npmjs.org/package/dva-immer)\n[![Build Status](https://img.shields.io/travis/dvajs/dva-immer.svg?style=flat)](https://travis-ci.org/dvajs/dva-immer)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-immer.svg?style=flat)](https://coveralls.io/r/dvajs/dva-immer)\n[![NPM downloads](http://img.shields.io/npm/dm/dva-immer.svg?style=flat)](https://npmjs.org/package/dva-immer)\n\nCreate the next immutable state tree by simply modifying the current tree\n\n---\n\n## Install\n\n```bash\n$ npm install dva-immer --save\n```\n\n## Usage\n\n```javascript\n\nconst app = dva();\napp.use(require('dva-immer').default());\n```\nsome like [umi-plugin-dva](https://github.com/umijs/umi/blob/master/packages/umi-plugin-dva/src/index.js) line 106\n\nLook more [Immer](https://github.com/mweststrate/immer)\n\n\n## License\n\n[MIT](https://tldrlegal.com/license/mit-license)\n"
  },
  {
    "path": "packages/dva-immer/package.json",
    "content": "{\n  \"name\": \"dva-immer\",\n  \"version\": \"1.0.2\",\n  \"description\": \"Auto loading data binding plugin for dva.\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"sideEffects\": false,\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.0.0\",\n    \"immer\": \"^8.0.4\"\n  },\n  \"peerDependencies\": {\n    \"dva\": \"^2.5.0-0\"\n  },\n  \"devDependencies\": {\n    \"dva\": \"3.0.0-alpha.1\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dvajs/dva/tree/master/packages/dva-immer\"\n  },\n  \"homepage\": \"https://github.com/dvajs/dva\",\n  \"author\": \"chencheng <sorrycc@gmail.com>\",\n  \"keywords\": [\n    \"dva\",\n    \"dva-plugin\",\n    \"immer\"\n  ],\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "packages/dva-immer/src/index.js",
    "content": "import produce from 'immer';\n\nexport { enableES5, enableAllPlugins } from 'immer';\n\nexport default function() {\n  return {\n    _handleActions(handlers, defaultState) {\n      return (state = defaultState, action) => {\n        const { type } = action;\n\n        const ret = produce(state, draft => {\n          const handler = handlers[type];\n          if (handler) {\n            const compatiableRet = handler(draft, action);\n            if (compatiableRet !== undefined) {\n              // which means you are use redux pattern\n              // it's compatiable. https://github.com/mweststrate/immer#returning-data-from-producers\n              return compatiableRet;\n            }\n          }\n        });\n        return ret === undefined ? {} : ret;\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/dva-immer/test/index.test.js",
    "content": "import dva from 'dva';\nimport userImmer from '../src/index';\n\ndescribe('dva-immer', () => {\n  it('normal', () => {\n    const app = dva();\n    app.use(userImmer());\n\n    app.model({\n      namespace: 'count',\n      state: {\n        a: {\n          b: {\n            c: 0,\n          },\n        },\n        m: {\n          b: {\n            c: 0,\n          },\n        },\n      },\n      reducers: {\n        add(state) {\n          state.a.b.c += 1;\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    const oldCount = app._store.getState().count;\n    app._store.dispatch({ type: 'count/add' });\n    const newCount = app._store.getState().count;\n    expect(oldCount.a.b.c).toEqual(0);\n    expect(newCount.a.b.c).toEqual(1);\n  });\n\n  it('compatibility with normal reducer usage', () => {\n    const app = dva();\n    app.use(userImmer());\n\n    app.model({\n      namespace: 'count',\n      state: {\n        a: {\n          b: {\n            c: 0,\n          },\n        },\n        m: {\n          b: {\n            c: 0,\n          },\n        },\n      },\n      reducers: {\n        add(state) {\n          return {\n            ...state,\n            a: {\n              ...state.a,\n              b: {\n                ...state.a.b,\n                c: state.a.b.c + 1,\n              },\n            },\n          };\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    const oldCount = app._store.getState().count;\n    app._store.dispatch({ type: 'count/add' });\n    const newCount = app._store.getState().count;\n    expect(oldCount.a.b.c).toEqual(0);\n    expect(newCount.a.b.c).toEqual(1);\n    expect(newCount.m.b.c).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "packages/dva-loading/.fatherrc.js",
    "content": "export default {\n  cjs: 'rollup',\n  esm: 'rollup',\n  runtimeHelpers: true,\n};\n"
  },
  {
    "path": "packages/dva-loading/README.md",
    "content": "# dva-loading\n\n[![NPM version](https://img.shields.io/npm/v/dva-loading.svg?style=flat)](https://npmjs.org/package/dva-loading)\n[![Build Status](https://img.shields.io/travis/dvajs/dva-loading.svg?style=flat)](https://travis-ci.org/dvajs/dva-loading)\n[![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-loading.svg?style=flat)](https://coveralls.io/r/dvajs/dva-loading)\n[![NPM downloads](http://img.shields.io/npm/dm/dva-loading.svg?style=flat)](https://npmjs.org/package/dva-loading)\n\nAuto loading data binding plugin for dva. :clap: You don't need to write `showLoading` and `hideLoading` any more.\n\n---\n\n## Install\n\n```bash\n$ npm install dva-loading --save\n```\n\n## Usage\n\n```javascript\nimport createLoading from 'dva-loading';\n\nconst app = dva();\napp.use(createLoading(opts));\n```\n\nThen we can access loading state from store.\n\n### opts\n\n- `opts.namespace`: property key on global state, type String, Default `loading`\n\n[See real project usage on dva-hackernews](https://github.com/dvajs/dva-hackernews/blob/2c3330b1c8ae728c94ebe1399b72486ad5a1a7a0/src/index.js#L4-L7).\n\n## State Structure\n\n```\nloading: {\n  global: false,\n  models: {\n    users: false,\n    todos: false,\n    ...\n  },\n}\n```\n\n## License\n\n[MIT](https://tldrlegal.com/license/mit-license)\n"
  },
  {
    "path": "packages/dva-loading/index.d.ts",
    "content": "export interface DvaLoadingState {\n  global: boolean;\n  models: { [type: string]: boolean | undefined };\n  effects: { [type: string]: boolean | undefined };\n}\n"
  },
  {
    "path": "packages/dva-loading/package.json",
    "content": "{\n  \"name\": \"dva-loading\",\n  \"version\": \"3.0.25\",\n  \"description\": \"Auto loading data binding plugin for dva.\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"sideEffects\": false,\n  \"typings\": \"index.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dvajs/dva\"\n  },\n  \"homepage\": \"https://github.com/dvajs/dva\",\n  \"keywords\": [\n    \"dva\",\n    \"dva-plugin\",\n    \"loading\"\n  ],\n  \"author\": \"chencheng <sorrycc@gmail.com>\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"dva\": \"3.0.0-alpha.1\",\n    \"dva-core\": \"2.0.4\"\n  },\n  \"peerDependencies\": {\n    \"dva-core\": \"^1.1.0 || ^1.5.0-0 || ^1.6.0-0\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/dva-loading/src/index.js",
    "content": "const SHOW = '@@DVA_LOADING/SHOW';\nconst HIDE = '@@DVA_LOADING/HIDE';\nconst NAMESPACE = 'loading';\n\nfunction createLoading(opts = {}) {\n  const namespace = opts.namespace || NAMESPACE;\n\n  const { only = [], except = [] } = opts;\n  if (only.length > 0 && except.length > 0) {\n    throw Error('It is ambiguous to configurate `only` and `except` items at the same time.');\n  }\n\n  const initialState = {\n    global: false,\n    models: {},\n    effects: {},\n  };\n\n  const extraReducers = {\n    [namespace](state = initialState, { type, payload }) {\n      const { namespace, actionType } = payload || {};\n      let ret;\n      switch (type) {\n        case SHOW:\n          ret = {\n            ...state,\n            global: true,\n            models: { ...state.models, [namespace]: true },\n            effects: { ...state.effects, [actionType]: true },\n          };\n          break;\n        case HIDE: {\n          const effects = { ...state.effects, [actionType]: false };\n          const models = {\n            ...state.models,\n            [namespace]: Object.keys(effects).some(actionType => {\n              const _namespace = actionType.split('/')[0];\n              if (_namespace !== namespace) return false;\n              return effects[actionType];\n            }),\n          };\n          const global = Object.keys(models).some(namespace => {\n            return models[namespace];\n          });\n          ret = {\n            ...state,\n            global,\n            models,\n            effects,\n          };\n          break;\n        }\n        default:\n          ret = state;\n          break;\n      }\n      return ret;\n    },\n  };\n\n  function onEffect(effect, { put }, model, actionType) {\n    const { namespace } = model;\n    if (\n      (only.length === 0 && except.length === 0) ||\n      (only.length > 0 && only.indexOf(actionType) !== -1) ||\n      (except.length > 0 && except.indexOf(actionType) === -1)\n    ) {\n      return function*(...args) {\n        yield put({ type: SHOW, payload: { namespace, actionType } });\n        yield effect(...args);\n        yield put({ type: HIDE, payload: { namespace, actionType } });\n      };\n    } else {\n      return effect;\n    }\n  }\n\n  return {\n    extraReducers,\n    onEffect,\n  };\n}\n\nexport default createLoading;\n"
  },
  {
    "path": "packages/dva-loading/test/core.test.js",
    "content": "import expect from 'expect';\nimport { create as dva } from 'dva-core';\nimport createLoading from '../src/index';\n\nconst delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));\n\ndescribe('dva-core', () => {\n  describe('dva-loading', () => {\n    it('normal', done => {\n      const app = dva();\n      app.use(createLoading());\n      app.model({\n        namespace: 'count',\n        state: 0,\n        reducers: {\n          add(state) {\n            return state + 1;\n          },\n        },\n        effects: {\n          *addRemote(action, { put }) {\n            yield delay(100);\n            yield put({ type: 'add' });\n          },\n        },\n      });\n      app.start();\n\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/addRemote' });\n      expect(app._store.getState().loading).toEqual({\n        global: true,\n        models: { count: true },\n        effects: { 'count/addRemote': true },\n      });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: false,\n          models: { count: false },\n          effects: { 'count/addRemote': false },\n        });\n        done();\n      }, 200);\n    });\n\n    it('opts.effects', done => {\n      const app = dva();\n      app.use(\n        createLoading({\n          effects: true,\n        }),\n      );\n      app.model({\n        namespace: 'count',\n        state: 0,\n        reducers: {\n          add(state) {\n            return state + 1;\n          },\n        },\n        effects: {\n          *addRemote(action, { put }) {\n            yield delay(100);\n            yield put({ type: 'add' });\n          },\n        },\n      });\n      app.start();\n\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/addRemote' });\n      expect(app._store.getState().loading).toEqual({\n        global: true,\n        models: { count: true },\n        effects: { 'count/addRemote': true },\n      });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: false,\n          models: { count: false },\n          effects: { 'count/addRemote': false },\n        });\n        done();\n      }, 200);\n    });\n\n    it('opts.namespace', () => {\n      const app = dva();\n      app.use(\n        createLoading({\n          namespace: 'fooLoading',\n        }),\n      );\n      app.model({\n        namespace: 'count',\n        state: 0,\n      });\n      app.start();\n      expect(app._store.getState().fooLoading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n    });\n\n    it('opts.only', () => {\n      const app = dva();\n      app.use(\n        createLoading({\n          only: ['count/a'],\n        }),\n      );\n      app.model({\n        namespace: 'count',\n        state: 0,\n        effects: {\n          *a(action, { call }) {\n            yield call(delay, 500);\n          },\n          *b(action, { call }) {\n            yield call(delay, 500);\n          },\n        },\n      });\n      app.start();\n\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/a' });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: true,\n          models: { count: true },\n          effects: { 'count/a': true },\n        });\n        app._store.dispatch({ type: 'count/b' });\n        setTimeout(() => {\n          expect(app._store.getState().loading).toEqual({\n            global: false,\n            models: { count: false },\n            effects: { 'count/a': false },\n          });\n        }, 300);\n      }, 300);\n    });\n\n    it('opts.except', () => {\n      const app = dva();\n      app.use(\n        createLoading({\n          except: ['count/a'],\n        }),\n      );\n      app.model({\n        namespace: 'count',\n        state: 0,\n        effects: {\n          *a(action, { call }) {\n            yield call(delay, 500);\n          },\n          *b(action, { call }) {\n            yield call(delay, 500);\n          },\n        },\n      });\n      app.start();\n\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/a' });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: false,\n          models: {},\n          effects: {},\n        });\n        app._store.dispatch({ type: 'count/b' });\n        setTimeout(() => {\n          expect(app._store.getState().loading).toEqual({\n            global: true,\n            models: { count: true },\n            effects: { 'count/b': true },\n          });\n        }, 300);\n      }, 300);\n    });\n\n    it('opts.only and opts.except ambiguous', () => {\n      expect(() => {\n        const app = dva();\n        app.use(\n          createLoading({\n            only: ['count/a'],\n            except: ['count/b'],\n          }),\n        );\n      }).toThrow('ambiguous');\n    });\n\n    it('takeLatest', done => {\n      const app = dva();\n      app.use(createLoading());\n      app.model({\n        namespace: 'count',\n        state: 0,\n        reducers: {\n          add(state) {\n            return state + 1;\n          },\n        },\n        effects: {\n          addRemote: [\n            function*(action, { put }) {\n              yield delay(100);\n              yield put({ type: 'add' });\n            },\n            { type: 'takeLatest' },\n          ],\n        },\n      });\n      app.start();\n\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/addRemote' });\n      app._store.dispatch({ type: 'count/addRemote' });\n      expect(app._store.getState().loading).toEqual({\n        global: true,\n        models: { count: true },\n        effects: { 'count/addRemote': true },\n      });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: false,\n          models: { count: false },\n          effects: { 'count/addRemote': false },\n        });\n        done();\n      }, 200);\n    });\n\n    it('multiple effects', done => {\n      const app = dva();\n      app.use(createLoading());\n      app.model({\n        namespace: 'count',\n        state: 0,\n        effects: {\n          *a(action, { call }) {\n            yield call(delay, 100);\n          },\n          *b(action, { call }) {\n            yield call(delay, 500);\n          },\n        },\n      });\n      app.start();\n      app._store.dispatch({ type: 'count/a' });\n      app._store.dispatch({ type: 'count/b' });\n      setTimeout(() => {\n        expect(app._store.getState().loading.models.count).toEqual(true);\n      }, 200);\n      setTimeout(() => {\n        expect(app._store.getState().loading.models.count).toEqual(false);\n        done();\n      }, 800);\n    });\n\n    it('error catch', done => {\n      const app = dva({\n        onError(err) {\n          err.preventDefault();\n          console.log('failed', err.message);\n        },\n      });\n      app.use(createLoading());\n      app.model({\n        namespace: 'count',\n        state: 0,\n        effects: {\n          *throwError(action, { call }) {\n            yield call(delay, 100);\n            throw new Error('haha');\n          },\n        },\n      });\n      app.start();\n\n      app._store.dispatch({ type: 'count/throwError' });\n      expect(app._store.getState().loading.global).toEqual(true);\n      setTimeout(() => {\n        expect(app._store.getState().loading.global).toEqual(false);\n        done();\n      }, 200);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/dva-loading/test/index.test.js",
    "content": "import expect from 'expect';\nimport dva from 'dva';\nimport createLoading from '../src/index';\n\nconst delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));\n\ndescribe('dva-loading', () => {\n  it('normal', done => {\n    const app = dva();\n    app.use(createLoading());\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        *addRemote(action, { put }) {\n          yield delay(100);\n          yield put({ type: 'add' });\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    expect(app._store.getState().loading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n    app._store.dispatch({ type: 'count/addRemote' });\n    expect(app._store.getState().loading).toEqual({\n      global: true,\n      models: { count: true },\n      effects: { 'count/addRemote': true },\n    });\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: { count: false },\n        effects: { 'count/addRemote': false },\n      });\n      done();\n    }, 200);\n  });\n\n  it('opts.effects', done => {\n    const app = dva();\n    app.use(\n      createLoading({\n        effects: true,\n      }),\n    );\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        *addRemote(action, { put }) {\n          yield delay(100);\n          yield put({ type: 'add' });\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    expect(app._store.getState().loading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n    app._store.dispatch({ type: 'count/addRemote' });\n    expect(app._store.getState().loading).toEqual({\n      global: true,\n      models: { count: true },\n      effects: { 'count/addRemote': true },\n    });\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: { count: false },\n        effects: { 'count/addRemote': false },\n      });\n      done();\n    }, 200);\n  });\n\n  it('opts.namespace', () => {\n    const app = dva();\n    app.use(\n      createLoading({\n        namespace: 'fooLoading',\n      }),\n    );\n    app.model({\n      namespace: 'count',\n      state: 0,\n    });\n    app.router(() => 1);\n    app.start();\n    expect(app._store.getState().fooLoading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n  });\n\n  it('opts.only', () => {\n    const app = dva();\n    app.use(\n      createLoading({\n        only: ['count/a'],\n      }),\n    );\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *a(action, { call }) {\n          yield call(delay, 500);\n        },\n        *b(action, { call }) {\n          yield call(delay, 500);\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    expect(app._store.getState().loading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n    app._store.dispatch({ type: 'count/a' });\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual({\n        global: true,\n        models: { count: true },\n        effects: { 'count/a': true },\n      });\n      app._store.dispatch({ type: 'count/b' });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: false,\n          models: { count: false },\n          effects: { 'count/a': false },\n        });\n      }, 300);\n    }, 300);\n  });\n\n  it('opts.except', () => {\n    const app = dva();\n    app.use(\n      createLoading({\n        except: ['count/a'],\n      }),\n    );\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *a(action, { call }) {\n          yield call(delay, 500);\n        },\n        *b(action, { call }) {\n          yield call(delay, 500);\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    expect(app._store.getState().loading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n    app._store.dispatch({ type: 'count/a' });\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: {},\n        effects: {},\n      });\n      app._store.dispatch({ type: 'count/b' });\n      setTimeout(() => {\n        expect(app._store.getState().loading).toEqual({\n          global: true,\n          models: { count: true },\n          effects: { 'count/b': true },\n        });\n      }, 300);\n    }, 300);\n  });\n\n  it('opts.only and opts.except ambiguous', () => {\n    expect(() => {\n      const app = dva();\n      app.use(\n        createLoading({\n          only: ['count/a'],\n          except: ['count/b'],\n        }),\n      );\n    }).toThrow('ambiguous');\n  });\n\n  it('takeLatest', done => {\n    const app = dva();\n    app.use(createLoading());\n    app.model({\n      namespace: 'count',\n      state: 0,\n      reducers: {\n        add(state) {\n          return state + 1;\n        },\n      },\n      effects: {\n        addRemote: [\n          function*(action, { put }) {\n            yield delay(100);\n            yield put({ type: 'add' });\n          },\n          { type: 'takeLatest' },\n        ],\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    expect(app._store.getState().loading).toEqual({\n      global: false,\n      models: {},\n      effects: {},\n    });\n    app._store.dispatch({ type: 'count/addRemote' });\n    app._store.dispatch({ type: 'count/addRemote' });\n    expect(app._store.getState().loading).toEqual({\n      global: true,\n      models: { count: true },\n      effects: { 'count/addRemote': true },\n    });\n    setTimeout(() => {\n      expect(app._store.getState().loading).toEqual({\n        global: false,\n        models: { count: false },\n        effects: { 'count/addRemote': false },\n      });\n      done();\n    }, 200);\n  });\n\n  it('multiple effects', done => {\n    const app = dva();\n    app.use(createLoading());\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *a(action, { call }) {\n          yield call(delay, 100);\n        },\n        *b(action, { call }) {\n          yield call(delay, 500);\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n    app._store.dispatch({ type: 'count/a' });\n    app._store.dispatch({ type: 'count/b' });\n    setTimeout(() => {\n      expect(app._store.getState().loading.models.count).toEqual(true);\n    }, 200);\n    setTimeout(() => {\n      expect(app._store.getState().loading.models.count).toEqual(false);\n      done();\n    }, 800);\n  });\n\n  it('error catch', done => {\n    const app = dva({\n      onError(err) {\n        err.preventDefault();\n        console.log('failed', err.message);\n      },\n    });\n    app.use(createLoading());\n    app.model({\n      namespace: 'count',\n      state: 0,\n      effects: {\n        *throwError(action, { call }) {\n          yield call(delay, 100);\n          throw new Error('haha');\n        },\n      },\n    });\n    app.router(() => 1);\n    app.start();\n\n    app._store.dispatch({ type: 'count/throwError' });\n    expect(app._store.getState().loading.global).toEqual(true);\n    setTimeout(() => {\n      expect(app._store.getState().loading.global).toEqual(false);\n      done();\n    }, 200);\n  });\n});\n"
  },
  {
    "path": "scripts/publish.js",
    "content": "#!/usr/bin/env node\n\nconst shell = require('shelljs');\nconst { join } = require('path');\nconst { fork } = require('child_process');\n\nif (\n  shell\n    .exec('npm config get registry')\n    .stdout.indexOf('https://registry.npmjs.org/') === -1\n) {\n  console.error(\n    'Failed: set npm registry to https://registry.npmjs.org/ first',\n  );\n  process.exit(1);\n}\n\nconst cwd = process.cwd();\nconst ret = shell.exec('./node_modules/.bin/lerna updated').stdout;\nconst updatedRepos = ret\n  .split('\\n')\n  .map(line => line.replace('- ', ''))\n  .filter(line => line !== '');\n\nif (updatedRepos.length === 0) {\n  console.log('No package is updated.');\n  process.exit(0);\n}\n\nconst { code: buildCode } = shell.exec('npm run build');\nif (buildCode === 1) {\n  console.error('Failed: npm run build');\n  process.exit(1);\n}\n\nconst cp = fork(\n  join(process.cwd(), 'node_modules/.bin/lerna'),\n  ['publish', '--skip-npm'].concat(process.argv.slice(2)),\n  {\n    stdio: 'inherit',\n    cwd: process.cwd(),\n  },\n);\ncp.on('error', err => {\n  console.log(err);\n});\ncp.on('close', code => {\n  console.log('code', code);\n  if (code === 1) {\n    console.error('Failed: lerna publish');\n    process.exit(1);\n  }\n\n  publishToNpm();\n});\n\nfunction publishToNpm() {\n  console.log(`repos to publish: ${updatedRepos.join(', ')}`);\n  updatedRepos.forEach(repo => {\n    shell.cd(join(cwd, 'packages', repo));\n    const { version } = require(join(cwd, 'packages', repo, 'package.json'));\n    if (\n      version.includes('-rc.') ||\n      version.includes('-beta.') ||\n      version.includes('-alpha.')\n    ) {\n      console.log(`[${repo}] npm publish --tag next`);\n      shell.exec(`npm publish --tag next`);\n    } else {\n      console.log(`[${repo}] npm publish`);\n      shell.exec(`npm publish`);\n    }\n  });\n}\n"
  },
  {
    "path": "website/.gitignore",
    "content": "/node_modules\n\n"
  },
  {
    "path": "website/now.json",
    "content": "{\n  \"name\": \"dva\",\n  \"version\": 2,\n  \"builds\": [\n    { \"src\": \"package.json\", \"use\": \"@now/static-build\" }\n  ]\n}\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"scripts\": {\n    \"now-build\": \"echo empty build\",\n    \"deploy\": \"vuepress build ../docs --dest ./dist && now && now alias\"\n  },\n  \"devDependencies\": {\n    \"now\": \"^13.1.3\",\n    \"vuepress\": \"^0.14.4\"\n  }\n}\n"
  }
]