1.dva介绍 #

dva

1.1 前置知识 #

2. 初始化项目 #

create-react-app zhufeng-dva-source
cd  zhufeng-dva-source
cnpm install dva  redux react-redux redux-saga react-router-dom connected-react-router  --save
npm start

3. 基本计数器 #

3.1 使用 #

3.1.1 index.js #

import React from 'react';
import dva, { connect } from './dva';
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter);
app.router(() => <ConnectedCounter />);
app.start('#root');

3.2 实现 #

3.2.1 dva\index.js #

src\dva\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { connect, Provider } from 'react-redux';
import prefixNamespace from './prefixNamespace';
export { connect };

function dva() {
    const app = {
        _models: [],
        model,
        router,
        _router: null,
        start
    }
    const initialReducers = {};
    function model(model) {
        const prefixedModel = prefixNamespace(model);
        app._models.push(prefixedModel);
        return prefixedModel;
    }
    function router(router) {
        app._router = router;
    }

    function start(root) {
        for (const model of app._models) {
            initialReducers[model.namespace] = getReducer(model);
        }
        let rootReducer = createReducer();
        let store = createStore(rootReducer);
        ReactDOM.render(<Provider store={store}>{app._router()}</Provider>, document.querySelector(root));
        function createReducer() {
            return combineReducers(initialReducers);
        }
    }

    return app;
}

function getReducer(model) {
    let { reducers, state: defaultState } = model;
    let reducer = (state = defaultState, action) => {
        let reducer = reducers[action.type];
        if (reducer) {
            return reducer(state, action);
        }
        return state;
    }
    return reducer;
}

export default dva;

3.2.2 prefixNamespace.js #

src\dva\prefixNamespace.js

import { NAMESPACE_SEP } from './constants';
function prefix(obj, namespace) {
    return Object.keys(obj).reduce((memo, key) => {
        const newKey = `${namespace}${NAMESPACE_SEP}${key}`;
        memo[newKey] = obj[key];
        return memo;
    }, {});
}
export default function prefixNamespace(model) {
    if (model.reducers)
        model.reducers = prefix(model.reducers, model.namespace);
    return model;
}

3.2.3 constants.js #

src\dva\constants.js

export const NAMESPACE_SEP = '/';

4. 支持effects #

4.1 使用 #

4.1.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from './dva';
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
+    effects: {
+        *asyncAdd(action, { call, put }) {
+            yield call(delay, 1000);
+            yield put({ type: 'counter/add' });
+        }
+    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
+            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter);
app.router(() => <ConnectedCounter />);
app.start('#root');

+function delay(ms) {
+    return new Promise((resolve) => {
+        setTimeout(function () {
+            resolve();
+        }, ms);
+    });
+}

4.2 实现 #

4.2.1 dva\index.js #

src\dva\index.js

import React from 'react';
import ReactDOM from 'react-dom';
+import { createStore, combineReducers, applyMiddleware } from 'redux';
+import createSagaMiddleware from 'redux-saga';
+import * as sagaEffects from 'redux-saga/effects';
+import { NAMESPACE_SEP } from './constants';
import { connect, Provider } from 'react-redux';
import prefixNamespace from './prefixNamespace';
export { connect };

function dva() {
    const app = {
        _models: [],
        model,
        router,
        _router: null,
        start
    }
    const initialReducers = {};
    function model(model) {
        const prefixedModel = prefixNamespace(model);
        app._models.push(prefixedModel);
        return prefixedModel;
    }
    function router(router) {
        app._router = router;
    }

    function start(root) {
        for (const model of app._models) {
            initialReducers[model.namespace] = getReducer(model);
        }
        let rootReducer = createReducer();
+        const sagas = getSagas(app);
+        const sagaMiddleware = createSagaMiddleware();
+        let store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
+        sagas.forEach(saga => sagaMiddleware.run(saga));
        ReactDOM.render(<Provider store={store}>{app._router()}</Provider>, document.querySelector(root));
        function createReducer() {
            return combineReducers(initialReducers);
        }
    }
+    function getSagas(app) {
+        let sagas = [];
+        for (const model of app._models) {
+            sagas.push(getSaga(model.effects, model));
+        }
+        return sagas;
+    }
    return app;
}
+function getSaga(effects, model) {
+    return function* () {
+        for (const key in effects) {
+            const watcher = getWatcher(key, model.effects[key], model);
+            yield sagaEffects.fork(watcher);
+        }
+    };
+}
+function getWatcher(key, effect, model) {
+    return function* () {
+        yield sagaEffects.takeEvery(key, function* sagaWithCatch(...args) {
+            yield effect(...args, { ...sagaEffects, put: action => sagaEffects.put({ ...action, +type: prefixType(action.type, model) }) });
+        });
+    };
+}
+function prefixType(type, model) {
+    if (type.indexOf('/') === -1) {
+        return `${model.namespace}${NAMESPACE_SEP}${type}`;
+    }
+    return type;
+}
function getReducer(model) {
    let { reducers, state: defaultState } = model;
    let reducer = (state = defaultState, action) => {
        let reducer = reducers[action.type];
        if (reducer) {
            return reducer(state, action);
        }
        return state;
    }
    return reducer;
}

