dva-core
Version:
The core lightweight library for dva, based on redux and redux-saga.
255 lines (221 loc) • 6.87 kB
JavaScript
import { combineReducers } from 'redux';
import createSagaMiddleware, * as saga from 'redux-saga';
import invariant from 'invariant';
import checkModel from './checkModel';
import prefixNamespace from './prefixNamespace';
import Plugin, { filterHooks } from './Plugin';
import createStore from './createStore';
import getSaga from './getSaga';
import getReducer from './getReducer';
import createPromiseMiddleware from './createPromiseMiddleware';
import { run as runSubscription, unlisten as unlistenSubscription } from './subscription';
import * as utils from './utils';
const { noop, findIndex } = utils;
// Internal model to update global state when do unmodel
const dvaModel = {
namespace: '@@dva',
state: 0,
reducers: {
UPDATE(state) {
return state + 1;
},
},
};
/**
* Create dva-core instance.
*
* @param hooksAndOpts
* @param createOpts
*/
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts;
const plugin = new Plugin();
plugin.use(filterHooks(hooksAndOpts));
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model,
start,
};
return app;
/**
* Register model before app is started.
*
* @param m {Object} model to register
*/
function model(m) {
if (process.env.NODE_ENV !== 'production') {
checkModel(m, app._models);
}
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
/**
* Inject model after app is started.
*
* @param createReducer
* @param onError
* @param unlisteners
* @param m
*/
function injectModel(createReducer, onError, unlisteners, m) {
m = model(m);
const store = app._store;
store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
store.replaceReducer(createReducer());
if (m.effects) {
store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
if (m.subscriptions) {
unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
}
}
/**
* Unregister model.
*
* @param createReducer
* @param reducers
* @param unlisteners
* @param namespace
*
* Unexpected key warn problem:
* https://github.com/reactjs/redux/issues/1636
*/
function unmodel(createReducer, reducers, unlisteners, namespace) {
const store = app._store;
// Delete reducers
delete store.asyncReducers[namespace];
delete reducers[namespace];
store.replaceReducer(createReducer());
store.dispatch({ type: '@@dva/UPDATE' });
// Cancel effects
store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
// Unlisten subscrioptions
unlistenSubscription(unlisteners, namespace);
// Delete model from app._models
app._models = app._models.filter(model => model.namespace !== namespace);
}
/**
* Replace a model if it exsits, if not, add it to app
* Attention:
* - Only available after dva.start gets called
* - Will not check origin m is strict equal to the new one
* Useful for HMR
* @param createReducer
* @param reducers
* @param unlisteners
* @param onError
* @param m
*/
function replaceModel(createReducer, reducers, unlisteners, onError, m) {
const store = app._store;
const { namespace } = m;
const oldModelIdx = findIndex(app._models, model => model.namespace === namespace);
if (~oldModelIdx) {
// Cancel effects
store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
// Delete reducers
delete store.asyncReducers[namespace];
delete reducers[namespace];
// Unlisten subscrioptions
unlistenSubscription(unlisteners, namespace);
// Delete model from app._models
app._models.splice(oldModelIdx, 1);
}
// add new version model to store
app.model(m);
store.dispatch({ type: '@@dva/UPDATE' });
}
/**
* Start the app.
*
* @returns void
*/
function start() {
// Global error handler
const onError = (err, extension) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = () => {
err._dontReject = true;
};
plugin.apply('onError', err => {
throw new Error(err.stack || err);
})(err, app._store.dispatch, extension);
}
};
const sagaMiddleware = createSagaMiddleware();
const promiseMiddleware = createPromiseMiddleware(app);
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
const reducerEnhancer = plugin.get('onReducer');
const extraReducers = plugin.get('extraReducers');
invariant(
Object.keys(extraReducers).every(key => !(key in reducers)),
`[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(
reducers,
).join(', ')}`,
);
// Create store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// Run sagas
sagas.forEach(sagaMiddleware.run);
// Setup app
setupApp(app);
// Run subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// Setup app.model and app.unmodel
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
/**
* Create global reducer for redux.
*
* @returns {Object}
*/
function createReducer() {
return reducerEnhancer(
combineReducers({
...reducers,
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
}
}
export { saga };
export { utils };