mutants-appfx
Version:
appfx module
594 lines (552 loc) • 16.9 kB
JavaScript
import {
action,
runInAction,
observable,
computed,
reaction
} from 'mobx';
import ViewModel from './viewmodels';
import {
DataModel,
DataList
} from './datamodels';
import Controller from './controllers';
import RuleSet from './ruleset';
import DataProviderManager from './DataProviderManager';
import AppFlowManager from './flow/AppFlowManager';
import UIFunction from './UIFunction';
import querystring from 'querystring';
import matchPath from "react-router/matchPath";
import {
i18n,
history,
dropByCacheKey
} from 'mutants-microfx';
import UIController from './viewmodels/UIController';
const t = i18n.getFixedT(null, 'appfx');
class Event {
name;
args;
constructor(name, args) {
this.name = name;
this.args = args;
}
}
class EventStamp {
name;
timestamp;
constructor(name, timestamp = new Date().getTime()) {
this.name = name;
this.timestamp = timestamp
}
}
function formatJson(json) {
return {
...json,
view: typeof json.view === 'string' ? {
name: json.view,
mode: 'single'
} : json.view,
views: Object.keys(json.views || {}).reduce((ret, key) => ({
...ret,
[key]: typeof json.views[key] === 'string' ? {
name: json.views[key],
mode: 'single'
} : json.views[key]
}), {})
}
}
class AppInstance {
manager;
cache;
dataList; // 所有数据的集合,集合里只是数据不是DM
dataModel; // 当前行的数据模型
controller;
viewController;
ruleset;
views = [];
viewModels = [];
mudule;
pid;
iid; // 页面实例ID
// 提供模块间数据共享,用map结构添加值可以被观察
state = observable.map({});
// 缓存所有加载的datamodel.id
ids = new Set();
// vm的单例集合
singles = {};
eventStack = [];
// 是否有事件正在执行
get eventHanding() {
return this.eventStack.length > 0;
}
get view() {
const vo = this.views.find(it => it.iid === this.iid);
if (!vo) {
return null;
}
return vo.view;
}
get viewModel() {
const vmo = this.viewModels.find(it => it.iid === this.iid);
if (!vmo) {
return null;
}
return vmo.viewModel;
}
get loaded() {
return this.viewModel.loaded;
}
constructor(manager, dataList, dataModel, viewController, controller, ruleset, module, state, cache) {
// MD和DL没有直接关系,DM靠DL数据行改变刷新数据
this.dataList = dataList;
this.dataModel = dataModel;
this.ruleset = ruleset;
this.manager = manager;
this.cache = cache;
this.viewController = viewController;
this.controller = controller;
this.module = module;
this.updateState(state);
this.viewController._init(this.viewModels);
this.controller._init(this.viewModels);
// 当前模型改变需要重置controller
this.listListener = reaction(() => this.dataList.current, async data => {
if (data) {
// 列表选中行改变需要加载明细
// todo 明细改变了还需要会写列表?
await this.dataModel.restore(data);
// 如果可能列表数据不全,需要取全
if (data.ID) {
await this.loadData(data.ID);
}
}
});
this.pageListener = reaction(() => this.iid, async iid => {
await this.viewController._curView(iid, this.pid);
await this.controller._curView(iid, this.pid);
await this.controller.ui._init(this.viewModel);
})
this.dataListener = reaction(() => this.dataModel.toJS(), data => {
if (!this.dataModel.getId()) {
// 没有保存不缓存,清空后无法恢复
return;
}
const cacheKey = this.module.sysid + '_' + this.module.mid + ':' + this.dataModel.getId();
this.cache.set(cacheKey, data);
}, {
delay: 500
})
}
updateState(state) {
Object.keys(state).forEach(key => {
this.state.set(key, state[key]);
})
}
async pushEvent(name) {
const et = new EventStamp(name);
// TODO 提供事件的依赖控制
this.eventStack.push(et);
console.log('events===>', this.eventStack.map(it => it.name));
return et.timestamp;
}
async popEvent(timestamp) {
const et = this.eventStack.find(et => et.timestamp === timestamp);
if (!et) {
console.error('event out!');
return;
}
this.eventStack.splice(this.eventStack.indexOf(et), 1);
console.log('events===>', this.eventStack.map(it => it.name));
}
async close() {
await this.manager.closeApp(this);
}
dispose() {
this.ruleset && this.ruleset.dispose();
this.listListener();
this.pageListener();
this.dataListener();
}
async executeAction(name, args) {
const dataModel = this.dataModel;
// 执行业务前需要生成数据快照,要是发生业务错误需要回滚
// 快照放在viewcontroller之前创建,有可能vc调用bc
const snapshot = await dataModel.toJS();
try {
// 先触发视图逻辑
if (typeof this.viewController[name] === 'function') {
// viewmodel不提供快照功能,vm改变会直接和用户交互
await this.viewController[name].bind(this.viewController)(args);
} else if (typeof this.controller[name] === 'function') { // 再触发业务逻辑
// TODO 业务计算过程的防抖问题,可以能一次调用多个action最后一个结束后才需要刷新一次界面
await this.controller[name].bind(this.controller)(args);
}
} catch (err) {
if (err.name === 'BizError') {
// message.error(err.message);
this.controller.ui.feedback(err.message, 'error');
}
console.error('action failed!', err);
if (snapshot) {
await dataModel.restore(snapshot);
}
}
}
async triggerEvent(name, args) {
await this.ruleset && this.ruleset.execute([this.viewModel, this.dataModel, new Event(name, args)])
}
async clearData() {
this.viewModel && await this.viewModel.refresh({
...this.state.toJS(),
...this.module,
pid: this.pid
});
this.dataModel && this.dataModel.restore();
this.viewController && this.viewController.init && await this.viewController._initData(this.dataModel);
this.controller && this.controller.init && await this.controller._initData(this.dataModel);
this.loadedId = null;
}
async loadData(id) {
if (this.loadedId === id || !id) {
return;
}
console.log('load data...', this.state.toJS())
//console.log('app.dataModel========',dataModel);
// 有缓存
const cacheKey = this.module.sysid + '_' + this.module.mid + ':' + id;
if (this.cache.has(cacheKey)) {
await this.dataModel.restore(this.cache.get(cacheKey));
} else {
await this.dataModel.load({
...this.state.toJS(),
...this.module,
id
});
}
this.ids.add(id);
this.loadedId = id;
}
async _close(i, backIndex) {
console.log('close view...')
// 返回距这个页面最近的view
const backView = this.views[backIndex ? backIndex : i - 1] || {
pid: null,
iid: null
};
const removeVm = this.viewModels[i]
// 这里好像不太好,open是靠数据驱动,但是close可能会null一下
await this.viewController.closeView(removeVm.viewModel);
runInAction(() => {
this.views.remove(this.views[i]);
this.viewModels.remove(removeVm);
this.pid = backView.pid;
this.iid = backView.iid;
});
}
async popView() {
const i = this.views.length - 1;
if (i <= 0) {
return;
}
await this._close(i);
}
async closeView(pid) {
pid = pid || 'index';
const lastOpened = [...this.views].reverse().find(it => it.pid === pid);
if (!lastOpened) {
return;
}
const i = this.views.indexOf(lastOpened);
// if (i <= 0) {
// return;
// }
await this._close(i);
if (i <= 0) {
// 最后一个页面关闭直接关闭整个模块
await this.manager.close(this.module);
}
}
async openView(pid, action = 'PUSH') {
if (action === 'REPLACE') {
return;
}
pid = pid || 'index';
// view支持多实例
const opened = this.views.find(it => it.pid === pid);
const vi = ViewModel.getView({
...this.state.toJS(),
...this.module,
pid
});
const iid = new Date().getTime();
if (opened) {
if (action === 'POP') {
this.pid = pid;
this.iid = opened.iid;
return opened;
}
//const vi = this.module.views[pid] || this.module.view;
if (vi.mode !== 'multi') {
this.views.push({
...opened,
iid
});
this.viewModels.push({
...this.viewModels.find(it => it.pid === pid),
iid
});
this.pid = pid;
this.iid = iid;
// // 采用改变栈顺序代替压栈
// const i = this.views.indexOf(opened);
// const removeViews = this.views.splice(i, 1);
// const removeViewModels = this.viewModels.splice(i, 1);
// this.views.push(...removeViews);
// this.viewModels.push(...removeViewModels);
// this.pid = pid;
// this.iid = opened.iid;
return {
...opened,
iid
};
}
}
console.log('open view...')
const view = vi.type;
//console.log('app.view========',view);
const vm = await ViewModel.load({
...this.state.toJS(),
...this.module,
pid
}, this.dataModel, this.dataList, this.singles);
//console.log('app.viewModel========',viewModel);
const newView = {
iid,
pid,
view,
mode: vi.mode
};
runInAction(() => {
this.views.push(newView);
this.viewModels.push({
iid,
pid,
viewModel: vm,
mode: vi.mode
});
this.pid = pid;
this.iid = iid;
});
return newView;
}
}
class AppManager {
apps = observable.map();
// 缓存数据实例
cache = new Map();
async open({
pid,
sysid,
mid,
id,
action,
...state
}) {
const appKey = sysid + '_' + mid;
if (this.apps.has(appKey)) {
const app = this.apps.get(appKey);
app.updateState(state);
await app.openView(pid, action);
if (id) {
await app.loadData(id);
} else {
// 一个模块多个页面跳转时不应该清空数据
// 要是关闭重新打开,需要调用close
//await app.clearData();
}
return app;
}
console.log('open app...')
const module = formatJson((await DataProviderManager.getModule({
sysid,
mid
})));
const appFlow = AppFlowManager.getAppFlow({
sysid,
mid
});
if (appFlow.useUIFunction) {
await UIFunction.loadUIFunction(module);
if (!UIFunction.get(module.mid).has('Read')) {
throw new Error(t('你没有当前模块的权限,无法打开此模块'))
}
}
console.log('app.module========', module);
// todo register task
// DL暂时只加载一个简单的数据列表
const dataList = new DataList(module);
// 调整下加载dm和vm顺序 适应T+的后台服务
const dataModel = await DataModel.create(module);
// 加载一个业务控制器,由view发送event触发其action执行
const uicontroller = new UIController();
const controller = await Controller.createBizController(module, null, uicontroller);
// 增加一个视图控制器
const viewController = await Controller.createViewController(module, controller, uicontroller);
// nools规则引擎,编写事件、数据规则,调用vm、dm和controller
let ruleset;
if (appFlow.useRuleEngine) {
ruleset = await RuleSet.load(module, {
ViewModel: ViewModel,
DataModel: DataModel,
Event: Event
}, {
controller, // 业务控制器
uicontroller, // 抽象 UI 控制器
viewController, // 视图控制器
uifunction: UIFunction.get(module.mid) // 功能权限
});
}
const app = new AppInstance(this, dataList, dataModel, viewController, controller, ruleset, module, state, this.cache);
// 这里不能放在构造函数中,保证业务逻辑的发生在所有app对象构造完毕之后
await app.openView(pid);
// 加载或者新增一个空DM
// 这里好像应该是VM决定是否加载一行还是取一页,取决于界面是单据还是列表的交互形式
// 这里保证没有id也要调用load创建一个空对象
await app.loadData(id);
await viewController._initData(dataModel);
await controller._initData(dataModel);
if (appFlow.useRuleEngine) {
// 执行规则计算
app.whenDisposer = reaction(() => [app.viewModel, app.dataModel],
(models) => ruleset.execute(models)
);
}
// 需要报set集合中放到最后,有可能加载viewinfo或者datainfo时出错
if (appFlow.noCache) {
console.warn(`app [${appKey}] is forbidden cache`);
// for(const it of this.apps){
// await this.close(it.module);
// }
} else {
runInAction(() => {
this.apps.set(appKey, app);
});
}
return app;
}
async close({
sysid,
mid,
}) {
const appKey = sysid + '_' + mid;
if (this.apps.has(appKey)) {
console.log('close app...')
const inst = this.apps.get(appKey);
inst.dispose();
const ids = inst.ids;
for (const id of ids) {
this.cache.delete(appKey + ':' + id);
}
ids.clear();
this.apps.delete(appKey);
//if (this.apps.length<=0){
dropByCacheKey('templatePage');
//}
}
}
// TODO 废弃改用close
async closeApp(...args) {
this.close(...args);
}
async closeView({
pid,
sysid,
mid,
}) {
const appKey = sysid + '_' + mid;
if (this.apps.has(appKey)) {
const app = this.apps.get(appKey);
pid ? await app.closeView(pid) : await app.popView();
return app;
}
}
constructor() {
let lastMatch;
let tasks = [];
let isActiving = false;
const executeTask = async () => {
const it = tasks[0];
if (!it) {
isActiving = false;
return;
}
isActiving = true;
await it();
tasks.shift();
await executeTask();
}
const addTask = (...items) => {
tasks.push(...items);
if (!isActiving) {
executeTask().catch(console.error);
}
}
history.listen((location, action) => {
const {
pid
} = querystring.parse(location.search.substr(1));
const match = matchPath(location.pathname, {
path: '/:sysid/:mid/:id?'
});
if (!match) {
lastMatch = match;
return;
}
const app = this.apps.get(match.params.sysid + '_' + match.params.mid);
if (app) {
// for (const v of app.views) {
// // 多实例视图切换页面自动关闭
// if (v.mode === 'multi' && v.pid !== pid) {
// app.closeView(v.pid).catch(console.error);
// }
// }
if (action === 'POP') {
addTask(async () => {
const curIndex = app.views.indexOf([...app.views].reverse().find(it => it.pid === (pid || 'index')));
const removes = [];
for (let i = curIndex + 1, l = app.views.length; i < l; i++) {
if (app.views[i].mode !== 'single') {
removes.push(app.views[i].pid);
}
}
for (const pid of removes) {
await app.closeView(pid);
}
})
} else if (action === 'REPLACE' && lastMatch) {
// 需要先开后关,保证不会出现白屏
addTask(async () => {
const curIndex = app.views.indexOf([...app.views].reverse().find(it => it.pid === (app.pid || 'index')));
const removeIndexs = [];
for (let i = curIndex, l = app.views.length; i < l; i++) {
if (app.views[i].mode !== 'single') {
removeIndexs.push(i);
}
}
app.updateState(location.state);
const view = await app.openView(pid);
for (let i of removeIndexs) {
await app._close(i, app.views.findIndex(v => v.iid === view.iid));
}
if (match.params.id) {
await app.loadData(match.params.id);
}
});
}
}
lastMatch = match;
});
}
}
export default global.$AppManager = new AppManager();