export default dva;

4.2.2 prefixNamespace.js #

src\dva\prefixNamespace.js

import { NAMESPACE_SEP } from './constants';
function prefix(obj, namespace) {
    return Object.keys(obj).reduce((memo, key) => {
        const newKey = `${namespace}${NAMESPACE_SEP}${key}`;
        memo[newKey] = obj[key];
        return memo;
    }, {});
}
export default function prefixNamespace(model) {
    if (model.reducers)
        model.reducers = prefix(model.reducers, model.namespace);
+    if (model.effects) {
+       model.effects = prefix(model.effects, model.namespace);
+    }    
    return model;
}

5. 支持路由 #

5.1 使用 #

5.1.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from './dva';
+import { Router, Route,Link } from './dva/router';
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
    effects: {
        *asyncAdd(action, { call, put }) {
            yield call(delay, 1000);
            yield put({ type: 'counter/add' });
        }
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter);
+const Home = () => <div>Home</div>
+app.router((api) => (
+    <Router history={api.history}>
+        <>
+            <Link to="/">Home</Link>
+            <Link to="/counter">Counter</Link>
+            <Route path="/" exact={true} component={Home} />
+            <Route path="/counter" component={ConnectedCounter} />
+        </>
+    </Router>
+));
app.start('#root');

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(function () {
            resolve();
        }, ms);
    });
}

5.2 实现 #

5.2.1 dva\index.js #

src\dva\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import * as sagaEffects from 'redux-saga/effects';
import { NAMESPACE_SEP } from './constants';
import { connect, Provider } from 'react-redux';
import prefixNamespace from './prefixNamespace';
+import { createHashHistory } from 'history';
+let history = createHashHistory();
export { connect };

function dva() {
    const app = {
        _models: [],
        model,
        router,
        _router: null,
        start
    }
    const initialReducers = {};
    function model(model) {
        const prefixedModel = prefixNamespace(model);
        app._models.push(prefixedModel);
        return prefixedModel;
    }
    function router(router) {
        app._router = router;
    }

    function start(root) {
        for (const model of app._models) {
            initialReducers[model.namespace] = getReducer(model);
        }
        let rootReducer = createReducer();
        const sagas = getSagas(app);
        const sagaMiddleware = createSagaMiddleware();
        let store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
        sagas.forEach(saga => sagaMiddleware.run(saga));
+        ReactDOM.render(<Provider store={store}>{app._router({ history })}</Provider>, document.querySelector(root))
        function createReducer() {
            return combineReducers(initialReducers);
        }
    }
    function getSagas(app) {
        let sagas = [];
        for (const model of app._models) {
            sagas.push(getSaga(model.effects, model));
        }
        return sagas;
    }
    return app;
}
function getSaga(effects, model) {
    return function* () {
        for (const key in effects) {
            const watcher = getWatcher(key, model.effects[key], model);
            yield sagaEffects.fork(watcher);
        }
    };
}
function getWatcher(key, effect, model) {
    return function* () {
        yield sagaEffects.takeEvery(key, function* sagaWithCatch(...args) {
            yield effect(...args, { ...sagaEffects, put: action => sagaEffects.put({ ...action, type: prefixType(action.type, model) }) });
        });
    };
}
function prefixType(type, model) {
    if (type.indexOf('/') === -1) {
        return `${model.namespace}${NAMESPACE_SEP}${type}`;
    }
    return type;
}
function getReducer(model) {
    let { reducers, state: defaultState } = model;
    let reducer = (state = defaultState, action) => {
        let reducer = reducers[action.type];
        if (reducer) {
            return reducer(state, action);
        }
        return state;
    }
    return reducer;
}

