Skip to content

Commit

Permalink
feat: 支持数据源事件 (#605)
Browse files Browse the repository at this point in the history
* feat: 添加observedData

* feat: 修改错误

* fix: 修复单测报错问题

* feat: 完善数据源事件

* fix: 修复数据源事件调用组件方法时报错的异常

* fix: 修复多个相同类型的数据源数据变化的事件混淆的问题

* chore: 删除无用代码

* feat: 默认使用SimpleObservedData

* feat: 删除无用代码

---------

Co-authored-by: marchyang <[email protected]>
  • Loading branch information
qwertyyb and marchyang authored May 13, 2024
1 parent 8312046 commit 88c04c6
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 77 deletions.
43 changes: 42 additions & 1 deletion packages/core/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { EventEmitter } from 'events';

import { has, isEmpty } from 'lodash-es';

import { createDataSourceManager, DataSourceManager } from '@tmagic/data-source';
import { createDataSourceManager, DataSourceManager, ObservedDataClass } from '@tmagic/data-source';
import {
ActionType,
type AppCore,
Expand All @@ -35,6 +35,7 @@ import {
type MApp,
type RequestFunction,
} from '@tmagic/schema';
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX } from '@tmagic/utils';

import Env from './Env';
import { bindCommonEventListener, isCommonMethod, triggerCommonMethod } from './events';
Expand All @@ -52,6 +53,7 @@ interface AppOptionsConfig {
useMock?: boolean;
transformStyle?: (style: Record<string, any>) => Record<string, any>;
request?: RequestFunction;
DataSourceObservedData?: ObservedDataClass;
}

interface EventCache {
Expand Down Expand Up @@ -79,6 +81,7 @@ class App extends EventEmitter implements AppCore {
public eventQueueMap: Record<string, EventCache[]> = {};

private eventList = new Map<(fromCpt: Node, ...args: any[]) => void, string>();
private dataSourceEventList = new Map<string, Map<string, (...args: any[]) => void>>();

constructor(options: AppOptionsConfig) {
super();
Expand Down Expand Up @@ -272,6 +275,7 @@ class App extends EventEmitter implements AppCore {
this.on(eventName, eventHandler);
});
}
this.bindDataSourceEvents();
}

public emit(name: string | symbol, ...args: any[]): boolean {
Expand Down Expand Up @@ -356,6 +360,43 @@ class App extends EventEmitter implements AppCore {
}
}

private bindDataSourceEvents() {
if (this.platform === 'editor') return;

// 先清掉之前注册的事件,重新注册
Array.from(this.dataSourceEventList.keys()).forEach((dataSourceId) => {
const dataSourceEvent = this.dataSourceEventList.get(dataSourceId)!;
Array.from(dataSourceEvent.keys()).forEach((eventName) => {
const [prefix, ...path] = eventName.split('.');
if (prefix === DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX) {
this.dataSourceManager?.offDataChange(dataSourceId, path.join('.'), dataSourceEvent.get(eventName)!);
} else {
this.dataSourceManager?.get(dataSourceId)?.off(prefix, dataSourceEvent.get(eventName)!);
}
});
});

(this.dsl?.dataSources || []).forEach((dataSource) => {
const dataSourceEvent = this.dataSourceEventList.get(dataSource.id) ?? new Map<string, (args: any) => void>();
(dataSource.events || []).forEach((event) => {
const [prefix, ...path] = event.name?.split('.') || [];
if (!prefix) return;
const handler = (...args: any[]) => {
this.eventHandler(event, this.dataSourceManager?.get(dataSource.id), args);
};
dataSourceEvent.set(event.name, handler);
if (prefix === DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX) {
// 数据源数据变化
this.dataSourceManager?.onDataChange(dataSource.id, path.join('.'), handler);
} else {
// 数据源自定义事件
this.dataSourceManager?.get(dataSource.id)?.on(prefix, handler);
}
});
this.dataSourceEventList.set(dataSource.id, dataSourceEvent);
});
}

/**
* 事件联动处理函数
* @param eventsConfigIndex 事件配置索引,可以通过此索引从node.event中获取最新事件配置
Expand Down
3 changes: 2 additions & 1 deletion packages/data-source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@
],
"dependencies": {
"@tmagic/dep": "workspace:*",
"@tmagic/utils": "workspace:*",
"@tmagic/schema": "workspace:*",
"@tmagic/utils": "workspace:*",
"deep-state-observer": "^5.5.13",
"events": "^3.3.0",
"lodash-es": "^4.17.21"
},
Expand Down
18 changes: 17 additions & 1 deletion packages/data-source/src/DataSourceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import { cloneDeep } from 'lodash-es';
import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema';
import { compiledNode } from '@tmagic/utils';

import { SimpleObservedData } from './observed-data/SimpleObservedData';
import { DataSource, HttpDataSource } from './data-sources';
import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions } from './types';
import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions, ObservedDataClass } from './types';
import { compiledNodeField, compliedConditions, compliedIteratorItems } from './utils';

class DataSourceManager extends EventEmitter {
private static dataSourceClassMap = new Map<string, typeof DataSource>();
// eslint-disable-next-line @typescript-eslint/naming-convention
private static ObservedDataClass: ObservedDataClass = SimpleObservedData;

public static register<T extends typeof DataSource = typeof DataSource>(type: string, dataSource: T) {
DataSourceManager.dataSourceClassMap.set(type, dataSource);
Expand All @@ -45,6 +48,10 @@ class DataSourceManager extends EventEmitter {
return DataSourceManager.dataSourceClassMap.get(type);
}

public static registerObservedData(ObservedDataClass: ObservedDataClass) {
DataSourceManager.ObservedDataClass = ObservedDataClass;
}

public app: AppCore;

public dataSourceMap = new Map<string, DataSource>();
Expand Down Expand Up @@ -133,6 +140,7 @@ class DataSourceManager extends EventEmitter {
request: this.app.request,
useMock: this.useMock,
initialData: this.data[config.id],
ObservedDataClass: DataSourceManager.ObservedDataClass,
});

this.dataSourceMap.set(config.id, ds);
Expand Down Expand Up @@ -210,6 +218,14 @@ class DataSourceManager extends EventEmitter {
});
this.dataSourceMap.clear();
}

public onDataChange(id: string, path: string, callback: (newVal: any) => void) {
return this.get(id)?.onDataChange(path, callback);
}

public offDataChange(id: string, path: string, callback: (newVal: any) => void) {
return this.get(id)?.offDataChange(path, callback);
}
}

DataSourceManager.register('http', HttpDataSource as any);
Expand Down
41 changes: 27 additions & 14 deletions packages/data-source/src/data-sources/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import EventEmitter from 'events';

import type { AppCore, CodeBlockContent, DataSchema, DataSourceSchema } from '@tmagic/schema';
import { getDefaultValueFromFields, setValueByKeyPath } from '@tmagic/utils';
import { getDefaultValueFromFields } from '@tmagic/utils';

import { ObservedData } from '@data-source/observed-data/ObservedData';
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
import type { ChangeEvent, DataSourceOptions } from '@data-source/types';

/**
Expand All @@ -28,8 +30,6 @@ import type { ChangeEvent, DataSourceOptions } from '@data-source/types';
export default class DataSource<T extends DataSourceSchema = DataSourceSchema> extends EventEmitter {
public isInit = false;

public data: Record<string, any> = {};

/** @tmagic/core 实例 */
public app: AppCore;

Expand All @@ -38,6 +38,7 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
#type = 'base';
#id: string;
#schema: T;
#observedData: ObservedData;

/** 数据源自定义字段配置 */
#fields: DataSchema[] = [];
Expand All @@ -55,22 +56,27 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
this.setFields(options.schema.fields);
this.setMethods(options.schema.methods || []);

let data = options.initialData;
// eslint-disable-next-line @typescript-eslint/naming-convention
const ObservedDataClass = options.ObservedDataClass || SimpleObservedData;
if (this.app.platform === 'editor') {
// 编辑器中有mock使用mock,没有使用默认值
this.mockData = options.schema.mocks?.find((mock) => mock.useInEditor)?.data || this.getDefaultData();
this.setData(this.mockData);
data = this.mockData;
} else if (typeof options.useMock === 'boolean' && options.useMock) {
// 设置了使用mock就使用mock数据
this.mockData = options.schema.mocks?.find((mock) => mock.enable)?.data || this.getDefaultData();
this.setData(this.mockData);
data = this.mockData;
} else if (!options.initialData) {
this.setData(this.getDefaultData());
data = this.getDefaultData();
} else {
// 在ssr模式下,会将server端获取的数据设置到initialData
this.setData(options.initialData);
this.#observedData = new ObservedDataClass(options.initialData ?? {});
// 设置isInit,防止manager中执行init方法
this.isInit = true;
return;
}
this.#observedData = new ObservedDataClass(data ?? {});
}

