UNPKG

mutants-appfx

Version:

appfx module

594 lines (552 loc) 16.9 kB
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; @observable dataList; // 所有数据的集合,集合里只是数据不是DM @observable dataModel; // 当前行的数据模型 controller; viewController; ruleset; @observable views = []; @observable viewModels = []; mudule; @observable pid; @observable iid; // 页面实例ID // 提供模块间数据共享,用map结构添加值可以被观察 @observable state = observable.map({}); // 缓存所有加载的datamodel.id ids = new Set(); // vm的单例集合 singles = {}; @observable eventStack = []; // 是否有事件正在执行 @computed get eventHanding() { return this.eventStack.length > 0; } @computed get view() { const vo = this.views.find(it => it.iid === this.iid); if (!vo) { return null; } return vo.view; } @computed get viewModel() { const vmo = this.viewModels.find(it => it.iid === this.iid); if (!vmo) { return null; } return vmo.viewModel; } @computed 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 }) } @action updateState(state) { Object.keys(state).forEach(key => { this.state.set(key, state[key]); }) } @action 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; } @action 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); } } } @action async triggerEvent(name, args) { await this.ruleset && this.ruleset.execute([this.viewModel, this.dataModel, new Event(name, args)]) } @action 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; } @action 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; } @action 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; }); } @action async popView() { const i = this.views.length - 1; if (i <= 0) { return; } await this._close(i); } @action 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); } } @action 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 { @observable apps = observable.map(); // 缓存数据实例 cache = new Map(); @action 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; } @action 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 @action async closeApp(...args) { this.close(...args); } @action 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();