├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── bug_report_cn.md │ ├── feature_request.md │ └── rfc_cn.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── README_zh-CN.md ├── docs ├── .vuepress │ ├── config.js │ └── override.styl ├── API.md ├── Concepts.md ├── GettingStarted.md ├── README.md ├── api │ └── README.md ├── guide │ ├── README.md │ ├── concepts.md │ ├── develop-complex-spa.md │ ├── examples-and-boilerplates.md │ ├── fig-show.md │ ├── getting-started.md │ ├── introduce-class.md │ └── source-code-explore.md └── knowledgemap │ └── README.md ├── examples ├── func-test │ ├── .eslintrc │ ├── .roadhogrc │ ├── .roadhogrc.mock.js │ ├── package.json │ └── src │ │ ├── assets │ │ └── yay.jpg │ │ ├── components │ │ └── Example.js │ │ ├── index.css │ │ ├── index.ejs │ │ ├── index.js │ │ ├── models │ │ └── example.js │ │ ├── router.js │ │ ├── routes │ │ ├── IndexPage.css │ │ └── IndexPage.js │ │ ├── services │ │ └── example.js │ │ └── utils │ │ └── request.js ├── user-dashboard │ ├── .editorconfig │ ├── .eslintrc │ ├── .gitignore │ ├── .umirc.js │ ├── .webpackrc │ ├── README.md │ ├── package.json │ └── src │ │ ├── assets │ │ └── yay.jpg │ │ ├── constants.js │ │ ├── global.css │ │ ├── layouts │ │ ├── Header.js │ │ ├── index.css │ │ └── index.js │ │ ├── pages │ │ ├── .umi │ │ │ ├── DvaContainer.js │ │ │ ├── registerServiceWorker.js │ │ │ ├── router.js │ │ │ └── umi.js │ │ ├── index.css │ │ ├── index.js │ │ └── users │ │ │ ├── components │ │ │ └── Users │ │ │ │ ├── UserModal.js │ │ │ │ ├── Users.css │ │ │ │ └── Users.js │ │ │ ├── models │ │ │ └── users.js │ │ │ ├── page.css │ │ │ ├── page.js │ │ │ └── services │ │ │ └── users.js │ │ ├── plugins │ │ └── onError.js │ │ └── utils │ │ └── request.js ├── with-immer │ ├── .umirc.js │ ├── dva.js │ ├── model.js │ ├── package.json │ └── pages │ │ └── index.js ├── with-nextjs │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── model │ │ ├── homepage.js │ │ └── index.js │ ├── package.json │ ├── pages │ │ ├── index.js │ │ └── users.js │ └── utils │ │ └── store.js ├── with-react-router-3 │ ├── .eslintrc │ ├── .roadhogrc │ ├── .roadhogrc.mock.js │ ├── package.json │ └── src │ │ ├── assets │ │ └── yay.jpg │ │ ├── components │ │ └── Example.js │ │ ├── index.css │ │ ├── index.ejs │ │ ├── index.js │ │ ├── models │ │ └── example.js │ │ ├── router.js │ │ ├── routes │ │ ├── IndexPage.css │ │ └── IndexPage.js │ │ ├── services │ │ └── example.js │ │ └── utils │ │ └── request.js └── with-redux-undo │ ├── .babelrc │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── package.json │ └── src │ ├── index.html │ ├── index.js │ └── models │ └── counter.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── dva-core │ ├── .fatherrc.js │ ├── README.md │ ├── package.json │ ├── saga.js │ ├── src │ │ ├── Plugin.js │ │ ├── checkModel.js │ │ ├── constants.js │ │ ├── createPromiseMiddleware.js │ │ ├── createStore.js │ │ ├── getReducer.js │ │ ├── getSaga.js │ │ ├── handleActions.js │ │ ├── index.js │ │ ├── prefixNamespace.js │ │ ├── prefixType.js │ │ ├── prefixedDispatch.js │ │ ├── subscription.js │ │ └── utils.js │ ├── test │ │ ├── checkModel.test.js │ │ ├── effects.test.js │ │ ├── handleActions.test.js │ │ ├── model.test.js │ │ ├── optsAndHooks.test.js │ │ ├── plugin.test.js │ │ ├── reducers.test.js │ │ ├── repalceModel.test.js │ │ ├── subscriptions.test.js │ │ └── utils.test.js │ └── yarn.lock ├── dva-immer │ ├── .fatherrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.js │ ├── test │ │ └── index.test.js │ └── yarn.lock ├── dva-loading │ ├── .fatherrc.js │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src │ │ └── index.js │ ├── test │ │ ├── core.test.js │ │ └── index.test.js │ └── yarn.lock └── dva │ ├── .fatherrc.js │ ├── README.md │ ├── dynamic.d.ts │ ├── dynamic.js │ ├── fetch.d.ts │ ├── fetch.js │ ├── index.d.ts │ ├── package.json │ ├── router.d.ts │ ├── router.js │ ├── saga.js │ ├── src │ ├── dynamic.js │ └── index.js │ ├── test │ ├── index.e2e.js │ └── index.test.js │ ├── warnAboutDeprecatedCJSRequire.js │ └── yarn.lock ├── scripts └── publish.js ├── website ├── .gitignore ├── now.json └── package.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | node: 4 | docker: 5 | - image: circleci/node:10.13-browsers 6 | working_directory: ~/dva 7 | 8 | environment: 9 | NODE_ENV: test 10 | NODE_OPTIONS: --max_old_space_size=4096 11 | NPM_CONFIG_LOGLEVEL: error 12 | JOBS: max # https://gist.github.com/ralphtheninja/f7c45bdee00784b41fed 13 | 14 | jobs: 15 | yarn_build: 16 | executor: node 17 | steps: 18 | - checkout 19 | - run: yarn install 20 | - run: yarn bootstrap 21 | - run: yarn build 22 | - run: 23 | command: yarn test -- --forceExit --detectOpenHandles --runInBand --maxWorkers=2 24 | no_output_timeout: 300m 25 | - run: bash <(curl -s https://codecov.io/bash) 26 | cnpm_build: 27 | executor: node 28 | steps: 29 | - checkout 30 | - run: sudo npm install -g cnpm 31 | - run: cnpm install --registry=https://registry.npmjs.org 32 | - run: cnpm run bootstrap -- --npm-client=cnpm 33 | - run: cnpm run build 34 | - run: 35 | command: npm run test -- --forceExit --detectOpenHandles --runInBand --maxWorkers=2 36 | no_output_timeout: 300m 37 | - run: bash <(curl -s https://codecov.io/bash) 38 | workflows: 39 | version: 2 40 | build-test: 41 | jobs: 42 | - yarn_build: 43 | filters: 44 | branches: 45 | ignore: 46 | - gh-pages 47 | - /release\/.*/ 48 | - cnpm_build: 49 | filters: 50 | branches: 51 | ignore: 52 | - gh-pages 53 | - /release\/.*/ 54 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | es/ 4 | dist/ 5 | packages/dva-example/ 6 | packages/dva-example-react-router-3/ 7 | packages/dva-example-nextjs// 8 | packages/dva-example-user-dashboard/ 9 | packages/dva/*.js 10 | packages/dva-react-router-3/*.js 11 | packages/dva-no-router/*.js 12 | scripts/ 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "prettier" 5 | ], 6 | "extends": [ 7 | "airbnb", 8 | "prettier" 9 | ], 10 | "env": { 11 | "browser": true, 12 | "jest": true 13 | }, 14 | "rules": { 15 | "prettier/prettier": "error", 16 | "jsx-a11y/href-no-hash": [ 17 | 0 18 | ], 19 | "jsx-a11y/click-events-have-key-events": [ 20 | 0 21 | ], 22 | "jsx-a11y/anchor-is-valid": [ 23 | "error", 24 | { 25 | "components": [ 26 | "Link" 27 | ], 28 | "specialLink": [ 29 | "to" 30 | ] 31 | } 32 | ], 33 | "generator-star-spacing": [ 34 | 0 35 | ], 36 | "consistent-return": [ 37 | 0 38 | ], 39 | "radix": [ 40 | 1 41 | ], 42 | "react/react-in-jsx-scope": [ 43 | 0 44 | ], 45 | "react/forbid-prop-types": [ 46 | 0 47 | ], 48 | "react/jsx-filename-extension": [ 49 | 1, 50 | { 51 | "extensions": [ 52 | ".js" 53 | ] 54 | } 55 | ], 56 | "global-require": [ 57 | 0 58 | ], 59 | "import/prefer-default-export": [ 60 | 0 61 | ], 62 | "react/jsx-no-bind": [ 63 | 0 64 | ], 65 | "react/prop-types": [ 66 | 0 67 | ], 68 | "react/prefer-stateless-function": [ 69 | 0 70 | ], 71 | "react/jsx-one-expression-per-line": [ 72 | 0 73 | ], 74 | "react/button-has-type": [ 75 | 0 76 | ], 77 | "no-else-return": [ 78 | 0 79 | ], 80 | "no-restricted-syntax": [ 81 | 0 82 | ], 83 | "import/no-extraneous-dependencies": [ 84 | 0 85 | ], 86 | "no-use-before-define": [ 87 | 0 88 | ], 89 | "jsx-a11y/no-static-element-interactions": [ 90 | 0 91 | ], 92 | "no-nested-ternary": [ 93 | 0 94 | ], 95 | "arrow-body-style": [ 96 | 0 97 | ], 98 | "import/extensions": [ 99 | 0 100 | ], 101 | "no-bitwise": [ 102 | 0 103 | ], 104 | "no-cond-assign": [ 105 | 0 106 | ], 107 | "import/no-unresolved": [ 108 | 0 109 | ], 110 | "require-yield": [ 111 | 1 112 | ], 113 | "no-param-reassign": [ 114 | 0 115 | ], 116 | "no-shadow": [ 117 | 0 118 | ], 119 | "no-underscore-dangle": [ 120 | 0 121 | ], 122 | "spaced-comment": [ 123 | 0 124 | ], 125 | "indent": [ 126 | 0 127 | ], 128 | "quotes": [ 129 | 0 130 | ], 131 | "func-names": [ 132 | 0 133 | ], 134 | "arrow-parens": [ 135 | 0 136 | ], 137 | "space-before-function-paren": [ 138 | 0 139 | ], 140 | "no-useless-escape": [ 141 | 0 142 | ], 143 | "object-curly-newline": [ 144 | 0 145 | ], 146 | "function-paren-newline": [ 147 | 0 148 | ], 149 | "class-methods-use-this": [ 150 | 0 151 | ], 152 | "no-new": [ 153 | 0 154 | ], 155 | "import/newline-after-import": [ 156 | 0 157 | ], 158 | "no-console": [ 159 | 0 160 | ] 161 | }, 162 | "parserOptions": { 163 | "ecmaFeatures": { 164 | "experimentalObjectRestSpread": true 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug report' 3 | about: 'Report a bug to help us improve' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What happens? 11 | A clear and concise description of what the bug is. 12 | 13 | ## Mini Showcase Repository(REQUIRED) 14 | 15 | > Provide a mini GitHub repository which can reproduce the issue. 16 | > Use `yarn create umi`, select `app`, choose `dva`, then upload to your GitHub 17 | 18 | 19 | 20 | ## How To Reproduce 21 | **Steps to reproduce the behavior:** 22 | 1. 23 | 2. 24 | 25 | **Expected behavior** 26 | 1. 27 | 2. 28 | 29 | ## Context 30 | 31 | - **Dva Version**: 32 | - **Node Version**: 33 | - **Platform**: 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '缺陷问题反馈' 3 | about: '反馈问题以帮助我们改进' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## What happens? 15 | 16 | 17 | 18 | ## 最小可复现仓库 19 | 20 | > 请使用 `yarn create umi` 创建,选择 `app`,然后选上 `dva`,并上传到你的 GitHub 仓库 21 | 22 | 23 | 24 | ## 复现步骤,错误日志以及相关配置 25 | 26 | 27 | 28 | 29 | 30 | ## 相关环境信息 31 | 32 | - **Umi 版本**: 33 | - **Node 版本**: 34 | - **操作系统**: 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature request' 3 | about: 'Suggest an idea for this project' 4 | title: '[Feature Request] say something' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Background 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ## Proposal 15 | 16 | Describe the solution you'd like, better to provide some pseudo code. 17 | 18 | ## Additional context 19 | 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rfc_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'RFC Proposals' 3 | about: 'Provide a solution for this project' 4 | title: '[RFC] say something' 5 | labels: 'type: proposals' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 背景 11 | 12 | > 描述你希望解决的问题的现状,附上相关的 issue 地址 13 | 14 | ## 思路 15 | 16 | > 描述大概的解决思路,可以包含 API 设计和伪代码等 17 | 18 | ## 跟进 19 | 20 | - [ ] some task 21 | - [ ] PR URL 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | 15 | - [ ] `npm test` passes 16 | - [ ] tests are included 17 | - [ ] documentation is changed or added 18 | - [ ] commit message follows commit guidelines 19 | 20 | 21 | ##### Description of change 22 | 23 | 24 | 25 | - any feature? 26 | - close https://github.com/dvajs/dva/ISSUE_URL 27 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node_version: [10.x, 12.x] 11 | os: [ubuntu-latest] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js ${{ matrix.node_version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node_version }} 18 | - run: npm install 19 | - run: npm run bootstrap 20 | - run: npm run build 21 | - run: npm run test -- --forceExit 22 | env: 23 | CI: true 24 | HEADLESS: false 25 | PROGRESS: none 26 | NODE_ENV: test 27 | NODE_OPTIONS: --max_old_space_size=4096 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /coverage 3 | /.changelog 4 | /examples/**/.umi 5 | /examples/**/.umi-production 6 | /website/dist 7 | /node_modules 8 | /packages/**/node_modules 9 | /packages/**/dist 10 | /lerna-debug.log 11 | 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present ChenCheng (sorrycc@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README_zh-CN.md) 2 | 3 | # dva 4 | 5 | [![codecov](https://codecov.io/gh/dvajs/dva/branch/master/graph/badge.svg)](https://codecov.io/gh/dvajs/dva) 6 | [![CircleCI](https://circleci.com/gh/dvajs/dva.svg?style=svg)](https://circleci.com/gh/dvajs/dva) 7 | [![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva) 8 | [![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva) 9 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva) 10 | [![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva) 11 | [![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva) 12 | [![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) 13 | 14 | Lightweight 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)) 15 | 16 | --- 17 | 18 | ## Features 19 | 20 | * **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)** 21 | * **Elm concepts**: organize models with `reducers`, `effects` and `subscriptions` 22 | * **Support HMR**: support HMR for components, routes and models with [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 23 | * **Plugin system**: e.g. we have [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) plugin to handle loading state automatically 24 | 25 | ## Demos 26 | 27 | * [Count](https://stackblitz.com/edit/dva-example-count): Simple count example 28 | * [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): User management dashboard 29 | * [AntDesign Pro](https://github.com/ant-design/ant-design-pro):([Demo](https://preview.pro.ant.design/)),out-of-box UI solution for enterprise applications 30 | * [HackerNews](https://github.com/dvajs/dva-hackernews): ([Demo](https://dvajs.github.io/dva-hackernews/)),HackerNews Clone 31 | * [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 32 | * [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f)),Github star management application 33 | * [Account System](https://github.com/yvanwangl/AccountSystem.git): A small inventory management system 34 | * [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): react-native example integrated dva and react-navigation 35 | 36 | ## Quick Start 37 | 38 | * [Real project with dva](https://dvajs.com/guide/getting-started.html) 39 | * [dva intro course](https://dvajs.com/guide/introduce-class.html) 40 | 41 | More documentation, checkout [https://dvajs.com/](https://dvajs.com/) 42 | 43 | ## FAQ 44 | 45 | ### Why is it called dva? 46 | 47 | > 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. 48 | 49 | —— From [OverWatch](http://ow.blizzard.cn/heroes/dva) 50 | 51 | 52 | 53 | ### Is it production ready? 54 | 55 | Sure! We have 1000+ projects using dva in Alibaba. 56 | 57 | ### Does it support IE8? 58 | 59 | No. 60 | 61 | ## Next 62 | 63 | Some basic articles. 64 | 65 | * The [8 Concepts](https://github.com/dvajs/dva/blob/master/docs/Concepts.md), and know how they are connected together 66 | * [dva APIs](https://github.com/dvajs/dva/blob/master/docs/API.md) 67 | * Checkout [dva knowledgemap](https://github.com/dvajs/dva-knowledgemap), including all the basic knowledge with ES6, React, dva 68 | * Checkout [more FAQ](https://github.com/dvajs/dva/issues?q=is%3Aissue+is%3Aclosed+label%3Afaq) 69 | * 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) 70 | 71 | Want more? 72 | 73 | * 看看 dva 的前身 [React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1),知道 dva 是怎么来的 74 | * 在 gitc 分享 dva 的 PPT :[React 应用框架在蚂蚁金服的实践](http://slides.com/sorrycc/dva) 75 | * 如果还在用 dva@1.x,请尽快 [升级到 2.x](https://github.com/sorrycc/blog/issues/48) 76 | 77 | ## Community 78 | 79 | | Slack Group | Github Issue | 钉钉群 | 微信群 | 80 | | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 81 | | [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | | | 82 | 83 | ## License 84 | 85 | [MIT](https://tldrlegal.com/license/mit-license) 86 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 简体中文 2 | 3 | # dva 4 | 5 | [![codecov](https://codecov.io/gh/dvajs/dva/branch/master/graph/badge.svg)](https://codecov.io/gh/dvajs/dva) 6 | [![CircleCI](https://circleci.com/gh/dvajs/dva.svg?style=svg)](https://circleci.com/gh/dvajs/dva) 7 | [![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva) 8 | [![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva) 9 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva) 10 | [![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva) 11 | [![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva) 12 | [![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) 13 | 14 | 基于 [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)) 15 | 16 | --- 17 | 18 | ## 特性 19 | 20 | * **易学易用**,仅有 6 个 api,对 redux 用户尤其友好,**[配合 umi 使用](https://umijs.org/guide/with-dva.html)后更是降低为 0 API** 21 | * **elm 概念**,通过 reducers, effects 和 subscriptions 组织 model 22 | * **插件机制**,比如 [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading 23 | * **支持 HMR**,基于 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 实现 components、routes 和 models 的 HMR 24 | 25 | ## 快速上手 26 | 27 | * [dva 和 antd 项目快速上手](https://dvajs.com/guide/getting-started.html) 28 | * [dva 入门课](https://dvajs.com/guide/introduce-class.html) 29 | 30 | 更多文档,详见:[https://dvajs.com/](https://dvajs.com/) 31 | 32 | ## 他是怎么来的? 33 | 34 | * [Why dva and what's dva](https://github.com/dvajs/dva/issues/1) 35 | * [支付宝前端应用架构的发展和选择](https://www.github.com/sorrycc/blog/issues/6) 36 | 37 | ## 例子 38 | 39 | * [Count](https://stackblitz.com/edit/dva-example-count): 简单计数器 40 | * [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): 用户管理 41 | * [AntDesign Pro](https://github.com/ant-design/ant-design-pro):([Demo](https://preview.pro.ant.design/)),开箱即用的中台前端/设计解决方案 42 | * [HackerNews](https://github.com/dvajs/dva-hackernews): ([Demo](https://dvajs.github.io/dva-hackernews/)),HackerNews Clone 43 | * [antd-admin](https://github.com/zuiidea/antd-admin): ([Demo](http://antd-admin.zuiidea.com/)),基于 antd 和 dva 的后台管理应用 44 | * [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f)),Github Star 管理应用 45 | * [Account System](https://github.com/yvanwangl/AccountSystem.git): 小型库存管理系统 46 | * [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): 集成了 dva 和 react-navigation 典型应用场景的 React Native 实例 47 | 48 | ## FAQ 49 | 50 | ### 命名由来? 51 | 52 | > D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。 53 | 54 | —— 来自 [守望先锋](http://ow.blizzard.cn/heroes/overwatch-dva) 。 55 | 56 | 57 | 58 | ### 是否可用于生产环境? 59 | 60 | 当然!公司内用于生产环境的项目估计已经有 1000+ 。 61 | 62 | ### 是否支持 IE8 ? 63 | 64 | 不支持。 65 | 66 | ## 下一步 67 | 68 | 以下能帮你更好地理解和使用 dva : 69 | 70 | * 理解 dva 的 [8 个概念](https://dvajs.com/guide/concepts.html) ,以及他们是如何串起来的 71 | * 掌握 dva 的[所有 API](https://dvajs.com/api/) 72 | * 查看 [dva 知识地图](https://dvajs.com/knowledgemap/) ,包含 ES6, React, dva 等所有基础知识 73 | * 查看 [更多 FAQ](https://github.com/dvajs/dva/issues?q=is%3Aissue+is%3Aclosed+label%3Afaq),看看别人通常会遇到什么问题 74 | * 如果你基于 dva-cli 创建项目,最好了解他的 [配置方式](https://github.com/sorrycc/roadhog/blob/master/README_zh-cn.md#配置) 75 | 76 | 还要了解更多? 77 | 78 | * 看看 dva 的前身 [React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1),知道 dva 是怎么来的 79 | * 在 gitc 分享 dva 的 PPT :[React 应用框架在蚂蚁金服的实践](http://slides.com/sorrycc/dva) 80 | * 如果还在用 dva@1.x,请尽快 [升级到 2.x](https://github.com/sorrycc/blog/issues/48) 81 | 82 | ## 社区 83 | 84 | | Slack Group | Github Issue | 钉钉群 | 微信群 | 85 | | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 86 | | [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | | | 87 | 88 | ## License 89 | 90 | [MIT](https://tldrlegal.com/license/mit-license) 91 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'DvaJS', 3 | description: 'React and redux based, lightweight and elm-style framework.', 4 | themeConfig: { 5 | repo: 'dvajs/dva', 6 | lastUpdated: 'Last Updated', 7 | editLinks: true, 8 | editLinkText: '在 GitHub 上编辑此页', 9 | docsDir: 'docs', 10 | nav: [ 11 | { text: '指南', link: '/guide/' }, 12 | { text: 'API', link: '/api/' }, 13 | { text: '知识地图', link: '/knowledgemap/' }, 14 | { text: '发布日志', link: 'https://github.com/dvajs/dva/releases' }, 15 | ], 16 | sidebar: { 17 | '/guide/': [ 18 | { 19 | title: '指南', 20 | collapsable: false, 21 | children: [ 22 | '', 23 | 'getting-started', 24 | 'examples-and-boilerplates', 25 | 'concepts', 26 | 'introduce-class', 27 | ], 28 | }, 29 | { 30 | title: '社区', 31 | collapsable: false, 32 | children: ['fig-show', 'develop-complex-spa', 'source-code-explore'], 33 | }, 34 | ], 35 | '/api/': [''], 36 | '/knowledgemap/': [''], 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /docs/.vuepress/override.styl: -------------------------------------------------------------------------------- 1 | $accentColor = #fc54c3 2 | $textColor = #2c3e50 3 | $borderColor = #eaecef 4 | $codeBgColor = #282c34 5 | -------------------------------------------------------------------------------- /docs/Concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | [以中文版查看此文](https://dvajs.com/guide/concepts.html) 4 | 5 | ## Data Flow 6 | 7 | 8 | 9 | ## Models 10 | 11 | ### State 12 | 13 | `type State = any` 14 | 15 | The state tree of your models. Usually, the state is a JavaScript object (although technically it can be any type) which is immutable data. 16 | 17 | In dva, you can access top state tree data by `_store`. 18 | 19 | ```javascript 20 | const app = dva(); 21 | console.log(app._store); // top state 22 | ``` 23 | 24 | ### Action 25 | 26 | `type AsyncAction = any` 27 | 28 | Just 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.) 29 | 30 | ```javascript 31 | dispatch({ 32 | type: 'add', 33 | }); 34 | ``` 35 | 36 | ### dispatch function 37 | 38 | `type dispatch = (a: Action) => Action` 39 | 40 | A 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. 41 | 42 | Dispatching 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. 43 | 44 | ```javascript 45 | dispatch({ 46 | type: 'user/add', // if in model outside, need to add namespace 47 | payload: {}, 48 | }); 49 | ``` 50 | 51 | ### Reducer 52 | 53 | `type Reducer = (state: S, action: A) => S` 54 | 55 | Just 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. 56 | 57 | Reducer's concepts from FP: 58 | 59 | ```javascript 60 | [{x:1},{y:2},{z:3}].reduce(function(prev, next){ 61 | return Object.assign({}, prev, next); 62 | }) 63 | //return {x:1, y:2, z:3} 64 | ``` 65 | 66 | In 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). 67 | 68 | ### Effect 69 | 70 | In dva, we use [redux-sagas](https://redux-saga.js.org/) to control asynchronous flow. 71 | You can learn more in [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide). 72 | 73 | In 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. 74 | 75 | ### Subscription 76 | 77 | Subscriptions is a way to get data from source, it is come from elm. 78 | 79 | Data source can be: the current time, the websocket connection of server, keyboard input, geolocation change, history router change, etc.. 80 | 81 | ```javascript 82 | import key from 'keymaster'; 83 | ... 84 | app.model({ 85 | namespace: 'count', 86 | subscriptions: { 87 | keyEvent(dispatch) { 88 | key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) }); 89 | }, 90 | } 91 | }); 92 | ``` 93 | 94 | ## Router 95 | 96 | Hereby 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. 97 | 98 | dva provide `router` function to control router, based on [react-router](https://github.com/reactjs/react-router)。 99 | 100 | ```javascript 101 | import { Router, Route } from 'dva/router'; 102 | app.router(({history}) => 103 | 104 | 105 | 106 | ); 107 | ``` 108 | 109 | ## Route Components 110 | 111 | In dva, we restrict container components to route components, because we use page dimension to design container components. 112 | 113 | therefore, almost all connected model components are route components, route components in `/routes/` directory, presentational Components in `/components/` directory. 114 | 115 | ## References 116 | - [redux docs](http://redux.js.org/docs/Glossary.html) 117 | - [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide) 118 | - [choo docs](https://github.com/yoshuawuyts/choo) 119 | - [elm](http://elm-lang.org/blog/farewell-to-frp) 120 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: 快速上手 → 4 | actionLink: /guide/ 5 | features: 6 | - title: 易学易用 7 | details: 仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API 8 | - title: elm 概念 9 | details: 通过 reducers, effects 和 subscriptions 组织 model,简化 redux 和 redux-saga 引入的概念 10 | - title: 插件机制 11 | details: 比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading 12 | footer: MIT Licensed | Copyright © 2017-present 13 | --- 14 | 15 | ## 社区 16 | 17 | | Slack Group | Github Issue | 钉钉群 | 微信群 | 18 | | ------------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 19 | | [sorrycc.slack.com](https://join.slack.com/t/sorrycc/shared_invite/enQtNTUzMTYxNDQ5MzE4LTg1NjEzYWUwNDQzMWU3YjViYjcyM2RkZDdjMzE0NzIxMTg3MzIwMDM2YjUwNTZkNDdhNTY5ZTlhYzc1Nzk2NzI) | [umijs/umi/issues](https://github.com/umijs/umi/issues) | | | 20 | [https://t.me/joinchat/G0DdHw9tDZC-_NmdKY2jYg](https://t.me/joinchat/G0DdHw9tDZC-_NmdKY2jYg) 21 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | dva 首先是一个基于 [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),所以也可以理解为一个轻量级的应用框架。 4 | 5 | ## 特性 6 | 7 | * **易学易用**,仅有 6 个 api,对 redux 用户尤其友好,[配合 umi 使用](https://umijs.org/guide/with-dva.html)后更是降低为 0 API 8 | * **elm 概念**,通过 reducers, effects 和 subscriptions 组织 model 9 | * **插件机制**,比如 [dva-loading](https://github.com/dvajs/dva/tree/master/packages/dva-loading) 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading 10 | * **支持 HMR**,基于 [babel-plugin-dva-hmr](https://github.com/dvajs/babel-plugin-dva-hmr) 实现 components、routes 和 models 的 HMR 11 | 12 | ## 他是如何工作的? 13 | 14 | ## 他是怎么来的? 15 | 16 | * [Why dva and what's dva](https://github.com/dvajs/dva/issues/1) 17 | * [支付宝前端应用架构的发展和选择](https://www.github.com/sorrycc/blog/issues/6) 18 | 19 | ## 谁在用? 20 | 21 | ## 为什么不是...? 22 | 23 | ### redux 24 | ### mobx 25 | 26 | ## 命名由来? 27 | 28 | > D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。 29 | 30 | —— 来自 [守望先锋](https://ow.blizzard.cn/heroes/overwatch-dva) 。 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/guide/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # Dva 概念 6 | 7 | ## 数据流向 8 | 9 | 数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 `dispatch` 发起一个 action,如果是同步行为会直接通过 `Reducers` 改变 `State` ,如果是异步行为(副作用)会先触发 `Effects` 然后流向 `Reducers` 最终改变 `State`,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。 10 | 11 | 12 | 13 | ## Models 14 | 15 | ### State 16 | 17 | `type State = any` 18 | 19 | State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。 20 | 21 | 在 dva 中你可以通过 dva 的实例属性 `_store` 看到顶部的 state 数据,但是通常你很少会用到: 22 | 23 | ```javascript 24 | const app = dva(); 25 | console.log(app._store); // 顶部的 state 数据 26 | ``` 27 | 28 | ### Action 29 | 30 | `type AsyncAction = any` 31 | 32 | Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 `type` 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 `dispatch` 函数;需要注意的是 `dispatch` 是在组件 connect Models以后,通过 props 传入的。 33 | ``` 34 | dispatch({ 35 | type: 'add', 36 | }); 37 | ``` 38 | 39 | ### dispatch 函数 40 | 41 | `type dispatch = (a: Action) => Action` 42 | 43 | dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。 44 | 45 | 在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如: 46 | 47 | ```javascript 48 | dispatch({ 49 | type: 'user/add', // 如果在 model 外调用,需要添加 namespace 50 | payload: {}, // 需要传递的信息 51 | }); 52 | ``` 53 | 54 | ### Reducer 55 | 56 | `type Reducer = (state: S, action: A) => S` 57 | 58 | Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。 59 | 60 | Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中: 61 | 62 | ```javascript 63 | [{x:1},{y:2},{z:3}].reduce(function(prev, next){ 64 | return Object.assign(prev, next); 65 | }) 66 | //return {x:1, y:2, z:3} 67 | ``` 68 | 69 | 在 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),这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。 70 | 71 | ### Effect 72 | 73 | Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。 74 | 75 | dva 为了控制副作用的操作,底层引入了[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)。 76 | 77 | ### Subscription 78 | 79 | Subscriptions 是一种从 __源__ 获取数据的方法,它来自于 elm。 80 | 81 | Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。 82 | 83 | ```javascript 84 | import key from 'keymaster'; 85 | ... 86 | app.model({ 87 | namespace: 'count', 88 | subscriptions: { 89 | keyEvent({dispatch}) { 90 | key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) }); 91 | }, 92 | } 93 | }); 94 | ``` 95 | 96 | ## Router 97 | 98 | 这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 [History API](http://mdn.beonex.com/en/DOM/window.history.html) 可以监听浏览器url的变化,从而控制路由相关操作。 99 | 100 | dva 实例提供了 router 方法来控制路由,使用的是[react-router](https://github.com/reactjs/react-router)。 101 | 102 | ```javascript 103 | import { Router, Route } from 'dva/router'; 104 | app.router(({history}) => 105 | 106 | 107 | 108 | ); 109 | ``` 110 | 111 | ## Route Components 112 | 113 | 在[组件设计方法](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。 114 | 115 | 所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在`/routes/`目录下,而`/components/`目录下则是纯组件(Presentational Components)。 116 | 117 | ## 参考 118 | 119 | - [redux docs](http://redux.js.org/docs/Glossary.html) 120 | - [redux docs 中文](http://cn.redux.js.org/index.html) 121 | - [Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide) 122 | - [JS函数式编程指南](https://www.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details) 123 | - [choo docs](https://github.com/yoshuawuyts/choo) 124 | - [elm](http://elm-lang.org/blog/farewell-to-frp) 125 | -------------------------------------------------------------------------------- /docs/guide/examples-and-boilerplates.md: -------------------------------------------------------------------------------- 1 | # 例子和脚手架 2 | 3 | ## 官方 4 | 5 | * [Count](https://stackblitz.com/edit/dva-example-count): 简单计数器 6 | * [User Dashboard](https://github.com/dvajs/dva/tree/master/examples/user-dashboard): 用户管理 7 | * [AntDesign Pro](https://github.com/ant-design/ant-design-pro):([Demo](https://preview.pro.ant.design/)),开箱即用的中台前端/设计解决方案 8 | * [HackerNews](https://github.com/dvajs/dva-hackernews): ([Demo](https://dvajs.github.io/dva-hackernews/)),HackerNews Clone 9 | * [antd-admin](https://github.com/zuiidea/antd-admin): ([Demo](http://antd-admin.zuiidea.com/)),基于 antd 和 dva 的后台管理应用 10 | * [github-stars](https://github.com/sorrycc/github-stars): ([Demo](http://sorrycc.github.io/github-stars/#/?_k=rmj86f)),Github Star 管理应用 11 | 12 | ## 社区 13 | 14 | * [umi-dva-antd-mobile](https://github.com/hqwlkj/umi-dva-antd-mobile),来自 @Yanghc 的 umi + dva + antd-mobile 的 mobile 版本脚手架,支持 TypeScript。 15 | * [Account System](https://github.com/yvanwangl/AccountSystem.git): 小型库存管理系统 16 | * [react-native-dva-starter](https://github.com/nihgwu/react-native-dva-starter): 集成了 dva 和 react-navigation 典型应用场景的 React Native 实例 17 | -------------------------------------------------------------------------------- /docs/guide/fig-show.md: -------------------------------------------------------------------------------- 1 | # Dva 图解 2 | 3 | > 作者:至正
4 | > 原文链接:[https://yuque.com/flying.ni/the-tower/tvzasn](https://yuque.com/flying.ni/the-tower/tvzasn) 5 | 6 | ## 示例背景 7 | 8 | 最常见的 Web 类示例之一: TodoList = Todo list + Add todo button 9 | 10 | ## 图解一: React 表示法 11 | 12 | ![图片.png | left | 747x518](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436560812-2586a0b5-7a6a-4a07-895c-f822fa85d5de.png "") 13 | 14 | 按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是 `` 15 | 16 | ` ` 以及`` 本身不维持任何 state, 完全由父节点`` 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: `Pure Component` 17 | 18 | ## 图解二: Redux 表示法 19 | 20 | React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store 21 | 22 | ![图片.png | left | 747x558](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436134375-4c15f63d-72f1-4c73-94a6-55b220d2547c.png "") 23 | 24 | 与图一相比, 几个明显的改进点: 25 | 26 | 1. 状态及页面逻辑从 ``里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer 27 | 2. ` ` 及``都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新 28 | 3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging 29 | 30 | 这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好 31 | 32 | ## 图解三: 加入 Saga 33 | 34 | ![图片.png | left | 747x504](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436167824-7fa834ea-aa6c-4f9f-bab5-b8c5312bcf7e.png "") 35 | 36 | 上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子: 37 | 38 | 1. 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action 39 | 2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可 40 | 41 | ## 图解四: Dva 表示法 42 | 43 | ![图片.png | left | 747x490](https://cdn.yuque.com/yuque/0/2018/png/103904/1528436195004-cd3800f2-f13d-40ba-bb1f-4efba99cfe0d.png "") 44 | 45 | 有了前面的三步铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验: 46 | 47 | 1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面 48 | 2. 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作 49 | 3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️ 50 | 51 | `约定优于配置, 总是好的`😆 52 | 53 | ```js 54 | app.model({ 55 | namespace: 'count', 56 | state: { 57 | record: 0, 58 | current: 0, 59 | }, 60 | reducers: { 61 | add(state) { 62 | const newCurrent = state.current + 1; 63 | return { ...state, 64 | record: newCurrent > state.record ? newCurrent : state.record, 65 | current: newCurrent, 66 | }; 67 | }, 68 | minus(state) { 69 | return { ...state, current: state.current - 1}; 70 | }, 71 | }, 72 | effects: { 73 | *add(action, { call, put }) { 74 | yield call(delay, 1000); 75 | yield put({ type: 'minus' }); 76 | }, 77 | }, 78 | subscriptions: { 79 | keyboardWatcher({ dispatch }) { 80 | key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) }); 81 | }, 82 | }, 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | ## 安装 dva-cli 4 | 5 | 通过 npm 安装 dva-cli 并确保版本是 `0.9.1` 或以上。 6 | 7 | ```bash 8 | $ npm install dva-cli -g 9 | $ dva -v 10 | dva-cli version 0.9.1 11 | ``` 12 | 13 | ## 创建新应用 14 | 15 | 安装完 dva-cli 之后,就可以在命令行里访问到 `dva` 命令([不能访问?](http://stackoverflow.com/questions/15054388/global-node-modules-not-installing-correctly-command-not-found))。现在,你可以通过 `dva new` 创建新应用。 16 | 17 | ```bash 18 | $ dva new dva-quickstart 19 | ``` 20 | 21 | 这会创建 `dva-quickstart` 目录,包含项目初始化目录和文件,并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。 22 | 23 | 然后我们 `cd` 进入 `dva-quickstart` 目录,并启动开发服务器: 24 | 25 | ```bash 26 | $ cd dva-quickstart 27 | $ npm start 28 | ``` 29 | 30 | 几秒钟后,你会看到以下输出: 31 | 32 | ```bash 33 | Compiled successfully! 34 | 35 | The app is running at: 36 | 37 | http://localhost:8000/ 38 | 39 | Note that the development build is not optimized. 40 | To create a production build, use npm run build. 41 | ``` 42 | 43 | 在浏览器里打开 http://localhost:8000 ,你会看到 dva 的欢迎界面。 44 | 45 | ## 使用 antd 46 | 47 | 通过 npm 安装 `antd` 和 `babel-plugin-import` 。`babel-plugin-import` 是用来按需加载 antd 的脚本和样式的,详见 [repo](https://github.com/ant-design/babel-plugin-import) 。 48 | 49 | ```bash 50 | $ npm install antd babel-plugin-import --save 51 | ``` 52 | 53 | 编辑 `.webpackrc`,使 `babel-plugin-import` 插件生效。 54 | 55 | ```diff 56 | { 57 | + "extraBabelPlugins": [ 58 | + ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }] 59 | + ] 60 | } 61 | ``` 62 | 63 | > 注:dva-cli 基于 roadhog 实现 build 和 dev,更多 `.webpackrc` 的配置详见 [roadhog#配置](https://github.com/sorrycc/roadhog#配置) 64 | 65 | ## 定义路由 66 | 67 | 我们要写个应用来先显示产品列表。首先第一步是创建路由,路由可以想象成是组成应用的不同页面。 68 | 69 | 新建 route component `routes/Products.js`,内容如下: 70 | 71 | ```javascript 72 | import React from 'react'; 73 | 74 | const Products = (props) => ( 75 |

List of Products

76 | ); 77 | 78 | export default Products; 79 | ``` 80 | 81 | 添加路由信息到路由表,编辑 `router.js` : 82 | 83 | ```diff 84 | + import Products from './routes/Products'; 85 | ... 86 | + 87 | ``` 88 | 89 | 然后在浏览器里打开 http://localhost:8000/#/products ,你应该能看到前面定义的 `

