dva分析

974 阅读3分钟

dva源码分析

1.简介

dva首先是一个基于 reduxredux-saga 的数据流方案,为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架

dva工作流程

2.使用dva

  • history的最新版本为5.0,而connect-react-router使用的history版本为4.7,不兼容
  • 不显示安装history或者指定老版本history进行安装。否则自己安装history是使用history5

2.1.项目依赖

npm install dva redux react-redux redux-saga react-router-dom connected-reac-router -S

2.2.src/index.js

import React from 'react';
import dva, { connect } from 'dva';

//dva是一个函数,执行它可以得到app实例
const app = dva();
//调用model方法定义一个模型
app.model({
    namespace: 'counter',//命名空间
    state: { num: 0 },//此命名空间下的初始状态
    reducers: {//处理器
        add(state) {//处理器对象的key,其实就是action.type,此函数可以接收老状态,返回新状态
            return { num: state.num + 1 }
        }
    }
})

function Counter(props) {
    return <div>
        <p>{props.num}</p>
        <button onClick={()=>props.dispatch({ type: 'counter/add' })}>+</button>
    </div>
}

const ConnectedCounter = connect(state => state.counter)(Counter);

//定义路由
app.router(() => <ConnectedCounter />);

//开始渲染,把app.router方法的返回值渲染到root容器
app.start('#root');

3.基本功能实现

3.1.dva/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider,connect } from 'react-redux';
import prefixNamespace from "./prefixNamespace";
export { connect }
function dva() {
    const app = {
        _model: [],
        model,
        _router: null,
        router,
        start,
    };

    function model(model) {
        //经过转化后的model,将namespace拼接到reducers方法上
        const prefixedModel = prefixNamespace(model);
        app._model.push(prefixedModel);
    }

    function router(router) {//参数是一个函数
        app._router = router
    }

    const initialReducers = {};

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

        function createReducer() {
            return combineReducers(initialReducers)
        }
    }
    return app;
}

function getReducer(model) {
    const { state: initialState, reducers } = model;
    const reducer = (state = initialState, action) => {
        const reducer = reducers[action.type];//action.type=counter/add
        if (reducer) return reducer(state);
        return state;
    }
    return reducer;
}

export default dva;

3.2.dva/prefixNamespace.js

function prefixNamespace(model) {
    const { namespace, reducers } = model;
    model.reducers = prefix(namespace, reducers);
    return model;
}

function prefix(namespace, obj) {
    return Object.keys(obj).reduce((memo, key) => {
        const newKey = `${namespace}/${key}`;
        memo[newKey] = obj[key];
        return memo;
    }, {})
}
export default prefixNamespace;

4.支持effects

4.1.基本使用

import React from 'react';
import dva, { connect } from './dva';

//dva是一个函数,执行它可以得到app实例
const app = dva();
//调用model方法定义一个模型
app.model({
    namespace: 'counter',//命名空间
    state: { num: 0 },//此命名空间下的初始状态
    reducers: {//处理器
        add(state) {//处理器对象的key,其实就是action.type,此函数可以接收老状态,返回新状态
            return { num: state.num + 1 }
        }
    },
+    effects: {
        /**
         * 
         * @param {*} action 动作
         * @param {*} effects redux-saga/effects={put ,take,call,all,takeEvery,...}
         */
+        *asyncAdd(action, effects) {
+           const { call, put } = effects;
+           yield call(delay, 1000);
+           yield put({ type: 'add' })
+       }
+   }
})

function Counter(props) {
    return <div>
        <p>{props.num}</p>
        <button onClick={() => props.dispatch({ type: 'counter/add' })}>+</button>
+       <button onClick={() => props.dispatch({ type: 'counter/asyncAdd' })}>asyncAdd</button>
    </div>
}

const ConnectedCounter = connect(state => state.counter)(Counter);

//定义路由
app.router(() => <ConnectedCounter />);

//开始渲染,把app.router方法的返回值渲染到root容器
app.start('#root');

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

4.2.实现

4.2.1.dva/prefixNamespace.js
function prefixNamespace(model) {
    const { namespace, reducers, effects } = model;
    if (reducers) {
        model.reducers = prefix(namespace, reducers);
    }
+   if (effects) {
+       model.effects = prefix(namespace, effects);
+   }
    return model;
}

function prefix(namespace, obj) {
    return Object.keys(obj).reduce((memo, key) => {
        const newKey = `${namespace}/${key}`;
        memo[newKey] = obj[key];
        return memo;
    }, {})
}
export default prefixNamespace;
4.2.2.dva/index.js
	import React from 'react';
	import ReactDOM from 'react-dom';
+ import { createStore, combineReducers, applyMiddleware } from 'redux';
	import { Provider, connect } from 'react-redux';
	import prefixNamespace from "./prefixNamespace";
+ import createSagaMiddleware from 'redux-saga';
+ import * as sagaEffects from 'redux-saga/effects';
	export { connect }
  
function dva() {
    const app = {
        _model: [],
        model,
        _router: null,
        router,
        start,
    };

    function model(model) {
        //经过转化后的model,将namespace拼接到reducers方法上
        const prefixedModel = prefixNamespace(model);
        app._model.push(prefixedModel);
    }

    function router(router) {//参数是一个函数
        app._router = router
    }

    const initialReducers = {};

    function start(root) {
        for (const model of app._model) {
            initialReducers[model.namespace] = getReducer(model);
        }
        const rootReducer = createReducer()
+       const sagas = getSagas(app)
+       const sagaMiddleware = createSagaMiddleware();
+       const store = applyMiddleware(sagaMiddleware)(createStore)(rootReducer);
+       sagas.forEach(sagaMiddleware.run)
        ReactDOM.render(<Provider store={store}>{
            app._router()
        }</Provider>, document.querySelector(root));

        function createReducer() {
            return combineReducers(initialReducers)
        }
    }

+   function getSagas(app) {
+       const sagas = [];
+       for (const model of app._model) {
+           sagas.push(getSaga(model.effects, model))
+       }
+       return sagas;
+   }

    return app;
}

function getReducer(model) {
    const { state: initialState, reducers } = model;
    const reducer = (state = initialState, action) => {
        const reducer = reducers[action.type];//action.type=counter/add
        if (reducer) return reducer(state);
        return state;
    }
    return reducer;
}

+ function getSaga(effects, model) {
+   return function* () {
+       for (const key in effects) {
+           const watcher = getWatcher(key, effects[key], model);
+           yield sagaEffects.fork(watcher);
+       }
+   }
+ }

+ function getWatcher(key, effect, model) {
+   return function* () {
+       yield sagaEffects.takeEvery(key, function* (action) {
+           yield effect(action, {
+               ...sagaEffects,
+               put: action => sagaEffects.put({
+                   ...action,
+                   type: prefixType(action.type, model.namespace)
+               })
+           })
+       })
+   }
+ }

+ function prefixType(actionType, namespace) {
+   if (actionType.includes('/'))
+       return actionType
+   return `${namespace}/${actionType}`
+ }

export default dva;