手写dva

307 阅读6分钟

1.dva介绍

1.1前置知识

  • react
  • react-router-dom
  • redux
  • react-redux
  • connected-react-router
  • history
  • dva

2.初始化项目

create-react-app dva-hand
cd dva-hand
yarn add dva redux react-redux react-router-dom connected-react-router history redux-saga dva-loading
npm start

3.基本的计数器

3.1 src/index.js

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

let app = dva();

app.model({
  namespace: 'counter',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  }
})

app.model({
  namespace: 'counter2',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },
})

const Counter = connect(state => state.counter)(
  props => (
    <>
      <p>{props.number}</p>
      <button onClick={() => {
        props.dispatch({ type: 'counter/add' })
      }}>+</button>
    </>
  )
)

app.router(() => <Counter />)
app.start("#root")

3.2 src/dva/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore } from 'redux'
import { Provider, connect } from 'react-redux'

export { connect }

export default function () {
  const app = {
    _models: [], // 定义的模型
    model, // 添加模型的方法
    _router: null, //存放路由定义的函数
    router,
    start,
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routerConfig) {
    app._router = routerConfig
  }

  function start(containerId) {
    let reducers = {};
    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i]

      reducers[model.namespace] = function (state = model.state, action) {
        let actionType = action.type; //取得动作类型 counter/add
        const [namespace, type] = actionType.split('/')
              // namespace: 'counter',
              // state: { number: 1 },
              // reducers: {
              //   add(state) {
              //     return { number: state.number + 1 }
              //   }
              // }
        if (model.namespace === namespace) {
          let reducer = model.reducers[type]
          if (reducer) {
            return reducer(state, action)
          }
        }
        return state
      }
    }

    let finalReducer = combineReducers(reducers)
    let store = createStore(finalReducer)

    let App = app._router()
    ReactDOM.render(
      <Provider store={store}>
        {App}
      </Provider>, document.querySelector(containerId)
    )

  }
  return app
}

4.支持effects

4.1 src/index.js

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

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

let app = dva();

app.model({
  namespace: 'counter',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },
  effects: {
    *asyncAdd(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'counter/add' })
      // yield put({ type: 'add' })  实际项目中是没有counter/这个的,后期看看处理没
    }
  }
})

app.model({
  namespace: 'counter2',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },

})

const Counter = connect(state => state.counter)(
  props => (
    <>
      <p>{props.number}</p>
      <button onClick={() => {
        props.dispatch({ type: 'counter/add' })
      }}>+</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/asyncAdd' })
      }}>异步+1</button>
    </>
  )
)

app.router(() => <Counter />)
app.start("#root")

4.2 src/dva/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
import { createHashHistory } from 'history'

export { connect }

export default function () {
  const app = {
    _models: [], // 定义的模型
    model, // 添加模型的方法
    _router: null, //存放路由定义的函数
    router,
    start,
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routerConfig) {
    app._router = routerConfig
  }

  function start(containerId) {
    let history = createHashHistory()
    let reducers = {};
    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i]

      reducers[model.namespace] = function (state = model.state, action) {
        let actionType = action.type; //取得动作类型 counter/add
        const [namespace, type] = actionType.split('/')
        if (model.namespace === namespace) {
          let reducer = model.reducers[type]
          if (reducer) {
            return reducer(state, action)
          }
        }
        return state
      }
    }

    let finalReducer = combineReducers(reducers)

    let sagaMiddleware = createSagaMiddleware()

    let store = createStore(finalReducer, applyMiddleware(sagaMiddleware))

    function* rootSaga() {
      const { takeEvery } = sagaEffects
      for (const model of app._models) {
        const effects = model.effects
        for (const key in effects) { // key就是asyncAdd
          // 监听每一个动作,当动作发生的时候,执行对应的saga
          yield takeEvery(`${model.namespace}/${key}`, function* (action) {
            yield effects[key](action, sagaEffects)
          })
        }
      }
    }

    sagaMiddleware.run(rootSaga)

    let App = app._router()
    ReactDOM.render(
      <Provider store={store}>
        {App}
      </Provider>, document.querySelector(containerId)
    )

  }


  return app
}

      // namespace: 'counter',
      // state: { number: 1 },
      // reducers: {
      //   add(state) {
      //     return { number: state.number + 1 }
      //   }
      // }

5.支持路由

5.1 src/index.js

import React from 'react';
import dva, { connect } from 'dva'
import { Router, Route, Link } from './dva/router'

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

let app = dva();

app.model({
  namespace: 'counter',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },
  effects: {
    *asyncAdd(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'counter/add' })
      // yield put({ type: 'add' })  实际项目中是没有counter/这个的,后期看看处理没
    }
  }
})

app.model({
  namespace: 'counter2',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },

})