public get id() {
Expand Down Expand Up @@ -101,13 +107,12 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
this.#methods = methods;
}

public get data() {
return this.#observedData.getData('');
}

public setData(data: any, path?: string) {
if (path) {
setValueByKeyPath(path, data, this.data);
} else {
// todo: 校验数据,看是否符合 schema
this.data = data;
}
this.#observedData.update(data, path);

const changeEvent: ChangeEvent = {
updateData: data,
Expand All @@ -117,6 +122,14 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
this.emit('change', changeEvent);
}

public onDataChange(path: string, callback: (newVal: any) => void) {
this.#observedData.on(path, callback);
}

public offDataChange(path: string, callback: (newVal: any) => void) {
this.#observedData.off(path, callback);
}

public getDefaultData() {
return getDefaultValueFromFields(this.#fields);
}
Expand All @@ -126,8 +139,8 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
}

public destroy() {
this.data = {};
this.#fields = [];
this.removeAllListeners();
this.#observedData.destroy();
}
}
1 change: 1 addition & 0 deletions packages/data-source/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
export { default as DataSourceManager } from './DataSourceManager';
export * from './data-sources';
export * from './createDataSourceManager';
export * from './observed-data';
export * from './utils';
export * from './types';
49 changes: 49 additions & 0 deletions packages/data-source/src/observed-data/DeepObservedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import State from 'deep-state-observer';

