feat(core,data-source,utils,react-runtime-help,vue-runtime-help): 新增对页面片节点的管理

This commit is contained in:
roymondchen 2025-07-15 15:08:53 +08:00
parent aaf8046c63
commit 6a54720068
12 changed files with 189 additions and 73 deletions

View File

@ -29,7 +29,7 @@ import Flexible from './Flexible';
import FlowState from './FlowState';
import Node from './Node';
import Page from './Page';
import { AppOptionsConfig, ErrorHandler } from './type';
import { AppOptionsConfig, ErrorHandler, GetNodeOptions } from './type';
import { transformStyle as defaultTransformStyle } from './utils';
class App extends EventEmitter {
@ -45,6 +45,7 @@ class App extends EventEmitter {
public codeDsl?: CodeBlockDSL;
public dataSourceManager?: DataSourceManager;
public page?: Page;
public pageFragments: Map<Id, Page> = new Map();
public useMock = false;
public platform = 'mobile';
public jsEngine: JsEngine = 'browser';
@ -159,6 +160,10 @@ class App extends EventEmitter {
super.emit('dsl-change', { dsl: config, curPage: pageId });
this.pageFragments.forEach((page) => {
page.destroy();
});
this.pageFragments.clear();
this.setPage(pageId);
if (this.dataSourceManager) {
@ -192,6 +197,11 @@ class App extends EventEmitter {
for (const [, node] of this.page.nodes) {
this.eventHelper.bindNodeEvents(node);
}
for (const [, page] of this.pageFragments) {
for (const [, node] of page.nodes) {
this.eventHelper.bindNodeEvents(node);
}
}
}
super.emit('page-change', this.page);
@ -215,8 +225,8 @@ class App extends EventEmitter {
}
}
public getNode<T extends Node = Node>(id: Id, iteratorContainerId?: Id[], iteratorIndex?: number[]) {
return this.page?.getNode<T>(id, iteratorContainerId, iteratorIndex);
public getNode<T extends Node = Node>(id: Id, options?: GetNodeOptions) {
return this.page?.getNode<T>(id, options);
}
public registerComponent(type: string, Component: any) {
@ -299,7 +309,12 @@ class App extends EventEmitter {
public destroy() {
this.removeAllListeners();
this.page?.destroy();
this.page = undefined;
this.pageFragments.forEach((page) => {
page.destroy();
});
this.pageFragments.clear();
this.flexible?.destroy();
this.flexible = undefined;

View File

@ -258,19 +258,34 @@ export default class EventHelper extends EventEmitter {
[to, methodName] = methodName;
}
const toNodes = [];
const toNode = this.app.getNode(to);
if (!toNode) throw new Error(`ID为${to}的组件不存在`);
if (toNode.instance) {
if (typeof toNode.instance[methodName] === 'function') {
await toNode.instance[methodName](fromCpt, ...args);
}
} else {
toNode.addEventToQueue({
method: methodName,
fromCpt,
args,
});
if (toNode) {
toNodes.push(toNode);
}
for (const [, page] of this.app.pageFragments) {
const node = page.getNode(to);
if (node) {
toNodes.push(node);
}
}
const instanceMethodPropmise = [];
for (const node of toNodes) {
if (node.instance) {
if (typeof node.instance[methodName] === 'function') {
instanceMethodPropmise.push(node.instance[methodName](fromCpt, ...args));
}
} else {
node.addEventToQueue({
method: methodName,
fromCpt,
args,
});
}
}
await Promise.all(instanceMethodPropmise);
}
}

View File

@ -75,7 +75,13 @@ class Node extends EventEmitter {
this.events = events || [];
this.style = style || {};
try {
this.instance.config = data;
if (
this.instance &&
!Object.isFrozen(this.instance) &&
Object.getOwnPropertyDescriptor(this.instance, 'config')?.writable !== false
) {
this.instance.config = data;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {}

View File

@ -22,6 +22,7 @@ import App from './App';
import IteratorContainer from './IteratorContainer';
import type { default as TMagicNode } from './Node';
import Node from './Node';
import { GetNodeOptions } from './type';
interface ConfigOptions {
config: MPage | MPageFragment;
app: App;
@ -64,8 +65,15 @@ class Page extends Node {
if (config.type && this.app.pageFragmentContainerType.has(config.type) && config.pageFragmentId) {
const pageFragment = this.app.dsl?.items?.find((page) => page.id === config.pageFragmentId);
if (pageFragment) {
config.items = [pageFragment];
this.app.pageFragments.set(
config.id,
new Page({
config: pageFragment,
app: this.app,
}),
);
}
}
@ -76,13 +84,16 @@ class Page extends Node {
public getNode<T extends TMagicNode = TMagicNode>(
id: Id,
iteratorContainerId?: Id[],
iteratorIndex?: number[],
{ iteratorContainerId, iteratorIndex, pageFragmentContainerId }: GetNodeOptions = {},
): T | undefined {
if (this.nodes.has(id)) {
return this.nodes.get(id) as T;
}
if (pageFragmentContainerId) {
return this.app.pageFragments.get(pageFragmentContainerId)?.getNode(id, { iteratorContainerId, iteratorIndex });
}
if (Array.isArray(iteratorContainerId) && iteratorContainerId.length && Array.isArray(iteratorIndex)) {
let iteratorContainer = this.nodes.get(iteratorContainerId[0]) as IteratorContainer;

View File

@ -43,3 +43,9 @@ export type AfterEventHandler = (args: {
source: TMagicNode | DataSource | undefined;
args: any[];
}) => void;
export interface GetNodeOptions {
iteratorContainerId?: Id[];
iteratorIndex?: number[];
pageFragmentContainerId?: Id;
}

View File

@ -168,15 +168,21 @@ describe('App', () => {
1,
);
expect(app.getNode('text', ['iterator-container_1'], [0])?.data.text).toBe('1');
expect(app.getNode('text', ['iterator-container_1'], [1])?.data.text).toBe('2');
expect(app.getNode('text_page_fragment', ['iterator-container_1'], [0])?.data.text).toBe('text_page_fragment');
expect(app.getNode('text', { iteratorContainerId: ['iterator-container_1'], iteratorIndex: [0] })?.data.text).toBe(
'1',
);
expect(app.getNode('text', { iteratorContainerId: ['iterator-container_1'], iteratorIndex: [1] })?.data.text).toBe(
'2',
);
expect(
app.getNode('text_page_fragment', { iteratorContainerId: ['iterator-container_1'], iteratorIndex: [0] })?.data
.text,
).toBe('text_page_fragment');
const ic1 = app.getNode(
'iterator-container_11',
['iterator-container_1'],
[0],
) as unknown as TMagicIteratorContainer;
const ic1 = app.getNode('iterator-container_11', {
iteratorContainerId: ['iterator-container_1'],
iteratorIndex: [0],
}) as unknown as TMagicIteratorContainer;
ic1?.setNodes(
[
@ -200,11 +206,10 @@ describe('App', () => {
1,
);
const ic2 = app.getNode(
'iterator-container_11',
['iterator-container_1'],
[1],
) as unknown as TMagicIteratorContainer;
const ic2 = app.getNode('iterator-container_11', {
iteratorContainerId: ['iterator-container_1'],
iteratorIndex: [1],
}) as unknown as TMagicIteratorContainer;
ic2?.setNodes(
[
@ -228,10 +233,30 @@ describe('App', () => {
1,
);
expect(app.getNode('text', ['iterator-container_1', 'iterator-container_11'], [0, 0])?.data.text).toBe('111');
expect(app.getNode('text', ['iterator-container_1', 'iterator-container_11'], [0, 1])?.data.text).toBe('222');
expect(app.getNode('text', ['iterator-container_1', 'iterator-container_11'], [1, 0])?.data.text).toBe('11');
expect(app.getNode('text', ['iterator-container_1', 'iterator-container_11'], [1, 1])?.data.text).toBe('22');
expect(
app.getNode('text', {
iteratorContainerId: ['iterator-container_1', 'iterator-container_11'],
iteratorIndex: [0, 0],
})?.data.text,
).toBe('111');
expect(
app.getNode('text', {
iteratorContainerId: ['iterator-container_1', 'iterator-container_11'],
iteratorIndex: [0, 1],
})?.data.text,
).toBe('222');
expect(
app.getNode('text', {
iteratorContainerId: ['iterator-container_1', 'iterator-container_11'],
iteratorIndex: [1, 0],
})?.data.text,
).toBe('11');
expect(
app.getNode('text', {
iteratorContainerId: ['iterator-container_1', 'iterator-container_11'],
iteratorIndex: [1, 1],
})?.data.text,
).toBe('22');
ic.resetNodes();

View File

@ -18,7 +18,7 @@
import { union } from 'lodash-es';
import type { default as TMagicApp } from '@tmagic/core';
import { getDepNodeIds, getNodes, isPage } from '@tmagic/core';
import { getDepNodeIds, getNodes, isPage, isPageFragment } from '@tmagic/core';
import DataSourceManager from './DataSourceManager';
import type { ChangeEvent, DataSourceManagerData } from './types';
@ -52,18 +52,19 @@ export const createDataSourceManager = (app: TMagicApp, useMock?: boolean, initi
// ssr环境下数据应该是提前准备好的放到initialData中不应该发生变化无需监听
// 有initialData不一定是在ssr环境下
if (app.jsEngine !== 'nodejs') {
dataSourceManager.on('change', (sourceId: string, changeEvent: ChangeEvent) => {
const dep = dsl.dataSourceDeps?.[sourceId] || {};
const condDep = dsl.dataSourceCondDeps?.[sourceId] || {};
if (app.jsEngine === 'nodejs') {
return dataSourceManager;
}
const nodeIds = union([...Object.keys(condDep), ...Object.keys(dep)]);
dataSourceManager.on('change', (sourceId: string, changeEvent: ChangeEvent) => {
const dep = dsl.dataSourceDeps?.[sourceId] || {};
const condDep = dsl.dataSourceCondDeps?.[sourceId] || {};
const pages = app.page?.data && app.platform !== 'editor' ? [app.page.data] : dsl.items;
const nodeIds = union([...Object.keys(condDep), ...Object.keys(dep)]);
dataSourceManager.emit(
'update-data',
getNodes(nodeIds, pages).map((node) => {
for (const page of dsl.items) {
if (app.platform === 'editor' || (isPage(page) && page.id === app.page?.data.id) || isPageFragment(page)) {
const newNodes = getNodes(nodeIds, [page]).map((node) => {
if (app.platform !== 'editor') {
node.condResult = dataSourceManager.compliedConds(node);
}
@ -73,19 +74,26 @@ export const createDataSourceManager = (app: TMagicApp, useMock?: boolean, initi
if (typeof app.page?.setData === 'function') {
if (isPage(newNode)) {
app.page.setData(newNode);
} else if (isPageFragment(newNode)) {
for (const [, page] of app.pageFragments) {
if (page.data.id === node.id) {
page.setData(newNode);
}
}
} else {
const n = app.page.getNode(node.id);
n?.setData(newNode);
app.getNode(node.id)?.setData(newNode);
}
}
return newNode;
}),
sourceId,
changeEvent,
);
});
}
});
if (newNodes.length) {
dataSourceManager.emit('update-data', newNodes, sourceId, changeEvent, page.id);
}
}
}
});
return dataSourceManager;
};

View File

@ -328,12 +328,11 @@ export const getNodes = (ids: Id[], data: MNode[] = []): MNode[] => {
return;
}
for (let i = 0, l = data.length; i < l; i++) {
const item = data[i];
for (const item of data) {
const index = ids.findIndex((id: Id) => `${id}` === `${item.id}`);
if (index > -1) {
ids.slice(index, 1);
ids.splice(index, 1);
nodes.push(item);
}
@ -372,7 +371,7 @@ export const getDepNodeIds = (dataSourceDeps: DataSourceDeps = {}) =>
* @param data
* @param parentId id
*/
export const replaceChildNode = (newNode: MNode, data?: MNode[], parentId?: Id) => {
export const replaceChildNode = (newNode: MNode, data?: MNode[], parentId?: Id): void => {
const path = getNodePath(newNode.id, data);
const node = path.pop();
let parent = path.pop();
@ -390,6 +389,7 @@ export const replaceChildNode = (newNode: MNode, data?: MNode[], parentId?: Id)
export const DSL_NODE_KEY_COPY_PREFIX = '__tmagic__';
export const IS_DSL_NODE_KEY = '__tmagic__dslNode';
export const PAGE_FRAGMENT_CONTAINER_ID_KEY = 'tmagic-page-fragment-container-id';
export const compiledNode = (
compile: (value: any) => any,

View File

@ -32,17 +32,22 @@ export interface UseAppOptions<T extends MNodeInstance = MNodeInstance> {
config: T;
iteratorContainerId?: Id[];
iteratorIndex?: number[];
pageFragmentContainerId?: Id;
methods?: {
[key: string]: (...args: any[]) => any;
};
}
export const useNode = <T extends TMagicNode = TMagicNode>(
props: Pick<UseAppOptions, 'config' | 'iteratorContainerId' | 'iteratorIndex'>,
props: Pick<UseAppOptions, 'config' | 'iteratorContainerId' | 'iteratorIndex' | 'pageFragmentContainerId'>,
app = useContext(AppContent),
): T | undefined => {
if (isDslNode(props.config) && props.config.id) {
app?.getNode(props.config.id, props.iteratorContainerId, props.iteratorIndex);
app?.getNode(props.config.id, {
iteratorContainerId: props.iteratorContainerId,
iteratorIndex: props.iteratorIndex,
pageFragmentContainerId: props.pageFragmentContainerId,
});
}
return void 0;
};

View File

@ -30,15 +30,20 @@ interface UseAppOptions<T extends MNodeInstance = MNodeInstance> {
config: T;
iteratorContainerId?: Id[];
iteratorIndex?: number[];
pageFragmentContainerId?: Id;
methods?: Methods;
}
export const useNode = <T extends TMagicNode = TMagicNode>(
props: Pick<UseAppOptions, 'config' | 'iteratorContainerId' | 'iteratorIndex'>,
props: Pick<UseAppOptions, 'config' | 'iteratorContainerId' | 'iteratorIndex' | 'pageFragmentContainerId'>,
app = inject<TMagicApp>('app'),
): T | undefined => {
if (isDslNode(props.config) && props.config.id) {
return app?.getNode(props.config.id, props.iteratorContainerId, props.iteratorIndex);
return app?.getNode(props.config.id, {
iteratorContainerId: props.iteratorContainerId,
iteratorIndex: props.iteratorIndex,
pageFragmentContainerId: props.pageFragmentContainerId,
});
}
return void 0;
};

View File

@ -1,28 +1,47 @@
import { inject, nextTick, onBeforeUnmount, reactive, ref } from 'vue-demi';
import type TMagicApp from '@tmagic/core';
import type { ChangeEvent, MNode } from '@tmagic/core';
import { isPage, replaceChildNode } from '@tmagic/core';
import type { ChangeEvent, Id, MNode } from '@tmagic/core';
import { isPage, isPageFragment, replaceChildNode } from '@tmagic/core';
export const useDsl = (app = inject<TMagicApp>('app')) => {
export const useDsl = (app = inject<TMagicApp>('app'), pageFragmentConstainerId: Id) => {
if (!app) {
throw new Error('useDsl must be used after MagicApp is created');
}
const pageConfig = ref<MNode | undefined>(app.page?.data);
const pageFragment = pageFragmentConstainerId ? app.pageFragments.get(pageFragmentConstainerId) : null;
app.on('page-change', () => {
pageConfig.value = app.page?.data;
});
const pageConfig = ref<MNode | undefined>(pageFragmentConstainerId ? pageFragment?.data : app.page?.data);
const updateDataHandler = (nodes: MNode[], sourceId: string, changeEvent: ChangeEvent) => {
nodes.forEach((node) => {
if (isPage(node)) {
if (pageFragmentConstainerId) {
app.on('dsl-change', () => {
pageConfig.value = pageFragment?.data;
});
} else {
app.on('page-change', () => {
pageConfig.value = app.page?.data;
});
}
const updateDataHandler = (nodes: MNode[], sourceId: string, changeEvent: ChangeEvent, pageId: Id) => {
if (
!nodes.length ||
(pageFragmentConstainerId && pageFragment?.data.id !== pageId) ||
(!pageFragmentConstainerId && app.page?.data.id !== pageId)
) {
return;
}
for (const node of nodes) {
if (
(isPage(node) && !pageFragmentConstainerId && node.id === pageId) ||
(isPageFragment(node) && pageFragmentConstainerId)
) {
pageConfig.value = node;
} else {
replaceChildNode(reactive(node), [pageConfig.value as MNode]);
}
});
}
nextTick(() => {
app.emit('replaced-node', { nodes, sourceId, ...changeEvent });

View File

@ -33,6 +33,7 @@ export interface ComponentProps<T extends Omit<MComponent, 'id'> = MComponent> {
iteratorIndex?: number[];
iteratorContainerId?: Id[];
containerIndex?: number;
pageFragmentContainerId?: Id;
model?: any;
disabled?: boolean;
}