export default dva;

5.2.2 router.js #

src\dva\router.js

export * from 'react-router-dom';

6. 跳转路径 #

6.1 使用 #

6.1.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from './dva';
+import { Router, Route,Link,routerRedux } from './dva/router';
+import { ConnectedRouter } from 'connected-react-router';
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
    effects: {
        *asyncAdd(action, { call, put }) {
            yield call(delay, 1000);
            yield put({ type: 'counter/add' });
        },
+       *goto({ to }, { put }) {
+          yield put(routerRedux.push(to));
+       }
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
+            <button onClick={() => props.dispatch({ type: "counter/goto", to: '/' })}>跳转到/</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter);
const Home = () => <div>Home</div>
app.router((api) => (
+    <ConnectedRouter  history={api.history}>
        <>
            <Link to="/">Home</Link>
            <Link to="/counter">Counter</Link>
            <Route path="/" exact={true} component={Home} />
            <Route path="/counter" component={ConnectedCounter} />
        </>
    </ConnectedRouter >
));
app.start('#root');

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(function () {
            resolve();
        }, ms);
    });
}

6.2 实现 #

6.2.1 dva\index.js #

src\dva\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import * as sagaEffects from 'redux-saga/effects';
import { NAMESPACE_SEP } from './constants';
import { connect, Provider } from 'react-redux';
import prefixNamespace from './prefixNamespace';
import { createHashHistory } from 'history';
+import { routerMiddleware, connectRouter, ConnectedRouter } from "connected-react-router";
let history = createHashHistory();
export { connect };

function dva() {
    const app = {
        _models: [],
        model,
        router,
        _router: null,
        start
    }
+    const initialReducers = { router: connectRouter(history) };
    function model(model) {
        const prefixedModel = prefixNamespace(model);
        app._models.push(prefixedModel);
        return prefixedModel;
    }
    function router(router) {
        app._router = router;
    }

    function start(root) {
        for (const model of app._models) {
            initialReducers[model.namespace] = getReducer(model);
        }
        let rootReducer = createReducer();
        const sagas = getSagas(app);
        const sagaMiddleware = createSagaMiddleware();
+        let store = createStore(rootReducer, applyMiddleware(routerMiddleware(history), sagaMiddleware));
        sagas.forEach(saga => sagaMiddleware.run(saga));
        ReactDOM.render(<Provider store={store}>{app._router({ history })}</Provider>, document.querySelector(root))
        function createReducer() {
            return combineReducers(initialReducers);
        }
    }
    function getSagas(app) {
        let sagas = [];
        for (const model of app._models) {
            sagas.push(getSaga(model.effects, model));
        }
        return sagas;
    }
    return app;
}
function getSaga(effects, model) {
    return function* () {
        for (const key in effects) {
            const watcher = getWatcher(key, model.effects[key], model);
            yield sagaEffects.fork(watcher);
        }
    };
}
function getWatcher(key, effect, model) {
    return function* () {
        yield sagaEffects.takeEvery(key, function* sagaWithCatch(...args) {
            yield effect(...args, { ...sagaEffects, put: action => sagaEffects.put({ ...action, type: prefixType(action.type, model) }) });
        });
    };
}
function prefixType(type, model) {
    if (type.indexOf('/') === -1) {
        return `${model.namespace}${NAMESPACE_SEP}${type}`;
    }
    return type;
}
function getReducer(model) {
    let { reducers, state: defaultState } = model;
    let reducer = (state = defaultState, action) => {
        let reducer = reducers[action.type];
        if (reducer) {
            return reducer(state, action);
        }
        return state;
    }
    return reducer;
}

export default dva;

6.2.2 src\dva\router.js #

src\dva\router.js

import * as routerRedux from 'connected-react-router';
export * from 'react-router-dom';
export {
    routerRedux
}