Repository: dvajs/dva
Pull request #59 (2016-08-31 06:00:37)

Add on effect hook
比如：实现 effect 执行时，自动切换 loading 状态。

定义插件：

```javascript
const SHOW = '@@LOADING/SHOW';
const HIDE = '@@LOADING/HIDE';

function autoLoading() {
  return {
    extraReducers: {
      loading(state, action) {
        switch (action.type) {
          case SHOW:
            return true;
          case HIDE:
            return false;
          default:
            return false;
        }
      },
    },
    onEffect(effect, { put }, model) {
      modelNamespace = model.namespace;
      return function*() {
        count = count * 2;
        yield put({ type: SHOW });
        yield effect();
        yield put({ type: HIDE });
      };
    },
  };
}
```

使用：

```javascript
const app = dva();
app.use(autoLoading());
```

然后在 `state.loading` 就可以自动切换状态了。



Patch:
diff --git a/README.md b/README.md
index fafc100..4a35134 100644
--- a/README.md
+++ b/README.md
@@ -130,8 +130,9 @@ dva 的插件机制是通过 hooks 实现的，用于添加自定义行为和监
 - `onAction(Array|Function):` 等同于 redux middleware，支持数组
 - `onStateChange(listener):` 绑定 listner，state 变化时触发
 - `onReducer(reducerEnhancer):` 应用全局的 reducer enhancer，比如 [redux-undo](https://github.com/omnidan/redux-undo)
+- `onEffect(Function):` 封装 effect 方法的处理，比如可以实现自动切换 loading 状态
 - `onHmr(render => {}):` 提供 render 方法用于重新渲染 routes 和 components，暂还不支持 model
-- `extraReducers(obj):` 提供额外的 reducers，比如 [redux-form](https://github.com/erikras/redux-form) 需要全局 reducer `form`
+- `extraReducers(Object):` 提供额外的 reducers，比如 [redux-form](https://github.com/erikras/redux-form) 需要全局 reducer `form`
 
 ### `app.model(obj)`
 
diff --git a/package.json b/package.json
index 0006674..6338af5 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,9 @@
   "description": "Front-end framework based on react, redux, react-redux, react-router and redux-saga, inspired by elm and choo.",
   "repository": {
     "type": "git",
-    "url": "https://github.com/sorrycc/dva"
+    "url": "https://github.com/dvajs/dva"
   },
-  "homepage": "https://github.com/sorrycc/dva",
+  "homepage": "https://github.com/dvajs/dva",
   "keywords": [
     "dva",
     "ant",
@@ -23,7 +23,8 @@
   "author": "chencheng <sorrycc@gmail.com>",
   "license": "MIT",
   "scripts": {
-    "test": "cross-env NODE_ENV=test nyc mocha --require ./test/setup.js --no-timeouts",
+    "test": "cross-env NODE_ENV=test nyc mocha --no-timeouts",
+    "debug": "mocha --require babel-register --require ./test/setup.js --no-timeouts",
     "build": "rimraf lib && babel src --out-dir lib",
     "lint": "eslint --ext .js src test",
     "report": "nyc report --reporter=html",
@@ -34,7 +35,8 @@
       "src/**/*.js"
     ],
     "require": [
-      "babel-register"
+      "babel-register",
+      "./test/setup.js"
     ],
     "sourceMap": false,
     "instrument": false
diff --git a/src/createDva.js b/src/createDva.js
index d26e586..41cdea6 100644
--- a/src/createDva.js
+++ b/src/createDva.js
@@ -267,17 +267,20 @@ export default function createDva(createOpts) {
         }
       }
 
+      const onEffect = plugin.get('onEffect');
+      const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model);
+
       switch (type) {
         case 'watcher':
           return sagaWithCatch;
         case 'takeLatest':
           return function*() {
-            yield takeLatest(key, sagaWithCatch);
+            yield takeLatest(key, sagaWithOnEffect);
           };
         // takeEvery
         default:
           return function*() {
-            yield takeEvery(key, sagaWithCatch);
+            yield takeEvery(key, sagaWithOnEffect);
           };
       }
     }
@@ -321,5 +324,12 @@ export default function createDva(createOpts) {
       };
     }
 
+    function applyOnEffect(fns, effect, model) {
+      for (const fn of fns) {
+        effect = fn(effect, sagaEffects, model);
+      }
+      return effect;
+    }
+
   };
 }
diff --git a/src/plugin.js b/src/plugin.js
index 36f84f9..d47ff69 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -10,6 +10,7 @@ class Plugin {
       onAction: [],
       onHmr: [],
       onReducer: [],
+      onEffect: [],
       extraReducers: [],
     };
   }


Test patch:
diff --git a/test/effects-test.js b/test/effects-test.js
index 34ade5d..8bcfc85 100644
--- a/test/effects-test.js
+++ b/test/effects-test.js
@@ -203,5 +203,81 @@ describe('effects', () => {
     }).toThrow(/app.start: effect type should be takeEvery, takeLatest or watcher/);
   });
 
+  it.only('onEffect', done => {
+    const SHOW = '@@LOADING/SHOW';
+    const HIDE = '@@LOADING/HIDE';
+
+    const app = dva();
+
+    // Test model should be accessible
+    let modelNamespace = null;
+    // Test onEffect should be run orderly
+    let count = 0;
+
+    app.use({
+      extraReducers: {
+        loading(state, action) {
+          switch (action.type) {
+            case SHOW:
+              return true;
+            case HIDE:
+              return false;
+            default:
+              return false;
+          }
+        },
+      },
+      onEffect(effect, { put }, model) {
+        modelNamespace = model.namespace;
+        return function*(...args) {
+          count = count * 2;
+          yield put({ type: SHOW });
+          yield effect(...args);
+          yield put({ type: HIDE });
+        };
+      },
+    });
+
+    app.use({
+      onEffect(effect) {
+        return function*(...args) {
+          count = count + 2;
+          yield effect(...args);
+          count = count + 1;
+        };
+      },
+    });
+
+    app.model({
+      namespace: 'count',
+      state: 0,
+      reducers: {
+        add(state) { return state + 1; },
+      },
+      effects: {
+        *addRemote(action, { put }) {
+          yield delay(100);
+          yield put({ type: 'add' });
+        },
+      },
+    });
+
+    app.router(_ => <div />);
+    app.start();
+
+    expect(app._store.getState().loading).toEqual(false);
+
+    app._store.dispatch({ type: 'count/addRemote' });
+    expect(app._store.getState().loading).toEqual(true);
+    expect(modelNamespace).toEqual('count');
+
+    setTimeout(_ => {
+      expect(app._store.getState().loading).toEqual(false);
+      expect(app._store.getState().count).toEqual(1);
+      expect(count).toEqual(5);
+      done();
+    }, 200);
+  });
+
 });