` 标签。 90 | 91 | ## 编写 UI Component 92 | 93 | 随着应用的发展,你会需要在多个页面分享 UI 元素 (或在一个页面使用多次),在 dva 里你可以把这部分抽成 component 。 94 | 95 | 我们来编写一个 `ProductList` component,这样就能在不同的地方显示产品列表了。 96 | 97 | 新建 `components/ProductList.js` 文件: 98 | 99 | ```javascript 100 | import React from 'react'; 101 | import PropTypes from 'prop-types'; 102 | import { Table, Popconfirm, Button } from 'antd'; 103 | 104 | const ProductList = ({ onDelete, products }) => { 105 | const columns = [{ 106 | title: 'Name', 107 | dataIndex: 'name', 108 | }, { 109 | title: 'Actions', 110 | render: (text, record) => { 111 | return ( 112 | onDelete(record.id)}> 113 | 114 | 115 | ); 116 | }, 117 | }]; 118 | return ( 119 | 123 | ); 124 | }; 125 | 126 | ProductList.propTypes = { 127 | onDelete: PropTypes.func.isRequired, 128 | products: PropTypes.array.isRequired, 129 | }; 130 | 131 | export default ProductList; 132 | ``` 133 | 134 | ## 定义 Model 135 | 136 | 完成 UI 后,现在开始处理数据和逻辑。 137 | 138 | dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。 139 | 140 | 新建 model `models/products.js` : 141 | 142 | ```javascript 143 | export default { 144 | namespace: 'products', 145 | state: [], 146 | reducers: { 147 | 'delete'(state, { payload: id }) { 148 | return state.filter(item => item.id !== id); 149 | }, 150 | }, 151 | }; 152 | ``` 153 | 154 | 这个 model 里: 155 | 156 | - `namespace` 表示在全局 state 上的 key 157 | - `state` 是初始值,在这里是空数组 158 | - `reducers` 等同于 redux 里的 reducer,接收 action,同步更新 state 159 | 160 | 然后别忘记在 `index.js` 里载入他: 161 | 162 | ```diff 163 | // 3. Model 164 | + app.model(require('./models/products').default); 165 | ``` 166 | 167 | ## connect 起来 168 | 169 | 到这里,我们已经单独完成了 model 和 component,那么他们如何串联起来呢? 170 | 171 | dva 提供了 connect 方法。如果你熟悉 redux,这个 connect 就是 react-redux 的 connect 。 172 | 173 | 编辑 `routes/Products.js`,替换为以下内容: 174 | 175 | ```javascript 176 | import React from 'react'; 177 | import { connect } from 'dva'; 178 | import ProductList from '../components/ProductList'; 179 | 180 | const Products = ({ dispatch, products }) => { 181 | function handleDelete(id) { 182 | dispatch({ 183 | type: 'products/delete', 184 | payload: id, 185 | }); 186 | } 187 | return ( 188 |
189 |

List of Products

190 | 191 |
192 | ); 193 | }; 194 | 195 | // export default Products; 196 | export default connect(({ products }) => ({ 197 | products, 198 | }))(Products); 199 | ``` 200 | 201 | 最后,我们还需要一些初始数据让这个应用 run 起来。编辑 `index.js`: 202 | 203 | ```diff 204 | - const app = dva(); 205 | + const app = dva({ 206 | + initialState: { 207 | + products: [ 208 | + { name: 'dva', id: 1 }, 209 | + { name: 'antd', id: 2 }, 210 | + ], 211 | + }, 212 | + }); 213 | ``` 214 | 215 | 刷新浏览器,应该能看到以下效果: 216 | 217 |

218 | 219 |

220 | 221 | ## 构建应用 222 | 223 | 完成开发并且在开发环境验证之后,就需要部署给我们的用户了。先执行下面的命令: 224 | 225 | ```bash 226 | $ npm run build 227 | ``` 228 | 229 | 几秒后,输出应该如下: 230 | 231 | ```bash 232 | > @ build /private/tmp/myapp 233 | > roadhog build 234 | 235 | Creating an optimized production build... 236 | Compiled successfully. 237 | 238 | File sizes after gzip: 239 | 240 | 82.98 KB dist/index.js 241 | 270 B dist/index.css 242 | ``` 243 | 244 | `build` 命令会打包所有的资源,包含 JavaScript, CSS, web fonts, images, html 等。然后你可以在 `dist/` 目录下找到这些文件。 245 | -------------------------------------------------------------------------------- /docs/guide/introduce-class.md: -------------------------------------------------------------------------------- 1 | # 入门课 2 | 3 | ::: tip 4 | 内容来自之前为内部同学准备的入门课。 5 | ::: 6 | 7 | ## React 没有解决的问题 8 | 9 | React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。 10 | 11 | 如果开发大应用,还需要解决一个问题。 12 | 13 | * 通信:组件之间如何通信? 14 | * 数据流:数据如何和视图串联起来?路由和数据如何绑定?如何编写异步逻辑?等等 15 | 16 | ## 通信问题 17 | 组件会发生三种通信。 18 | 19 | * 向子组件发消息 20 | * 向父组件发消息 21 | * 向其他组件发消息 22 | 23 | React 只提供了一种通信手段:传参。对于大应用,很不方便。 24 | 25 | ## 组件通信的例子 26 | 27 | ### 步骤1 28 | 29 | ```js 30 | class Son extends React.Component { 31 | render() { 32 | return ; 33 | } 34 | } 35 | 36 | class Father extends React.Component { 37 | render() { 38 | return
39 | 40 |

这里显示 Son 组件的内容

