Skip to content

feat: 支持数据源事件 #605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 eventConfig 事件配置
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