redux
和 redux-saga
的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router
和 fetch
,所以也可以理解为一个轻量级的应用框架create-react-app zhufeng-dva-source
cd zhufeng-dva-source
npm install dva
npm install redux react-redux redux-saga react-router-dom redux-first-history --save
npm start
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 };
}
}
});
const actionCreator = app.createAction();
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
//,actionCreator
);
app.router(() => <ConnectedCounter />);
app.start('#root');
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,
createAction
}
const initialReducers = {};
function model(model) {
const prefixedModel = prefixNamespace(model);
app._models.push(prefixedModel);
return prefixedModel;
}
function router(router) {
app._router = router;
}
function createAction() {
let actionCreators = {};
for (const model of app._models) {
let { reducers } = model;
for (let key in reducers) {
// counter/add => action {type:'counter/add'}
actionCreators[key] = () => ({ type: key })
}
}
return actionCreators;
}
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;
src\dva\prefixNamespace.js
function prefix(obj, namespace) {
return Object.keys(obj).reduce((memo, key) => {
const newKey = `${namespace}/${key}`;
memo[newKey] = obj[key];
return memo;
}, {});
}
export default function prefixNamespace(model) {
if (model.reducers)
model.reducers = prefix(model.reducers, model.namespace);
return model;
}
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);
+ });
+}
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 { 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));
+ }
+ return sagas;
+ }
return app;
}
+function getSaga(model) {
+ const { effects } = model;
+ return function* () {
+ for (const key in effects) {//key=asyncAdd
+ yield sagaEffects.takeEvery(key, function* (action) {
+ yield effects[key](action, {
+ ...sagaEffects, put: action => sagaEffects.put(
+ { ...action, type: prefixType(action.type, model.namespace) })
+ });
+ });
+ }
+ }
+}
+function prefixType(type, model) {
+ if (type.indexOf('/') === -1) {
+ return `${model.namespace}/${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;
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;
}
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(() => (
+ <>
+ <Link to="/">Home</Link>
+ <Link to="/counter">Counter</Link>
+ <Routes>
+ <Route path="/" exact={true} element={<Home />} />
+ <Route path="/counter" element={<ConnectedCounter />} />
+ </Routes>
+ </>
+));
app.start('#root');
function delay(ms) {
return new Promise((resolve) => {
setTimeout(function () {
resolve();
}, ms);
});
}
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 { BrowserRouter } from 'react-router-dom';
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}>
+ <BrowserRouter>
+ {app._router()}
+ </BrowserRouter>
+ </Provider>,
document.querySelector(root))
function createReducer() {
return combineReducers(initialReducers);
}
}
function getSagas(app) {
let sagas = [];
for (const model of app._models) {
sagas.push(getSaga(model));
}
return sagas;
}
return app;
}
function getSaga(model) {
const { effects } = model;
return function* () {
for (const key in effects) {//key=asyncAdd
yield sagaEffects.takeEvery(key, function* (action) {
yield effects[key](action, {
...sagaEffects, put: action => sagaEffects.put(
{ ...action, type: prefixType(action.type, model.namespace) })
});
});
}
}
}
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;
src\dva\router.js
export * from 'react-router-dom';
src\index.js
import React from 'react';
import dva, { connect } from './dva';
+import { Router, Route,Link,routerRedux } 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' });
},
+ *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(() => (
<>
<Link to="/">Home</Link>
<Link to="/counter">Counter</Link>
<Route path="/" exact={true} component={Home} />
<Route path="/counter" component={ConnectedCounter} />
</>
));
app.start('#root');
function delay(ms) {
return new Promise((resolve) => {
setTimeout(function () {
resolve();
}, ms);
});
}
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 { createBrowserHistory } from 'history';
+import { HistoryRouter } from "redux-first-history/rr6";
+import { createReduxHistoryContext } from "redux-first-history";
+const { routerReducer, routerMiddleware, createReduxHistory } = createReduxHistoryContext({ history: createBrowserHistory() });
export { connect };
function dva() {
const app = {
_models: [],
model,
router,
_router: null,
start
}
+ const initialReducers = { router: routerReducer };
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();
+ const store = createStore(rootReducer, applyMiddleware(routerMiddleware, sagaMiddleware));
sagas.forEach(saga => sagaMiddleware.run(saga));
ReactDOM.render(
<Provider store={store}>
<HistoryRouter history={createReduxHistory(store)}>
{app._router()}
</HistoryRouter>
</Provider></Provider>, document.querySelector(root))
function createReducer() {
return combineReducers(initialReducers);
}
}
function getSagas(app) {
let sagas = [];
for (const model of app._models) {
sagas.push(getSaga(model));
}
return sagas;
}
return app;
}
function getSaga(model) {
const { effects } = model;
return function* () {
for (const key in effects) {
yield sagaEffects.takeEvery(key, function* (action) {
yield effects[key](action, {
...sagaEffects, put: action => sagaEffects.put(
{ ...action, type: prefixType(action.type, model.namespace) })
});
});
}
}
}
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;
src\dva\router.js
import * as routerRedux from 'redux-first-history';
export * from 'react-router-dom';
export {
routerRedux
}