41 |
; 42 | } 43 | } 44 | 45 | ReactDOM.render(, mountNode); 46 | ``` 47 | 48 | 看这个例子,想一想父组件如何拿到子组件的值。 49 | 50 | ### 步骤2 51 | 52 | ```js 53 | class Son extends React.Component { 54 | render() { 55 | return ; 56 | } 57 | } 58 | 59 | class Father extends React.Component { 60 | constructor() { 61 | super(); 62 | this.state = { 63 | son: "" 64 | } 65 | } 66 | changeHandler(e) { 67 | this.setState({ 68 | son: e.target.value 69 | }); 70 | } 71 | render() { 72 | return
73 | 74 |

这里显示 Son 组件的内容:{this.state.son}

75 |
; 76 | } 77 | } 78 | 79 | ReactDOM.render(, mountNode); 80 | ``` 81 | 82 | 看下这个例子,看懂源码,理解子组件如何通过父组件传入的函数,将自己的值再传回父组件。 83 | 84 | ## 数据流问题 85 | 86 | 目前流行的数据流方案有: 87 | 88 | * Flux,单向数据流方案,以 [Redux](https://github.com/reactjs/redux) 为代表 89 | * Reactive,响应式数据流方案,以 [Mobx](https://github.com/mobxjs/mobx) 为代表 90 | * 其他,比如 rxjs 等 91 | 92 | 到底哪一种架构最合适 React ? 93 | 94 | ## 目前最流行的数据流方案 95 | 96 | 截止 2017.1,最流行的社区 React 应用架构方案如下。 97 | 98 | * 路由: [React-Router](https://github.com/ReactTraining/react-router/tree/v2.8.1) 99 | * 架构: [Redux](https://github.com/reactjs/redux) 100 | * 异步操作: [Redux-saga](https://github.com/yelouafi/redux-saga) 101 | 102 | 缺点:要引入多个库,项目结构复杂。 103 | 104 | ## dva 是什么 105 | 106 | dva 是体验技术部开发的 React 应用框架,将上面三个 React 工具库包装在一起,简化了 API,让开发 React 应用更加方便和快捷。 107 | 108 | dva = React-Router + Redux + Redux-saga 109 | 110 | ## dva 应用的最简结构 111 | ```js 112 | import dva from 'dva'; 113 | const App = () =>
Hello dva
; 114 | 115 | // 创建应用 116 | const app = dva(); 117 | // 注册视图 118 | app.router(() => ); 119 | // 启动应用 120 | app.start('#root'); 121 | ``` 122 | 123 | ## 数据流图 124 | 125 | 126 | 127 | ## 核心概念 128 | * State:一个对象,保存整个应用状态 129 | * View:React 组件构成的视图层 130 | * Action:一个对象,描述事件 131 | * connect 方法:一个函数,绑定 State 到 View 132 | * dispatch 方法:一个函数,发送 Action 到 State 133 | 134 | ## State 和 View 135 | State 是储存数据的地方,收到 Action 以后,会更新数据。 136 | 137 | View 就是 React 组件构成的 UI 层,从 State 取数据后,渲染成 HTML 代码。只要 State 有变化,View 就会自动更新。 138 | 139 | ## Action 140 | Action 是用来描述 UI 层事件的一个对象。 141 | 142 | ```js 143 | { 144 | type: 'click-submit-button', 145 | payload: this.form.data 146 | } 147 | ``` 148 | 149 | ## connect 方法 150 | 151 | connect 是一个函数,绑定 State 到 View。 152 | 153 | ```js 154 | import { connect } from 'dva'; 155 | 156 | function mapStateToProps(state) { 157 | return { todos: state.todos }; 158 | } 159 | connect(mapStateToProps)(App); 160 | ``` 161 | 162 | connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。 163 | 164 | connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。 165 | 166 | ## dispatch 方法 167 | dispatch 是一个函数方法,用来将 Action 发送给 State。 168 | 169 | ```js 170 | dispatch({ 171 | type: 'click-submit-button', 172 | payload: this.form.data 173 | }) 174 | ``` 175 | 176 | dispatch 方法从哪里来?被 connect 的 Component 会自动在 props 中拥有 dispatch 方法。 177 | 178 | > connect 的数据从哪里来? 179 | 180 | ## dva 应用的最简结构(带 model) 181 | ```js 182 | // 创建应用 183 | const app = dva(); 184 | 185 | // 注册 Model 186 | app.model({ 187 | namespace: 'count', 188 | state: 0, 189 | reducers: { 190 | add(state) { return state + 1 }, 191 | }, 192 | effects: { 193 | *addAfter1Second(action, { call, put }) { 194 | yield call(delay, 1000); 195 | yield put({ type: 'add' }); 196 | }, 197 | }, 198 | }); 199 | 200 | // 注册视图 201 | app.router(() => ); 202 | 203 | // 启动应用 204 | app.start('#root'); 205 | ``` 206 | 207 | ## 数据流图 1 208 | 209 | 210 | 211 | ## 数据流图 2 212 | 213 | 214 | 215 | ## app.model 216 | 217 | dva 提供 app.model 这个对象,所有的应用逻辑都定义在它上面。 218 | 219 | ```js 220 | const app = dva(); 221 | 222 | // 新增这一行 223 | app.model({ /**/ }); 224 | 225 | app.router(() => ); 226 | app.start('#root'); 227 | ``` 228 | 229 | ## Model 对象的例子 230 | 231 | ```js 232 | { 233 | namespace: 'count', 234 | state: 0, 235 | reducers: { 236 | add(state) { return state + 1 }, 237 | }, 238 | effects: { 239 | *addAfter1Second(action, { call, put }) { 240 | yield call(delay, 1000); 241 | yield put({ type: 'add' }); 242 | }, 243 | }, 244 | } 245 | ``` 246 | 247 | ## Model 对象的属性 248 | 249 | * namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成 250 | * state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出 251 | * reducers: Action 处理器,处理同步动作,用来算出最新的 State 252 | * effects:Action 处理器,处理异步动作 253 | 254 | ## Reducer 255 | 256 | Reducer 是 Action 处理器,用来处理同步操作,可以看做是 state 的计算器。它的作用是根据 Action,从上一个 State 算出当前 State。 257 | 258 | 一些例子: 259 | 260 | ```js 261 | // count +1 262 | function add(state) { return state + 1; } 263 | 264 | // 往 [] 里添加一个新 todo 265 | function addTodo(state, action) { return [...state, action.payload]; } 266 | 267 | // 往 { todos: [], loading: true } 里添加一个新 todo,并标记 loading 为 false 268 | function addTodo(state, action) { 269 | return { 270 | ...state, 271 | todos: state.todos.concat(action.payload), 272 | loading: false 273 | }; 274 | } 275 | ``` 276 | 277 | ## Effect 278 | 279 | Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。 280 | 281 | ```js 282 | function *addAfter1Second(action, { put, call }) { 283 | yield call(delay, 1000); 284 | yield put({ type: 'add' }); 285 | } 286 | ``` 287 | 288 | ## Generator 函数 289 | 290 | Effect 是一个 Generator 函数,内部使用 yield 关键字,标识每一步的操作(不管是异步或同步)。 291 | 292 | ## call 和 put 293 | 294 | dva 提供多个 effect 函数内部的处理函数,比较常用的是 `call` 和 `put`。 295 | 296 | * call:执行异步函数 297 | * put:发出一个 Action,类似于 dispatch 298 | 299 | ## 课堂实战 300 | 写一个列表,包含删除按钮,点删除按钮后延迟 1 秒执行删除。 301 | 302 | 303 | -------------------------------------------------------------------------------- /examples/func-test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "generator-star-spacing": [0], 6 | "consistent-return": [0], 7 | "react/forbid-prop-types": [0], 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "global-require": [1], 10 | "import/prefer-default-export": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "react/prefer-stateless-function": [0], 14 | "no-else-return": [0], 15 | "no-restricted-syntax": [0], 16 | "import/no-extraneous-dependencies": [0], 17 | "no-use-before-define": [0], 18 | "jsx-a11y/no-static-element-interactions": [0], 19 | "no-nested-ternary": [0], 20 | "arrow-body-style": [0], 21 | "import/extensions": [0], 22 | "no-bitwise": [0], 23 | "no-cond-assign": [0], 24 | "import/no-unresolved": [0], 25 | "require-yield": [1] 26 | }, 27 | "parserOptions": { 28 | "ecmaFeatures": { 29 | "experimentalObjectRestSpread": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/func-test/.roadhogrc: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "src/index.js", 3 | "env": { 4 | "development": { 5 | "extraBabelPlugins": [ 6 | "dva-hmr", 7 | "transform-runtime" 8 | ] 9 | }, 10 | "production": { 11 | "extraBabelPlugins": [ 12 | "transform-runtime" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/func-test/.roadhogrc.mock.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /examples/func-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-example", 3 | "private": true, 4 | "scripts": { 5 | "start": "roadhog server", 6 | "build": "roadhog build", 7 | "lint": "eslint --ext .js src test" 8 | }, 9 | "engines": { 10 | "install-node": "6.9.2" 11 | }, 12 | "dependencies": { 13 | "babel-runtime": "^6.9.2", 14 | "dva": "^2.0.0-0", 15 | "react": "^16.0.0", 16 | "react-dom": "^16.0.0" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^7.1.1", 20 | "babel-plugin-dva-hmr": "^0.3.2", 21 | "babel-plugin-module-alias": "^1.6.0", 22 | "babel-plugin-transform-runtime": "^6.9.0", 23 | "eslint": "^3.12.2", 24 | "eslint-config-airbnb": "^13.0.0", 25 | "eslint-plugin-import": "^2.2.0", 26 | "eslint-plugin-jsx-a11y": "^2.2.3", 27 | "eslint-plugin-react": "^6.8.0", 28 | "expect": "^1.20.2", 29 | "redbox-react": "^1.3.2", 30 | "roadhog": "^1.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/func-test/src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvajs/dva/7490ae91234990f385e58b9abfa62eceb8cd849b/examples/func-test/src/assets/yay.jpg -------------------------------------------------------------------------------- /examples/func-test/src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Example = () => { 4 | return
Example
; 5 | }; 6 | 7 | Example.propTypes = {}; 8 | 9 | export default Example; 10 | -------------------------------------------------------------------------------- /examples/func-test/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, :global(#root) { 3 | height: 100%; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /examples/func-test/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dva Demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/func-test/src/index.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import './index.css'; 3 | 4 | // 1. Initialize 5 | const app = dva(); 6 | 7 | // 2. Plugins 8 | // app.use({}); 9 | 10 | // 3. Model 11 | app.model(require('./models/example')); 12 | 13 | // 4. Router 14 | app.router(require('./router')); 15 | 16 | // 5. Start 17 | app.start('#root'); 18 | -------------------------------------------------------------------------------- /examples/func-test/src/models/example.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'example', 3 | 4 | state: {}, 5 | 6 | subscriptions: { 7 | setup({ dispatch, history }) { 8 | // eslint-disable-line 9 | history.listen(location => { 10 | console.log(1, location); 11 | }); 12 | }, 13 | }, 14 | 15 | effects: { 16 | *fetch({ payload }, { call, put }) { 17 | // eslint-disable-line 18 | yield put({ type: 'save' }); 19 | }, 20 | }, 21 | 22 | reducers: { 23 | save(state, action) { 24 | return { ...state, ...action.payload }; 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /examples/func-test/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { routerRedux, Route, Switch } from 'dva/router'; 3 | import IndexPage from './routes/IndexPage'; 4 | 5 | const { ConnectedRouter } = routerRedux; 6 | 7 | function RouterConfig({ history }) { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default RouterConfig; 16 | -------------------------------------------------------------------------------- /examples/func-test/src/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /examples/func-test/src/routes/IndexPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import styles from './IndexPage.css'; 4 | 5 | function IndexPage() { 6 | return ( 7 |
8 |

Yay! Welcome to dva!

9 |
10 | 20 |
21 | ); 22 | } 23 | 24 | IndexPage.propTypes = {}; 25 | 26 | export default connect()(IndexPage); 27 | -------------------------------------------------------------------------------- /examples/func-test/src/services/example.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export async function query() { 4 | return request('/api/users'); 5 | } 6 | -------------------------------------------------------------------------------- /examples/func-test/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'dva/fetch'; 2 | 3 | function parseJSON(response) { 4 | return response.json(); 5 | } 6 | 7 | function checkStatus(response) { 8 | if (response.status >= 200 && response.status < 300) { 9 | return response; 10 | } 11 | 12 | const error = new Error(response.statusText); 13 | error.response = response; 14 | throw error; 15 | } 16 | 17 | /** 18 | * Requests a URL, returning a promise. 19 | * 20 | * @param {string} url The URL we want to request 21 | * @param {object} [options] The options we want to pass to "fetch" 22 | * @return {object} An object containing either "data" or "err" 23 | */ 24 | export default function request(url, options) { 25 | return fetch(url, options) 26 | .then(checkStatus) 27 | .then(parseJSON) 28 | .then(data => ({ data })) 29 | .catch(err => ({ err })); 30 | } 31 | -------------------------------------------------------------------------------- /examples/user-dashboard/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /examples/user-dashboard/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "umi" 3 | } -------------------------------------------------------------------------------- /examples/user-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | 5 | .idea/ 6 | -------------------------------------------------------------------------------- /examples/user-dashboard/.umirc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: ['umi-plugin-dva'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/user-dashboard/.webpackrc: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "@primary-color": "#dc6aac", 4 | "@link-color": "#dc6aac", 5 | "@border-radius-base": "2px", 6 | "@font-size-base": "16px", 7 | "@line-height-base": "1.2" 8 | }, 9 | "proxy": { 10 | "/api": { 11 | "target": "http://jsonplaceholder.typicode.com/", 12 | "changeOrigin": true, 13 | "pathRewrite": { "^/api" : "" } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/user-dashboard/README.md: -------------------------------------------------------------------------------- 1 | # dva-example-user-dashboard 2 | 3 | 详见[《12 步 30 分钟,完成用户管理的 CURD 应用 (react+dva+antd)》](https://github.com/sorrycc/blog/issues/18)。 4 | 5 | --- 6 | 7 |

8 | 9 |

10 | 11 | ## Getting Started 12 | Install dependencies. 13 | 14 | ```bash 15 | $ npm install 16 | ``` 17 | 18 | Start server. 19 | 20 | ```bash 21 | $ npm start 22 | ``` 23 | 24 | If success, app will be open in your default browser automatically. 25 | -------------------------------------------------------------------------------- /examples/user-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-example-user-dashboard", 3 | "private": true, 4 | "scripts": { 5 | "start": "umi dev", 6 | "build": "umi build", 7 | "test": "umi test", 8 | "lint": "eslint --ext .js src test", 9 | "precommit": "npm run lint" 10 | }, 11 | "dependencies": { 12 | "umi": "^1.0.0-0", 13 | "umi-plugin-dva": "^0.1.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^4.17.0", 17 | "eslint-config-umi": "^0.1.2", 18 | "eslint-plugin-flowtype": "^2.42.0", 19 | "eslint-plugin-import": "^2.8.0", 20 | "eslint-plugin-jsx-a11y": "^5.1.1", 21 | "eslint-plugin-react": "^7.6.1", 22 | "husky": "^0.13.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvajs/dva/7490ae91234990f385e58b9abfa62eceb8cd849b/examples/user-dashboard/src/assets/yay.jpg -------------------------------------------------------------------------------- /examples/user-dashboard/src/constants.js: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE = 3; 2 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/global.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, :global(#root) { 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/layouts/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu, Icon } from 'antd'; 3 | import Link from 'umi/link'; 4 | import withRouter from 'umi/withRouter'; 5 | 6 | function Header({ location }) { 7 | return ( 8 | 9 | 10 | 11 | Users 12 | 13 | 14 | 15 | 16 | Home 17 | 18 | 19 | 20 | 21 | 404 22 | 23 | 24 | 25 | dva 26 | 27 | 28 | ); 29 | } 30 | 31 | export default withRouter(Header); 32 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/layouts/index.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | } 7 | 8 | .content { 9 | flex: 1; 10 | display: flex; 11 | } 12 | 13 | .main { 14 | padding: 0 8px; 15 | flex: 1 0 auto; 16 | } 17 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.css'; 3 | import Header from './Header'; 4 | 5 | function MainLayout({ children, location }) { 6 | return ( 7 |
8 |
9 |
10 |
{children}
11 |
12 |
13 | ); 14 | } 15 | 16 | export default MainLayout; 17 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/.umi/DvaContainer.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import dva from 'dva'; 3 | import createLoading from 'dva-loading'; 4 | 5 | const app = dva({ 6 | history: window.g_history, 7 | }); 8 | window.g_app = app; 9 | app.use(createLoading()); 10 | 11 | app.model({ ...require('../../pages/users/models/users.js').default }); 12 | 13 | class DvaContainer extends Component { 14 | render() { 15 | app.router(() => this.props.children); 16 | return app.start()(); 17 | } 18 | } 19 | 20 | export default DvaContainer; 21 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/.umi/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('load', () => { 3 | navigator.serviceWorker 4 | .register(`${process.env.BASE_URL || ''}service-worker.js`) 5 | .then(reg => {}) 6 | .catch(e => {}); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/.umi/router.js: -------------------------------------------------------------------------------- 1 | import { Router as DefaultRouter, Route, Switch } from 'react-router-dom'; 2 | import dynamic from 'umi/dynamic'; 3 | import('/Users/chencheng/Documents/Work/Misc/dva/packages/dva-example-user-dashboard/src/global.css'); 4 | import Layout from '/Users/chencheng/Documents/Work/Misc/dva/packages/dva-example-user-dashboard/src/layouts/index.js'; 5 | 6 | const Router = window.g_CustomRouter || DefaultRouter; 7 | 8 | export default function() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/.umi/umi.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import createHistory from 'umi/_createHistory'; 4 | import FastClick from 'umi-fastclick'; 5 | 6 | document.addEventListener( 7 | 'DOMContentLoaded', 8 | () => { 9 | FastClick.attach(document.body); 10 | }, 11 | false 12 | ); 13 | 14 | // create history 15 | window.g_history = createHistory({ 16 | basename: window.routerBase, 17 | }); 18 | 19 | // render 20 | function render() { 21 | const DvaContainer = require('./DvaContainer').default; 22 | ReactDOM.render( 23 | React.createElement( 24 | DvaContainer, 25 | null, 26 | React.createElement(require('./router').default) 27 | ), 28 | document.getElementById('root') 29 | ); 30 | } 31 | render(); 32 | 33 | // hot module replacement 34 | if (module.hot) { 35 | module.hot.accept('./router', () => { 36 | render(); 37 | }); 38 | } 39 | 40 | if (process.env.NODE_ENV === 'development') { 41 | window.g_history.listen(function(location) { 42 | new Image().src = (window.routerBase + location.pathname).replace( 43 | /\/\//g, 44 | '/' 45 | ); 46 | }); 47 | } 48 | 49 | // Enable service worker 50 | if (process.env.NODE_ENV === 'production') { 51 | require('./registerServiceWorker'); 52 | } 53 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/index.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.css'; 3 | 4 | function IndexPage() { 5 | return ( 6 |
7 |

Yay! Welcome to dva!

8 |
9 | 19 |
20 | ); 21 | } 22 | 23 | export default IndexPage; 24 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/components/Users/UserModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Modal, Form, Input } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | class UserEditModal extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | visible: false, 11 | }; 12 | } 13 | 14 | showModelHandler = e => { 15 | if (e) e.stopPropagation(); 16 | this.setState({ 17 | visible: true, 18 | }); 19 | }; 20 | 21 | hideModelHandler = () => { 22 | this.setState({ 23 | visible: false, 24 | }); 25 | }; 26 | 27 | okHandler = () => { 28 | const { onOk } = this.props; 29 | this.props.form.validateFields((err, values) => { 30 | if (!err) { 31 | onOk(values); 32 | this.hideModelHandler(); 33 | } 34 | }); 35 | }; 36 | 37 | render() { 38 | const { children } = this.props; 39 | const { getFieldDecorator } = this.props.form; 40 | const { name, email, website } = this.props.record; 41 | const formItemLayout = { 42 | labelCol: { span: 6 }, 43 | wrapperCol: { span: 14 }, 44 | }; 45 | 46 | return ( 47 | 48 | {children} 49 | 55 |
56 | 57 | {getFieldDecorator('name', { 58 | initialValue: name, 59 | })()} 60 | 61 | 62 | {getFieldDecorator('email', { 63 | initialValue: email, 64 | })()} 65 | 66 | 67 | {getFieldDecorator('website', { 68 | initialValue: website, 69 | })()} 70 | 71 | 72 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | export default Form.create()(UserEditModal); 79 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/components/Users/Users.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | } 4 | 5 | .create { 6 | margin-bottom: 1.5em; 7 | } 8 | 9 | .operation a { 10 | margin: 0 .5em; 11 | } 12 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/components/Users/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Table, Pagination, Popconfirm, Button } from 'antd'; 4 | import { routerRedux } from 'dva/router'; 5 | import styles from './Users.css'; 6 | import { PAGE_SIZE } from '../../../../constants'; 7 | import UserModal from './UserModal'; 8 | 9 | function Users({ dispatch, list: dataSource, loading, total, page: current }) { 10 | function deleteHandler(id) { 11 | dispatch({ 12 | type: 'users/remove', 13 | payload: id, 14 | }); 15 | } 16 | 17 | function pageChangeHandler(page) { 18 | dispatch( 19 | routerRedux.push({ 20 | pathname: '/users', 21 | query: { page }, 22 | }) 23 | ); 24 | } 25 | 26 | function editHandler(id, values) { 27 | dispatch({ 28 | type: 'users/patch', 29 | payload: { id, values }, 30 | }); 31 | } 32 | 33 | function createHandler(values) { 34 | dispatch({ 35 | type: 'users/create', 36 | payload: values, 37 | }); 38 | } 39 | 40 | const columns = [ 41 | { 42 | title: 'Name', 43 | dataIndex: 'name', 44 | key: 'name', 45 | render: text => {text}, 46 | }, 47 | { 48 | title: 'Email', 49 | dataIndex: 'email', 50 | key: 'email', 51 | }, 52 | { 53 | title: 'Website', 54 | dataIndex: 'website', 55 | key: 'website', 56 | }, 57 | { 58 | title: 'Operation', 59 | key: 'operation', 60 | render: (text, record) => ( 61 | 62 | 63 | Edit 64 | 65 | 69 | Delete 70 | 71 | 72 | ), 73 | }, 74 | ]; 75 | 76 | return ( 77 |
78 |
79 |
80 | 81 | 82 | 83 |
84 |
record.id} 89 | pagination={false} 90 | /> 91 | 98 | 99 | 100 | ); 101 | } 102 | 103 | function mapStateToProps(state) { 104 | const { list, total, page } = state.users; 105 | return { 106 | loading: state.loading.models.users, 107 | list, 108 | total, 109 | page, 110 | }; 111 | } 112 | 113 | export default connect(mapStateToProps)(Users); 114 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/models/users.js: -------------------------------------------------------------------------------- 1 | import * as usersService from '../services/users'; 2 | 3 | export default { 4 | namespace: 'users', 5 | state: { 6 | list: [], 7 | total: null, 8 | page: null, 9 | }, 10 | reducers: { 11 | save(state, { payload: { data: list, total, page } }) { 12 | return { ...state, list, total, page }; 13 | }, 14 | }, 15 | effects: { 16 | *fetch({ payload: { page = 1 } }, { call, put }) { 17 | const { data, headers } = yield call(usersService.fetch, { page }); 18 | yield put({ 19 | type: 'save', 20 | payload: { 21 | data, 22 | total: parseInt(headers['x-total-count'], 10), 23 | page: parseInt(page, 10), 24 | }, 25 | }); 26 | }, 27 | *remove({ payload: id }, { call, put }) { 28 | yield call(usersService.remove, id); 29 | yield put({ type: 'reload' }); 30 | }, 31 | *patch({ payload: { id, values } }, { call, put }) { 32 | yield call(usersService.patch, id, values); 33 | yield put({ type: 'reload' }); 34 | }, 35 | *create({ payload: values }, { call, put }) { 36 | yield call(usersService.create, values); 37 | yield put({ type: 'reload' }); 38 | }, 39 | *reload(action, { put, select }) { 40 | const page = yield select(state => state.users.page); 41 | yield put({ type: 'fetch', payload: { page } }); 42 | }, 43 | }, 44 | subscriptions: { 45 | setup({ dispatch, history }) { 46 | return history.listen(({ pathname, query }) => { 47 | if (pathname === '/users') { 48 | dispatch({ type: 'fetch', payload: query }); 49 | } 50 | }); 51 | }, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/page.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | width: 900px; 4 | margin: 3em auto 0; 5 | } 6 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './page.css'; 3 | import UsersComponent from './components/Users/Users'; 4 | 5 | function Users() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default Users; 14 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/pages/users/services/users.js: -------------------------------------------------------------------------------- 1 | import request from '../../../utils/request'; 2 | import { PAGE_SIZE } from '../../../constants'; 3 | 4 | export function fetch({ page }) { 5 | return request(`/api/users?_page=${page}&_limit=${PAGE_SIZE}`); 6 | } 7 | 8 | export function remove(id) { 9 | return request(`/api/users/${id}`, { 10 | method: 'DELETE', 11 | }); 12 | } 13 | 14 | export function patch(id, values) { 15 | return request(`/api/users/${id}`, { 16 | method: 'PATCH', 17 | body: JSON.stringify(values), 18 | }); 19 | } 20 | 21 | export function create(values) { 22 | return request('/api/users', { 23 | method: 'POST', 24 | body: JSON.stringify(values), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/plugins/onError.js: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | 3 | const ERROR_MSG_DURATION = 3; // 3 秒 4 | 5 | export default { 6 | onError(e) { 7 | message.error(e.message, ERROR_MSG_DURATION); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/user-dashboard/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'dva/fetch'; 2 | 3 | function checkStatus(response) { 4 | if (response.status >= 200 && response.status < 300) { 5 | return response; 6 | } 7 | 8 | const error = new Error(response.statusText); 9 | error.response = response; 10 | throw error; 11 | } 12 | 13 | /** 14 | * Requests a URL, returning a promise. 15 | * 16 | * @param {string} url The URL we want to request 17 | * @param {object} [options] The options we want to pass to "fetch" 18 | * @return {object} An object containing either "data" or "err" 19 | */ 20 | async function request(url, options) { 21 | const response = await fetch(url, options); 22 | 23 | checkStatus(response); 24 | 25 | const data = await response.json(); 26 | 27 | const ret = { 28 | data, 29 | headers: {}, 30 | }; 31 | 32 | if (response.headers.get('x-total-count')) { 33 | ret.headers['x-total-count'] = response.headers.get('x-total-count'); 34 | } 35 | 36 | return ret; 37 | } 38 | 39 | export default request; 40 | -------------------------------------------------------------------------------- /examples/with-immer/.umirc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: ['umi-plugin-dva'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/with-immer/dva.js: -------------------------------------------------------------------------------- 1 | import useImmer from 'dva-immer'; 2 | 3 | export function config() { 4 | return { 5 | ...useImmer(), 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /examples/with-immer/model.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'count', 3 | state: { 4 | a: { 5 | b: { 6 | c: { 7 | count: 0, 8 | }, 9 | }, 10 | }, 11 | }, 12 | reducers: { 13 | add(state) { 14 | state.a.b.c.count += 1; 15 | }, 16 | setNewProp(state) { 17 | state.newProp = 'hi new prop'; 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/with-immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "umi": "*", 4 | "umi-plugin-dva": "*", 5 | "dva-immer": "*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/with-immer/pages/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'dva'; 2 | import { Button } from 'antd-mobile'; 3 | 4 | function App({ count, newProp, dispatch }) { 5 | return ( 6 |
7 |

Count: {count}

8 |

state.newProp: {newProp || 'not setted'}

9 | 18 | 27 |
28 | ); 29 | } 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | count: state.count.a.b.c.count, 34 | newProp: state.count.newProp, 35 | }; 36 | } 37 | 38 | export default connect(mapStateToProps)(App); 39 | -------------------------------------------------------------------------------- /examples/with-nextjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | ["module-resolver", { 7 | "alias": { 8 | "dva": "dva-no-router" 9 | } 10 | }] 11 | ] 12 | } -------------------------------------------------------------------------------- /examples/with-nextjs/.eslintignore: -------------------------------------------------------------------------------- 1 | .next/* 2 | node_modules/* 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/with-nextjs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "settings": { 5 | "import/resolver": { 6 | "babel-module": { 7 | "alias": { 8 | "test": "./test", 9 | "underscore": "lodash" 10 | } 11 | } 12 | } 13 | }, 14 | "rules": { 15 | "arrow-body-style": [0], 16 | "consistent-return": [0], 17 | "generator-star-spacing": [0], 18 | "global-require": [1], 19 | "import/extensions": [0], 20 | "import/no-extraneous-dependencies": [0], 21 | "import/no-unresolved": [0], 22 | "import/prefer-default-export": [0], 23 | "jsx-a11y/no-static-element-interactions": [0], 24 | "no-bitwise": [0], 25 | "no-cond-assign": [0], 26 | "no-else-return": [0], 27 | "no-nested-ternary": [0], 28 | "no-restricted-syntax": [0], 29 | "no-use-before-define": [0], 30 | "react/forbid-prop-types": [0], 31 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 32 | "react/jsx-no-bind": [0], 33 | "react/prefer-stateless-function": [0], 34 | "react/prop-types": [0], 35 | "require-yield": [1], 36 | "react/react-in-jsx-scope": [0], 37 | "jsx-a11y/anchor-is-valid": [0] 38 | }, 39 | "parserOptions": { 40 | "ecmaFeatures": { 41 | "experimentalObjectRestSpread": true 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /examples/with-nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.next/ -------------------------------------------------------------------------------- /examples/with-nextjs/README.md: -------------------------------------------------------------------------------- 1 | # dva_next 2 | compose dva.js with next.js 3 | 4 | 1.install 5 | 6 | ``` 7 | npm install 8 | ``` 9 | 10 | 2.start 11 | 12 | ``` 13 | npm start 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/with-nextjs/model/homepage.js: -------------------------------------------------------------------------------- 1 | const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout)); 2 | 3 | const model = { 4 | namespace: 'index', 5 | state: { 6 | name: 'hopperhuang', 7 | count: 0, 8 | init: false, 9 | }, 10 | reducers: { 11 | caculate(state, payload) { 12 | const { count } = state; 13 | const { delta } = payload; 14 | return { ...state, count: count + delta }; 15 | }, 16 | }, 17 | effects: { 18 | *init(action, { put }) { 19 | yield delay(2000); 20 | yield put({ type: 'caculate', delta: 1 }); 21 | }, 22 | }, 23 | }; 24 | 25 | export default model; 26 | 27 | -------------------------------------------------------------------------------- /examples/with-nextjs/model/index.js: -------------------------------------------------------------------------------- 1 | import homepage from './homepage'; 2 | 3 | const model = [ 4 | homepage, 5 | ]; 6 | 7 | export default model; 8 | -------------------------------------------------------------------------------- /examples/with-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-next.js", 3 | "version": "1.0.0", 4 | "description": "dva-next.js-example", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "next" 9 | }, 10 | "author": "hopperhuang", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dva-no-router": "^1.0.3", 14 | "next": "^5.0.0", 15 | "react": "^16.2.0", 16 | "react-dom": "^16.2.0" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^8.2.2", 20 | "babel-plugin-module-resolver": "^3.1.0", 21 | "eslint": "^4.19.1", 22 | "eslint-config-airbnb": "^16.1.0", 23 | "eslint-import-resolver-babel-module": "^4.0.0", 24 | "eslint-plugin-import": "^2.9.0", 25 | "eslint-plugin-jsx-a11y": "^6.0.3", 26 | "eslint-plugin-react": "^7.7.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/with-nextjs/pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import WithDva from '../utils/store'; 5 | 6 | class Page extends React.Component { 7 | static async getInitialProps(props) { 8 | // first time run in server side 9 | // other times run in client side ( client side init with default props 10 | // console.log('get init props'); 11 | const { 12 | pathname, query, isServer, store, 13 | } = props; 14 | // dispatch effects to fetch data here 15 | await props.store.dispatch({ type: 'index/init' }); 16 | return { 17 | // dont use store as property name, it will confilct with initial store 18 | pathname, query, isServer, dvaStore: store, 19 | }; 20 | } 21 | 22 | render() { 23 | const { index } = this.props; 24 | const { name, count } = index; 25 | // console.log('rendered!!'); 26 | return ( 27 |
28 | Hi,{name}!!   29 |

count:  {count}

30 |

31 | 34 |

35 |

36 | 39 |

40 |

41 | 42 | Go to /users 43 | 44 |

45 |
46 | ); 47 | } 48 | } 49 | 50 | export default WithDva((state) => { return { index: state.index }; })(Page); 51 | -------------------------------------------------------------------------------- /examples/with-nextjs/pages/users.js: -------------------------------------------------------------------------------- 1 | 2 | import Link from 'next/link'; 3 | 4 | export default function () { 5 | return ( 6 |
7 | Users 8 |
9 | 10 | 11 | Back 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-nextjs/utils/store.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dva, { connect } from 'dva-no-router'; 3 | import { Provider } from 'react-redux'; 4 | import model from '../model/index'; 5 | 6 | const checkServer = () => Object.prototype.toString.call(global.process) === '[object process]'; 7 | 8 | // eslint-disable-next-line 9 | const __NEXT_DVA_STORE__ = '__NEXT_DVA_STORE__' 10 | 11 | function createDvaStore(initialState) { 12 | let app; 13 | if (initialState) { 14 | app = dva({ 15 | initialState, 16 | }); 17 | } else { 18 | app = dva({}); 19 | } 20 | const isArray = Array.isArray(model); 21 | if (isArray) { 22 | model.forEach((m) => { 23 | app.model(m); 24 | }); 25 | } else { 26 | app.model(model); 27 | } 28 | app.router(() => {}); 29 | app.start(); 30 | // console.log(app); 31 | // eslint-disable-next-line 32 | const store = app._store 33 | return store; 34 | } 35 | 36 | function getOrCreateStore(initialState) { 37 | const isServer = checkServer(); 38 | if (isServer) { // run in server 39 | // console.log('server'); 40 | return createDvaStore(initialState); 41 | } 42 | // eslint-disable-next-line 43 | if (!window[__NEXT_DVA_STORE__]) { 44 | // console.log('client'); 45 | // eslint-disable-next-line 46 | window[__NEXT_DVA_STORE__] = createDvaStore(initialState); 47 | } 48 | // eslint-disable-next-line 49 | return window[__NEXT_DVA_STORE__]; 50 | } 51 | 52 | export default function withDva(...args) { 53 | return function CreateNextPage(Component) { 54 | const ComponentWithDva = (props = {}) => { 55 | const { store, initialProps, initialState } = props; 56 | const ConnectedComponent = connect(...args)(Component); 57 | return React.createElement( 58 | Provider, 59 | // in client side, it will init store with the initial state tranfer from server side 60 | { store: store && store.dispatch ? store : getOrCreateStore(initialState) }, 61 | // transfer next.js's props to the page 62 | React.createElement(ConnectedComponent, initialProps), 63 | ); 64 | }; 65 | ComponentWithDva.getInitialProps = async (props = {}) => { 66 | // console.log('get......'); 67 | const isServer = checkServer(); 68 | const store = getOrCreateStore(props.req); 69 | // call children's getInitialProps 70 | // get initProps and transfer in to the page 71 | const initialProps = Component.getInitialProps 72 | ? await Component.getInitialProps({ ...props, isServer, store }) 73 | : {}; 74 | return { 75 | store, 76 | initialProps, 77 | initialState: store.getState(), 78 | }; 79 | }; 80 | return ComponentWithDva; 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /examples/with-react-router-3/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "generator-star-spacing": [0], 6 | "consistent-return": [0], 7 | "react/forbid-prop-types": [0], 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "global-require": [1], 10 | "import/prefer-default-export": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "react/prefer-stateless-function": [0], 14 | "no-else-return": [0], 15 | "no-restricted-syntax": [0], 16 | "import/no-extraneous-dependencies": [0], 17 | "no-use-before-define": [0], 18 | "jsx-a11y/no-static-element-interactions": [0], 19 | "no-nested-ternary": [0], 20 | "arrow-body-style": [0], 21 | "import/extensions": [0], 22 | "no-bitwise": [0], 23 | "no-cond-assign": [0], 24 | "import/no-unresolved": [0], 25 | "require-yield": [1] 26 | }, 27 | "parserOptions": { 28 | "ecmaFeatures": { 29 | "experimentalObjectRestSpread": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/with-react-router-3/.roadhogrc: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "src/index.js", 3 | "env": { 4 | "development": { 5 | "extraBabelPlugins": [ 6 | "dva-hmr", 7 | "transform-runtime", 8 | ["module-resolver", { 9 | "alias": { 10 | "dva": "dva-react-router-3" 11 | } 12 | }] 13 | ] 14 | }, 15 | "production": { 16 | "extraBabelPlugins": [ 17 | "transform-runtime", 18 | ["module-resolver", { 19 | "alias": { 20 | "dva": "dva-react-router-3" 21 | } 22 | }] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/with-react-router-3/.roadhogrc.mock.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /examples/with-react-router-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-example-react-router-3", 3 | "private": true, 4 | "scripts": { 5 | "start": "roadhog server", 6 | "build": "roadhog build", 7 | "lint": "eslint --ext .js src test" 8 | }, 9 | "engines": { 10 | "install-node": "6.9.2" 11 | }, 12 | "dependencies": { 13 | "babel-runtime": "^6.9.2", 14 | "dva-react-router-3": "^0.3.0", 15 | "react": "^16.0.0", 16 | "react-dom": "^16.0.0" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^7.1.1", 20 | "babel-plugin-dva-hmr": "^0.3.2", 21 | "babel-plugin-module-resolver": "^2.7.1", 22 | "babel-plugin-transform-runtime": "^6.9.0", 23 | "eslint": "^3.12.2", 24 | "eslint-config-airbnb": "^13.0.0", 25 | "eslint-plugin-import": "^2.2.0", 26 | "eslint-plugin-jsx-a11y": "^2.2.3", 27 | "eslint-plugin-react": "^6.8.0", 28 | "expect": "^1.20.2", 29 | "redbox-react": "^1.3.2", 30 | "roadhog": "^1.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvajs/dva/7490ae91234990f385e58b9abfa62eceb8cd849b/examples/with-react-router-3/src/assets/yay.jpg -------------------------------------------------------------------------------- /examples/with-react-router-3/src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Example = () => { 4 | return
Example
; 5 | }; 6 | 7 | Example.propTypes = {}; 8 | 9 | export default Example; 10 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, :global(#root) { 3 | height: 100%; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dva Demo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/index.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import './index.css'; 3 | 4 | // 1. Initialize 5 | const app = dva(); 6 | 7 | // 2. Plugins 8 | // app.use({}); 9 | 10 | // 3. Model 11 | app.model(require('./models/example')); 12 | 13 | // 4. Router 14 | app.router(require('./router')); 15 | 16 | // 5. Start 17 | app.start('#root'); 18 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/models/example.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'example', 3 | 4 | state: {}, 5 | 6 | subscriptions: { 7 | setup({ dispatch, history }) { 8 | // eslint-disable-line 9 | }, 10 | }, 11 | 12 | effects: { 13 | *fetch({ payload }, { call, put }) { 14 | // eslint-disable-line 15 | yield put({ type: 'save' }); 16 | }, 17 | }, 18 | 19 | reducers: { 20 | save(state, action) { 21 | return { ...state, ...action.payload }; 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route } from 'dva/router'; 3 | import IndexPage from './routes/IndexPage'; 4 | 5 | function RouterConfig({ history }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default RouterConfig; 14 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/routes/IndexPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import styles from './IndexPage.css'; 4 | 5 | function IndexPage() { 6 | return ( 7 |
8 |

Yay! Welcome to dva!

9 |
10 | 20 |
21 | ); 22 | } 23 | 24 | IndexPage.propTypes = {}; 25 | 26 | export default connect()(IndexPage); 27 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/services/example.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export async function query() { 4 | return request('/api/users'); 5 | } 6 | -------------------------------------------------------------------------------- /examples/with-react-router-3/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'dva/fetch'; 2 | 3 | function parseJSON(response) { 4 | return response.json(); 5 | } 6 | 7 | function checkStatus(response) { 8 | if (response.status >= 200 && response.status < 300) { 9 | return response; 10 | } 11 | 12 | const error = new Error(response.statusText); 13 | error.response = response; 14 | throw error; 15 | } 16 | 17 | /** 18 | * Requests a URL, returning a promise. 19 | * 20 | * @param {string} url The URL we want to request 21 | * @param {object} [options] The options we want to pass to "fetch" 22 | * @return {object} An object containing either "data" or "err" 23 | */ 24 | export default function request(url, options) { 25 | return fetch(url, options) 26 | .then(checkStatus) 27 | .then(parseJSON) 28 | .then(data => ({ data })) 29 | .catch(err => ({ err })); 30 | } 31 | -------------------------------------------------------------------------------- /examples/with-redux-undo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-redux-undo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # development 25 | .cache 26 | *.map -------------------------------------------------------------------------------- /examples/with-redux-undo/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-redux-undo/README.md: -------------------------------------------------------------------------------- 1 | # Redux Undo Sample 2 | 3 | ``` 4 | npm install 5 | npm start 6 | ``` 7 | 8 | or 9 | 10 | ``` 11 | yarn 12 | yarn start 13 | ``` 14 | 15 | Open http://localhost:1234 to view Counter App 16 | --- 17 | 18 | Support 19 | https://github.com/omnidan/redux-undo 20 | -------------------------------------------------------------------------------- /examples/with-redux-undo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-example-redux-redu", 3 | "private": true, 4 | "scripts": { 5 | "start": "parcel ./src/index.html " 6 | }, 7 | "author": "sjy ", 8 | "devDependencies": { 9 | "babel-preset-env": "^1.6.1", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 12 | "parcel-bundler": "^1.4.1" 13 | }, 14 | "dependencies": { 15 | "dva": "^2.2.3", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0", 18 | "redux-undo": "^0.6.1" 19 | }, 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-redux-undo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | dva-example-redux-redu 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/with-redux-undo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dva, { connect } from 'dva'; 3 | import undoable, { ActionCreators } from 'redux-undo'; 4 | 5 | import counter from './models/counter'; 6 | 7 | // 1. Initialize 8 | const app = dva({ 9 | onReducer: reducer => (state, action) => { 10 | const newState = undoable(reducer, {})(state, action); 11 | return { ...newState }; 12 | }, 13 | }); 14 | 15 | // 2. Model 16 | app.model(counter); 17 | 18 | // 3. View 19 | const App = connect(({ present: { counter } }) => ({ 20 | counter, 21 | }))(props => { 22 | return ( 23 |
24 |