import { ObservedData } from './ObservedData';

const ignoreFirstCall = <F extends (...args: any[]) => any>(fn: F) => {
let calledTimes = 0;
return (...args: Parameters<F>) => {
if (calledTimes === 0) {
calledTimes += 1;
return;
}
return fn(...args);
};
};

export class DeepObservedData extends ObservedData {
state?: State;
subscribers = new Map<string, Map<Function, () => void>>();
constructor(initialData: Record<string, any>) {
super();
this.state = new State(initialData);
}
update = (data: any, path?: string) => {
this.state?.update(path ?? '', data);
};
on = (path: string, callback: (newVal: any) => void) => {
// subscribe 会立即执行一次,ignoreFirstCall 会忽略第一次执行
const unsubscribe = this.state!.subscribe(path, ignoreFirstCall(callback));

// 把取消监听的函数保存下来,供 off 时调用
const pathSubscribers = this.subscribers.get(path) ?? new Map<Function, () => void>();
pathSubscribers.set(callback, unsubscribe);
this.subscribers.set(path, pathSubscribers);
};
off = (path: string, callback: (newVal: any) => void) => {
const pathSubscribers = this.subscribers.get(path);
if (!pathSubscribers) return;

pathSubscribers.get(callback)?.();
pathSubscribers.delete(callback);
};
getData = (path: string) => (!this.state ? {} : this.state?.get(path));
destroy = () => {
// 销毁所有未被取消的监听
this.subscribers.forEach((pathSubscribers) => {
pathSubscribers.forEach((unsubscribe) => unsubscribe());
});
};
}
11 changes: 11 additions & 0 deletions packages/data-source/src/observed-data/ObservedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export abstract class ObservedData {
abstract update(data: any, path?: string): void;

abstract on(path: string, callback: (newVal: any) => void): void;

abstract off(path: string, callback: (newVal: any) => void): void;

abstract getData(path: string): any;

abstract destroy(): void;
}
38 changes: 38 additions & 0 deletions packages/data-source/src/observed-data/SimpleObservedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EventEmitter } from 'events';

import { getValueByKeyPath, setValueByKeyPath } from '@tmagic/utils';

import { ObservedData } from './ObservedData';

export class SimpleObservedData extends ObservedData {
data: Record<string, any> = {};
private event = new EventEmitter();

constructor(initialData: Record<string, any>) {
super();
this.data = initialData;
}
update(data: any, path?: string): void {
if (path) {
setValueByKeyPath(path, data, this.data);
} else {
this.data = data;
}

const changeEvent = {
updateData: data,
path: path ?? '',
};
this.event.emit(path ?? '', changeEvent);
}
on(path: string, callback: (newVal: any) => void): void {
this.event.on(path, callback);
}
off(path: string, callback: (newVal: any) => void): void {
this.event.off(path, callback);
}
getData(path: string) {
return path ? getValueByKeyPath(path, this.data) : this.data;
}
destroy(): void {}
}
3 changes: 3 additions & 0 deletions packages/data-source/src/observed-data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ObservedData } from './ObservedData';
export { DeepObservedData } from './DeepObservedData';
export { SimpleObservedData } from './SimpleObservedData';
4 changes: 4 additions & 0 deletions packages/data-source/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import type { AppCore, DataSourceSchema, HttpOptions, RequestFunction } from '@t

import type DataSource from './data-sources/Base';
import type HttpDataSource from './data-sources/Http';
import { ObservedData } from './observed-data/ObservedData';

export type ObservedDataClass = new (...args: any[]) => ObservedData;

export interface DataSourceOptions<T extends DataSourceSchema = DataSourceSchema> {
schema: T;
app: AppCore;
initialData?: Record<string, any>;
useMock?: boolean;
request?: RequestFunction;
ObservedDataClass?: ObservedDataClass;
[key: string]: any;
}

Expand Down
Loading

0 comments on commit 88c04c6

Please sign in to comment.