const Counter = connect(state => state.counter)(
  props => (
    <>
      <p>{props.number}</p>
      <button onClick={() => {
        props.dispatch({ type: 'counter/add' })
      }}>+</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/asyncAdd' })
      }}>异步+1</button>
    </>
  )
)

const Home = () => <div>首页</div>


app.router(({ history }) => (
  <Router history={history}>
    <>
      <Link to={'/'}>首页</Link>
      <Link to={'/counter'}>计数器</Link> 
      <Route path="/" exact component={Home} />
      <Route path="/counter" component={Counter} />
    </>
  </Router>
))
app.start("#root")

5.2 src/dva/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
import { createHashHistory } from 'history'

export { connect }

export default function () {
  const app = {
    _models: [], // 定义的模型
    model, // 添加模型的方法
    _router: null, //存放路由定义的函数
    router,
    start,
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routerConfig) {
    app._router = routerConfig
  }

  function start(containerId) {
    let history = createHashHistory()
    let reducers = {};
    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i]

      reducers[model.namespace] = function (state = model.state, action) {
        let actionType = action.type; //取得动作类型 counter/add
        const [namespace, type] = actionType.split('/')
        if (model.namespace === namespace) {
          let reducer = model.reducers[type]
          if (reducer) {
            return reducer(state, action)
          }
        }
        return state
      }
    }

    let finalReducer = combineReducers(reducers)

    let sagaMiddleware = createSagaMiddleware()

    let store = createStore(finalReducer, applyMiddleware(sagaMiddleware))

    function* rootSaga() {
      const { takeEvery } = sagaEffects
      for (const model of app._models) {
        const effects = model.effects
        for (const key in effects) { // key就是asyncAdd
          // 监听每一个动作,当动作发生的时候,执行对应的saga
          yield takeEvery(`${model.namespace}/${key}`, function* (action) {
            yield effects[key](action, sagaEffects)
          })
        }
      }
    }

    sagaMiddleware.run(rootSaga)

    let App = app._router({ history })
    ReactDOM.render(
      <Provider store={store}>
        {App}
      </Provider>, document.querySelector(containerId)
    )

  }

  return app
}

      // namespace: 'counter',
      // state: { number: 1 },
      // reducers: {
      //   add(state) {
      //     return { number: state.number + 1 }
      //   }
      // }

5.3 src/dva/router.js

// export * from 'react-router-dom'
module.exports = require('react-router-dom')
// 两种方式都可以
create-react-app 内置了webpack配置脚手架
roadhog 相当于一个可配置的create-react-app
umi = roadhog + 路由系统
dva 管理数据流

6.支持跳转

  • connect-react-router的使用方式 github.com/supasate/co…
  • 这个库的三步:
    • 合并reducer时,添加router这个状态
    • 添加中间件applyMiddleware(routerMiddleware(history),为了派发使用
    • 添加这个路由容器组件

6.1 src/index.js

import React from 'react';
import dva, { connect } from './dva'
import { Router, Route, Link, routerRedux } from './dva/router'

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

let app = dva();

app.model({
  namespace: 'counter',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },
  effects: {
    *asyncAdd(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'counter/add' })
      // yield put({ type: 'add' })  实际项目中是没有counter/这个的,后期看看处理没
    },
    *goto({ payload: { pathname } }, { call, put }) {
      yield put(routerRedux.push(pathname)) // push这个库就是来自于connected-react-router
    }
  }
})

app.model({
  namespace: 'counter2',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },

})

const Counter = connect(state => state.counter)(
  props => (
    <>
      <p>{props.number}</p>
      <button onClick={() => {
        props.dispatch({ type: 'counter/add' })
      }}>+</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/asyncAdd' })
      }}>异步+1</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/goto', payload: { pathname: "/" } })
      }}>跳转到首页</button>
    </>
  )
)

const Home = () => <div>首页</div>


app.router(({ history }) => (
  // <Router history={history}>
    <>
      <Link to={'/'}>首页</Link>
      <Link to={'/counter'}>计数器</Link>
      <Route path="/" exact component={Home} />
      <Route path="/counter" component={Counter} />
    </>
  // </Router>
))
app.start("#root")

6.2 src/dva/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
import { createHashHistory } from 'history'

import {
  connectRouter, // 创建router中间件
  routerMiddleware, // 用来创建router reducer
  ConnectedRouter // 取代Router
} from 'connected-react-router'


export { connect }

