提交 acfd0ac2 authored 作者: vipcxj's avatar vipcxj

更动态的模块加载,支持服务单配置模块参数,客户端根据配置的参数实例化模块

上级 89bae37a
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import dva from 'dva'; import dva from 'dva';
import createLoading from 'dva-loading'; import createLoading from 'dva-loading';
import moment from 'moment'; import moment from 'moment';
// import { REHYDRATE } from 'redux-persist/lib/constants';
import persistReducer from 'redux-persist/lib/persistReducer'; import persistReducer from 'redux-persist/lib/persistReducer';
import storage from 'redux-persist/lib/storage'; import storage from 'redux-persist/lib/storage';
import { getHistory } from './services/route'; import { getHistory } from './services/route';
......
import moment from 'moment'; export default (info, layout) => ({
import config from '../../../../utils/config';
import { countTasks, fetchTasks } from '../../../../services/bpm';
export default {
namespace: 'task', namespace: 'task',
state: { state: {
num: 0, coordinate: layout.option.coordinate,
list: [],
},
reducers: {
queryCountSuccess(state, { payload: num }) {
return {
...state,
num,
};
},
queryTasksSuccess(state, { payload: list }) {
return {
...state,
list,
};
},
},
effects: {
*fetchTasks({ payload: { pst, psz, filters } }, { put, call }) {
const num = Number.parseInt(yield call(countTasks, filters), 10);
yield put({ type: 'queryCountSuccess', payload: num });
const tasks = yield call(fetchTasks, { pst, psz, filters });
yield put({
type: 'queryTasksSuccess',
payload: tasks.map((task) => {
return {
key: `${task.pId}-${task.nId}`,
pName: task.pName,
nName: task.nName,
state: task.state,
date: moment(task.date, config.defaultDateTimeFormat),
deadline: moment(task.deadline, config.defaultDateTimeFormat),
};
}),
});
},
}, },
reducers: {},
effects: {},
subscriptions: {}, subscriptions: {},
}; });
...@@ -6,6 +6,7 @@ import { LocaleProvider, Spin } from 'antd'; ...@@ -6,6 +6,7 @@ import { LocaleProvider, Spin } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN'; import zhCN from 'antd/lib/locale-provider/zh_CN';
import persistStore from 'redux-persist/lib/persistStore'; import persistStore from 'redux-persist/lib/persistStore';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import { REHYDRATE } from 'redux-persist/lib/constants';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
import get from 'lodash/get'; import get from 'lodash/get';
import config from './utils/config'; import config from './utils/config';
...@@ -13,14 +14,16 @@ import { getUser, isAuthed, hasDomain, histories } from './utils/auth'; ...@@ -13,14 +14,16 @@ import { getUser, isAuthed, hasDomain, histories } from './utils/auth';
import { processError } from './utils/error'; import { processError } from './utils/error';
import App from './routes/app'; import App from './routes/app';
import { getMenus, getModuleInfo, getModuleLayout } from './data/modules'; import { getMenus, getModuleInfo, getModuleLayout } from './data/modules';
import { bindModel } from './utils/model';
import Monk from './routes/main/monk'; import Monk from './routes/main/monk';
import styles from './index.css'; import styles from './index.css';
import { getStore } from './index';
const Loading = <Spin size="large" className={styles.globalSpin} />; const Loading = <Spin size="large" className={styles.globalSpin} />;
const registerModel = (app, model) => { const registerModel = (app, model) => {
// eslint-disable-next-line no-underscore-dangle // noinspection JSUnresolvedVariable
if (!(app._models.filter(m => m.namespace === model.namespace).length === 1)) { if (!(app._models.filter(m => m.namespace === model.namespace).length === 1)) {
app.model(model); app.model(model);
} }
...@@ -75,32 +78,33 @@ const createRoute = async (app, group, basePath) => { ...@@ -75,32 +78,33 @@ const createRoute = async (app, group, basePath) => {
}; };
}; };
const moduleHook = (app, uid, lastModule, currentModule) => { const moduleLeaveHook = (app, module) => {
// noinspection JSUnresolvedVariable const models = (app._models || []).filter(m => m.namespace.startsWith(`${module.name}/`));
const store = app._store; const store = app._store;
if (store) { models.forEach((m) => {
if (lastModule && lastModule.model && (lastModule.model.reducerExitHook || lastModule.model.effectExitHook)) { if (m.global) {
store.dispatch({ const { reducers, effects } = m;
type: `${lastModule.model.namespace}/@@exit`, if ((reducers && reducers['@@exit']) || (effects && effects['@@exit'])) {
data: { store.dispatch({ type: `${m.namespace}/@@exit`, payload: module });
from: lastModule,
to: currentModule,
},
});
} }
if (currentModule && currentModule.model && (currentModule.model.reducerEnterHook || currentModule.model.effectEnterHook)) { } else {
store.dispatch({ app.unmodel(m.namespace);
type: `${currentModule.model.namespace}/@@enter`,
data: {
from: lastModule,
to: currentModule,
},
});
} }
});
};
const moduleEnterHook = (app, uid, module) => {
const models = (app._models || []).filter(m => m.namespace.startsWith(`${module.name}/`));
const store = app._store;
models.forEach((m) => {
if (m.global) {
const { reducers, effects } = m;
if ((reducers && reducers['@@enter']) || (effects && effects['@@enter'])) {
store.dispatch({ type: `${m.namespace}/@@enter`, payload: module });
} }
if (currentModule) {
histories.pushHistory('module', uid, currentModule);
} }
});
histories.pushHistory('module', uid, module);
}; };
const createRoutes = async (app, modules, groups, basePath) => { const createRoutes = async (app, modules, groups, basePath) => {
...@@ -116,7 +120,7 @@ const createRoutes = async (app, modules, groups, basePath) => { ...@@ -116,7 +120,7 @@ const createRoutes = async (app, modules, groups, basePath) => {
info = module; info = module;
layout = module.layout; layout = module.layout;
} }
const { name, showName, icon, description } = info; const { name, showName } = info;
const route = { const route = {
path: name, path: name,
fullPath: combinePath(basePath, name), fullPath: combinePath(basePath, name),
...@@ -124,15 +128,16 @@ const createRoutes = async (app, modules, groups, basePath) => { ...@@ -124,15 +128,16 @@ const createRoutes = async (app, modules, groups, basePath) => {
}; };
let modelBundle; let modelBundle;
if (layout.route) { if (layout.route) {
modelBundle = await import(`./models/main/modules/${layout.route}`); // modelBundle = await import(`./models/main/modules/${layout.route}`);
modelBundle = modelBundle.default; // modelBundle = modelBundle.default;
registerModel(app, modelBundle); // if (typeof modelBundle === 'function') {
const { subModels } = modelBundle; // modelBundle = modelBundle(info, layout);
if (subModels && subModels.forEach) { // }
subModels.forEach(m => registerModel(app, m)); // registerModel(app, modelBundle);
}
let routeBundle = await import(`./routes/main/modules/${layout.route}`); let routeBundle = await import(`./routes/main/modules/${layout.route}`);
routeBundle = routeBundle.default || routeBundle; routeBundle = routeBundle.default || routeBundle;
const binder = bindModel(app, info, layout);
routeBundle = routeBundle(binder, info, layout);
route.component = routeBundle; route.component = routeBundle;
if (routeBundle.route) { if (routeBundle.route) {
for (const key in routeBundle.route) { for (const key in routeBundle.route) {
...@@ -155,23 +160,32 @@ const createRoutes = async (app, modules, groups, basePath) => { ...@@ -155,23 +160,32 @@ const createRoutes = async (app, modules, groups, basePath) => {
model.reducerExitHook = modelBundle.reducers && !!modelBundle.reducers['@@exit']; model.reducerExitHook = modelBundle.reducers && !!modelBundle.reducers['@@exit'];
model.effectExitHook = modelBundle.effects && !!modelBundle.effects['@@exit']; model.effectExitHook = modelBundle.effects && !!modelBundle.effects['@@exit'];
} }
const infoEx = {
...info,
path: route.fullPath,
};
const { onLeave } = route;
if (onLeave) {
route.onLeave = (preState) => {
if (get(preState, 'location.pathname') === route.fullPath) {
moduleLeaveHook(app, infoEx);
}
onLeave(preState);
};
} else {
route.onLeave = (preState) => {
if (get(preState, 'location.pathname') === route.fullPath) {
moduleLeaveHook(app, infoEx);
}
};
}
if (route.onEnter) { if (route.onEnter) {
const onEnter = route.onEnter; const onEnter = route.onEnter;
route.onEnter = (nextState, replace, cb) => { route.onEnter = (nextState, replace, cb) => {
if (get(nextState, 'location.pathname') === route.fullPath) { if (get(nextState, 'location.pathname') === route.fullPath) {
getUser() getUser()
.then(u => (u ? histories.getLatest('module', u.id).then(m => [u.id, m]) : [null, null])) .then(u => u.id)
.then(([uid, lastModule]) => { .then(uid => moduleEnterHook(app, uid, infoEx))
const currentModule = {
name,
showName,
icon,
description,
path: route.fullPath,
model,
};
moduleHook(app, uid, lastModule, currentModule);
})
.then(() => new Promise((resolve, reject) => { .then(() => new Promise((resolve, reject) => {
onEnter(nextState, replace, (err, res) => { onEnter(nextState, replace, (err, res) => {
if (err) { if (err) {
...@@ -191,18 +205,8 @@ const createRoutes = async (app, modules, groups, basePath) => { ...@@ -191,18 +205,8 @@ const createRoutes = async (app, modules, groups, basePath) => {
route.onEnter = (nextState, replace, cb) => { route.onEnter = (nextState, replace, cb) => {
if (get(nextState, 'location.pathname') === route.fullPath) { if (get(nextState, 'location.pathname') === route.fullPath) {
getUser() getUser()
.then(u => (u ? histories.getLatest('module', u.id).then(m => [u.id, m]) : [null, null])) .then(u => u.id)
.then(([uid, lastModule]) => { .then(uid => moduleEnterHook(app, uid, infoEx))
const currentModule = {
name,
showName,
icon,
description,
path: route.fullPath,
model,
};
moduleHook(app, uid, lastModule, currentModule);
})
.then(() => cb()) .then(() => cb())
.catch(err => cb(err)); .catch(err => cb(err));
} else { } else {
...@@ -337,3 +341,9 @@ RouterConfig.propTypes = { ...@@ -337,3 +341,9 @@ RouterConfig.propTypes = {
}; };
export default RouterConfig; export default RouterConfig;
if (module.hot) {
module.hot.accept(() => {
getStore().dispatch({ type: REHYDRATE });
});
}
import { connect } from 'dva';
import route from '../../../../components/hoc/routes'; import route from '../../../../components/hoc/routes';
import Main from './main'; import Main from './main';
export default connect(({ apiDoc }) => ({ apiDoc }))(route({ export default () => route({
childRoutes: [{ childRoutes: [{
path: 'main', path: 'main',
name: '文档', name: '文档',
component: Main, component: Main,
}], }],
})); });
import { connect } from 'dva';
import { withRouter4Compat as withRouter } from 'react-router-4-compat';
import List from './list'; import List from './list';
import Detail from './detail'; import Detail from './detail';
import route from '../../../../components/hoc/routes'; import route from '../../../../components/hoc/routes';
import model from '../../../../models/main/modules/task';
export default connect(({ task }) => ({ task }))(route({ export default binder => route({
childRoutes: [ childRoutes: [
{ {
path: 'list', path: 'list',
name: '列表', name: '列表',
component: withRouter(List, { withRef: true }), component: binder(model)(({ task }) => ({ task }))(List),
}, },
{ {
path: 'detail', path: 'detail',
name: '详细', name: '详细',
component: withRouter(Detail, { withRef: true }), component: binder(model)(({ task }) => ({ task }))(Detail),
}, },
], ],
})); });
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import DsTable from '../../../../components/table/dstable';
import moment from 'moment';
import { Button } from 'antd';
import { connect } from 'dva';
import TableEx from '../../../../components/table/index';
import config from '../../../../utils/config';
import { thisPush } from '../../../../services/route';
import styles from './list.less';
const columns = [{
title: '流程',
dataIndex: 'pName',
key: 'pName',
filterType: 'text',
}, {
title: '任务',
dataIndex: 'nName',
key: 'nName',
filterType: 'text',
}, {
title: '状态',
dataIndex: 'state',
key: 'state',
filterType: 'enum',
filterEnums: [{
text: '状态1',
value: '状态1',
}, {
text: '状态2',
value: '状态2',
}, {
text: '状态3',
value: '状态3',
}, {
text: '状态4',
value: '状态4',
}, {
text: '状态5',
value: '状态5',
}],
}, {
title: '日期',
dataIndex: 'date',
key: 'date',
filterType: 'date',
render(date) {
return date.format(config.defaultDateTimeFormat);
},
}, {
title: '期限',
dataIndex: 'deadline',
key: 'deadline',
render(deadline) {
const now = moment();
const late = deadline.diff(now, 'days', true);
if (late < 0) {
const style = {
color: '#f04134',
};
return <span style={style}>{ `超时 ${deadline.from(now, true)}` }</span>;
} else if (late < 1) {
const style = {
color: '#ffbf00',
};
return <span style={style}>{ `仅剩 ${deadline.to(now, true)}` }</span>;
} else {
const style = {
color: '#00a854',
};
return <span style={style}>{ `还剩 ${deadline.to(now, true)}` }</span>;
}
},
}];
class List extends React.Component { class List extends React.Component {
constructor(props, context) {
super(props, context);
this.loadData = this::this.loadData;
this.getCurrent = this::this.getCurrent;
this.state = {
filters: [],
current: 1,
pageSize: 10,
};
}
componentDidMount() {
this.loadData();
}
getCurrent() {
const { num } = this.props.task;
const pageNum = ((num / this.state.pageSize) | 0) + 1;
return this.state.current > pageNum ? pageNum : this.state.current;
}
loadData() {
const filters0 = this.state.filters
.filter(({ filter }) => !!filter)
.map(({ key, filter }) => ([
`f-${key}`,
filter,
]));
const psz = this.state.pageSize; // eslint-disable-line no-shadow
const pst = (this.state.current - 1) * psz;
this.props.dispatch({ type: 'task/fetchTasks', payload: { pst, psz, filters: filters0 } });
}
render() { render() {
const { list, num } = this.props.task;
const tableProps = {
dataSource: list,
columns,
filters: this.state.filters.map(filter => filter.filter),
loading: this.props.loading.effects['task/fetchTasks'],
pagination: {
current: this.state.current,
total: num,
pageSize: this.state.pageSize,
},
onChange: (pagination) => {
this.setState({
current: pagination.current,
pageSize: pagination.pageSize,
}, () => {
this.loadData();
});
},
onFilter: (filters) => {
this.setState({
filters,
current: 1,
}, () => {
this.loadData();
});
},
};
return ( return (
<div className={styles.wrapper}> <DsTable coordinate={this.props.task.coordinate} />
<div className={styles.container}>
<Button onClick={() => { thisPush(this, { pathname: '../detail', state: { a: 1, b: 2, c: 3 } }); }}>detail</Button>
<TableEx {...tableProps} />
</div>
</div>
); );
} }
} }
List.propTypes = { export default List;
task: PropTypes.object.isRequired,
};
export default connect(({ task, loading }) => ({ task, loading }))(List);
/* eslint-disable no-underscore-dangle,no-param-reassign */
import { connect } from 'dva';
import { shallowEqual } from './helper';
const registerModel = (app, model) => {
// noinspection JSUnresolvedVariable
if (app._models.filter(m => m.namespace === model.namespace).length === 1) {
if (model.global) {
return;
}
app.unmodel(model.namespace);
}
app.model(model);
};
const normLocalState = (preState, state) => {
if (!preState || !state) {
return state;
}
const { loading: preLoading, ...preModels } = preState;
const { loading, ...models } = state;
if (shallowEqual(preModels, models)) {
if (!preLoading && !loading) {
return preState;
}
if (!preLoading || !loading) {
return state;
}
const { global: preGlobal, models: preModelsLoading, effects: preEffectsLoading } = preLoading;
const { global, models: modelsLoading, effects: effectsLoading } = loading;
if ((preGlobal && !global) || (!preGlobal && global)) {
return state;
}
return (shallowEqual(preModelsLoading, modelsLoading) && shallowEqual(preEffectsLoading, effectsLoading)) ? preState : state;
} else {
return state;
}
};
const hackDispatch = (module, dispatch) => action => dispatch({
...action,
type: `${module}/${action.type}`,
});
const hackSagaEffects = (module, sagaEffects) => {
const put = (action) => {
const { type } = action;
return sagaEffects.put({ ...action, type: `${module}/${type}` });
};
put.resolve = (action) => {
const { type } = action;
return sagaEffects.put.resolve({ ...action, type: `${module}/${type}` });
};
const take = (type) => {
const { take: oTake } = sagaEffects;
if (typeof type === 'string') {
return oTake(`${module}/${type}`);
} else if (Array.isArray(type)) {
return oTake(type.map((t) => {
if (typeof t === 'string') {
return `${module}/${type}`;
}
return t;
}));
} else {
return oTake(type);
}
};
return { ...sagaEffects, put, take };
};
const hackEffect = (module, effect) => function * effectGenerator(action, sagaEffects) {
return yield effect(action, hackSagaEffects(module, sagaEffects));
};
const hackEffects = (module, effects) => {
const hackedEffects = {};
for (const key of Object.keys(effects)) {
const effect = effects[key];
if (Array.isArray(effect)) {
hackedEffects[key] = [hackEffect(module, effect[0]), effect[1]];
} else {
hackedEffects[key] = hackEffect(module, effect);
}
}
return hackedEffects;
};
const hackSubscriptions = (module, subscriptions) => {
const hackedSubscriptions = {};
for (const key of Object.keys(subscriptions)) {
const subscription = subscriptions[key];
hackedSubscriptions[key] = (api, done) => subscription(
{ ...api, dispatch: hackDispatch(module, api.dispatch) },
done,
);
}
return hackedSubscriptions;
};
export const hackModel = (module, model) => {
model = { ...model };
model.namespace = `${module}/${model.namespace}`;
model.initialState = model.state;
model.effects = hackEffects(module, model.effects || {});
model.subscriptions = hackSubscriptions(module, model.subscriptions || {});
return model;
};
export const bindModel = (app, info, layout) => (...models) => {
const _models = [];
for (let model of models) {
if (typeof model === 'function') {
model = hackModel(info.name, model(info, layout));
} else {
model = hackModel(info.name, model);
}
_models.push(model);
registerModel(app, model);
}
const getLocalNamespace = ns => ns.substring(info.name.length + 1);
return (mapStateToProps, mapDispatchToProps, mergeProps) => (route) => {
let preLocalState = {};
const createLocalState = (state) => {
const localState = {};
for (const model of _models) {
const localNamespace = getLocalNamespace(model.namespace);
localState[localNamespace] = state[model.namespace];
if (state.loading) {
localState.loading = {};
if (state.loading.models) {
localState.loading.models = {};
localState.loading.models[localNamespace] = state.loading.models[model.namespace];
}
if (state.loading.effects) {
localState.loading.effects = {};
const effects = state.loading.effects;
for (const key of Object.keys(effects)) {
if (key.startsWith(`${model.namespace}/`)) {
localState.loading.effects[getLocalNamespace(key)] = state.loading.effects[key];
}
}
localState.loading.global = Object.values(localState.loading.models).some(e => e)
|| Object.values(localState.loading.effects).some(e => e);
}
}
}
return localState;
};
if (!mapStateToProps) {
mapStateToProps = localState => localState;
}
const _mapStateToProps = (state, ownProps) => {
preLocalState = normLocalState(preLocalState, createLocalState(state));
return mapStateToProps(preLocalState, state, ownProps);
};
if (!mapDispatchToProps) {
mapDispatchToProps = dispatch => ({ dispatch });
}
const _mapDispatchToProps = (dispatch, ownProps) => mapDispatchToProps(action => dispatch({
...action,
type: `${info.name}/${action.type}`,
}), ownProps);
return connect(_mapStateToProps, _mapDispatchToProps, mergeProps)(route);
};
};
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论