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

View File

@ -258,19 +258,34 @@ export default class EventHelper extends EventEmitter {
[to, methodName] = methodName; [to, methodName] = methodName;
} }
const toNodes = [];
const toNode = this.app.getNode(to); const toNode = this.app.getNode(to);
if (!toNode) throw new Error(`ID为${to}的组件不存在`); if (toNode) {
toNodes.push(toNode);
if (toNode.instance) {
if (typeof toNode.instance[methodName] === 'function') {
await toNode.instance[methodName](fromCpt, ...args);
}
} else {
toNode.addEventToQueue({
method: methodName,
fromCpt,
args,
});
} }
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.events = events || [];
this.style = style || {}; this.style = style || {};
try { 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {} } catch (e: any) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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