export default function () {
  const app = {
    _models: [], // 定义的模型
    model, // 添加模型的方法
    _router: null, //存放路由定义的函数
    router,
    start,
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routerConfig) {
    app._router = routerConfig
  }

  function start(containerId) {
    let history = createHashHistory()
    let reducers = {
      router: connectRouter(history), //用来把路径信息同步到仓库中的
    };
    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i]

      reducers[model.namespace] = function (state = model.state, action) {
        let actionType = action.type; //取得动作类型 counter/add
        const [namespace, type] = actionType.split('/')
        if (model.namespace === namespace) {
          let reducer = model.reducers[type]
          if (reducer) {
            return reducer(state, action)
          }
        }
        return state
      }
    }

    let finalReducer = combineReducers(reducers)

    let sagaMiddleware = createSagaMiddleware()

    let store = createStore(finalReducer, applyMiddleware(routerMiddleware(history), sagaMiddleware))

    function* rootSaga() {
      const { takeEvery } = sagaEffects
      for (const model of app._models) {
        const effects = model.effects
        for (const key in effects) { // key就是asyncAdd
          // 监听每一个动作,当动作发生的时候,执行对应的saga
          yield takeEvery(`${model.namespace}/${key}`, function* (action) {
            yield effects[key](action, sagaEffects)
          })
        }
      }
    }

    sagaMiddleware.run(rootSaga)

    let App = app._router({ history })
    ReactDOM.render(
      <Provider store={store}>
        <ConnectedRouter history={history}>
          {App}
        </ConnectedRouter>
      </Provider>, document.querySelector(containerId)
    )

  }

  return app
}

      // namespace: 'counter',
      // state: { number: 1 },
      // reducers: {
      //   add(state) {
      //     return { number: state.number + 1 }
      //   }
      // }

6.3 src/dva/router.js

// export * from 'react-router-dom'
module.exports = require('react-router-dom')
module.exports.routerRedux = require('connected-react-router')

7.支持钩子

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
});

7.1 src/index.js

import React from 'react';
// import dva, { connect } from 'dva'
// import { Router, Route, Link, routerRedux } from 'dva/router'
import dva, { connect } from './dva'
import { Router, Route, Link, routerRedux } from './dva/router'
import { createBrowserHistory } from 'history'
import createLoading from 'dva-loading';

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

let history = createBrowserHistory()

function logger({ getState, dispatch }) {
  return function (next) {
    return function (action) {
      console.log('老状态', getState())
      next(action)
      console.log('新状态', getState())
    }
  }
}

const SHOW = 'SHOW' // 显示
const HIDE = 'HIDE' // 隐藏

// namespace=loading
let initialLoadingState = {
  global: false, // 全局的
  models: {}, // 自己命名空间的
  effects: {}, // 本次请求的
}

let app = dva({
  history,
  initialState: { counter: { number: 5 } },
  onError: (error) => alert(error),
  onAction: logger, // 可以放一个数组,如果是多个中间件可以是一个数组,如果是一个,那么直接放就可以了
  onStateChange: state => localStorage.setItem("state", JSON.stringify(state)), // 用处就是当状态发生改变的时候会触发
  onReducer: reducer => (state, action) => { // 它其实是对reducer的封装或改进
    console.log('准备要执行reducer了')
    return reducer(state, action)
  },
  // extraEnhancers: [StoreCreator => {
  //   return StoreCreator
  // }],
  onEffect: (effect, { put }, model, actionType) => {
    const { namespace } = model
    return function* (...args) {
      yield put({ type: SHOW, payload: { namespace, actionType } })
      yield effect(...args)
      yield put({ type: HIDE, payload: { namespace, actionType } })
    }
  },
  extraReducers: {
    loading(state = initialLoadingState, { type, payload }) {
      const { namespace, actionType } = payload || {}
     console.log('namespace, actionType',type,namespace, actionType)
      switch (type) {
        case SHOW:
          return {
            global: true, // 只要有一个执行那么global就是true
            effects: { ...state.effects, [`${actionType}`]: true },
            models: { ...state.models, [namespace]: true, }
          }
        case HIDE: {
          let effects = { ...state.effects, [`${actionType}`]: false }
          let modelStatus = Object.keys(effects).filter(item => item.startsWith(namespace + '/')).some(item => effects[item])
          let models = { ...state.models, [namespace]: modelStatus }
          let global = Object.keys(models).some(namespace => models[namespace])
          return {
            global, // 这个global不一定是false,因为可能别人还在执行,只要别人还在执行那么global就是true
            effects,
            models
          }
        }
        default:
          return state
      }
    }
  },

});

// app.use(createLoading());

app.model({
  namespace: 'counter',
  state: { number: 1 },
  subscriptions: {
    setup() {
      console.log('subscriptions执行')
    }
  },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },
  effects: {
    *asyncAdd(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'add', payload: { counter: '123456' } })
      // yield put({ type: 'add' })  实际项目中是没有counter/这个的,后期看看处理没
      // throw new Error('asyncAdd error')
    },
    *goto({ payload: { pathname } }, { call, put }) {
      yield put(routerRedux.push(pathname)) // push这个库就是来自于connected-react-router
    }
  }
})

