From 6a547200681991ae8abefd0e3d72a603d21f12d4 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 15 Jul 2025 15:08:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(core,data-source,utils,react-runtime-help,?= =?UTF-8?q?vue-runtime-help):=20=E6=96=B0=E5=A2=9E=E5=AF=B9=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=89=87=E8=8A=82=E7=82=B9=E7=9A=84=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/App.ts | 21 ++++++- packages/core/src/EventHelper.ts | 39 ++++++++---- packages/core/src/Node.ts | 8 ++- packages/core/src/Page.ts | 17 +++++- packages/core/src/type.ts | 6 ++ packages/core/tests/App.spec.ts | 59 +++++++++++++------ .../src/createDataSourceManager.ts | 44 ++++++++------ packages/utils/src/index.ts | 8 +-- .../react-runtime-help/src/hooks/use-app.ts | 9 ++- runtime/vue-runtime-help/src/hooks/use-app.ts | 9 ++- runtime/vue-runtime-help/src/hooks/use-dsl.ts | 41 +++++++++---- runtime/vue-runtime-help/src/index.ts | 1 + 12 files changed, 189 insertions(+), 73 deletions(-) diff --git a/packages/core/src/App.ts b/packages/core/src/App.ts index 7a63cc2b..c7ea216f 100644 --- a/packages/core/src/App.ts +++ b/packages/core/src/App.ts @@ -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 = 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(id: Id, iteratorContainerId?: Id[], iteratorIndex?: number[]) { - return this.page?.getNode(id, iteratorContainerId, iteratorIndex); + public getNode(id: Id, options?: GetNodeOptions) { + return this.page?.getNode(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; diff --git a/packages/core/src/EventHelper.ts b/packages/core/src/EventHelper.ts index 43726921..ee44ca60 100644 --- a/packages/core/src/EventHelper.ts +++ b/packages/core/src/EventHelper.ts @@ -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); } } diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts index 7d56208a..0f2b4514 100644 --- a/packages/core/src/Node.ts +++ b/packages/core/src/Node.ts @@ -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) {} diff --git a/packages/core/src/Page.ts b/packages/core/src/Page.ts index 135ffc7a..39c77fdb 100644 --- a/packages/core/src/Page.ts +++ b/packages/core/src/Page.ts @@ -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( 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; diff --git a/packages/core/src/type.ts b/packages/core/src/type.ts index 4ac8aa8f..d6a60eb3 100644 --- a/packages/core/src/type.ts +++ b/packages/core/src/type.ts @@ -43,3 +43,9 @@ export type AfterEventHandler = (args: { source: TMagicNode | DataSource | undefined; args: any[]; }) => void; + +export interface GetNodeOptions { + iteratorContainerId?: Id[]; + iteratorIndex?: number[]; + pageFragmentContainerId?: Id; +} diff --git a/packages/core/tests/App.spec.ts b/packages/core/tests/App.spec.ts index 5aa3d903..fa3ce015 100644 --- a/packages/core/tests/App.spec.ts +++ b/packages/core/tests/App.spec.ts @@ -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(); diff --git a/packages/data-source/src/createDataSourceManager.ts b/packages/data-source/src/createDataSourceManager.ts index edb2a1b5..1026f29e 100644 --- a/packages/data-source/src/createDataSourceManager.ts +++ b/packages/data-source/src/createDataSourceManager.ts @@ -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; }; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 06f2588c..3d63a295 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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, diff --git a/runtime/react-runtime-help/src/hooks/use-app.ts b/runtime/react-runtime-help/src/hooks/use-app.ts index 98d9bb43..825b9ecd 100644 --- a/runtime/react-runtime-help/src/hooks/use-app.ts +++ b/runtime/react-runtime-help/src/hooks/use-app.ts @@ -32,17 +32,22 @@ export interface UseAppOptions { config: T; iteratorContainerId?: Id[]; iteratorIndex?: number[]; + pageFragmentContainerId?: Id; methods?: { [key: string]: (...args: any[]) => any; }; } export const useNode = ( - props: Pick, + props: Pick, 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; }; diff --git a/runtime/vue-runtime-help/src/hooks/use-app.ts b/runtime/vue-runtime-help/src/hooks/use-app.ts index 88caf011..f905e450 100644 --- a/runtime/vue-runtime-help/src/hooks/use-app.ts +++ b/runtime/vue-runtime-help/src/hooks/use-app.ts @@ -30,15 +30,20 @@ interface UseAppOptions { config: T; iteratorContainerId?: Id[]; iteratorIndex?: number[]; + pageFragmentContainerId?: Id; methods?: Methods; } export const useNode = ( - props: Pick, + props: Pick, app = inject('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; }; diff --git a/runtime/vue-runtime-help/src/hooks/use-dsl.ts b/runtime/vue-runtime-help/src/hooks/use-dsl.ts index f6251b63..423f04e8 100644 --- a/runtime/vue-runtime-help/src/hooks/use-dsl.ts +++ b/runtime/vue-runtime-help/src/hooks/use-dsl.ts @@ -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('app')) => { +export const useDsl = (app = inject('app'), pageFragmentConstainerId: Id) => { if (!app) { throw new Error('useDsl must be used after MagicApp is created'); } - const pageConfig = ref(app.page?.data); + const pageFragment = pageFragmentConstainerId ? app.pageFragments.get(pageFragmentConstainerId) : null; - app.on('page-change', () => { - pageConfig.value = app.page?.data; - }); + const pageConfig = ref(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 }); diff --git a/runtime/vue-runtime-help/src/index.ts b/runtime/vue-runtime-help/src/index.ts index 13835279..573a412d 100644 --- a/runtime/vue-runtime-help/src/index.ts +++ b/runtime/vue-runtime-help/src/index.ts @@ -33,6 +33,7 @@ export interface ComponentProps = MComponent> { iteratorIndex?: number[]; iteratorContainerId?: Id[]; containerIndex?: number; + pageFragmentContainerId?: Id; model?: any; disabled?: boolean; }