├── .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 | [](https://codecov.io/gh/dvajs/dva)
6 | [](https://circleci.com/gh/dvajs/dva)
7 | [](https://npmjs.org/package/dva)
8 | [](https://travis-ci.org/dvajs/dva)
9 | [](https://coveralls.io/r/dvajs/dva)
10 | [](https://npmjs.org/package/dva)
11 | [](https://david-dm.org/dvajs/dva)
12 | [](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 | [](https://codecov.io/gh/dvajs/dva)
6 | [](https://circleci.com/gh/dvajs/dva)
7 | [](https://npmjs.org/package/dva)
8 | [](https://travis-ci.org/dvajs/dva)
9 | [](https://coveralls.io/r/dvajs/dva)
10 | [](https://npmjs.org/package/dva)
11 | [](https://david-dm.org/dvajs/dva)
12 | [](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 | 
13 |
14 | 按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是 ``
15 |
16 | ` ` 以及`` 本身不维持任何 state, 完全由父节点`` 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: `Pure Component`
17 |
18 | ## 图解二: Redux 表示法
19 |
20 | React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store
21 |
22 | 
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 | 
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 | 
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 |
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 |
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 |
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 |
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 | [](https://npmjs.org/package/dva-core)
4 | [](https://travis-ci.org/dvajs/dva-core)
5 | [](https://coveralls.io/r/dvajs/dva-core)
6 | [](https://npmjs.org/package/dva-core)
7 | [](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 | [](https://npmjs.org/package/dva-immer)
4 | [](https://travis-ci.org/dvajs/dva-immer)
5 | [](https://coveralls.io/r/dvajs/dva-immer)
6 | [](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 | [](https://npmjs.org/package/dva-loading)
4 | [](https://travis-ci.org/dvajs/dva-loading)
5 | [](https://coveralls.io/r/dvajs/dva-loading)
6 | [](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 | [](https://npmjs.org/package/dva)
4 | [](https://travis-ci.org/dvajs/dva)
5 | [](https://coveralls.io/r/dvajs/dva)
6 | [](https://npmjs.org/package/dva)
7 | [](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 |
--------------------------------------------------------------------------------