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;
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 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' })
}
}
})
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;
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) {
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
}
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' })
}
}
})
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;
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) {
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
}
5.3 src/dva/router.js
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' })
},
*goto({ payload: { pathname } }, { call, put }) {
yield put(routerRedux.push(pathname))
}
}
})
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 }) => (
<>
<Link to={'/'}>首页</Link>
<Link to={'/counter'}>计数器</Link>
<Route path="/" exact component={Home} />
<Route path="/counter" component={Counter} />
</>
))
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,
routerMiddleware,
ConnectedRouter
} 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;
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) {
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
}
6.3 src/dva/router.js
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 { 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'
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) => {
console.log('准备要执行reducer了')
return reducer(state, action)
},
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,
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,
effects,
models
}
}
default:
return state
}
}
},
});
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' } })
},
*goto({ payload: { pathname } }, { call, put }) {
yield put(routerRedux.push(pathname))
}
}
})
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")
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,
routerMiddleware,
ConnectedRouter
} 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;
let [namespace, type] = actionType.split('/')
if (typeof type === 'undefined') {
type = namespace
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 = []
}
let store = createStore(finalReducer,
options.initialState || {},
applyMiddleware(routerMiddleware(history), sagaMiddleware, ...options.onAction))
app._store = store
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) {
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
}
8.参考