app.model({
  namespace: 'counter2',
  state: { number: 1 },
  reducers: {
    add(state) {
      return { number: state.number + 1 }
    }
  },

})

const Counter = connect(state => state.counter)(
  props => (
    <>
      <p>{props.number}</p>
      <button onClick={() => {
        props.dispatch({ type: 'counter/add' })
      }}>+</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/asyncAdd' })
      }}>异步+1</button>
      <button onClick={() => {
        props.dispatch({ type: 'counter/goto', payload: { pathname: "/" } })
      }}>跳转到首页</button>
    </>
  )
)

const Home = () => <div>首页</div>


app.router(({ history }) => (
  <Router history={history}>
    <>
      <Link to={'/'}>首页</Link>
      <Link to={'/counter'}>计数器</Link>
      <Route path="/" exact component={Home} />
      <Route path="/counter" component={Counter} />
    </>
  </Router>
))
app.start("#root")

// console.log(app._store)
window.getState = app._store.getState

7.2 src/dva/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import * as sagaEffects from 'redux-saga/effects'
import { createHashHistory } from 'history'

import {
  connectRouter, // 创建router中间件
  routerMiddleware, // 用来创建router reducer
  ConnectedRouter // 取代Router
} from 'connected-react-router'


export { connect }

export default function (options = {}) {
  const app = {
    _models: [], // 定义的模型
    model, // 添加模型的方法
    _router: null, //存放路由定义的函数
    router,
    start,
  }

  function model(model) {
    app._models.push(model)
  }

  function router(routerConfig) {
    app._router = routerConfig
  }

  app.use = function (plugin) {
    options = { ...options, ...plugin }
  }

  function start(containerId) {
    let history = options.history || createHashHistory()
    let reducers = {
      router: connectRouter(history), //用来把路径信息同步到仓库中的
    };
    if (options.extraReducers) {
      reducers = {
        ...reducers,
        ...options.extraReducers
      }
    }


    for (let i = 0; i < app._models.length; i++) {
      let model = app._models[i]
      reducers[model.namespace] = function (state = model.state, action) {
        let actionType = action.type; //取得动作类型 counter/add
        let [namespace, type] = actionType.split('/')

        // 因为put({type:"counter/add"}) 或者 put({type:"add"})
        if (typeof type === 'undefined') {
          type = namespace // 此时[namespace,type] = 'add'.split('/')
          namespace = model.namespace
        }

        if (model.namespace === namespace) {
          let reducer = model.reducers[type]
          if (reducer) {
            return reducer(state, action)
          }
        }
        return state
      }
    }
    
    let finalReducer = combineReducers(reducers)
   
    finalReducer = function (state, action) {
      let newState = combineReducers(reducers)(state, action)
      options.onStateChange && options.onStateChange(newState)
      return newState
    }

    if (options.onReducer) {
      finalReducer = options.onReducer(finalReducer)
    }

    let sagaMiddleware = createSagaMiddleware()

    if (options.onAction) {
      if (typeof options.onAction === 'function') {
        options.onAction = [options.onAction]
      }
    } else {
      options.onAction = []
    }

    // if (options.extraEnhancers) { // 是一个数组,没有做处理
    //   createStore = options.extraEnhancers(createStore)
    // }

    let store = createStore(finalReducer,
      options.initialState || {},
      applyMiddleware(routerMiddleware(history), sagaMiddleware, ...options.onAction))
    app._store = store

    // 执行subscriptions
    for (const model of app._models) {
      if (model.subscriptions) {
        for (const key in model.subscriptions) {
          model.subscriptions[key]({ history, dispatch: store.dispatch })
        }
      }
    }

    function* rootSaga() {
      const { takeEvery } = sagaEffects
      for (const model of app._models) {

        const effects = model.effects
        for (const key in effects) { // key就是asyncAdd
          // 监听每一个动作,当动作发生的时候,执行对应的saga
          yield takeEvery(`${model.namespace}/${key}`, function* (action) {
            try {
              let effect = effects[key]
              if (options.onEffect) {
                effect = options.onEffect(effect, sagaEffects, model, action.type)
              }
              yield effect(action, sagaEffects)
            } catch (error) {
              options.onError && options.onError(error)
            }

          })
        }
      }
    }

    sagaMiddleware.run(rootSaga)

    let App = app._router({ history })
    ReactDOM.render(
      <Provider store={store}>
        <ConnectedRouter history={history}>
          {App}
        </ConnectedRouter>
      </Provider>, document.querySelector(containerId)
    )

  }

  return app
}

// namespace: 'counter',
// state: { number: 1 },
// reducers: {
//   add(state) {
//     return { number: state.number + 1 }
//   }
// }

8.参考