diff --git a/package.json b/package.json
index 91b4c3c..0139322 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"precommit": "npm run lint"
},
"dependencies": {
- "antd": "^2.6.4",
+ "antd": "^2.8.3",
"babel-runtime": "^6.22.0",
"dva": "^1.2.0",
"dva-loading": "^0.2.0",
diff --git a/src/components/Logger/Logger.js b/src/components/Logger/Logger.js
new file mode 100644
index 0000000..544e603
--- /dev/null
+++ b/src/components/Logger/Logger.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { connect } from 'dva';
+
+function Logger({ logs }) {
+ return (
+
+
+
Logger
+
+ { logs.map(log => (- {log.text}
)) }
+
+
+ );
+}
+
+Logger.propTypes = {
+};
+
+function mapStateToProps(state) {
+ const { logs } = state.logger;
+ return {
+ logs,
+ };
+}
+export default connect(mapStateToProps)(Logger);
diff --git a/src/components/MainLayout/Header.js b/src/components/MainLayout/Header.js
index e247711..be1aa6f 100644
--- a/src/components/MainLayout/Header.js
+++ b/src/components/MainLayout/Header.js
@@ -15,10 +15,19 @@ function Header({ location }) {
Home
+
+ Tasks
+
+
+ A
+
+
+ B
+
404
-
+
dva
diff --git a/src/components/Reusable/Reusable.js b/src/components/Reusable/Reusable.js
new file mode 100644
index 0000000..156850d
--- /dev/null
+++ b/src/components/Reusable/Reusable.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { connect } from 'dva';
+
+function Reusable({ messages }) {
+ return (
+
+
+
Messages
+
+ { messages.map(message => (- {message.text}
)) }
+
+
+ );
+}
+
+Reusable.propTypes = {
+};
+
+function mapStateToProps(state) {
+ const { messages } = state.reusable;
+ return {
+ messages,
+ };
+}
+export default connect(mapStateToProps)(Reusable);
diff --git a/src/index.js b/src/index.js
index ec545b9..653003e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,9 @@ import { message } from 'antd';
import './index.html';
import './index.css';
+import logger from './models/logger';
+import reusable from './models/reusable';
+
const ERROR_MSG_DURATION = 3; // 3 秒
// 1. Initialize
@@ -20,6 +23,8 @@ app.use(createLoading());
// 3. Model
// Moved to router.js
+app.model(logger);
+app.model(reusable);
// 4. Router
app.router(require('./router'));
diff --git a/src/models/a.js b/src/models/a.js
new file mode 100644
index 0000000..9d2e606
--- /dev/null
+++ b/src/models/a.js
@@ -0,0 +1,15 @@
+import { delay } from '../services/delay';
+
+export default {
+ namespace: 'a',
+ state: '',
+ reducers: {
+ },
+ effects: {
+ *foo(action, { call, put }) {
+ yield call(delay, 1000);
+ yield put({ type: 'reusable/addMessage', payload: '666' });
+ },
+ },
+};
+
diff --git a/src/models/b.js b/src/models/b.js
new file mode 100644
index 0000000..5a84177
--- /dev/null
+++ b/src/models/b.js
@@ -0,0 +1,28 @@
+import { delay } from '../services/delay';
+
+export default {
+ namespace: 'b',
+ state: '',
+ reducers: {
+ },
+ effects: {
+ addWatcher: [function *watcher({ take, put }) {
+ /* eslint-disable */
+ while (true) {
+ /* eslint-enable */
+ // watch foo action from model a
+ const data1 = yield take('a/foo');
+ yield put({ type: 'reusable/addMessage', payload: `observe an action from model A: ${data1}` });
+
+ // watch addLogSuccess action from model reuseable
+ const data2 = yield take('reusable/addMessageSuccess');
+ yield put({ type: 'reusable/addMessage', payload: `observe an action from model reusable: ${data2}` });
+ }
+ }, { type: 'watcher' }],
+
+ *foo(action, { call, put }) {
+ yield call(delay, 3000);
+ yield put({ type: 'reusable/addMessage', payload: '233' });
+ },
+ },
+};
diff --git a/src/models/logger.js b/src/models/logger.js
new file mode 100644
index 0000000..b0c9b28
--- /dev/null
+++ b/src/models/logger.js
@@ -0,0 +1,21 @@
+export default {
+ namespace: 'logger',
+ state: {
+ logs: [],
+ },
+ reducers: {
+ addLog(state, { payload }) {
+ const log = {
+ id: state.logs.length,
+ text: payload,
+ };
+
+ const logs = state.logs.concat([log]);
+ return {
+ logs,
+ };
+ },
+ },
+ effects: {
+ },
+};
diff --git a/src/models/reusable.js b/src/models/reusable.js
new file mode 100644
index 0000000..49c7a18
--- /dev/null
+++ b/src/models/reusable.js
@@ -0,0 +1,34 @@
+import { delay } from '../services/delay';
+
+export default {
+ namespace: 'reusable',
+ state: {
+ messages: [],
+ },
+ reducers: {
+ addMessageSuccess(state, { payload }) {
+ const message = {
+ id: state.messages.length,
+ text: payload,
+ };
+
+ const messages = state.messages.concat([message]);
+ return {
+ messages,
+ };
+ },
+ },
+ effects: {
+ *addMessage(action, { call, put }) {
+ const { payload } = action;
+ const { resolve } = payload;
+ yield call(delay, 1000);
+
+ if (resolve) {
+ resolve(payload);
+ }
+
+ yield put({ type: 'addMessageSuccess', payload });
+ },
+ },
+};
diff --git a/src/models/task.js b/src/models/task.js
new file mode 100644
index 0000000..5f044ab
--- /dev/null
+++ b/src/models/task.js
@@ -0,0 +1,44 @@
+import { delay } from '../services/delay';
+import { makeService } from '../services/makeService';
+
+export default {
+ namespace: 'task',
+ state: {
+ },
+ reducers: {
+ },
+ effects: {
+ *sequential(action, { call, put }) {
+ yield call(delay, 3000);
+ yield put({ type: 'logger/addLog', payload: 'step1' });
+ yield call(delay, 5000);
+ yield put({ type: 'logger/addLog', payload: 'step2' });
+ },
+ *parallel(action, { call, put }) {
+ yield put({ type: 'logger/addLog', payload: 'all started' });
+
+ // 注意这里不要写yield*,不然会顺序执行的
+ yield [
+ call(makeService(3000), 'service 1'),
+ call(makeService(5000), 'service 2'),
+ call(makeService(7000), 'service 3'),
+ ];
+
+ yield put({ type: 'logger/addLog', payload: 'all completed' });
+ },
+ *race(action, { call, put, race }) {
+ yield put({ type: 'logger/addLog', payload: 'start race' });
+
+ const { data, timeout } = yield race({
+ data: call(makeService(2000), 'some data'),
+ timeout: call(delay, 3000),
+ });
+
+ if (data) {
+ yield put({ type: 'logger/addLog', payload: data });
+ } else {
+ yield put({ type: 'logger/addLog', payload: `timeout: ${timeout}` });
+ }
+ },
+ },
+};
diff --git a/src/models/users.js b/src/models/users.js
index 3914c5d..a066b6d 100644
--- a/src/models/users.js
+++ b/src/models/users.js
@@ -1,6 +1,9 @@
import * as usersService from '../services/users';
-export default {
+// selector放在这里,而且要export出来,不然不好写测试……
+export const pageSelector = state => state.users.page;
+
+export const user = {
namespace: 'users',
state: {
list: [],
@@ -37,7 +40,7 @@ export default {
yield put({ type: 'reload' });
},
*reload(action, { put, select }) {
- const page = yield select(state => state.users.page);
+ const page = yield select(pageSelector);
yield put({ type: 'fetch', payload: { page } });
},
},
diff --git a/src/router.js b/src/router.js
index d86e921..dfd6525 100644
--- a/src/router.js
+++ b/src/router.js
@@ -25,11 +25,41 @@ function RouterConfig({ history, app }) {
name: 'UsersPage',
getComponent(nextState, cb) {
require.ensure([], (require) => {
- registerModel(app, require('./models/users'));
+ registerModel(app, require('./models/users').user);
cb(null, require('./routes/Users'));
});
},
},
+ {
+ path: '/tasks',
+ name: 'TasksPage',
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ cb(null, require('./routes/Tasks'));
+ registerModel(app, require('./models/task'));
+ });
+ },
+ },
+ {
+ path: '/a',
+ name: 'APage',
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ cb(null, require('./routes/A'));
+ registerModel(app, require('./models/a'));
+ });
+ },
+ },
+ {
+ path: '/b',
+ name: 'BPage',
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ cb(null, require('./routes/B'));
+ registerModel(app, require('./models/b'));
+ });
+ },
+ },
];
return ;
diff --git a/src/routes/A.js b/src/routes/A.js
new file mode 100644
index 0000000..8ac2d33
--- /dev/null
+++ b/src/routes/A.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { connect } from 'dva';
+import MainLayout from '../components/MainLayout/MainLayout';
+import Reusable from '../components/Reusable/Reusable';
+import Logger from '../components/Logger/Logger';
+
+function A({ dispatch, location }) {
+ const bar = () => {
+ new Promise((resolve, reject) => {
+ dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } });
+ })
+ .then(() => {
+ // console.log(`after a long time, ${data} returns`);
+ });
+ };
+
+ return (
+
+ Component A
+
+
+
+
+
+
+
+ );
+}
+
+A.propTypes = {
+};
+
+export default connect()(A);
diff --git a/src/routes/B.js b/src/routes/B.js
new file mode 100644
index 0000000..c267c85
--- /dev/null
+++ b/src/routes/B.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import { connect } from 'dva';
+import MainLayout from '../components/MainLayout/MainLayout';
+import Reusable from '../components/Reusable/Reusable';
+import Logger from '../components/Logger/Logger';
+
+function B({ dispatch, location }) {
+ return (
+
+ Component B
+
+
+
+
+
+
+ );
+}
+
+B.propTypes = {
+};
+
+export default connect()(B);
diff --git a/src/routes/Tasks.js b/src/routes/Tasks.js
new file mode 100644
index 0000000..d4a0793
--- /dev/null
+++ b/src/routes/Tasks.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { connect } from 'dva';
+import MainLayout from '../components/MainLayout/MainLayout';
+import Logger from '../components/Logger/Logger';
+
+function Tasks({ dispatch, location }) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+Tasks.propTypes = {
+};
+
+export default connect()(Tasks);
diff --git a/src/services/delay.js b/src/services/delay.js
new file mode 100644
index 0000000..09b2154
--- /dev/null
+++ b/src/services/delay.js
@@ -0,0 +1,5 @@
+export const delay = (timeout) => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, timeout);
+ });
+};
diff --git a/src/services/makeService.js b/src/services/makeService.js
new file mode 100644
index 0000000..c9783d3
--- /dev/null
+++ b/src/services/makeService.js
@@ -0,0 +1,12 @@
+export const makeService = (time) => {
+ return (data) => {
+ // console.log(`${data} start`);
+
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(data);
+ // console.log(`${data} end`);
+ }, time);
+ });
+ };
+};
diff --git a/test/a.spec.js b/test/a.spec.js
new file mode 100644
index 0000000..d1bf0ea
--- /dev/null
+++ b/test/a.spec.js
@@ -0,0 +1,26 @@
+import expect from 'expect';
+import { effects } from 'dva/saga';
+import { delay } from '../src/services/delay';
+import a from '../src/models/a';
+
+describe('A Model', () => {
+ it('loads', () => {
+ expect(a).toExist();
+ });
+
+ describe('effects', () => {
+ it('foo should work', () => {
+ const { call, put } = effects;
+
+ const sagas = a.effects;
+ const saga = sagas.foo;
+ const generator = saga({ type: 'a/foo' }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(delay, 1000));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'reusable/addMessage', payload: '666' }));
+ });
+ });
+});
diff --git a/test/b.spec.js b/test/b.spec.js
new file mode 100644
index 0000000..600f346
--- /dev/null
+++ b/test/b.spec.js
@@ -0,0 +1,26 @@
+import expect from 'expect';
+import { effects } from 'dva/saga';
+import { delay } from '../src/services/delay';
+import b from '../src/models/b';
+
+describe('B Model', () => {
+ it('loads', () => {
+ expect(b).toExist();
+ });
+
+ describe('effects', () => {
+ it('foo should work', () => {
+ const { call, put } = effects;
+
+ const sagas = b.effects;
+ const saga = sagas.foo;
+ const generator = saga({ type: 'b/foo' }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(delay, 3000));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'reusable/addMessage', payload: '233' }));
+ });
+ });
+});
diff --git a/test/logger.spec.js b/test/logger.spec.js
new file mode 100644
index 0000000..70ceaba
--- /dev/null
+++ b/test/logger.spec.js
@@ -0,0 +1,19 @@
+import expect from 'expect';
+import logger from '../src/models/logger';
+
+describe('Logger Model', () => {
+ it('loads', () => {
+ expect(logger).toExist();
+ });
+
+ describe('reducers', () => {
+ it('addLog should work', () => {
+ const reducers = logger.reducers;
+ const reducer = reducers.addLog;
+ const state = {
+ logs: [],
+ };
+ expect(reducer(state, { payload: 'log' })).toEqual({ logs: [{ id: 0, text: 'log' }] });
+ });
+ });
+});
diff --git a/test/reusable.spec.js b/test/reusable.spec.js
new file mode 100644
index 0000000..6f37194
--- /dev/null
+++ b/test/reusable.spec.js
@@ -0,0 +1,37 @@
+import expect from 'expect';
+import { effects } from 'dva/saga';
+import { delay } from '../src/services/delay';
+import reusable from '../src/models/reusable';
+
+describe('Reusable Model', () => {
+ it('loads', () => {
+ expect(reusable).toExist();
+ });
+
+ describe('reducers', () => {
+ it('addMessageSuccess should work', () => {
+ const reducers = reusable.reducers;
+ const reducer = reducers.addMessageSuccess;
+ const state = {
+ messages: [],
+ };
+ expect(reducer(state, { payload: 'message' })).toEqual({ messages: [{ id: 0, text: 'message' }] });
+ });
+ });
+
+ describe('effects', () => {
+ it('addMessage should work', () => {
+ const { call, put } = effects;
+
+ const sagas = reusable.effects;
+ const saga = sagas.addMessage;
+ const generator = saga({ type: 'reusable/addMessage', payload: '666' }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(delay, 1000));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'addMessageSuccess', payload: '666' }));
+ });
+ });
+});
diff --git a/test/task.spec.js b/test/task.spec.js
new file mode 100644
index 0000000..5beb6a2
--- /dev/null
+++ b/test/task.spec.js
@@ -0,0 +1,80 @@
+import expect from 'expect';
+import { effects } from 'dva/saga';
+
+import { delay } from '../src/services/delay';
+import { makeService } from '../src/services/makeService';
+
+import task from '../src/models/task';
+
+describe('Task Model', () => {
+ it('loads', () => {
+ expect(task).toExist();
+ });
+
+ describe('effects', () => {
+ it('sequential should work', () => {
+ const { call, put } = effects;
+
+ const sagas = task.effects;
+ const saga = sagas.sequential;
+ const generator = saga({ type: 'task/sequential' }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(delay, 3000));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'step1' }));
+
+ next = generator.next();
+ expect(next.value).toEqual(call(delay, 5000));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'step2' }));
+ });
+
+ it('parallel should work', () => {
+ const { call, put } = effects;
+
+ const sagas = task.effects;
+ const saga = sagas.parallel;
+ const generator = saga({ type: 'task/parallel' }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'all started' }));
+
+ next = generator.next();
+ expect(next.value).toEqual([
+ call(makeService(3000), 'service 1'),
+ call(makeService(5000), 'service 2'),
+ call(makeService(7000), 'service 3'),
+ ]);
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'all completed' }));
+ });
+
+ it('race shold work', () => {
+ const { call, put, race } = effects;
+
+ const sagas = task.effects;
+ const saga = sagas.race;
+ const generator = saga({ type: 'task/race' }, { call, put, race });
+
+ let next = generator.next();
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'start race' }));
+
+ next = generator.next();
+ expect(next.value).toEqual(race({
+ data: call(makeService(2000), 'some data'),
+ timeout: call(delay, 3000),
+ }));
+
+ // 因为在单元测试里,跑不了被测逻辑的那个if
+ // 那个if是依赖于实际执行逻辑的,但saga的单元测试只是测action的序列化结果是否一致,并不跑真实逻辑
+ // 所以要手动注入一个结果进去
+ const data = 'some data';
+ next = generator.next({ data });
+ expect(next.value).toEqual(put({ type: 'logger/addLog', payload: data }));
+ });
+ });
+});
diff --git a/test/user.spec.js b/test/user.spec.js
new file mode 100644
index 0000000..768a96d
--- /dev/null
+++ b/test/user.spec.js
@@ -0,0 +1,143 @@
+import expect from 'expect';
+import { effects } from 'dva/saga';
+
+import * as usersService from '../src/services/users';
+
+import { user, pageSelector } from '../src/models/users';
+
+describe('User Model', () => {
+ it('loads', () => {
+ expect(user).toExist();
+ });
+
+ describe('reducers', () => {
+ it('save should work', () => {
+ const reducers = user.reducers;
+ const reducer = reducers.save;
+ const state = {
+ list: [],
+ total: null,
+ page: null,
+ };
+
+ const action = {
+ type: 'save',
+ payload: {
+ data: [{ id: '01' }, { id: '02' }],
+ total: 111,
+ page: 1,
+ },
+ };
+
+ expect(reducer(state, action)).toEqual({
+ list: [{ id: '01' }, { id: '02' }],
+ total: 111,
+ page: 1,
+ });
+ });
+ });
+
+ describe('effects', () => {
+ it('fetch should work', () => {
+ const { call, put } = effects;
+
+ const sagas = user.effects;
+ const saga = sagas.fetch;
+
+ const generator = saga({ type: 'fetch', payload: { page: 1 } }, { call, put });
+
+ let next = generator.next();
+ const page = 1;
+ expect(next.value).toEqual(call(usersService.fetch, { page }));
+
+ next = generator.next({ data: [],
+ headers: {
+ 'x-total-count': '111',
+ },
+ });
+
+ expect(next.value).toEqual(put({
+ type: 'save',
+ payload: {
+ data: [],
+ total: 111,
+ page: 1,
+ },
+ }));
+ });
+
+ it('remove should work', () => {
+ const { call, put } = effects;
+
+ const sagas = user.effects;
+ const saga = sagas.remove;
+
+ const id = 'id111';
+
+ const generator = saga({ type: 'remove', payload: id }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(usersService.remove, id));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'reload' }));
+ });
+
+ it('patch should work', () => {
+ const { call, put } = effects;
+
+ const sagas = user.effects;
+ const saga = sagas.patch;
+
+ const id = 'id111';
+ const values = [];
+
+ const generator = saga({ type: 'patch', payload: { id, values } }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(usersService.patch, id, values));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'reload' }));
+ });
+
+ it('create should work', () => {
+ const { call, put } = effects;
+
+ const sagas = user.effects;
+ const saga = sagas.create;
+
+ const values = [];
+
+ const generator = saga({ type: 'create', payload: values }, { call, put });
+
+ let next = generator.next();
+ expect(next.value).toEqual(call(usersService.create, values));
+
+ next = generator.next();
+ expect(next.value).toEqual(put({ type: 'reload' }));
+ });
+
+ it('reload should work', () => {
+ const { select, put } = effects;
+
+ const sagas = user.effects;
+ const saga = sagas.reload;
+
+ const generator = saga({ type: 'reload' }, { select, put });
+
+ const fakeState = {
+ users: {
+ page: 1,
+ },
+ };
+ let next = generator.next(fakeState);
+ // 下面这句的select,源码和测试两边需要是同一个selector函数才可以……
+ // 然而,如果源model里面不把这个selector命名并且export出来,这里肯定过不了
+ expect(next.value).toEqual(select(pageSelector));
+
+ next = generator.next(2);
+ expect(next.value).toEqual(put({ type: 'fetch', payload: { page: 2 } }));
+ });
+ });
+});