Count: {props.counter}

25 | 32 | 39 | 47 | 54 |
55 | ); 56 | }); 57 | 58 | // 4. Router 59 | app.router(() => ); 60 | 61 | // 5. Start 62 | app.start('#root'); 63 | -------------------------------------------------------------------------------- /examples/with-redux-undo/src/models/counter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'counter', 3 | state: 0, 4 | reducers: { 5 | add(state) { 6 | return state + 1; 7 | }, 8 | minus(state) { 9 | return state - 1; 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['packages/**/src/*.{ts,tsx,js,jsx}'], 3 | }; 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelog": { 3 | "repo": "dvajs/dva", 4 | "labels": { 5 | "pr(enhancement)": ":rocket: Enhancement", 6 | "pr(bug)": ":bug: Bug Fix", 7 | "pr(documentation)": ":book: Documentation", 8 | "pr(dependency)": ":deciduous_tree: Dependency", 9 | "pr(chore)": ":turtle: Chore" 10 | }, 11 | "cacheDir": ".changelog" 12 | }, 13 | "packages": [ 14 | "packages/*" 15 | ], 16 | "command": { 17 | "version": { 18 | "exact": true 19 | } 20 | }, 21 | "npmClient": "yarn", 22 | "version": "independent" 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "father-build", 5 | "doc:dev": "./website/node_modules/.bin/vuepress dev ./docs", 6 | "doc:deploy": "rm -rf ./website/yarn.lock && cd ./website && npm run deploy && cd -", 7 | "changelog": "lerna-changelog", 8 | "test": "npm run debug -- --coverage", 9 | "debug": "umi-test", 10 | "coveralls": "cat ./coverage/lcov.info | coveralls", 11 | "lint": "eslint --ext .js packages", 12 | "precommit": "lint-staged", 13 | "release": "./scripts/publish.js", 14 | "bootstrap": "lerna bootstrap" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^24.0.22", 18 | "babel-eslint": "^9.0.0", 19 | "chalk": "^2.3.2", 20 | "coveralls": "^3.0.0", 21 | "eslint": "^5.6.0", 22 | "eslint-config-airbnb": "^17.1.0", 23 | "eslint-config-prettier": "^4.3.0", 24 | "eslint-plugin-import": "^2.14.0", 25 | "eslint-plugin-jsx-a11y": "^6.0.2", 26 | "eslint-plugin-prettier": "^3.1.0", 27 | "eslint-plugin-react": "^7.11.1", 28 | "father-build": "^1.14.0", 29 | "husky": "^0.14.3", 30 | "lerna": "^3.4.0", 31 | "lerna-changelog": "^0.8.0", 32 | "lint-staged": "^7.2.2", 33 | "prettier": "^1.14.3", 34 | "react": "^18.0.0", 35 | "react-dom": "^18.0.0", 36 | "react-testing-library": "^6.0.0", 37 | "shelljs": "^0.8.1", 38 | "umi-test": "^1.5.2" 39 | }, 40 | "lint-staged": { 41 | "*.js": [ 42 | "prettier --trailing-comma all --single-quote --write", 43 | "git add" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/dva-core/.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cjs: 'rollup', 3 | esm: 'rollup', 4 | runtimeHelpers: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/dva-core/README.md: -------------------------------------------------------------------------------- 1 | # dva-core 2 | 3 | [![NPM version](https://img.shields.io/npm/v/dva-core.svg?style=flat)](https://npmjs.org/package/dva-core) 4 | [![Build Status](https://img.shields.io/travis/dvajs/dva-core.svg?style=flat)](https://travis-ci.org/dvajs/dva-core) 5 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-core.svg?style=flat)](https://coveralls.io/r/dvajs/dva-core) 6 | [![NPM downloads](http://img.shields.io/npm/dm/dva-core.svg?style=flat)](https://npmjs.org/package/dva-core) 7 | [![Dependencies](https://david-dm.org/dvajs/dva-core/status.svg)](https://david-dm.org/dvajs/dva-core) 8 | 9 | The core lightweight library for dva, based on redux and redux-saga. 10 | 11 | ## LICENSE 12 | 13 | MIT 14 | 15 | -------------------------------------------------------------------------------- /packages/dva-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-core", 3 | "version": "2.0.4", 4 | "description": "The core lightweight library for dva, based on redux and redux-saga.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/dvajs/dva" 11 | }, 12 | "homepage": "https://github.com/dvajs/dva", 13 | "keywords": [ 14 | "dva", 15 | "alibaba", 16 | "redux", 17 | "redux-saga", 18 | "elm", 19 | "framework", 20 | "frontend" 21 | ], 22 | "authors": [ 23 | "chencheng (https://github.com/sorrycc)" 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/dvajs/dva/issues" 28 | }, 29 | "dependencies": { 30 | "@babel/runtime": "^7.0.0", 31 | "flatten": "^1.0.2", 32 | "global": "^4.3.2", 33 | "invariant": "^2.2.1", 34 | "is-plain-object": "^2.0.3", 35 | "redux-saga": "^0.16.0", 36 | "warning": "^3.0.0" 37 | }, 38 | "peerDependencies": { 39 | "redux": "4.x" 40 | }, 41 | "devDependencies": { 42 | "mm": "^2.5.0", 43 | "redux": "^4.0.1" 44 | }, 45 | "files": [ 46 | "dist", 47 | "lib", 48 | "src", 49 | "index.js", 50 | "saga.js" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/dva-core/saga.js: -------------------------------------------------------------------------------- 1 | module.exports = require('redux-saga'); 2 | -------------------------------------------------------------------------------- /packages/dva-core/src/Plugin.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isPlainObject } from './utils'; 3 | 4 | const hooks = [ 5 | 'onError', 6 | 'onStateChange', 7 | 'onAction', 8 | 'onHmr', 9 | 'onReducer', 10 | 'onEffect', 11 | 'extraReducers', 12 | 'extraEnhancers', 13 | '_handleActions', 14 | ]; 15 | 16 | export function filterHooks(obj) { 17 | return Object.keys(obj).reduce((memo, key) => { 18 | if (hooks.indexOf(key) > -1) { 19 | memo[key] = obj[key]; 20 | } 21 | return memo; 22 | }, {}); 23 | } 24 | 25 | export default class Plugin { 26 | constructor() { 27 | this._handleActions = null; 28 | this.hooks = hooks.reduce((memo, key) => { 29 | memo[key] = []; 30 | return memo; 31 | }, {}); 32 | } 33 | 34 | use(plugin) { 35 | invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object'); 36 | const { hooks } = this; 37 | for (const key in plugin) { 38 | if (Object.prototype.hasOwnProperty.call(plugin, key)) { 39 | invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`); 40 | if (key === '_handleActions') { 41 | this._handleActions = plugin[key]; 42 | } else if (key === 'extraEnhancers') { 43 | hooks[key] = plugin[key]; 44 | } else { 45 | hooks[key].push(plugin[key]); 46 | } 47 | } 48 | } 49 | } 50 | 51 | apply(key, defaultHandler) { 52 | const { hooks } = this; 53 | const validApplyHooks = ['onError', 'onHmr']; 54 | invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`); 55 | const fns = hooks[key]; 56 | 57 | return (...args) => { 58 | if (fns.length) { 59 | for (const fn of fns) { 60 | fn(...args); 61 | } 62 | } else if (defaultHandler) { 63 | defaultHandler(...args); 64 | } 65 | }; 66 | } 67 | 68 | get(key) { 69 | const { hooks } = this; 70 | invariant(key in hooks, `plugin.get: hook ${key} cannot be got`); 71 | if (key === 'extraReducers') { 72 | return getExtraReducers(hooks[key]); 73 | } else if (key === 'onReducer') { 74 | return getOnReducer(hooks[key]); 75 | } else { 76 | return hooks[key]; 77 | } 78 | } 79 | } 80 | 81 | function getExtraReducers(hook) { 82 | let ret = {}; 83 | for (const reducerObj of hook) { 84 | ret = { ...ret, ...reducerObj }; 85 | } 86 | return ret; 87 | } 88 | 89 | function getOnReducer(hook) { 90 | return function(reducer) { 91 | for (const reducerEnhancer of hook) { 92 | reducer = reducerEnhancer(reducer); 93 | } 94 | return reducer; 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /packages/dva-core/src/checkModel.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isArray, isFunction, isPlainObject } from './utils'; 3 | 4 | export default function checkModel(model, existModels) { 5 | const { namespace, reducers, effects, subscriptions } = model; 6 | 7 | // namespace 必须被定义 8 | invariant(namespace, `[app.model] namespace should be defined`); 9 | // 并且是字符串 10 | invariant( 11 | typeof namespace === 'string', 12 | `[app.model] namespace should be string, but got ${typeof namespace}`, 13 | ); 14 | // 并且唯一 15 | invariant( 16 | !existModels.some(model => model.namespace === namespace), 17 | `[app.model] namespace should be unique`, 18 | ); 19 | 20 | // state 可以为任意值 21 | 22 | // reducers 可以为空,PlainObject 或者数组 23 | if (reducers) { 24 | invariant( 25 | isPlainObject(reducers) || isArray(reducers), 26 | `[app.model] reducers should be plain object or array, but got ${typeof reducers}`, 27 | ); 28 | // 数组的 reducers 必须是 [Object, Function] 的格式 29 | invariant( 30 | !isArray(reducers) || (isPlainObject(reducers[0]) && isFunction(reducers[1])), 31 | `[app.model] reducers with array should be [Object, Function]`, 32 | ); 33 | } 34 | 35 | // effects 可以为空,PlainObject 36 | if (effects) { 37 | invariant( 38 | isPlainObject(effects), 39 | `[app.model] effects should be plain object, but got ${typeof effects}`, 40 | ); 41 | } 42 | 43 | if (subscriptions) { 44 | // subscriptions 可以为空,PlainObject 45 | invariant( 46 | isPlainObject(subscriptions), 47 | `[app.model] subscriptions should be plain object, but got ${typeof subscriptions}`, 48 | ); 49 | 50 | // subscription 必须为函数 51 | invariant(isAllFunction(subscriptions), `[app.model] subscription should be function`); 52 | } 53 | } 54 | 55 | function isAllFunction(obj) { 56 | return Object.keys(obj).every(key => isFunction(obj[key])); 57 | } 58 | -------------------------------------------------------------------------------- /packages/dva-core/src/constants.js: -------------------------------------------------------------------------------- 1 | export const NAMESPACE_SEP = '/'; 2 | -------------------------------------------------------------------------------- /packages/dva-core/src/createPromiseMiddleware.js: -------------------------------------------------------------------------------- 1 | import { NAMESPACE_SEP } from './constants'; 2 | 3 | export default function createPromiseMiddleware(app) { 4 | return () => next => action => { 5 | const { type } = action; 6 | if (isEffect(type)) { 7 | return new Promise((resolve, reject) => { 8 | next({ 9 | __dva_resolve: resolve, 10 | __dva_reject: reject, 11 | ...action, 12 | }); 13 | }); 14 | } else { 15 | return next(action); 16 | } 17 | }; 18 | 19 | function isEffect(type) { 20 | if (!type || typeof type !== 'string') return false; 21 | const [namespace] = type.split(NAMESPACE_SEP); 22 | const model = app._models.filter(m => m.namespace === namespace)[0]; 23 | if (model) { 24 | if (model.effects && model.effects[type]) { 25 | return true; 26 | } 27 | } 28 | 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/dva-core/src/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import flatten from 'flatten'; 3 | import invariant from 'invariant'; 4 | import win from 'global/window'; 5 | import { returnSelf, isArray } from './utils'; 6 | 7 | export default function({ 8 | reducers, 9 | initialState, 10 | plugin, 11 | sagaMiddleware, 12 | promiseMiddleware, 13 | createOpts: { setupMiddlewares = returnSelf }, 14 | }) { 15 | // extra enhancers 16 | const extraEnhancers = plugin.get('extraEnhancers'); 17 | invariant( 18 | isArray(extraEnhancers), 19 | `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`, 20 | ); 21 | 22 | const extraMiddlewares = plugin.get('onAction'); 23 | const middlewares = setupMiddlewares([ 24 | promiseMiddleware, 25 | sagaMiddleware, 26 | ...flatten(extraMiddlewares), 27 | ]); 28 | 29 | const composeEnhancers = 30 | process.env.NODE_ENV !== 'production' && win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 31 | ? win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, maxAge: 30 }) 32 | : compose; 33 | 34 | const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers]; 35 | 36 | return createStore(reducers, initialState, composeEnhancers(...enhancers)); 37 | } 38 | -------------------------------------------------------------------------------- /packages/dva-core/src/getReducer.js: -------------------------------------------------------------------------------- 1 | import defaultHandleActions from './handleActions'; 2 | 3 | export default function getReducer(reducers, state, handleActions) { 4 | // Support reducer enhancer 5 | // e.g. reducers: [realReducers, enhancer] 6 | if (Array.isArray(reducers)) { 7 | return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state)); 8 | } else { 9 | return (handleActions || defaultHandleActions)(reducers || {}, state); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/dva-core/src/getSaga.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import warning from 'warning'; 3 | import { effects as sagaEffects } from 'redux-saga'; 4 | import { NAMESPACE_SEP } from './constants'; 5 | import prefixType from './prefixType'; 6 | 7 | export default function getSaga(effects, model, onError, onEffect, opts = {}) { 8 | return function*() { 9 | for (const key in effects) { 10 | if (Object.prototype.hasOwnProperty.call(effects, key)) { 11 | const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts); 12 | const task = yield sagaEffects.fork(watcher); 13 | yield sagaEffects.fork(function*() { 14 | yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`); 15 | yield sagaEffects.cancel(task); 16 | }); 17 | } 18 | } 19 | }; 20 | } 21 | 22 | function getWatcher(key, _effect, model, onError, onEffect, opts) { 23 | let effect = _effect; 24 | let type = 'takeEvery'; 25 | let ms; 26 | let delayMs; 27 | 28 | if (Array.isArray(_effect)) { 29 | [effect] = _effect; 30 | const opts = _effect[1]; 31 | if (opts && opts.type) { 32 | ({ type } = opts); 33 | if (type === 'throttle') { 34 | invariant(opts.ms, 'app.start: opts.ms should be defined if type is throttle'); 35 | ({ ms } = opts); 36 | } 37 | if (type === 'poll') { 38 | invariant(opts.delay, 'app.start: opts.delay should be defined if type is poll'); 39 | ({ delay: delayMs } = opts); 40 | } 41 | } 42 | invariant( 43 | ['watcher', 'takeEvery', 'takeLatest', 'throttle', 'poll'].indexOf(type) > -1, 44 | 'app.start: effect type should be takeEvery, takeLatest, throttle, poll or watcher', 45 | ); 46 | } 47 | 48 | function noop() {} 49 | 50 | function* sagaWithCatch(...args) { 51 | const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } = 52 | args.length > 0 ? args[0] : {}; 53 | try { 54 | yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` }); 55 | const ret = yield effect(...args.concat(createEffects(model, opts))); 56 | yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` }); 57 | resolve(ret); 58 | } catch (e) { 59 | onError(e, { 60 | key, 61 | effectArgs: args, 62 | }); 63 | if (!e._dontReject) { 64 | reject(e); 65 | } 66 | } 67 | } 68 | 69 | const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key); 70 | 71 | switch (type) { 72 | case 'watcher': 73 | return sagaWithCatch; 74 | case 'takeLatest': 75 | return function*() { 76 | yield sagaEffects.takeLatest(key, sagaWithOnEffect); 77 | }; 78 | case 'throttle': 79 | return function*() { 80 | yield sagaEffects.throttle(ms, key, sagaWithOnEffect); 81 | }; 82 | case 'poll': 83 | return function*() { 84 | function delay(timeout) { 85 | return new Promise(resolve => setTimeout(resolve, timeout)); 86 | } 87 | function* pollSagaWorker(sagaEffects, action) { 88 | const { call } = sagaEffects; 89 | while (true) { 90 | yield call(sagaWithOnEffect, action); 91 | yield call(delay, delayMs); 92 | } 93 | } 94 | const { call, take, race } = sagaEffects; 95 | while (true) { 96 | const action = yield take(`${key}-start`); 97 | yield race([call(pollSagaWorker, sagaEffects, action), take(`${key}-stop`)]); 98 | } 99 | }; 100 | default: 101 | return function*() { 102 | yield sagaEffects.takeEvery(key, sagaWithOnEffect); 103 | }; 104 | } 105 | } 106 | 107 | function createEffects(model, opts) { 108 | function assertAction(type, name) { 109 | invariant(type, 'dispatch: action should be a plain Object with type'); 110 | 111 | const { namespacePrefixWarning = true } = opts; 112 | 113 | if (namespacePrefixWarning) { 114 | warning( 115 | type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0, 116 | `[${name}] ${type} should not be prefixed with namespace ${model.namespace}`, 117 | ); 118 | } 119 | } 120 | function put(action) { 121 | const { type } = action; 122 | assertAction(type, 'sagaEffects.put'); 123 | return sagaEffects.put({ ...action, type: prefixType(type, model) }); 124 | } 125 | 126 | // The operator `put` doesn't block waiting the returned promise to resolve. 127 | // Using `put.resolve` will wait until the promsie resolve/reject before resuming. 128 | // It will be helpful to organize multi-effects in order, 129 | // and increase the reusability by seperate the effect in stand-alone pieces. 130 | // https://github.com/redux-saga/redux-saga/issues/336 131 | function putResolve(action) { 132 | const { type } = action; 133 | assertAction(type, 'sagaEffects.put.resolve'); 134 | return sagaEffects.put.resolve({ 135 | ...action, 136 | type: prefixType(type, model), 137 | }); 138 | } 139 | put.resolve = putResolve; 140 | 141 | function take(type) { 142 | if (typeof type === 'string') { 143 | assertAction(type, 'sagaEffects.take'); 144 | return sagaEffects.take(prefixType(type, model)); 145 | } else if (Array.isArray(type)) { 146 | return sagaEffects.take( 147 | type.map(t => { 148 | if (typeof t === 'string') { 149 | assertAction(t, 'sagaEffects.take'); 150 | return prefixType(t, model); 151 | } 152 | return t; 153 | }), 154 | ); 155 | } else { 156 | return sagaEffects.take(type); 157 | } 158 | } 159 | return { ...sagaEffects, put, take }; 160 | } 161 | 162 | function applyOnEffect(fns, effect, model, key) { 163 | for (const fn of fns) { 164 | effect = fn(effect, sagaEffects, model, key); 165 | } 166 | return effect; 167 | } 168 | -------------------------------------------------------------------------------- /packages/dva-core/src/handleActions.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | 3 | function identify(value) { 4 | return value; 5 | } 6 | 7 | function handleAction(actionType, reducer = identify) { 8 | return (state, action) => { 9 | const { type } = action; 10 | invariant(type, 'dispatch: action should be a plain Object with type'); 11 | if (actionType === type) { 12 | return reducer(state, action); 13 | } 14 | return state; 15 | }; 16 | } 17 | 18 | function reduceReducers(...reducers) { 19 | return (previous, current) => reducers.reduce((p, r) => r(p, current), previous); 20 | } 21 | 22 | function handleActions(handlers, defaultState) { 23 | const reducers = Object.keys(handlers).map(type => handleAction(type, handlers[type])); 24 | const reducer = reduceReducers(...reducers); 25 | return (state = defaultState, action) => reducer(state, action); 26 | } 27 | 28 | export default handleActions; 29 | -------------------------------------------------------------------------------- /packages/dva-core/src/prefixNamespace.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning'; 2 | import { isArray } from './utils'; 3 | import { NAMESPACE_SEP } from './constants'; 4 | 5 | function prefix(obj, namespace, type) { 6 | return Object.keys(obj).reduce((memo, key) => { 7 | warning( 8 | key.indexOf(`${namespace}${NAMESPACE_SEP}`) !== 0, 9 | `[prefixNamespace]: ${type} ${key} should not be prefixed with namespace ${namespace}`, 10 | ); 11 | const newKey = `${namespace}${NAMESPACE_SEP}${key}`; 12 | memo[newKey] = obj[key]; 13 | return memo; 14 | }, {}); 15 | } 16 | 17 | export default function prefixNamespace(model) { 18 | const { namespace, reducers, effects } = model; 19 | 20 | if (reducers) { 21 | if (isArray(reducers)) { 22 | // 需要复制一份,不能直接修改 model.reducers[0], 会导致微前端场景下,重复添加前缀 23 | const [reducer, ...rest] = reducers; 24 | model.reducers = [prefix(reducer, namespace, 'reducer'), ...rest]; 25 | } else { 26 | model.reducers = prefix(reducers, namespace, 'reducer'); 27 | } 28 | } 29 | if (effects) { 30 | model.effects = prefix(effects, namespace, 'effect'); 31 | } 32 | return model; 33 | } 34 | -------------------------------------------------------------------------------- /packages/dva-core/src/prefixType.js: -------------------------------------------------------------------------------- 1 | import { NAMESPACE_SEP } from './constants'; 2 | 3 | export default function prefixType(type, model) { 4 | const prefixedType = `${model.namespace}${NAMESPACE_SEP}${type}`; 5 | const typeWithoutAffix = prefixedType.replace(/\/@@[^/]+?$/, ''); 6 | 7 | const reducer = Array.isArray(model.reducers) 8 | ? model.reducers[0][typeWithoutAffix] 9 | : model.reducers && model.reducers[typeWithoutAffix]; 10 | if (reducer || (model.effects && model.effects[typeWithoutAffix])) { 11 | return prefixedType; 12 | } 13 | return type; 14 | } 15 | -------------------------------------------------------------------------------- /packages/dva-core/src/prefixedDispatch.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import warning from 'warning'; 3 | import { NAMESPACE_SEP } from './constants'; 4 | import prefixType from './prefixType'; 5 | 6 | export default function prefixedDispatch(dispatch, model) { 7 | return action => { 8 | const { type } = action; 9 | invariant(type, 'dispatch: action should be a plain Object with type'); 10 | warning( 11 | type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0, 12 | `dispatch: ${type} should not be prefixed with namespace ${model.namespace}`, 13 | ); 14 | return dispatch({ ...action, type: prefixType(type, model) }); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/dva-core/src/subscription.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning'; 2 | import { isFunction } from './utils'; 3 | import prefixedDispatch from './prefixedDispatch'; 4 | 5 | export function run(subs, model, app, onError) { 6 | const funcs = []; 7 | const nonFuncs = []; 8 | for (const key in subs) { 9 | if (Object.prototype.hasOwnProperty.call(subs, key)) { 10 | const sub = subs[key]; 11 | const unlistener = sub( 12 | { 13 | dispatch: prefixedDispatch(app._store.dispatch, model), 14 | history: app._history, 15 | }, 16 | onError, 17 | ); 18 | if (isFunction(unlistener)) { 19 | funcs.push(unlistener); 20 | } else { 21 | nonFuncs.push(key); 22 | } 23 | } 24 | } 25 | return { funcs, nonFuncs }; 26 | } 27 | 28 | export function unlisten(unlisteners, namespace) { 29 | if (!unlisteners[namespace]) return; 30 | 31 | const { funcs, nonFuncs } = unlisteners[namespace]; 32 | warning( 33 | nonFuncs.length === 0, 34 | `[app.unmodel] subscription should return unlistener function, check these subscriptions ${nonFuncs.join( 35 | ', ', 36 | )}`, 37 | ); 38 | for (const unlistener of funcs) { 39 | unlistener(); 40 | } 41 | delete unlisteners[namespace]; 42 | } 43 | -------------------------------------------------------------------------------- /packages/dva-core/src/utils.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'is-plain-object'; 2 | export { isPlainObject }; 3 | export const isArray = Array.isArray.bind(Array); 4 | export const isFunction = o => typeof o === 'function'; 5 | export const returnSelf = m => m; 6 | export const noop = () => {}; 7 | export const findIndex = (array, predicate) => { 8 | for (let i = 0, { length } = array; i < length; i += 1) { 9 | if (predicate(array[i], i)) return i; 10 | } 11 | 12 | return -1; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/dva-core/test/checkModel.test.js: -------------------------------------------------------------------------------- 1 | import { create } from '../src/index'; 2 | 3 | describe('checkModel', () => { 4 | it('namespace should be defined', () => { 5 | const app = create(); 6 | expect(() => { 7 | app.model({}); 8 | }).toThrow(/\[app\.model\] namespace should be defined/); 9 | }); 10 | 11 | it('namespace should be unique', () => { 12 | const app = create(); 13 | expect(() => { 14 | app.model({ 15 | namespace: 'repeat', 16 | }); 17 | app.model({ 18 | namespace: 'repeat', 19 | }); 20 | }).toThrow(/\[app\.model\] namespace should be unique/); 21 | }); 22 | 23 | it('reducers can be specified array', () => { 24 | const app = create(); 25 | expect(() => { 26 | app.model({ 27 | namespace: '_array', 28 | reducers: [{}, () => {}], 29 | }); 30 | }).not.toThrow(); 31 | }); 32 | 33 | it('reducers can be object', () => { 34 | const app = create(); 35 | expect(() => { 36 | app.model({ 37 | namespace: '_object', 38 | reducers: {}, 39 | }); 40 | }).not.toThrow(); 41 | }); 42 | 43 | it('reducers can not be string', () => { 44 | const app = create(); 45 | expect(() => { 46 | app.model({ 47 | namespace: '_neither', 48 | reducers: '_', 49 | }); 50 | }).toThrow(/\[app\.model\] reducers should be plain object or array/); 51 | }); 52 | 53 | it('reducers in array should be [Object, Function]', () => { 54 | const app = create(); 55 | expect(() => { 56 | app.model({ 57 | namespace: '_none', 58 | reducers: [], 59 | }); 60 | }).toThrow(/\[app\.model\] reducers with array should be \[Object, Function\]/); 61 | }); 62 | 63 | it('subscriptions should be plain object', () => { 64 | const app = create(); 65 | expect(() => { 66 | app.model({ 67 | namespace: '_', 68 | subscriptions: [], 69 | }); 70 | }).toThrow(/\[app\.model\] subscriptions should be plain object/); 71 | expect(() => { 72 | app.model({ 73 | namespace: '_', 74 | subscriptions: '_', 75 | }); 76 | }).toThrow(/\[app\.model\] subscriptions should be plain object/); 77 | }); 78 | 79 | it('subscriptions can be undefined', () => { 80 | const app = create(); 81 | expect(() => { 82 | app.model({ 83 | namespace: '_', 84 | }); 85 | }).not.toThrow(); 86 | }); 87 | 88 | it('effects should be plain object', () => { 89 | const app = create(); 90 | expect(() => { 91 | app.model({ 92 | namespace: '_', 93 | effects: [], 94 | }); 95 | }).toThrow(/\[app\.model\] effects should be plain object/); 96 | expect(() => { 97 | app.model({ 98 | namespace: '_', 99 | effects: '_', 100 | }); 101 | }).toThrow(/\[app\.model\] effects should be plain object/); 102 | expect(() => { 103 | app.model({ 104 | namespace: '_', 105 | effects: {}, 106 | }); 107 | }).not.toThrow(); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/dva-core/test/handleActions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import handleActions from '../src/handleActions'; 3 | 4 | describe('handleActions', () => { 5 | const LOGIN_START = 'user/login/start'; 6 | 7 | const LOGIN_END = 'user/login/end'; 8 | 9 | const LOGIN_SAVE = 'user/login/save'; 10 | 11 | const initialState = { 12 | isLoading: false, 13 | }; 14 | 15 | const reducers = handleActions( 16 | { 17 | [LOGIN_START](state) { 18 | return { 19 | ...state, 20 | isLoading: true, 21 | }; 22 | }, 23 | 24 | [LOGIN_END](state) { 25 | return { 26 | ...state, 27 | isLoading: false, 28 | }; 29 | }, 30 | 31 | [LOGIN_SAVE]: undefined, 32 | }, 33 | initialState, 34 | ); 35 | 36 | it('LOGIN_START', () => { 37 | expect(reducers(initialState, { type: LOGIN_START })).toEqual({ 38 | isLoading: true, 39 | }); 40 | }); 41 | 42 | it('LOGIN_END', () => { 43 | expect(reducers(initialState, { type: LOGIN_END })).toEqual({ 44 | isLoading: false, 45 | }); 46 | }); 47 | 48 | it('uses the identity if the specified reducer is undefined', () => { 49 | expect(reducers(initialState, { type: LOGIN_SAVE })).toBe(initialState); 50 | }); 51 | 52 | it('dispatch not valid action', () => { 53 | expect(() => { 54 | reducers(initialState, { type: '' }); 55 | }).toThrow(/dispatch: action should be a plain Object with type/); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/dva-core/test/model.test.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import { create } from '../src/index'; 3 | 4 | describe('app.model', () => { 5 | it('dynamic model', () => { 6 | let count = 0; 7 | 8 | const app = create(); 9 | app.model({ 10 | namespace: 'users', 11 | state: [], 12 | reducers: { 13 | add(state, { payload }) { 14 | return [...state, payload]; 15 | }, 16 | }, 17 | }); 18 | app.start(); 19 | 20 | // inject model 21 | app.model({ 22 | namespace: 'tasks', 23 | state: [], 24 | reducers: { 25 | add(state, { payload }) { 26 | return [...state, payload]; 27 | }, 28 | }, 29 | effects: {}, 30 | subscriptions: { 31 | setup() { 32 | count += 1; 33 | }, 34 | }, 35 | }); 36 | 37 | // subscriptions 38 | expect(count).toEqual(1); 39 | 40 | // reducers 41 | app._store.dispatch({ type: 'tasks/add', payload: 'foo' }); 42 | app._store.dispatch({ type: 'users/add', payload: 'foo' }); 43 | const state = app._store.getState(); 44 | expect(state.users).toEqual(['foo']); 45 | expect(state.tasks).toEqual(['foo']); 46 | }); 47 | 48 | it("don't inject if exists", () => { 49 | const app = create(); 50 | 51 | const model = { 52 | namespace: 'count', 53 | state: 0, 54 | subscriptions: { 55 | setup() {}, 56 | }, 57 | }; 58 | 59 | app.model(model); 60 | app.start(); 61 | expect(() => { 62 | app.model(model); 63 | }).toThrow(/\[app\.model\] namespace should be unique/); 64 | }); 65 | 66 | it('unmodel', () => { 67 | const emitter = new EventEmitter(); 68 | let emitterCount = 0; 69 | 70 | const app = create(); 71 | app.model({ 72 | namespace: 'a', 73 | state: 0, 74 | reducers: { 75 | add(state) { 76 | return state + 1; 77 | }, 78 | }, 79 | }); 80 | app.model({ 81 | namespace: 'b', 82 | state: 0, 83 | reducers: { 84 | add(state) { 85 | return state + 1; 86 | }, 87 | }, 88 | effects: { 89 | *addBoth(action, { put }) { 90 | yield put({ type: 'a/add' }); 91 | yield put({ type: 'add' }); 92 | }, 93 | }, 94 | subscriptions: { 95 | setup() { 96 | emitter.on('event', () => { 97 | emitterCount += 1; 98 | }); 99 | return () => { 100 | emitter.removeAllListeners(); 101 | }; 102 | }, 103 | }, 104 | }); 105 | app.start(); 106 | 107 | emitter.emit('event'); 108 | app.unmodel('b'); 109 | emitter.emit('event'); 110 | 111 | app._store.dispatch({ type: 'b/addBoth' }); 112 | 113 | const { a, b } = app._store.getState(); 114 | expect(emitterCount).toEqual(1); 115 | expect({ a, b }).toEqual({ a: 0, b: undefined }); 116 | }); 117 | 118 | it("don't run saga when effects is not provided", () => { 119 | let count = 0; 120 | 121 | const app = create(); 122 | app.model({ 123 | namespace: 'users', 124 | state: [], 125 | reducers: { 126 | add(state, { payload }) { 127 | return [...state, payload]; 128 | }, 129 | }, 130 | }); 131 | app.start(); 132 | 133 | // inject model 134 | app.model({ 135 | namespace: 'tasks', 136 | state: [], 137 | reducers: { 138 | add(state, { payload }) { 139 | return [...state, payload]; 140 | }, 141 | }, 142 | effects: null, 143 | subscriptions: { 144 | setup() { 145 | count += 1; 146 | }, 147 | }, 148 | }); 149 | 150 | // subscriptions 151 | expect(count).toEqual(1); 152 | 153 | // reducers 154 | app._store.dispatch({ type: 'tasks/add', payload: 'foo' }); 155 | app._store.dispatch({ type: 'users/add', payload: 'foo' }); 156 | const state = app._store.getState(); 157 | expect(state.users).toEqual(['foo']); 158 | expect(state.tasks).toEqual(['foo']); 159 | 160 | // effects is not taken 161 | expect(count).toEqual(1); 162 | }); 163 | 164 | it('unmodel with asyncReducers', () => { 165 | const app = create(); 166 | app.model({ 167 | namespace: 'a', 168 | state: 0, 169 | reducers: { 170 | add(state) { 171 | return state + 1; 172 | }, 173 | }, 174 | }); 175 | app.start(); 176 | 177 | app.model({ 178 | namespace: 'b', 179 | state: 0, 180 | reducers: { 181 | add(state) { 182 | return state + 1; 183 | }, 184 | }, 185 | effects: { 186 | *addBoth(action, { put }) { 187 | yield put({ type: 'a/add' }); 188 | yield put({ type: 'add' }); 189 | }, 190 | }, 191 | }); 192 | 193 | app._store.dispatch({ type: 'b/addBoth' }); 194 | app.unmodel('b'); 195 | app._store.dispatch({ type: 'b/addBoth' }); 196 | const { a, b } = app._store.getState(); 197 | expect({ a, b }).toEqual({ a: 1, b: undefined }); 198 | }); 199 | 200 | it("unmodel, warn user if subscription don't return function", () => { 201 | const app = create(); 202 | app.model({ 203 | namespace: 'a', 204 | state: 0, 205 | subscriptions: { 206 | a() {}, 207 | }, 208 | }); 209 | app.start(); 210 | app.unmodel('a'); 211 | }); 212 | 213 | it('unmodel with other type of effects', () => { 214 | const app = create(); 215 | let countA = 0; 216 | let countB = 0; 217 | let countC = 0; 218 | let countD = 0; 219 | 220 | app.model({ 221 | namespace: 'a', 222 | state: 0, 223 | effects: { 224 | a: [ 225 | function*() { 226 | yield (countA += 1); 227 | }, 228 | { type: 'throttle', ms: 100 }, 229 | ], 230 | b: [ 231 | function*() { 232 | yield (countB += 1); 233 | }, 234 | { type: 'takeEvery' }, 235 | ], 236 | c: [ 237 | function*() { 238 | yield (countC += 1); 239 | }, 240 | { type: 'takeLatest' }, 241 | ], 242 | d: [ 243 | function*({ take }) { 244 | while (true) { 245 | yield take('a/d'); 246 | countD += 1; 247 | } 248 | }, 249 | { type: 'watcher' }, 250 | ], 251 | }, 252 | }); 253 | 254 | app.start(); 255 | 256 | app._store.dispatch({ type: 'a/a' }); 257 | app._store.dispatch({ type: 'a/b' }); 258 | app._store.dispatch({ type: 'a/c' }); 259 | app._store.dispatch({ type: 'a/d' }); 260 | 261 | expect([countA, countB, countC, countD]).toEqual([1, 1, 1, 1]); 262 | 263 | app.unmodel('a'); 264 | 265 | app._store.dispatch({ type: 'a/b' }); 266 | app._store.dispatch({ type: 'a/c' }); 267 | app._store.dispatch({ type: 'a/d' }); 268 | 269 | expect([countA, countB, countC, countD]).toEqual([1, 1, 1, 1]); 270 | }); 271 | 272 | it('register the model without affecting itself', () => { 273 | const countModel = { 274 | namespace: 'count', 275 | state: 0, 276 | reducers: { 277 | add() {}, 278 | }, 279 | }; 280 | const app = create(); 281 | app.model(countModel); 282 | app.start(); 283 | expect(Object.keys(countModel.reducers)).toEqual(['add']); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /packages/dva-core/test/optsAndHooks.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | function delay(timeout) { 5 | return new Promise(resolve => setTimeout(resolve, timeout)); 6 | } 7 | 8 | describe('opts and hooks', () => { 9 | it('basic', done => { 10 | const app = create(); 11 | 12 | app.model({ 13 | namespace: 'loading', 14 | state: false, 15 | reducers: { 16 | show() { 17 | return true; 18 | }, 19 | hide() { 20 | return false; 21 | }, 22 | }, 23 | }); 24 | 25 | const nsAction = namespace => action => `${namespace}/${action}`; 26 | 27 | const ADD = 'add'; 28 | const ADD_DELAY = 'addDelay'; 29 | const countAction = nsAction('count'); 30 | const loadingAction = nsAction('loading'); 31 | 32 | app.model({ 33 | namespace: 'count', 34 | state: 0, 35 | subscriptions: { 36 | setup({ dispatch }) { 37 | dispatch({ type: ADD }); 38 | }, 39 | }, 40 | reducers: { 41 | [ADD](state, { payload }) { 42 | return state + payload || 1; 43 | }, 44 | }, 45 | effects: { 46 | *[ADD_DELAY]({ payload }, { call, put }) { 47 | yield put({ type: loadingAction('show') }); 48 | yield call(delay, 100); 49 | yield put({ type: ADD, payload }); 50 | yield put({ type: loadingAction('hide') }); 51 | }, 52 | }, 53 | }); 54 | app.start(); 55 | 56 | expect(app._store.getState().count).toEqual(1); 57 | expect(app._store.getState().loading).toEqual(false); 58 | app._store.dispatch({ type: countAction(ADD_DELAY), payload: 2 }); 59 | expect(app._store.getState().loading).toEqual(true); 60 | 61 | setTimeout(() => { 62 | expect(app._store.getState().count).toEqual(3); 63 | expect(app._store.getState().loading).toEqual(false); 64 | done(); 65 | }, 500); 66 | }); 67 | 68 | it('opts.onError prevent reject error', done => { 69 | let rejectCount = 0; 70 | const app = create({ 71 | onError(e) { 72 | e.preventDefault(); 73 | }, 74 | }); 75 | app.model({ 76 | namespace: 'count', 77 | state: 0, 78 | effects: { 79 | // eslint-disable-next-line require-yield 80 | *add() { 81 | throw new Error('add failed'); 82 | }, 83 | }, 84 | }); 85 | app.start(); 86 | app._store 87 | .dispatch({ 88 | type: 'count/add', 89 | }) 90 | .catch(() => { 91 | rejectCount += 1; 92 | }); 93 | 94 | setTimeout(() => { 95 | expect(rejectCount).toEqual(0); 96 | done(); 97 | }, 200); 98 | }); 99 | 100 | it('opts.initialState', () => { 101 | const app = create({ 102 | initialState: { count: 1 }, 103 | }); 104 | app.model({ 105 | namespace: 'count', 106 | state: 0, 107 | }); 108 | app.start(); 109 | expect(app._store.getState().count).toEqual(1); 110 | }); 111 | 112 | it('opts.onAction', () => { 113 | let count; 114 | const countMiddleware = () => () => () => { 115 | count += 1; 116 | }; 117 | 118 | const app = create({ 119 | onAction: countMiddleware, 120 | }); 121 | app.start(); 122 | 123 | count = 0; 124 | app._store.dispatch({ type: 'test' }); 125 | expect(count).toEqual(1); 126 | }); 127 | 128 | it('opts.onAction with array', () => { 129 | let count; 130 | const countMiddleware = () => next => action => { 131 | count += 1; 132 | next(action); 133 | }; 134 | const count2Middleware = () => next => action => { 135 | count += 2; 136 | next(action); 137 | }; 138 | 139 | const app = create({ 140 | onAction: [countMiddleware, count2Middleware], 141 | }); 142 | app.start(); 143 | 144 | count = 0; 145 | app._store.dispatch({ type: 'test' }); 146 | expect(count).toEqual(3); 147 | }); 148 | 149 | it('opts.extraEnhancers', () => { 150 | let count = 0; 151 | const countEnhancer = storeCreator => (reducer, preloadedState, enhancer) => { 152 | const store = storeCreator(reducer, preloadedState, enhancer); 153 | const oldDispatch = store.dispatch; 154 | store.dispatch = action => { 155 | count += 1; 156 | oldDispatch(action); 157 | }; 158 | return store; 159 | }; 160 | const app = create({ 161 | extraEnhancers: [countEnhancer], 162 | }); 163 | app.start(); 164 | 165 | app._store.dispatch({ type: 'abc' }); 166 | expect(count).toEqual(1); 167 | }); 168 | 169 | it('opts.onStateChange', () => { 170 | let savedState = null; 171 | 172 | const app = create({ 173 | onStateChange(state) { 174 | savedState = state; 175 | }, 176 | }); 177 | app.model({ 178 | namespace: 'count', 179 | state: 0, 180 | reducers: { 181 | add(state) { 182 | return state + 1; 183 | }, 184 | }, 185 | }); 186 | app.start(); 187 | 188 | app._store.dispatch({ type: 'count/add' }); 189 | expect(savedState.count).toEqual(1); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /packages/dva-core/test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import Plugin from '../src/Plugin'; 3 | 4 | describe('plugin', () => { 5 | it('basic', () => { 6 | let hmrCount = 0; 7 | let errorMessage = ''; 8 | 9 | function onError(err) { 10 | errorMessage = err.message; 11 | } 12 | 13 | const plugin = new Plugin(); 14 | 15 | plugin.use({ 16 | onHmr: x => { 17 | hmrCount += 1 * x; 18 | }, 19 | onStateChange: 2, 20 | onAction: 1, 21 | extraReducers: { form: 1 }, 22 | onReducer: r => { 23 | return (state, action) => { 24 | const res = r(state, action); 25 | return res + 1; 26 | }; 27 | }, 28 | }); 29 | plugin.use({ 30 | onHmr: x => { 31 | hmrCount += 2 + x; 32 | }, 33 | extraReducers: { user: 2 }, 34 | onReducer: r => { 35 | return (state, action) => { 36 | const res = r(state, action); 37 | return res * 2; 38 | }; 39 | }, 40 | }); 41 | 42 | plugin.apply('onHmr')(2); 43 | plugin.apply('onError', onError)({ message: 'hello dva' }); 44 | 45 | expect(hmrCount).toEqual(6); 46 | expect(errorMessage).toEqual('hello dva'); 47 | 48 | expect(plugin.get('extraReducers')).toEqual({ form: 1, user: 2 }); 49 | expect(plugin.get('onAction')).toEqual([1]); 50 | expect(plugin.get('onStateChange')).toEqual([2]); 51 | 52 | expect(plugin.get('onReducer')(state => state + 1)(0)).toEqual(4); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/dva-core/test/reducers.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | describe('reducers', () => { 5 | it('enhancer', () => { 6 | function enhancer(reducer) { 7 | return (state, action) => { 8 | if (action.type === 'square') { 9 | return state * state; 10 | } 11 | return reducer(state, action); 12 | }; 13 | } 14 | 15 | const app = create(); 16 | app.model({ 17 | namespace: 'count', 18 | state: 3, 19 | reducers: [ 20 | { 21 | add(state, { payload }) { 22 | return state + (payload || 1); 23 | }, 24 | }, 25 | enhancer, 26 | ], 27 | }); 28 | app.start(); 29 | 30 | app._store.dispatch({ type: 'square' }); 31 | app._store.dispatch({ type: 'count/add' }); 32 | expect(app._store.getState().count).toEqual(10); 33 | }); 34 | 35 | it('extraReducers', () => { 36 | const reducers = { 37 | count: (state, { type }) => { 38 | if (type === 'add') { 39 | return state + 1; 40 | } 41 | // default state 42 | return 0; 43 | }, 44 | }; 45 | const app = create({ 46 | extraReducers: reducers, 47 | }); 48 | app.start(); 49 | 50 | expect(app._store.getState().count).toEqual(0); 51 | app._store.dispatch({ type: 'add' }); 52 | expect(app._store.getState().count).toEqual(1); 53 | }); 54 | 55 | // core 没有 routing 这个 reducer,所以用例无效了 56 | xit('extraReducers: throw error if conflicts', () => { 57 | const app = create({ 58 | extraReducers: { routing() {} }, 59 | }); 60 | expect(() => { 61 | app.start(); 62 | }).toThrow(/\[app\.start\] extraReducers is conflict with other reducers/); 63 | }); 64 | 65 | it('onReducer with saveAndLoad', () => { 66 | let savedState = null; 67 | const saveAndLoad = r => (state, action) => { 68 | const newState = r(state, action); 69 | if (action.type === 'save') { 70 | savedState = newState; 71 | } 72 | if (action.type === 'load') { 73 | return savedState; 74 | } 75 | return newState; 76 | }; 77 | const app = create({ 78 | onReducer: saveAndLoad, 79 | }); 80 | app.model({ 81 | namespace: 'count', 82 | state: 0, 83 | reducers: { 84 | add(state) { 85 | return state + 1; 86 | }, 87 | }, 88 | }); 89 | app.start(); 90 | 91 | app._store.dispatch({ type: 'count/add' }); 92 | expect(app._store.getState().count).toEqual(1); 93 | app._store.dispatch({ type: 'save' }); 94 | expect(app._store.getState().count).toEqual(1); 95 | app._store.dispatch({ type: 'count/add' }); 96 | app._store.dispatch({ type: 'count/add' }); 97 | expect(app._store.getState().count).toEqual(3); 98 | app._store.dispatch({ type: 'load' }); 99 | expect(app._store.getState().count).toEqual(1); 100 | }); 101 | 102 | it('onReducer', () => { 103 | const undo = r => (state, action) => { 104 | const newState = r(state, action); 105 | return { present: newState, routing: newState.routing }; 106 | }; 107 | const app = create({ 108 | onReducer: undo, 109 | }); 110 | app.model({ 111 | namespace: 'count', 112 | state: 0, 113 | reducers: { 114 | update(state) { 115 | return state + 1; 116 | }, 117 | }, 118 | }); 119 | app.start(); 120 | 121 | expect(app._store.getState().present.count).toEqual(0); 122 | }); 123 | 124 | it('effects put reducers when reducers is array', () => { 125 | const enhancer = r => (state, action) => { 126 | const newState = r(state, action); 127 | return newState; 128 | }; 129 | const app = create(); 130 | app.model({ 131 | namespace: 'count', 132 | state: 0, 133 | effects: { 134 | *putSetState(action, { put }) { 135 | yield put({ type: 'setState' }); 136 | }, 137 | }, 138 | reducers: [ 139 | { 140 | setState(state) { 141 | return state + 1; 142 | }, 143 | }, 144 | enhancer, 145 | ], 146 | }); 147 | app.start(); 148 | 149 | app._store.dispatch({ type: 'count/putSetState' }); 150 | expect(app._store.getState().count).toEqual(1); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /packages/dva-core/test/repalceModel.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import EventEmitter from 'events'; 3 | import { create } from '../src/index'; 4 | 5 | describe('app.replaceModel', () => { 6 | it('should not be available before app.start() get called', () => { 7 | const app = create(); 8 | 9 | expect('replaceModel' in app).toEqual(false); 10 | }); 11 | 12 | it("should add model if it doesn't exist", () => { 13 | const app = create(); 14 | app.start(); 15 | 16 | const oldCount = app._models.length; 17 | 18 | app.replaceModel({ 19 | namespace: 'users', 20 | state: [], 21 | reducers: { 22 | add(state, { payload }) { 23 | return [...state, payload]; 24 | }, 25 | }, 26 | }); 27 | 28 | expect(app._models.length).toEqual(oldCount + 1); 29 | 30 | app._store.dispatch({ type: 'users/add', payload: 'jack' }); 31 | const state = app._store.getState(); 32 | expect(state.users).toEqual(['jack']); 33 | }); 34 | 35 | it('should run new reducers if model exists', () => { 36 | const app = create(); 37 | app.model({ 38 | namespace: 'users', 39 | state: ['foo'], 40 | reducers: { 41 | add(state, { payload }) { 42 | return [...state, payload]; 43 | }, 44 | }, 45 | }); 46 | app.start(); 47 | 48 | const oldCount = app._models.length; 49 | 50 | app.replaceModel({ 51 | namespace: 'users', 52 | state: ['bar'], 53 | reducers: { 54 | add(state, { payload }) { 55 | return [...state, 'world', payload]; 56 | }, 57 | clear() { 58 | return []; 59 | }, 60 | }, 61 | }); 62 | 63 | expect(app._models.length).toEqual(oldCount); 64 | let state = app._store.getState(); 65 | expect(state.users).toEqual(['foo']); 66 | 67 | app._store.dispatch({ type: 'users/add', payload: 'jack' }); 68 | state = app._store.getState(); 69 | expect(state.users).toEqual(['foo', 'world', 'jack']); 70 | 71 | // test new added action 72 | app._store.dispatch({ type: 'users/clear' }); 73 | 74 | state = app._store.getState(); 75 | expect(state.users).toEqual([]); 76 | }); 77 | 78 | it('should run new effects if model exists', () => { 79 | const app = create(); 80 | app.model({ 81 | namespace: 'users', 82 | state: [], 83 | reducers: { 84 | setter(state, { payload }) { 85 | return [...state, payload]; 86 | }, 87 | }, 88 | effects: { 89 | *add({ payload }, { put }) { 90 | yield put({ 91 | type: 'setter', 92 | payload, 93 | }); 94 | }, 95 | }, 96 | }); 97 | app.start(); 98 | 99 | app.replaceModel({ 100 | namespace: 'users', 101 | state: [], 102 | reducers: { 103 | setter(state, { payload }) { 104 | return [...state, payload]; 105 | }, 106 | }, 107 | effects: { 108 | *add(_, { put }) { 109 | yield put({ 110 | type: 'setter', 111 | payload: 'mock', 112 | }); 113 | }, 114 | }, 115 | }); 116 | 117 | app._store.dispatch({ type: 'users/add', payload: 'jack' }); 118 | const state = app._store.getState(); 119 | expect(state.users).toEqual(['mock']); 120 | }); 121 | 122 | it('should run subscriptions after replaceModel', () => { 123 | const app = create(); 124 | app.model({ 125 | namespace: 'users', 126 | state: [], 127 | reducers: { 128 | add(state, { payload }) { 129 | return [...state, payload]; 130 | }, 131 | }, 132 | subscriptions: { 133 | setup({ dispatch }) { 134 | // should return unlistener but omitted here 135 | dispatch({ type: 'add', payload: 1 }); 136 | }, 137 | }, 138 | }); 139 | app.start(); 140 | 141 | app.replaceModel({ 142 | namespace: 'users', 143 | state: [], 144 | reducers: { 145 | add(state, { payload }) { 146 | return [...state, payload]; 147 | }, 148 | }, 149 | subscriptions: { 150 | setup({ dispatch }) { 151 | // should return unlistener but omitted here 152 | dispatch({ type: 'add', payload: 2 }); 153 | }, 154 | }, 155 | }); 156 | 157 | const state = app._store.getState(); 158 | // This should be an issue but can't be avoided with dva 159 | // To avoid, in client code, setup method should be idempotent when running multiple times 160 | expect(state.users).toEqual([1, 2]); 161 | }); 162 | 163 | it('should remove old subscription listeners after replaceModel', () => { 164 | const app = create(); 165 | const emitter = new EventEmitter(); 166 | let emitterCount = 0; 167 | 168 | app.model({ 169 | namespace: 'users', 170 | state: [], 171 | subscriptions: { 172 | setup() { 173 | emitter.on('event', () => { 174 | emitterCount += 1; 175 | }); 176 | return () => { 177 | emitter.removeAllListeners(); 178 | }; 179 | }, 180 | }, 181 | }); 182 | app.start(); 183 | 184 | emitter.emit('event'); 185 | 186 | app.replaceModel({ 187 | namespace: 'users', 188 | state: [], 189 | }); 190 | 191 | emitter.emit('event'); 192 | 193 | expect(emitterCount).toEqual(1); 194 | }); 195 | 196 | it('should trigger onError if error is thown after replaceModel', () => { 197 | let triggeredError = false; 198 | const app = create({ 199 | onError() { 200 | triggeredError = true; 201 | }, 202 | }); 203 | app.model({ 204 | namespace: 'users', 205 | state: [], 206 | }); 207 | app.start(); 208 | 209 | app.replaceModel({ 210 | namespace: 'users', 211 | state: [], 212 | effects: { 213 | *add() { 214 | yield 'fake'; 215 | 216 | throw new Error('fake error'); 217 | }, 218 | }, 219 | }); 220 | 221 | app._store.dispatch({ 222 | type: 'users/add', 223 | }); 224 | 225 | expect(triggeredError).toEqual(true); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /packages/dva-core/test/subscriptions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { create } from '../src/index'; 3 | 4 | describe('subscriptions', () => { 5 | it('dispatch action', () => { 6 | const app = create(); 7 | app.model({ 8 | namespace: 'count', 9 | state: 0, 10 | reducers: { 11 | add(state, { payload }) { 12 | return state + payload || 1; 13 | }, 14 | }, 15 | subscriptions: { 16 | setup({ dispatch }) { 17 | dispatch({ type: 'add', payload: 2 }); 18 | }, 19 | }, 20 | }); 21 | app.start(); 22 | expect(app._store.getState().count).toEqual(2); 23 | }); 24 | 25 | it('dispatch action with namespace will get a warn', () => { 26 | const app = create(); 27 | app.model({ 28 | namespace: 'count', 29 | state: 0, 30 | reducers: { 31 | add(state, { payload }) { 32 | return state + payload || 1; 33 | }, 34 | }, 35 | subscriptions: { 36 | setup({ dispatch }) { 37 | dispatch({ type: 'add', payload: 2 }); 38 | }, 39 | }, 40 | }); 41 | app.start(); 42 | expect(app._store.getState().count).toEqual(2); 43 | }); 44 | 45 | it('dispatch not valid action', () => { 46 | const app = create(); 47 | app.model({ 48 | namespace: 'count', 49 | state: 0, 50 | subscriptions: { 51 | setup({ dispatch }) { 52 | dispatch('add'); 53 | }, 54 | }, 55 | }); 56 | expect(() => { 57 | app.start(); 58 | }).toThrow(/dispatch: action should be a plain Object with type/); 59 | }); 60 | 61 | it('dispatch action for other models', () => { 62 | const app = create(); 63 | app.model({ 64 | namespace: 'loading', 65 | state: false, 66 | reducers: { 67 | show() { 68 | return true; 69 | }, 70 | }, 71 | }); 72 | app.model({ 73 | namespace: 'count', 74 | state: 0, 75 | subscriptions: { 76 | setup({ dispatch }) { 77 | dispatch({ type: 'loading/show' }); 78 | }, 79 | }, 80 | }); 81 | app.start(); 82 | expect(app._store.getState().loading).toEqual(true); 83 | }); 84 | 85 | it('onError', () => { 86 | const errors = []; 87 | const app = create({ 88 | onError: error => { 89 | errors.push(error.message); 90 | }, 91 | }); 92 | app.model({ 93 | namespace: '-', 94 | state: {}, 95 | subscriptions: { 96 | setup(_obj, done) { 97 | done('subscription error'); 98 | }, 99 | }, 100 | }); 101 | app.start(); 102 | expect(errors).toEqual(['subscription error']); 103 | }); 104 | 105 | it('onError async', done => { 106 | const errors = []; 107 | const app = create({ 108 | onError: error => { 109 | errors.push(error.message); 110 | }, 111 | }); 112 | app.model({ 113 | namespace: '-', 114 | state: {}, 115 | subscriptions: { 116 | setup(_obj, done) { 117 | setTimeout(() => { 118 | done('subscription error'); 119 | }, 100); 120 | }, 121 | }, 122 | }); 123 | app.start(); 124 | expect(errors).toEqual([]); 125 | setTimeout(() => { 126 | expect(errors).toEqual(['subscription error']); 127 | done(); 128 | }, 200); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /packages/dva-core/test/utils.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { findIndex } from '../src/utils'; 3 | 4 | describe('utils', () => { 5 | describe('#findIndex', () => { 6 | it('should return -1 when no item matches', () => { 7 | const array = [1, 2, 3]; 8 | const action = i => i === 4; 9 | 10 | expect(findIndex(array, action)).toEqual(-1); 11 | }); 12 | 13 | it('should return index of the match item in array', () => { 14 | const array = ['a', 'b', 'c']; 15 | const action = i => i === 'b'; 16 | 17 | const actualValue = findIndex(array, action); 18 | const expectedValue = 1; 19 | 20 | expect(actualValue).toEqual(expectedValue); 21 | }); 22 | 23 | it('should return the first match if more than one items match', () => { 24 | const target = { 25 | id: 1, 26 | }; 27 | 28 | const array = [target, { id: 1 }]; 29 | const action = i => i.id === 1; 30 | 31 | expect(findIndex(array, action)).toEqual(0); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/dva-core/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.0.0": 6 | version "7.3.4" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" 8 | integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== 9 | dependencies: 10 | regenerator-runtime "^0.12.0" 11 | 12 | any-promise@^1.0.0: 13 | version "1.3.0" 14 | resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" 15 | integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= 16 | 17 | core-util-is@^1.0.2: 18 | version "1.0.2" 19 | resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 20 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 21 | 22 | dom-walk@^0.1.0: 23 | version "0.1.1" 24 | resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" 25 | integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg= 26 | 27 | flatten@^1.0.2: 28 | version "1.0.2" 29 | resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" 30 | integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= 31 | 32 | global@^4.3.2: 33 | version "4.3.2" 34 | resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" 35 | integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8= 36 | dependencies: 37 | min-document "^2.19.0" 38 | process "~0.5.1" 39 | 40 | invariant@^2.2.1: 41 | version "2.2.4" 42 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" 43 | integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== 44 | dependencies: 45 | loose-envify "^1.0.0" 46 | 47 | is-class-hotfix@~0.0.6: 48 | version "0.0.6" 49 | resolved "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz#a527d31fb23279281dde5f385c77b5de70a72435" 50 | integrity sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ== 51 | 52 | is-plain-object@^2.0.3: 53 | version "2.0.4" 54 | resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" 55 | integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== 56 | dependencies: 57 | isobject "^3.0.1" 58 | 59 | is-type-of@^1.0.0: 60 | version "1.2.1" 61 | resolved "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.1.tgz#e263ec3857aceb4f28c47130ec78db09a920f8c5" 62 | integrity sha512-uK0kyX9LZYhSDS7H2sVJQJop1UnWPWmo5RvR3q2kFH6AUHYs7sOrVg0b4nyBHw29kRRNFofYN/JbHZDlHiItTA== 63 | dependencies: 64 | core-util-is "^1.0.2" 65 | is-class-hotfix "~0.0.6" 66 | isstream "~0.1.2" 67 | 68 | isobject@^3.0.1: 69 | version "3.0.1" 70 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 71 | integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= 72 | 73 | isstream@~0.1.2: 74 | version "0.1.2" 75 | resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 76 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 77 | 78 | "js-tokens@^3.0.0 || ^4.0.0": 79 | version "4.0.0" 80 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 81 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 82 | 83 | ko-sleep@^1.0.2: 84 | version "1.0.3" 85 | resolved "https://registry.npmjs.org/ko-sleep/-/ko-sleep-1.0.3.tgz#28a2a0a1485e8b7f415ff488dee17d24788ab082" 86 | integrity sha1-KKKgoUhei39BX/SI3uF9JHiKsII= 87 | dependencies: 88 | ms "^2.0.0" 89 | 90 | loose-envify@^1.0.0, loose-envify@^1.4.0: 91 | version "1.4.0" 92 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 93 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 94 | dependencies: 95 | js-tokens "^3.0.0 || ^4.0.0" 96 | 97 | min-document@^2.19.0: 98 | version "2.19.0" 99 | resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" 100 | integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= 101 | dependencies: 102 | dom-walk "^0.1.0" 103 | 104 | mm@^2.5.0: 105 | version "2.5.0" 106 | resolved "https://registry.npmjs.org/mm/-/mm-2.5.0.tgz#dfb993762c1468b591c4c4fcd47dff45ed01378a" 107 | integrity sha512-ilm+lGEBNm7Cw45um9ax0tbApiNwQV3PY6Yk1ol+wtA8c98hHuJqTgmdKB6rYQJTUC2QrhBfoWwN+/766ZlrYA== 108 | dependencies: 109 | is-type-of "^1.0.0" 110 | ko-sleep "^1.0.2" 111 | muk-prop "^1.0.0" 112 | thenify "^3.2.1" 113 | 114 | ms@^2.0.0: 115 | version "2.1.2" 116 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 117 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 118 | 119 | muk-prop@^1.0.0: 120 | version "1.2.1" 121 | resolved "https://registry.npmjs.org/muk-prop/-/muk-prop-1.2.1.tgz#40fa3d6e93553b2016a9fb77d8918568c57ae14d" 122 | integrity sha512-NdkOVav3GoIkBZqMUneU435HW0a90zitpuO1erPRhOQdPtl65dXD3G9/1k46G6/0ZMau4CJFFUHkMKVsyNZT+w== 123 | 124 | process@~0.5.1: 125 | version "0.5.2" 126 | resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" 127 | integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= 128 | 129 | redux-saga@^0.16.0: 130 | version "0.16.2" 131 | resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.16.2.tgz#993662e86bc945d8509ac2b8daba3a8c615cc971" 132 | integrity sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w== 133 | 134 | redux@^4.0.1: 135 | version "4.0.1" 136 | resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" 137 | integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== 138 | dependencies: 139 | loose-envify "^1.4.0" 140 | symbol-observable "^1.2.0" 141 | 142 | regenerator-runtime@^0.12.0: 143 | version "0.12.1" 144 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" 145 | integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== 146 | 147 | symbol-observable@^1.2.0: 148 | version "1.2.0" 149 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" 150 | integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== 151 | 152 | thenify@^3.2.1: 153 | version "3.3.1" 154 | resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" 155 | integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== 156 | dependencies: 157 | any-promise "^1.0.0" 158 | 159 | warning@^3.0.0: 160 | version "3.0.0" 161 | resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" 162 | integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= 163 | dependencies: 164 | loose-envify "^1.0.0" 165 | -------------------------------------------------------------------------------- /packages/dva-immer/.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cjs: 'rollup', 3 | esm: 'rollup', 4 | runtimeHelpers: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/dva-immer/README.md: -------------------------------------------------------------------------------- 1 | # dva-immer 2 | 3 | [![NPM version](https://img.shields.io/npm/v/dva-immer.svg?style=flat)](https://npmjs.org/package/dva-immer) 4 | [![Build Status](https://img.shields.io/travis/dvajs/dva-immer.svg?style=flat)](https://travis-ci.org/dvajs/dva-immer) 5 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-immer.svg?style=flat)](https://coveralls.io/r/dvajs/dva-immer) 6 | [![NPM downloads](http://img.shields.io/npm/dm/dva-immer.svg?style=flat)](https://npmjs.org/package/dva-immer) 7 | 8 | Create the next immutable state tree by simply modifying the current tree 9 | 10 | --- 11 | 12 | ## Install 13 | 14 | ```bash 15 | $ npm install dva-immer --save 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```javascript 21 | 22 | const app = dva(); 23 | app.use(require('dva-immer').default()); 24 | ``` 25 | some like [umi-plugin-dva](https://github.com/umijs/umi/blob/master/packages/umi-plugin-dva/src/index.js) line 106 26 | 27 | Look more [Immer](https://github.com/mweststrate/immer) 28 | 29 | 30 | ## License 31 | 32 | [MIT](https://tldrlegal.com/license/mit-license) 33 | -------------------------------------------------------------------------------- /packages/dva-immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-immer", 3 | "version": "1.0.2", 4 | "description": "Auto loading data binding plugin for dva.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "sideEffects": false, 8 | "dependencies": { 9 | "@babel/runtime": "^7.0.0", 10 | "immer": "^8.0.4" 11 | }, 12 | "peerDependencies": { 13 | "dva": "^2.5.0-0" 14 | }, 15 | "devDependencies": { 16 | "dva": "3.0.0-alpha.1" 17 | }, 18 | "files": [ 19 | "dist", 20 | "src" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/dvajs/dva/tree/master/packages/dva-immer" 25 | }, 26 | "homepage": "https://github.com/dvajs/dva", 27 | "author": "chencheng ", 28 | "keywords": [ 29 | "dva", 30 | "dva-plugin", 31 | "immer" 32 | ], 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /packages/dva-immer/src/index.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | export { enableES5, enableAllPlugins } from 'immer'; 4 | 5 | export default function() { 6 | return { 7 | _handleActions(handlers, defaultState) { 8 | return (state = defaultState, action) => { 9 | const { type } = action; 10 | 11 | const ret = produce(state, draft => { 12 | const handler = handlers[type]; 13 | if (handler) { 14 | const compatiableRet = handler(draft, action); 15 | if (compatiableRet !== undefined) { 16 | // which means you are use redux pattern 17 | // it's compatiable. https://github.com/mweststrate/immer#returning-data-from-producers 18 | return compatiableRet; 19 | } 20 | } 21 | }); 22 | return ret === undefined ? {} : ret; 23 | }; 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/dva-immer/test/index.test.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import userImmer from '../src/index'; 3 | 4 | describe('dva-immer', () => { 5 | it('normal', () => { 6 | const app = dva(); 7 | app.use(userImmer()); 8 | 9 | app.model({ 10 | namespace: 'count', 11 | state: { 12 | a: { 13 | b: { 14 | c: 0, 15 | }, 16 | }, 17 | m: { 18 | b: { 19 | c: 0, 20 | }, 21 | }, 22 | }, 23 | reducers: { 24 | add(state) { 25 | state.a.b.c += 1; 26 | }, 27 | }, 28 | }); 29 | app.router(() => 1); 30 | app.start(); 31 | 32 | const oldCount = app._store.getState().count; 33 | app._store.dispatch({ type: 'count/add' }); 34 | const newCount = app._store.getState().count; 35 | expect(oldCount.a.b.c).toEqual(0); 36 | expect(newCount.a.b.c).toEqual(1); 37 | }); 38 | 39 | it('compatibility with normal reducer usage', () => { 40 | const app = dva(); 41 | app.use(userImmer()); 42 | 43 | app.model({ 44 | namespace: 'count', 45 | state: { 46 | a: { 47 | b: { 48 | c: 0, 49 | }, 50 | }, 51 | m: { 52 | b: { 53 | c: 0, 54 | }, 55 | }, 56 | }, 57 | reducers: { 58 | add(state) { 59 | return { 60 | ...state, 61 | a: { 62 | ...state.a, 63 | b: { 64 | ...state.a.b, 65 | c: state.a.b.c + 1, 66 | }, 67 | }, 68 | }; 69 | }, 70 | }, 71 | }); 72 | app.router(() => 1); 73 | app.start(); 74 | 75 | const oldCount = app._store.getState().count; 76 | app._store.dispatch({ type: 'count/add' }); 77 | const newCount = app._store.getState().count; 78 | expect(oldCount.a.b.c).toEqual(0); 79 | expect(newCount.a.b.c).toEqual(1); 80 | expect(newCount.m.b.c).toEqual(0); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/dva-immer/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.0.0": 6 | version "7.3.4" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" 8 | integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== 9 | dependencies: 10 | regenerator-runtime "^0.12.0" 11 | 12 | immer@^8.0.4: 13 | version "8.0.4" 14 | resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" 15 | integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== 16 | 17 | regenerator-runtime@^0.12.0: 18 | version "0.12.1" 19 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" 20 | integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== 21 | -------------------------------------------------------------------------------- /packages/dva-loading/.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cjs: 'rollup', 3 | esm: 'rollup', 4 | runtimeHelpers: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/dva-loading/README.md: -------------------------------------------------------------------------------- 1 | # dva-loading 2 | 3 | [![NPM version](https://img.shields.io/npm/v/dva-loading.svg?style=flat)](https://npmjs.org/package/dva-loading) 4 | [![Build Status](https://img.shields.io/travis/dvajs/dva-loading.svg?style=flat)](https://travis-ci.org/dvajs/dva-loading) 5 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva-loading.svg?style=flat)](https://coveralls.io/r/dvajs/dva-loading) 6 | [![NPM downloads](http://img.shields.io/npm/dm/dva-loading.svg?style=flat)](https://npmjs.org/package/dva-loading) 7 | 8 | Auto loading data binding plugin for dva. :clap: You don't need to write `showLoading` and `hideLoading` any more. 9 | 10 | --- 11 | 12 | ## Install 13 | 14 | ```bash 15 | $ npm install dva-loading --save 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```javascript 21 | import createLoading from 'dva-loading'; 22 | 23 | const app = dva(); 24 | app.use(createLoading(opts)); 25 | ``` 26 | 27 | Then we can access loading state from store. 28 | 29 | ### opts 30 | 31 | - `opts.namespace`: property key on global state, type String, Default `loading` 32 | 33 | [See real project usage on dva-hackernews](https://github.com/dvajs/dva-hackernews/blob/2c3330b1c8ae728c94ebe1399b72486ad5a1a7a0/src/index.js#L4-L7). 34 | 35 | ## State Structure 36 | 37 | ``` 38 | loading: { 39 | global: false, 40 | models: { 41 | users: false, 42 | todos: false, 43 | ... 44 | }, 45 | } 46 | ``` 47 | 48 | ## License 49 | 50 | [MIT](https://tldrlegal.com/license/mit-license) 51 | -------------------------------------------------------------------------------- /packages/dva-loading/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface DvaLoadingState { 2 | global: boolean; 3 | models: { [type: string]: boolean | undefined }; 4 | effects: { [type: string]: boolean | undefined }; 5 | } 6 | -------------------------------------------------------------------------------- /packages/dva-loading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva-loading", 3 | "version": "3.0.25", 4 | "description": "Auto loading data binding plugin for dva.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "sideEffects": false, 8 | "typings": "index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dvajs/dva" 12 | }, 13 | "homepage": "https://github.com/dvajs/dva", 14 | "keywords": [ 15 | "dva", 16 | "dva-plugin", 17 | "loading" 18 | ], 19 | "author": "chencheng ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "dva": "3.0.0-alpha.1", 23 | "dva-core": "2.0.4" 24 | }, 25 | "peerDependencies": { 26 | "dva-core": "^1.1.0 || ^1.5.0-0 || ^1.6.0-0" 27 | }, 28 | "files": [ 29 | "dist", 30 | "src" 31 | ], 32 | "dependencies": { 33 | "@babel/runtime": "^7.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/dva-loading/src/index.js: -------------------------------------------------------------------------------- 1 | const SHOW = '@@DVA_LOADING/SHOW'; 2 | const HIDE = '@@DVA_LOADING/HIDE'; 3 | const NAMESPACE = 'loading'; 4 | 5 | function createLoading(opts = {}) { 6 | const namespace = opts.namespace || NAMESPACE; 7 | 8 | const { only = [], except = [] } = opts; 9 | if (only.length > 0 && except.length > 0) { 10 | throw Error('It is ambiguous to configurate `only` and `except` items at the same time.'); 11 | } 12 | 13 | const initialState = { 14 | global: false, 15 | models: {}, 16 | effects: {}, 17 | }; 18 | 19 | const extraReducers = { 20 | [namespace](state = initialState, { type, payload }) { 21 | const { namespace, actionType } = payload || {}; 22 | let ret; 23 | switch (type) { 24 | case SHOW: 25 | ret = { 26 | ...state, 27 | global: true, 28 | models: { ...state.models, [namespace]: true }, 29 | effects: { ...state.effects, [actionType]: true }, 30 | }; 31 | break; 32 | case HIDE: { 33 | const effects = { ...state.effects, [actionType]: false }; 34 | const models = { 35 | ...state.models, 36 | [namespace]: Object.keys(effects).some(actionType => { 37 | const _namespace = actionType.split('/')[0]; 38 | if (_namespace !== namespace) return false; 39 | return effects[actionType]; 40 | }), 41 | }; 42 | const global = Object.keys(models).some(namespace => { 43 | return models[namespace]; 44 | }); 45 | ret = { 46 | ...state, 47 | global, 48 | models, 49 | effects, 50 | }; 51 | break; 52 | } 53 | default: 54 | ret = state; 55 | break; 56 | } 57 | return ret; 58 | }, 59 | }; 60 | 61 | function onEffect(effect, { put }, model, actionType) { 62 | const { namespace } = model; 63 | if ( 64 | (only.length === 0 && except.length === 0) || 65 | (only.length > 0 && only.indexOf(actionType) !== -1) || 66 | (except.length > 0 && except.indexOf(actionType) === -1) 67 | ) { 68 | return function*(...args) { 69 | yield put({ type: SHOW, payload: { namespace, actionType } }); 70 | yield effect(...args); 71 | yield put({ type: HIDE, payload: { namespace, actionType } }); 72 | }; 73 | } else { 74 | return effect; 75 | } 76 | } 77 | 78 | return { 79 | extraReducers, 80 | onEffect, 81 | }; 82 | } 83 | 84 | export default createLoading; 85 | -------------------------------------------------------------------------------- /packages/dva-loading/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.0.0": 6 | version "7.3.4" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" 8 | integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== 9 | dependencies: 10 | regenerator-runtime "^0.12.0" 11 | 12 | regenerator-runtime@^0.12.0: 13 | version "0.12.1" 14 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" 15 | integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== 16 | -------------------------------------------------------------------------------- /packages/dva/.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: ['src/index.js', 'src/dynamic.js'], 3 | cjs: 'rollup', 4 | esm: 'rollup', 5 | runtimeHelpers: true, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/dva/README.md: -------------------------------------------------------------------------------- 1 | # dva 2 | 3 | [![NPM version](https://img.shields.io/npm/v/dva.svg?style=flat)](https://npmjs.org/package/dva) 4 | [![Build Status](https://img.shields.io/travis/dvajs/dva.svg?style=flat)](https://travis-ci.org/dvajs/dva) 5 | [![Coverage Status](https://img.shields.io/coveralls/dvajs/dva.svg?style=flat)](https://coveralls.io/r/dvajs/dva) 6 | [![NPM downloads](http://img.shields.io/npm/dm/dva.svg?style=flat)](https://npmjs.org/package/dva) 7 | [![Dependencies](https://david-dm.org/dvajs/dva/status.svg)](https://david-dm.org/dvajs/dva) 8 | 9 | Official React bindings for dva, with react-router@4. 10 | 11 | ## LICENSE 12 | 13 | MIT 14 | -------------------------------------------------------------------------------- /packages/dva/dynamic.d.ts: -------------------------------------------------------------------------------- 1 | declare const dynamic: (resolve: (value?: PromiseLike) => void) => void; 2 | export default dynamic; 3 | -------------------------------------------------------------------------------- /packages/dva/dynamic.js: -------------------------------------------------------------------------------- 1 | require('./warnAboutDeprecatedCJSRequire.js')('dynamic'); 2 | module.exports = require('./dist/dynamic'); 3 | -------------------------------------------------------------------------------- /packages/dva/fetch.d.ts: -------------------------------------------------------------------------------- 1 | import * as isomorphicFetch from 'isomorphic-fetch'; 2 | 3 | export = isomorphicFetch; 4 | -------------------------------------------------------------------------------- /packages/dva/fetch.js: -------------------------------------------------------------------------------- 1 | require('./warnAboutDeprecatedCJSRequire.js')('fetch'); 2 | module.exports = require('isomorphic-fetch'); 3 | -------------------------------------------------------------------------------- /packages/dva/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Reducer, 3 | Action, 4 | AnyAction, 5 | ReducersMapObject, 6 | MiddlewareAPI, 7 | StoreEnhancer, 8 | bindActionCreators 9 | } from 'redux'; 10 | 11 | import { History } from "history"; 12 | 13 | export interface Dispatch { 14 | (action: T): Promise | T; 15 | } 16 | 17 | export interface onActionFunc { 18 | (api: MiddlewareAPI): void, 19 | } 20 | 21 | export interface ReducerEnhancer { 22 | (reducer: Reducer): void, 23 | } 24 | 25 | export interface Hooks { 26 | onError?: (e: Error, dispatch: Dispatch) => void, 27 | onAction?: onActionFunc | onActionFunc[], 28 | onStateChange?: () => void, 29 | onReducer?: ReducerEnhancer, 30 | onEffect?: () => void, 31 | onHmr?: () => void, 32 | extraReducers?: ReducersMapObject, 33 | extraEnhancers?: StoreEnhancer[], 34 | } 35 | 36 | export type DvaOption = Hooks & { 37 | namespacePrefixWarning?: boolean, 38 | initialState?: Object, 39 | history?: Object, 40 | } 41 | 42 | export interface EffectsCommandMap { 43 | put: (action: A) => any, 44 | call: Function, 45 | select: Function, 46 | take: Function, 47 | cancel: Function, 48 | [key: string]: any, 49 | } 50 | 51 | export type Effect = (action: AnyAction, effects: EffectsCommandMap) => void; 52 | export type EffectType = 'takeEvery' | 'takeLatest' | 'watcher' | 'throttle'; 53 | export type EffectWithType = [Effect, { type: EffectType }]; 54 | export type Subscription = (api: SubscriptionAPI, done: Function) => void; 55 | export type ReducersMapObjectWithEnhancer = [ReducersMapObject, ReducerEnhancer]; 56 | 57 | export interface EffectsMapObject { 58 | [key: string]: Effect | EffectWithType, 59 | } 60 | 61 | export interface SubscriptionAPI { 62 | history: History, 63 | dispatch: Dispatch, 64 | } 65 | 66 | export interface SubscriptionsMapObject { 67 | [key: string]: Subscription, 68 | } 69 | 70 | export interface Model { 71 | namespace: string, 72 | state?: any, 73 | reducers?: ReducersMapObject | ReducersMapObjectWithEnhancer, 74 | effects?: EffectsMapObject, 75 | subscriptions?: SubscriptionsMapObject, 76 | } 77 | 78 | export interface RouterAPI { 79 | history: History, 80 | app: DvaInstance, 81 | } 82 | 83 | export interface Router { 84 | (api?: RouterAPI): JSX.Element | Object, 85 | } 86 | 87 | export interface DvaInstance { 88 | /** 89 | * Register an object of hooks on the application. 90 | * 91 | * @param hooks 92 | */ 93 | use: (hooks: Hooks) => void, 94 | 95 | /** 96 | * Register a model. 97 | * 98 | * @param model 99 | */ 100 | model: (model: Model) => void, 101 | 102 | /** 103 | * Unregister a model. 104 | * 105 | * @param namespace 106 | */ 107 | unmodel: (namespace: string) => void, 108 | 109 | /** 110 | * Config router. Takes a function with arguments { history, dispatch }, 111 | * and expects router config. It use the same api as react-router, 112 | * return jsx elements or JavaScript Object for dynamic routing. 113 | * 114 | * @param router 115 | */ 116 | router: (router: Router) => void, 117 | 118 | /** 119 | * Start the application. Selector is optional. If no selector 120 | * arguments, it will return a function that return JSX elements. 121 | * 122 | * @param selector 123 | */ 124 | start: (selector?: HTMLElement | string) => any, 125 | } 126 | 127 | export default function dva(opts?: DvaOption): DvaInstance; 128 | 129 | export { bindActionCreators }; 130 | 131 | export { 132 | connect, connectAdvanced, useSelector, useDispatch, useStore, 133 | DispatchProp, shallowEqual 134 | } from 'react-redux'; 135 | 136 | import * as routerRedux from 'connected-react-router'; 137 | export { routerRedux }; 138 | 139 | import * as fetch from 'isomorphic-fetch'; 140 | export { fetch }; 141 | 142 | import * as router from 'react-router-dom'; 143 | export { router }; 144 | export { useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom'; 145 | -------------------------------------------------------------------------------- /packages/dva/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dva", 3 | "version": "3.0.0-alpha.1", 4 | "description": "React and redux based, lightweight and elm-style framework.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "typings": "index.d.ts", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dvajs/dva" 12 | }, 13 | "homepage": "https://github.com/dvajs/dva", 14 | "keywords": [ 15 | "dva", 16 | "alibaba", 17 | "react", 18 | "react-native", 19 | "redux", 20 | "redux-saga", 21 | "elm", 22 | "framework", 23 | "frontend" 24 | ], 25 | "authors": [ 26 | "chencheng (https://github.com/sorrycc)" 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/dvajs/dva/issues" 31 | }, 32 | "dependencies": { 33 | "@babel/runtime": "^7.0.0", 34 | "@types/isomorphic-fetch": "^0.0.35", 35 | "@types/react-redux": "^7.1.0", 36 | "@types/react-router-dom": "^5.1.2", 37 | "connected-react-router": "6.5.2", 38 | "dva-core": "2.0.4", 39 | "global": "^4.3.2", 40 | "history": "^4.7.2", 41 | "invariant": "^2.2.4", 42 | "isomorphic-fetch": "^2.2.1", 43 | "react-redux": "^7.1.0", 44 | "react-router-dom": "^5.1.2", 45 | "redux": "^4.0.1" 46 | }, 47 | "peerDependencies": { 48 | "react": ">=18", 49 | "react-dom": ">=18" 50 | }, 51 | "files": [ 52 | "dist", 53 | "src", 54 | "dynamic.js", 55 | "fetch.js", 56 | "index.js", 57 | "router.js", 58 | "saga.js", 59 | "warnAboutDeprecatedCJSRequire.js", 60 | "*.d.ts" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/dva/router.d.ts: -------------------------------------------------------------------------------- 1 | import * as routerRedux from 'connected-react-router'; 2 | 3 | export * from 'react-router-dom'; 4 | export { routerRedux }; 5 | -------------------------------------------------------------------------------- /packages/dva/router.js: -------------------------------------------------------------------------------- 1 | require('./warnAboutDeprecatedCJSRequire.js')('router'); 2 | module.exports = require('react-router-dom'); 3 | module.exports.routerRedux = require('connected-react-router'); 4 | -------------------------------------------------------------------------------- /packages/dva/saga.js: -------------------------------------------------------------------------------- 1 | require('./warnAboutDeprecatedCJSRequire.js')('saga'); 2 | module.exports = require('dva-core/saga'); 3 | -------------------------------------------------------------------------------- /packages/dva/src/dynamic.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const cached = {}; 4 | function registerModel(app, model) { 5 | model = model.default || model; 6 | if (!cached[model.namespace]) { 7 | app.model(model); 8 | cached[model.namespace] = 1; 9 | } 10 | } 11 | 12 | let defaultLoadingComponent = () => null; 13 | 14 | function asyncComponent(config) { 15 | const { resolve } = config; 16 | 17 | return class DynamicComponent extends Component { 18 | constructor(...args) { 19 | super(...args); 20 | this.LoadingComponent = config.LoadingComponent || defaultLoadingComponent; 21 | this.state = { 22 | AsyncComponent: null, 23 | }; 24 | this.load(); 25 | } 26 | 27 | componentDidMount() { 28 | this.mounted = true; 29 | } 30 | 31 | componentWillUnmount() { 32 | this.mounted = false; 33 | } 34 | 35 | load() { 36 | resolve().then(m => { 37 | const AsyncComponent = m.default || m; 38 | if (this.mounted) { 39 | this.setState({ AsyncComponent }); 40 | } else { 41 | this.state.AsyncComponent = AsyncComponent; // eslint-disable-line 42 | } 43 | }); 44 | } 45 | 46 | render() { 47 | const { AsyncComponent } = this.state; 48 | const { LoadingComponent } = this; 49 | if (AsyncComponent) return ; 50 | 51 | return ; 52 | } 53 | }; 54 | } 55 | 56 | export default function dynamic(config) { 57 | const { app, models: resolveModels, component: resolveComponent } = config; 58 | return asyncComponent({ 59 | resolve: 60 | config.resolve || 61 | function() { 62 | const models = typeof resolveModels === 'function' ? resolveModels() : []; 63 | const component = resolveComponent(); 64 | return new Promise(resolve => { 65 | Promise.all([...models, component]).then(ret => { 66 | if (!models || !models.length) { 67 | return resolve(ret[0]); 68 | } else { 69 | const len = models.length; 70 | ret.slice(0, len).forEach(m => { 71 | m = m.default || m; 72 | if (!Array.isArray(m)) { 73 | m = [m]; 74 | } 75 | m.map(_ => registerModel(app, _)); 76 | }); 77 | resolve(ret[len]); 78 | } 79 | }); 80 | }); 81 | }, 82 | ...config, 83 | }); 84 | } 85 | 86 | dynamic.setDefaultLoadingComponent = LoadingComponent => { 87 | defaultLoadingComponent = LoadingComponent; 88 | }; 89 | -------------------------------------------------------------------------------- /packages/dva/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import invariant from 'invariant'; 3 | import { createBrowserHistory, createMemoryHistory, createHashHistory } from 'history'; 4 | import document from 'global/document'; 5 | import { 6 | Provider, 7 | connect, 8 | connectAdvanced, 9 | useSelector, 10 | useDispatch, 11 | useStore, 12 | shallowEqual, 13 | } from 'react-redux'; 14 | import { bindActionCreators } from 'redux'; 15 | import { utils, create, saga } from 'dva-core'; 16 | import * as router from 'react-router-dom'; 17 | import * as routerRedux from 'connected-react-router'; 18 | 19 | const { connectRouter, routerMiddleware } = routerRedux; 20 | const { isFunction } = utils; 21 | const { useHistory, useLocation, useParams, useRouteMatch } = router; 22 | 23 | export default function(opts = {}) { 24 | const history = opts.history || createHashHistory(); 25 | const createOpts = { 26 | initialReducer: { 27 | router: connectRouter(history), 28 | }, 29 | setupMiddlewares(middlewares) { 30 | return [routerMiddleware(history), ...middlewares]; 31 | }, 32 | setupApp(app) { 33 | app._history = patchHistory(history); 34 | }, 35 | }; 36 | 37 | const app = create(opts, createOpts); 38 | const oldAppStart = app.start; 39 | app.router = router; 40 | app.start = start; 41 | return app; 42 | 43 | function router(router) { 44 | invariant( 45 | isFunction(router), 46 | `[app.router] router should be function, but got ${typeof router}`, 47 | ); 48 | app._router = router; 49 | } 50 | 51 | function start(container) { 52 | // 允许 container 是字符串,然后用 querySelector 找元素 53 | if (isString(container)) { 54 | container = document.querySelector(container); 55 | invariant(container, `[app.start] container ${container} not found`); 56 | } 57 | 58 | // 并且是 HTMLElement 59 | invariant( 60 | !container || isHTMLElement(container), 61 | `[app.start] container should be HTMLElement`, 62 | ); 63 | 64 | // 路由必须提前注册 65 | invariant(app._router, `[app.start] router must be registered before app.start()`); 66 | 67 | if (!app._store) { 68 | oldAppStart.call(app); 69 | } 70 | const store = app._store; 71 | 72 | // export _getProvider for HMR 73 | // ref: https://github.com/dvajs/dva/issues/469 74 | app._getProvider = getProvider.bind(null, store, app); 75 | 76 | // If has container, render; else, return react component 77 | if (container) { 78 | render(container, store, app, app._router); 79 | app._plugin.apply('onHmr')(render.bind(null, container, store, app)); 80 | } else { 81 | return getProvider(store, this, this._router); 82 | } 83 | } 84 | } 85 | 86 | function isHTMLElement(node) { 87 | return typeof node === 'object' && node !== null && node.nodeType && node.nodeName; 88 | } 89 | 90 | function isString(str) { 91 | return typeof str === 'string'; 92 | } 93 | 94 | function getProvider(store, app, router) { 95 | const DvaRoot = extraProps => ( 96 | {router({ app, history: app._history, ...extraProps })} 97 | ); 98 | return DvaRoot; 99 | } 100 | 101 | function render(container, store, app, router) { 102 | const ReactDOM = require('react-dom/client'); // eslint-disable-line 103 | ReactDOM.createRoot(container).render(React.createElement(getProvider(store, app, router))); 104 | } 105 | 106 | function patchHistory(history) { 107 | const oldListen = history.listen; 108 | history.listen = callback => { 109 | // TODO: refact this with modified ConnectedRouter 110 | // Let ConnectedRouter to sync history to store first 111 | // connected-react-router's version is locked since the check function may be broken 112 | // min version of connected-react-router 113 | // e.g. 114 | // function (e, t) { 115 | // var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2]; 116 | // r.inTimeTravelling ? r.inTimeTravelling = !1 : a(e, t, n) 117 | // } 118 | // ref: https://github.com/umijs/umi/issues/2693 119 | const cbStr = callback.toString(); 120 | const isConnectedRouterHandler = 121 | (callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) || 122 | (cbStr.indexOf('.inTimeTravelling') > -1 && 123 | cbStr.indexOf('.inTimeTravelling') > -1 && 124 | cbStr.indexOf('arguments[2]') > -1); 125 | // why add __isDvaPatch: true 126 | // since it's a patch from dva, we need to identify it in the listen handlers 127 | callback(history.location, history.action, { __isDvaPatch: true }); 128 | return oldListen.call(history, (...args) => { 129 | if (isConnectedRouterHandler) { 130 | callback(...args); 131 | } else { 132 | // Delay all listeners besides ConnectedRouter 133 | setTimeout(() => { 134 | callback(...args); 135 | }); 136 | } 137 | }); 138 | }; 139 | return history; 140 | } 141 | 142 | export fetch from 'isomorphic-fetch'; 143 | export dynamic from './dynamic'; 144 | export { connect, connectAdvanced, useSelector, useDispatch, useStore, shallowEqual }; 145 | export { bindActionCreators }; 146 | export { router }; 147 | export { saga }; 148 | export { routerRedux }; 149 | export { createBrowserHistory, createMemoryHistory, createHashHistory }; 150 | export { useHistory, useLocation, useParams, useRouteMatch }; 151 | -------------------------------------------------------------------------------- /packages/dva/test/index.e2e.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, cleanup } from 'react-testing-library'; 3 | import dva, { 4 | connect, 5 | useDispatch, 6 | useSelector, 7 | useStore, 8 | createMemoryHistory, 9 | router, 10 | routerRedux, 11 | shallowEqual, 12 | } from '../dist/index'; 13 | 14 | const { Link, Switch, Route, Router } = router; 15 | 16 | afterEach(cleanup); 17 | 18 | const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout)); 19 | 20 | test('normal', () => { 21 | const app = dva(); 22 | app.model({ 23 | namespace: 'count', 24 | state: 0, 25 | reducers: { 26 | add(state) { 27 | return state + 1; 28 | }, 29 | }, 30 | }); 31 | app.router(() =>
); 32 | app.start(); 33 | 34 | expect(app._store.getState().count).toEqual(0); 35 | app._store.dispatch({ type: 'count/add' }); 36 | expect(app._store.getState().count).toEqual(1); 37 | }); 38 | 39 | test('subscription execute multiple times', async () => { 40 | const app = dva(); 41 | app.model({ 42 | namespace: 'count', 43 | state: 0, 44 | subscriptions: { 45 | setup({ history, dispatch }) { 46 | return history.listen(() => { 47 | dispatch({ 48 | type: 'add', 49 | }); 50 | }); 51 | }, 52 | }, 53 | reducers: { 54 | add(state) { 55 | return state + 1; 56 | }, 57 | }, 58 | }); 59 | 60 | const Count = connect(state => ({ count: state.count }))(function(props) { 61 | return
{props.count}
; 62 | }); 63 | 64 | function Home() { 65 | return
; 66 | } 67 | 68 | function Users() { 69 | return
; 70 | } 71 | 72 | app.router(({ history }) => { 73 | return ( 74 | 75 | <> 76 | Home 77 | Users 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }); 87 | 88 | const { getByTestId, getByText } = render(React.createElement(app.start())); 89 | expect(getByTestId('count').innerHTML).toEqual('1'); 90 | fireEvent.click(getByText('Users')); 91 | await delay(100); 92 | expect(getByTestId('count').innerHTML).toEqual('2'); 93 | fireEvent.click(getByText('Home')); 94 | await delay(100); 95 | expect(getByTestId('count').innerHTML).toEqual('3'); 96 | }); 97 | 98 | test('connect', () => { 99 | const app = dva(); 100 | app.model({ 101 | namespace: 'count', 102 | state: 0, 103 | reducers: { 104 | add(state) { 105 | return state + 1; 106 | }, 107 | }, 108 | }); 109 | const App = connect(state => ({ count: state.count }))(({ count, dispatch }) => { 110 | return ( 111 | <> 112 |
{count}
113 | 120 | 121 | ); 122 | }); 123 | app.router(() => ); 124 | 125 | const { getByTestId, getByText } = render(React.createElement(app.start())); 126 | expect(getByTestId('count').innerHTML).toEqual('0'); 127 | fireEvent.click(getByText('add')); 128 | expect(getByTestId('count').innerHTML).toEqual('1'); 129 | }); 130 | 131 | test('hooks api: useDispatch, useSelector shallowEqual, and useStore', () => { 132 | const app = dva(); 133 | app.model({ 134 | namespace: 'count', 135 | state: 0, 136 | reducers: { 137 | add(state) { 138 | return state + 1; 139 | }, 140 | }, 141 | }); 142 | 143 | const useShallowEqualSelector = selector => useSelector(selector, shallowEqual); 144 | 145 | const App = () => { 146 | const dispatch = useDispatch(); 147 | const store = useStore(); 148 | const { count } = useSelector(state => ({ count: state.count })); 149 | const { shallowEqualCount } = useShallowEqualSelector(state => ({ 150 | shallowEqualCount: state.count, 151 | })); 152 | 153 | return ( 154 | <> 155 |
{count}
156 |
{shallowEqualCount}
157 |
{store.getState().count}
158 | 165 | 166 | ); 167 | }; 168 | app.router(() => ); 169 | 170 | const { getByTestId, getByText } = render(React.createElement(app.start())); 171 | expect(getByTestId('count').innerHTML).toEqual('0'); 172 | expect(getByTestId('shallowEqualCount').innerHTML).toEqual('0'); 173 | fireEvent.click(getByText('add')); 174 | expect(getByTestId('count').innerHTML).toEqual('1'); 175 | expect(getByTestId('shallowEqualCount').innerHTML).toEqual('1'); 176 | expect(getByTestId('state').innerHTML).toEqual('1'); 177 | }); 178 | 179 | test('navigate', async () => { 180 | const history = createMemoryHistory({ 181 | initialEntries: ['/'], 182 | }); 183 | const app = dva({ 184 | history, 185 | }); 186 | 187 | function Home() { 188 | return

You are on Home

; 189 | } 190 | function Users() { 191 | return

You are on Users

; 192 | } 193 | app.router(({ history }) => { 194 | return ( 195 | 196 | <> 197 | Home 198 | Users 199 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | ); 213 | }); 214 | 215 | const { getByTestId, getByText } = render(React.createElement(app.start())); 216 | expect(getByTestId('title').innerHTML).toEqual('You are on Home'); 217 | fireEvent.click(getByText('Users')); 218 | await delay(100); 219 | expect(getByTestId('title').innerHTML).toEqual('You are on Users'); 220 | fireEvent.click(getByText('RouterRedux to Home')); 221 | await delay(100); 222 | expect(getByTestId('title').innerHTML).toEqual('You are on Home'); 223 | }); 224 | -------------------------------------------------------------------------------- /packages/dva/test/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import dva, { 4 | useDispatch, 5 | useSelector, 6 | useStore, 7 | useHistory, 8 | useLocation, 9 | useParams, 10 | useRouteMatch, 11 | } from '../src/index'; 12 | 13 | const countModel = { 14 | namespace: 'count', 15 | state: 0, 16 | reducers: { 17 | add(state, { payload }) { 18 | return state + payload || 1; 19 | }, 20 | minus(state, { payload }) { 21 | return state - payload || 1; 22 | }, 23 | }, 24 | }; 25 | 26 | describe('index', () => { 27 | xit('normal', () => { 28 | const app = dva(); 29 | app.model({ ...countModel }); 30 | app.router(() =>
); 31 | app.start('#root'); 32 | }); 33 | 34 | it('start without container', () => { 35 | const app = dva(); 36 | app.model({ ...countModel }); 37 | app.router(() =>
); 38 | app.start(); 39 | }); 40 | 41 | it('throw error if no routes defined', () => { 42 | const app = dva(); 43 | expect(() => { 44 | app.start(); 45 | }).toThrow(/router must be registered before app.start/); 46 | }); 47 | 48 | it('opts.initialState', () => { 49 | const app = dva({ 50 | initialState: { count: 1 }, 51 | }); 52 | app.model({ ...countModel }); 53 | app.router(() =>
); 54 | app.start(); 55 | expect(app._store.getState().count).toEqual(1); 56 | }); 57 | 58 | it('opts.onAction', () => { 59 | let count; 60 | const countMiddleware = () => () => () => { 61 | count += 1; 62 | }; 63 | 64 | const app = dva({ 65 | onAction: countMiddleware, 66 | }); 67 | app.router(() =>
); 68 | app.start(); 69 | 70 | count = 0; 71 | app._store.dispatch({ type: 'test' }); 72 | expect(count).toEqual(1); 73 | }); 74 | 75 | it('opts.onAction with array', () => { 76 | let count; 77 | const countMiddleware = () => next => action => { 78 | count += 1; 79 | next(action); 80 | }; 81 | const count2Middleware = () => next => action => { 82 | count += 2; 83 | next(action); 84 | }; 85 | 86 | const app = dva({ 87 | onAction: [countMiddleware, count2Middleware], 88 | }); 89 | app.router(() =>
); 90 | app.start(); 91 | 92 | count = 0; 93 | app._store.dispatch({ type: 'test' }); 94 | expect(count).toEqual(3); 95 | }); 96 | 97 | it('opts.extraEnhancers', () => { 98 | let count = 0; 99 | const countEnhancer = storeCreator => (reducer, preloadedState, enhancer) => { 100 | const store = storeCreator(reducer, preloadedState, enhancer); 101 | const oldDispatch = store.dispatch; 102 | store.dispatch = action => { 103 | count += 1; 104 | oldDispatch(action); 105 | }; 106 | return store; 107 | }; 108 | const app = dva({ 109 | extraEnhancers: [countEnhancer], 110 | }); 111 | app.router(() =>
); 112 | app.start(); 113 | 114 | app._store.dispatch({ type: 'test' }); 115 | expect(count).toEqual(1); 116 | }); 117 | 118 | it('opts.onStateChange', () => { 119 | let savedState = null; 120 | 121 | const app = dva({ 122 | onStateChange(state) { 123 | savedState = state; 124 | }, 125 | }); 126 | app.model({ 127 | namespace: 'count', 128 | state: 0, 129 | reducers: { 130 | add(state) { 131 | return state + 1; 132 | }, 133 | }, 134 | }); 135 | app.router(() =>
); 136 | app.start(); 137 | 138 | app._store.dispatch({ type: 'count/add' }); 139 | expect(savedState.count).toEqual(1); 140 | }); 141 | 142 | it('hooks should not be undifined', () => { 143 | [useSelector, useDispatch, useStore, useHistory, useLocation, useParams, useRouteMatch].forEach( 144 | hook => { 145 | expect(hook !== undefined).toEqual(true); 146 | }, 147 | ); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/dva/warnAboutDeprecatedCJSRequire.js: -------------------------------------------------------------------------------- 1 | var printWarning = function() {}; 2 | 3 | if (process.env.NODE_ENV !== 'production') { 4 | printWarning = function(format, subs) { 5 | var index = 0; 6 | var message = 7 | 'Warning: ' + 8 | (subs.length > 0 9 | ? format.replace(/%s/g, function() { 10 | return subs[index++]; 11 | }) 12 | : format); 13 | 14 | if (typeof console !== 'undefined') { 15 | console.error(message); 16 | } 17 | 18 | try { 19 | // --- Welcome to debugging history --- 20 | // This error was thrown as a convenience so that you can use the 21 | // stack trace to find the callsite that triggered this warning. 22 | throw new Error(message); 23 | } catch (e) {} 24 | }; 25 | } 26 | 27 | module.exports = function(member) { 28 | printWarning( 29 | 'Please use `require("dva").%s` instead of `require("dva/%s")`. ' + 30 | 'Support for the latter will be removed in the next major release.', 31 | [member, member] 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shell = require('shelljs'); 4 | const { join } = require('path'); 5 | const { fork } = require('child_process'); 6 | 7 | if ( 8 | shell 9 | .exec('npm config get registry') 10 | .stdout.indexOf('https://registry.npmjs.org/') === -1 11 | ) { 12 | console.error( 13 | 'Failed: set npm registry to https://registry.npmjs.org/ first', 14 | ); 15 | process.exit(1); 16 | } 17 | 18 | const cwd = process.cwd(); 19 | const ret = shell.exec('./node_modules/.bin/lerna updated').stdout; 20 | const updatedRepos = ret 21 | .split('\n') 22 | .map(line => line.replace('- ', '')) 23 | .filter(line => line !== ''); 24 | 25 | if (updatedRepos.length === 0) { 26 | console.log('No package is updated.'); 27 | process.exit(0); 28 | } 29 | 30 | const { code: buildCode } = shell.exec('npm run build'); 31 | if (buildCode === 1) { 32 | console.error('Failed: npm run build'); 33 | process.exit(1); 34 | } 35 | 36 | const cp = fork( 37 | join(process.cwd(), 'node_modules/.bin/lerna'), 38 | ['publish', '--skip-npm'].concat(process.argv.slice(2)), 39 | { 40 | stdio: 'inherit', 41 | cwd: process.cwd(), 42 | }, 43 | ); 44 | cp.on('error', err => { 45 | console.log(err); 46 | }); 47 | cp.on('close', code => { 48 | console.log('code', code); 49 | if (code === 1) { 50 | console.error('Failed: lerna publish'); 51 | process.exit(1); 52 | } 53 | 54 | publishToNpm(); 55 | }); 56 | 57 | function publishToNpm() { 58 | console.log(`repos to publish: ${updatedRepos.join(', ')}`); 59 | updatedRepos.forEach(repo => { 60 | shell.cd(join(cwd, 'packages', repo)); 61 | const { version } = require(join(cwd, 'packages', repo, 'package.json')); 62 | if ( 63 | version.includes('-rc.') || 64 | version.includes('-beta.') || 65 | version.includes('-alpha.') 66 | ) { 67 | console.log(`[${repo}] npm publish --tag next`); 68 | shell.exec(`npm publish --tag next`); 69 | } else { 70 | console.log(`[${repo}] npm publish`); 71 | shell.exec(`npm publish`); 72 | } 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | -------------------------------------------------------------------------------- /website/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dvajs.com", 3 | "version": 2, 4 | "builds": [ 5 | { "src": "package.json", "use": "@now/static-build" } 6 | ], 7 | "alias": [ 8 | "dvajs.com" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "now-build": "echo empty build", 4 | "deploy": "vuepress build ../docs --dest ./dist && now && now alias" 5 | }, 6 | "devDependencies": { 7 | "now": "^13.1.3", 8 | "vuepress": "^0.14.4" 9 | } 10 | } 11 | --------------------------------------------------------------------------------