diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index fc5a36873..208d59115 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -441,7 +441,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost void) { - this.emitter.on('activity', cb); - return () => { - this.emitter.off('activity', cb); - }; - } - - mutedActivityEvent: boolean = false; - muteActivityEvent() { - this.mutedActivityEvent = true; - } - - unmuteActivityEvent() { - this.mutedActivityEvent = false; - } - - runWithoutActivity(action: () => void) { - this.muteActivityEvent(); - action(); - this.unmuteActivityEvent(); - } - - setupRendererChannel() { - const { editor } = this.designer; - editor.on('node.innerProp.change', ({ node, prop, oldValue, newValue }) => { - // 在 Node 初始化阶段的属性变更都跳过 - if (!node.isInited) return; - // 静音状态不触发事件,通常是非局部更新操作 - if (this.mutedActivityEvent) return; - this.postEvent( - 'activity', - { - type: 'modified', - payload: { - schema: node.export(TransformStage.Render, { bypassChildren: true }), - oldValue, - newValue, - prop, - }, - }, - { doc: this.currentDocument }, - ); - }); - // editor.on('node.add', ({ node }) => { - // console.log('add node', node); - // this.postEvent('activity', { - // type: 'added', - // payload: { - // schema: node.export(TransformStage.Render), - // location: { - // parent: { - // nodeId: node.parent.id, - // index: node.index, - // }, - // }, - // }, - // }); - // }); - // editor.on('node.remove.topLevel', ({ node, index }) => { - // console.log('remove node', node); - // this.postEvent('activity', { - // type: 'deleted', - // payload: { - // schema: node.export(TransformStage.Render), - // location: { - // parent: { - // nodeId: node.parent.id, - // index, - // }, - // }, - // }, - // }); - // }); - } - setupDragAndClick() { const { designer } = this; const doc = this.contentDocument!; @@ -1302,8 +1226,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost 1 ? instances.find( - (_inst) => this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance, - ) + (_inst) => this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance, + ) : instances[0] : null; const rect = inst diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index 4c9363682..e8c5445cb 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -178,10 +178,10 @@ export class ComponentMeta { this._title = typeof title === 'string' ? { - type: 'i18n', - 'en-US': this.componentName, - 'zh-CN': title, - } + type: 'i18n', + 'en-US': this.componentName, + 'zh-CN': title, + } : title; } @@ -429,7 +429,7 @@ const builtinComponentActions: ComponentAction[] = [ icon: IconHidden, title: intlNode('hide'), action(node: Node) { - node.getExtraProp('hidden', true)?.setValue(true); + node.setVisible(false); }, }, condition: (node: Node) => { diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts index 43e4a882a..f764e01d8 100644 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ b/packages/designer/src/designer/setting/setting-prop-entry.ts @@ -1,5 +1,5 @@ import { obx, computed, makeObservable, runInAction } from '@ali/lowcode-editor-core'; -import { IEditor, isJSExpression } from '@ali/lowcode-types'; +import { GlobalEvent, IEditor, isJSExpression } from '@ali/lowcode-types'; import { uniqueId } from '@ali/lowcode-utils'; import { SettingEntry } from './setting-entry'; import { Node } from '../../document'; @@ -290,7 +290,7 @@ export class SettingPropEntry implements SettingEntry { } notifyValueChange(oldValue: any, newValue:any) { - this.editor.emit('node.prop.change', { node: this.getNode(), prop: this, oldValue, newValue }); + this.editor.emit(GlobalEvent.Node.Prop.Change, { node: this.getNode(), prop: this, oldValue, newValue }); } getDefaultValue() { diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts index d7eca26ae..3090662e9 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -140,13 +140,8 @@ export class DocumentModel { this.history = new History( () => this.export(TransformStage.Serilize), (schema) => { - if (this.simulator) { - this.simulator.runWithoutActivity(() => { - this.import(schema as RootSchema, true); - }); - } else { - this.import(schema as RootSchema, true); - } + this.import(schema as RootSchema, true); + this.simulator?.rerender(); }, ); @@ -199,6 +194,13 @@ export class DocumentModel { return this._nodesMap.get(id) || null; } + /** + * 根据 id 获取节点 + */ + getNodeCount(): number { + return this._nodesMap.size; + } + /** * 是否存在节点 */ @@ -364,16 +366,7 @@ export class DocumentModel { if (node.isRoot()) return; this.internalRemoveAndPurgeNode(node, true); }); - // foreachReverse(this.rootNode?.children, (node: Node) => { - // this.internalRemoveAndPurgeNode(node, true); - // }); - if (this.designer.project.simulator) { - this.designer.project.simulator.runWithoutActivity(() => { - this.rootNode?.import(schema as any, checkId); - }); - } else { - this.rootNode?.import(schema as any, checkId); - } + this.rootNode?.import(schema as any, checkId); // todo: select added and active track added if (drillDownNodeId) { diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts index bc37cfd20..3e7869bcc 100644 --- a/packages/designer/src/document/node/node-children.ts +++ b/packages/designer/src/document/node/node-children.ts @@ -7,6 +7,11 @@ import { EventEmitter } from 'events'; import { foreachReverse } from '../../utils/tree'; import { NodeRemoveOptions } from '../../types'; +export interface IOnChangeOptions { + type: string; + node: Node; +} + export class NodeChildren { @obx.shallow private children: Node[]; @@ -117,6 +122,10 @@ export class NodeChildren { return false; } this.children.splice(i, 1); + this.emitter.emit('change', { + type: 'unlink', + node, + }); } /** @@ -150,7 +159,10 @@ export class NodeChildren { document.unlinkNode(node); document.selection.remove(node.id); document.destroyNode(node); - this.emitter.emit('change'); + this.emitter.emit('change', { + type: 'delete', + node, + }); if (useMutator) { this.reportModified(node, this.owner, { type: 'remove', propagated: false, isSubDeleting: this.owner.isPurging, removeIndex: i, removeNode: node }); } @@ -197,7 +209,10 @@ export class NodeChildren { children.splice(index, 0, node); } - this.emitter.emit('change'); + this.emitter.emit('change', { + type: 'insert', + node, + }); this.emitter.emit('insert', node); if (globalContext.has('editor')) { globalContext.get('editor').emit('node.add', { node }); @@ -352,7 +367,7 @@ export class NodeChildren { } } - onChange(fn: () => void) { + onChange(fn: (info?: IOnChangeOptions) => void): () => void { this.emitter.on('change', fn); return () => { this.emitter.removeListener('change', fn); diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index dd43cabc4..d8f101081 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -14,6 +14,7 @@ import { ComponentSchema, NodeStatus, CompositeValue, + GlobalEvent, } from '@ali/lowcode-types'; import { compatStage } from '@ali/lowcode-utils'; import { SettingTopEntry } from '@ali/lowcode-designer'; @@ -544,7 +545,7 @@ export class Node { return !this.getExtraProp('hidden', false)?.getValue(); } - onVisibleChange(func: (flag: boolean) => any) { + onVisibleChange(func: (flag: boolean) => any): () => void { this.emitter.on('visibleChange', func); return () => { this.emitter.removeListener('visibleChange', func); @@ -904,7 +905,7 @@ export class Node { return this.props; } - onChildrenChange(fn: () => void) { + onChildrenChange(fn: (param?: { type: string, node: Node }) => void): (() => void) | undefined { return this.children?.onChange(fn); } @@ -1097,6 +1098,17 @@ export class Node { toString() { return this.id; } + + emitPropChange(val: PropChangeOptions) { + this.emitter?.emit('propChange', val); + } + + onPropChange(func: (info: PropChangeOptions) => void): Function { + this.emitter.on('propChange', func); + return () => { + this.emitter.removeListener('propChange', func); + }; + } } function ensureNode(node: any, document: DocumentModel): Node { @@ -1120,6 +1132,8 @@ export interface LeafNode extends Node { readonly children: null; } +export type PropChangeOptions = Omit; + export type SlotNode = ParentalNode; export type PageNode = ParentalNode; export type ComponentNode = ParentalNode; diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index 3ef16c34e..4cba4671e 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -1,5 +1,5 @@ import { untracked, computed, obx, engineConfig, action, makeObservable, mobx } from '@ali/lowcode-editor-core'; -import { CompositeValue, FieldConfig, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@ali/lowcode-types'; +import { CompositeValue, GlobalEvent, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@ali/lowcode-types'; import { uniqueId, isPlainObject, hasOwnProperty, compatStage } from '@ali/lowcode-utils'; import { valueToSource } from './value-to-source'; import { Props } from './props'; @@ -262,12 +262,19 @@ export class Prop implements IPropParent { } if (oldValue !== this._value) { - editor?.emit('node.innerProp.change', { - node: this.owner, + const propsInfo = { + key: this.key, prop: this, oldValue, newValue: this._value, + }; + + editor?.emit(GlobalEvent.Node.Prop.InnerChange, { + node: this.owner as any, + ...propsInfo, }); + + this.owner?.emitPropChange?.(propsInfo); } } diff --git a/packages/designer/src/simulator.ts b/packages/designer/src/simulator.ts index 36f77b279..5b3970ee1 100644 --- a/packages/designer/src/simulator.ts +++ b/packages/designer/src/simulator.ts @@ -152,21 +152,9 @@ export interface ISimulatorHost

extends ISensor { getDropContainer(e: LocateEvent): DropContainer | null; - /** - * 关闭 activity 事件 - */ - muteActivityEvent(): void; - /** - * 打开 activity 事件 - */ - unmuteActivityEvent(): void; - /** - * 不触发 activity 事件的条件下运行指定 action - * @param action - */ - runWithoutActivity(action: () => void): void; - postEvent(evtName: string, evtData: any): void; + + rerender(): void; /** * 销毁 */ diff --git a/packages/editor-core/src/editor.ts b/packages/editor-core/src/editor.ts index 0a527524f..79f717fae 100644 --- a/packages/editor-core/src/editor.ts +++ b/packages/editor-core/src/editor.ts @@ -1,3 +1,4 @@ +import { StrictEventEmitter } from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; import { IEditor, @@ -8,6 +9,7 @@ import { HookConfig, ComponentDescription, RemoteComponentDescription, + GlobalEvent, } from '@ali/lowcode-types'; import { globalLocale } from './intl'; import * as utils from './utils'; @@ -16,9 +18,8 @@ import { AssetsJson, AssetLoader } from '@ali/lowcode-utils'; EventEmitter.defaultMaxListeners = 100; -export declare interface Editor { +export declare interface Editor extends StrictEventEmitter { addListener(event: string | symbol, listener: (...args: any[]) => void): this; - on(event: string | symbol, listener: (...args: any[]) => void): this; once(event: string | symbol, listener: (...args: any[]) => void): this; removeListener(event: string | symbol, listener: (...args: any[]) => void): this; off(event: string | symbol, listener: (...args: any[]) => void): this; @@ -27,7 +28,6 @@ export declare interface Editor { getMaxListeners(): number; listeners(event: string | symbol): Function[]; rawListeners(event: string | symbol): Function[]; - emit(event: string | symbol, ...args: any[]): boolean; listenerCount(type: string | symbol): number; // Added in Node 6... prependListener(event: string | symbol, listener: (...args: any[]) => void): this; @@ -35,7 +35,7 @@ export declare interface Editor { eventNames(): Array; } -export class Editor extends EventEmitter implements IEditor { +export class Editor extends (EventEmitter as any) implements IEditor { /** * Ioc Container */ diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx index 883c4a437..ccd33c1ed 100644 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ b/packages/rax-simulator-renderer/src/renderer-view.tsx @@ -5,6 +5,8 @@ import { useRouter } from './rax-use-router'; import { DocumentInstance, SimulatorRendererContainer } from './renderer'; import './renderer.less'; import { uniqueId } from '@ali/lowcode-utils'; +import { GlobalEvent } from '@ali/lowcode-types'; +import { host } from './host'; // patch cloneElement avoid lost keyProps const originCloneElement = (window as any).Rax.cloneElement; @@ -145,6 +147,7 @@ class Renderer extends Component<{ }> { private unlisten: any; private key: string; + private startTime: number | null = null; componentWillMount() { this.key = uniqueId('renderer'); @@ -169,12 +172,28 @@ class Renderer extends Component<{ return false; } + componentDidUpdate() { + if (this.startTime) { + const time = Date.now() - this.startTime; + const nodeCount = host.designer.currentDocument?.getNodeCount?.(); + host.designer.editor?.emit(GlobalEvent.Node.Rerender, { + componentName: 'Renderer', + type: 'All', + time, + nodeCount, + }); + } + } + render() { const { documentInstance } = this.props; const { container } = documentInstance; const { designMode, device } = container; const { rendererContainer: renderer } = this.props; + this.startTime = Date.now(); + return ( + // @ts-ignore { diff --git a/packages/rax-simulator-renderer/src/renderer.ts b/packages/rax-simulator-renderer/src/renderer.ts index 883e782cd..18f786896 100644 --- a/packages/rax-simulator-renderer/src/renderer.ts +++ b/packages/rax-simulator-renderer/src/renderer.ts @@ -103,20 +103,12 @@ export class DocumentInstance { private emitter = new EventEmitter(); - @obx.ref private _schema?: RootSchema; - @computed get schema(): any { - return this._schema; + get schema(): any { + return this.document.export(TransformStage.Render); } - private dispose?: () => void; - constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) { makeObservable(this); - this.dispose = host.autorun(() => { - // sync schema - this._schema = document.export(TransformStage.Render); - this.emitter.emit('rerender'); - }); } @computed get suspended(): any { @@ -498,6 +490,10 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { }; } + rerender() { + this.currentDocumentInstance?.refresh(); + } + createComponent(schema: NodeSchema): Component | null { const _schema: any = { ...compatibleLegaoSchema(schema), diff --git a/packages/react-renderer/README.md b/packages/react-renderer/README.md index 1252f5e03..77a5a2280 100644 --- a/packages/react-renderer/README.md +++ b/packages/react-renderer/README.md @@ -1,3 +1,7 @@ # Ali Low-Code React Renderer > 低代码引擎渲染模块。 + + +👉 [使用文档](https://yuque.antfin-inc.com/docs/share/755c6899-4739-46a1-8d7e-3f5a94d910b8?# 《渲染模块》) + diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index caa4860ee..9afbb2a02 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -4,13 +4,13 @@ import cn from 'classnames'; import { Node } from '@ali/lowcode-designer'; import LowCodeRenderer from '@ali/lowcode-react-renderer'; import { observer } from 'mobx-react'; -import { isFromVC, getClosestNode } from '@ali/lowcode-utils'; +import { getClosestNode } from '@ali/lowcode-utils'; +import { GlobalEvent } from '@ali/lowcode-types'; import { SimulatorRendererContainer, DocumentInstance } from './renderer'; +import { host } from './host'; import './renderer.less'; -const DEFAULT_SIMULATOR_LOCALE = 'zh-CN'; - // patch cloneElement avoid lost keyProps const originCloneElement = window.React.cloneElement; (window as any).React.cloneElement = (child: any, { _leaf, ...props }: any = {}, ...rest: any[]) => { @@ -129,11 +129,27 @@ class Renderer extends Component<{ rendererContainer: SimulatorRendererContainer, documentInstance: DocumentInstance, }> { + startTime: number | null = null; + + componentDidUpdate() { + if (this.startTime) { + const time = Date.now() - this.startTime; + const nodeCount = host.designer.currentDocument?.getNodeCount?.(); + host.designer.editor?.emit(GlobalEvent.Node.Rerender, { + componentName: 'Renderer', + type: 'All', + time, + nodeCount, + }); + } + } + render() { const { documentInstance, rendererContainer: renderer } = this.props; const { container } = documentInstance; const { designMode, device, locale } = container; const messages = container.context?.utils?.i18n?.messages || {}; + this.startTime = Date.now(); if (!container.autoRender) return null; return ( @@ -154,9 +170,7 @@ class Renderer extends Component<{ const { __id, __designMode, ...viewProps } = props; viewProps.componentId = __id; const leaf = documentInstance.getNode(__id) as Node; - if (isFromVC(leaf?.componentMeta)) { - viewProps._leaf = leaf; - } + viewProps._leaf = leaf; viewProps._componentName = leaf?.componentName; // 如果是容器 && 无children && 高宽为空 增加一个占位容器,方便拖动 if ( @@ -207,12 +221,11 @@ class Renderer extends Component<{ leaf?.isContainer() ? (children == null ? [] : Array.isArray(children) ? children : [children]) : children, ); }} + __host={host} + __container={container} onCompGetRef={(schema: any, ref: ReactInstance | null) => { documentInstance.mountInstance(schema.id, ref); }} - // onCompGetCtx={(schema: any, ctx: object) => { - // documentInstance.mountContext(schema.id, ctx); - // }} /> ); } diff --git a/packages/react-simulator-renderer/src/renderer.ts b/packages/react-simulator-renderer/src/renderer.ts index c212212dd..10605fc9b 100644 --- a/packages/react-simulator-renderer/src/renderer.ts +++ b/packages/react-simulator-renderer/src/renderer.ts @@ -26,7 +26,7 @@ import { createMemoryHistory, MemoryHistory } from 'history'; import Slot from './builtin-components/slot'; import Leaf from './builtin-components/leaf'; import { withQueryParams, parseQuery } from './utils/url'; -import { supportsQuickPropSetting, getUppermostPropKey, setInstancesProp } from './utils/misc'; + const loader = new AssetLoader(); const DELAY_THRESHOLD = 10; const FULL_RENDER_THRESHOLD = 500; @@ -35,60 +35,14 @@ configure({ enforceActions: 'never' }); export class DocumentInstance { public instancesMap = new Map(); - @obx.ref private _schema?: RootSchema; - @computed get schema(): any { - return this._schema; + get schema(): any { + return this.document.export(TransformStage.Render); } private disposeFunctions: Array<() => void> = []; constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) { makeObservable(this); - // 标识当前文档导出的状态,用来控制 reaction effect 是否执行 - let asleep = false; - const setDocSchema = (value?: any) => { - this._schema = value || document.export(TransformStage.Render); - }; - const documentExportDisposer = host.reaction(() => { - return document.export(TransformStage.Render); - }, (value) => { - if (asleep) return; - setDocSchema(value); - }, { delay: DELAY_THRESHOLD, fireImmediately: true }); - this.disposeFunctions.push(documentExportDisposer); - let tid: NodeJS.Timeout; - this.disposeFunctions.push(host.onActivityEvent((data: ActivityData, ctx: any) => { - if (host.mutedActivityEvent || (ctx && ctx.doc !== this.document)) return; - - // 当节点数小数 200 个时,不走入增量更新逻辑,后面优化、细化逻辑后逐步放开限制 - if (this.document.nodesMap.size < 200) return; - - if (tid) clearTimeout(tid); - // 临时关闭全量计算 schema 的逻辑,在增量计算结束后,来一次全量计算 - asleep = true; - if (data.type === ActivityType.MODIFIED) { - // 对于修改场景,优先判断是否能走「快捷设置」逻辑 - if (supportsQuickPropSetting(data, this)) { - setInstancesProp(data, this); - } else { - // this._schema = applyActivities(this._schema!, data); - } - } else if (data.type === ActivityType.ADDED) { - // FIXME: 待补充 节点增加 逻辑 - } else if (data.type === ActivityType.DELETED) { - // FIXME: 待补充 节点删除 逻辑 - } else if (data.type === ActivityType.COMPOSITE) { - // FIXME: 待补充逻辑 - } - - tid = setTimeout(() => { - asleep = false; - setDocSchema(); - }, FULL_RENDER_THRESHOLD); - // TODO: 调试增量模式,打开以下代码 - // this._deltaData = data; - // this._deltaMode = true; - })); } @obx.ref private _components: any = {}; diff --git a/packages/react-simulator-renderer/src/utils/misc.ts b/packages/react-simulator-renderer/src/utils/misc.ts index faf63296a..e121420a1 100644 --- a/packages/react-simulator-renderer/src/utils/misc.ts +++ b/packages/react-simulator-renderer/src/utils/misc.ts @@ -1,8 +1,3 @@ -import { ReactInstance } from 'react'; -import { ActivityData, isJSSlot } from '@ali/lowcode-types'; -import { DocumentInstance } from '../renderer'; -import { isReactClass } from '@ali/lowcode-utils'; - interface UtilsMetadata { name: string; npm: { @@ -29,92 +24,3 @@ export function getProjectUtils(librayMap: LibrayMap, utilsMetadata: UtilsMetada }); } } - -/** - * 获取最靠近 Props / PropStash 的 prop 实例 - * @param prop - * @returns - */ -export function getUppermostPropKey(prop: any): string { - let curProp = prop; - while (curProp.parent.isProp) { - curProp = curProp.parent; - } - return curProp.key; -} - -function haveForceUpdate(instances: any[]): boolean { - return instances.every(inst => 'forceUpdate' in inst); -} - -function isPropWritable(props: any, propName: string): boolean { - const descriptor = Object.getOwnPropertyDescriptor(props, propName); - return !!descriptor?.writable; -} - -/** - * 是否支持快捷属性值设值 - * @param data - * @param doc - * @returns - */ -export function supportsQuickPropSetting(data: ActivityData, doc: DocumentInstance) { - const { payload } = data; - const { schema, prop } = payload; - const { componentName, id: nodeId } = schema; - // const key = data.payload.prop.key; - const instances = doc.instancesMap.get(nodeId!); - const propKey = getUppermostPropKey(prop); - let value = (schema.props as any)[propKey]; - - return ( - nodeId && - Array.isArray(instances) && - instances.length > 0 && - propKey && - // 不是 extraProp - !propKey.startsWith('___') && - // props[propKey] 有可能是 readyonly 的 - instances.every(inst => isPropWritable(inst.props, propKey)) && - !isJSSlot(value) && - // functional component 不支持 ref.props 直接设值,是 readonly 的 - isReactClass((doc.container?.components as any)[componentName]) && - haveForceUpdate(instances) && - // 黑名单组件通常会把 schema / children 魔改,导致直接设置 props 失效 - isAllowedComponent(componentName) - ); -} - -// 不允许走快捷设置的组件黑名单 -const DISABLED__QUICK_SETTING_COMPONENT_NAMES = [ - 'Filter', // 查询组件 - 'Filter2', // 查询组件 - 'TableField', // 明细组件 -]; -function isAllowedComponent(name: string): boolean { - return !DISABLED__QUICK_SETTING_COMPONENT_NAMES.includes(name); -} - -/** - * 设置属性值 - * @param data - * @param doc - */ -export function setInstancesProp(data: ActivityData, doc: DocumentInstance) { - const { payload } = data; - const { schema, prop, newValue } = payload; - const nodeId = schema.id!; - const instances = doc.instancesMap.get(nodeId)!; - const propKey = getUppermostPropKey(prop); - let value = (schema.props as any)[propKey]; - // 当 prop 是在 PropStash 中产生时,该 prop 需要在下一个 obx 的时钟周期才能挂载到相应位置, - // 而 schema 是同步 export 得到的,此时 schema 中还没有对应的值,所以直接取 newValue - if (prop.parent.isPropStash) { - value = newValue; - } - - instances.forEach((inst: any) => { - inst.props[propKey] = value; - inst.forceUpdate(); - }); -} \ No newline at end of file diff --git a/packages/renderer-core/package.json b/packages/renderer-core/package.json index bfe417d18..f6d17c7cc 100644 --- a/packages/renderer-core/package.json +++ b/packages/renderer-core/package.json @@ -17,6 +17,7 @@ "@ali/bzb-request": "^2.6.0-beta.13", "@ali/lib-mtop": "^2.5.1", "@ali/lowcode-datasource-engine": "^1.0.22", + "@ali/lowcode-types": "1.0.67", "classnames": "^2.2.6", "debug": "^4.1.1", "fetch-jsonp": "^1.1.3", diff --git a/packages/renderer-core/src/hoc/leaf.tsx b/packages/renderer-core/src/hoc/leaf.tsx new file mode 100644 index 000000000..0a2031757 --- /dev/null +++ b/packages/renderer-core/src/hoc/leaf.tsx @@ -0,0 +1,300 @@ +import { BuiltinSimulatorHost, Node, PropChangeOptions } from '@ali/lowcode-designer'; +import { GlobalEvent, TransformStage } from '@ali/lowcode-types'; +import { EngineOptions } from '@ali/lowcode-editor-core'; +import adapter from '../adapter'; +import * as types from '../types/index'; + +const compDefaultPropertyNames = ['$$typeof', 'render', 'defaultProps']; + +export interface IComponentHocInfo { + schema: any; + baseRenderer: types.IBaseRendererInstance; + componentInfo: any; +} + +type DesignMode = Pick['designMode']; + +export type IComponentHoc = { + designMode: DesignMode | DesignMode[]; + hoc: IComponentConstruct, +}; + +export type IComponentConstruct = (Comp: types.IBaseRenderer, info: IComponentHocInfo) => types.Constructor; + +const whitelist: string[] = []; + +interface IProps { + _leaf: Node | undefined; + + visible: boolean; + + componentId?: number; + + children?: Node[]; +} + +enum RerenderType { + All = 'All', + ChildChanged = 'ChildChanged', + PropsChanged = 'PropsChanged', + VisibleChanged = 'VisibleChanged', + LangChanged = 'LangChanged', + I18nChanged = 'I18nChanged', +} + +// 给每个组件包裹一个 HOC Leaf,支持组件内部属性变化,自响应渲染 +export function leafWrapper(Comp: types.IBaseRenderer, { + schema, + baseRenderer, + componentInfo, +}: IComponentHocInfo) { + const { + __debug, + __getComponentProps: getProps, + __getSchemaChildrenVirtualDom: getChildren, + } = baseRenderer; + const engine = baseRenderer.context.engine; + const host: BuiltinSimulatorHost = baseRenderer.props.__host; + const container: BuiltinSimulatorHost = baseRenderer.props.__container; + const editor = host?.designer?.editor; + const { Component } = adapter.getRuntime(); + + class LeafWrapper extends Component { + recordInfo: { + startTime?: number | null; + type?: string; + node?: Node; + } = {}; + + static displayName = schema.componentName; + + disposeFunctions: ((() => void) | Function)[] = []; + + recordTime = () => { + if (!this.recordInfo.startTime) { + return; + } + const endTime = Date.now(); + const nodeCount = host.designer.currentDocument?.getNodeCount?.(); + const componentName = this.recordInfo.node?.componentName || this.leaf?.componentName || 'UnknownComponent'; + editor?.emit(GlobalEvent.Node.Rerender, { + componentName, + time: endTime - this.recordInfo.startTime, + type: this.recordInfo.type, + nodeCount, + }); + this.recordInfo.startTime = null; + }; + + componentDidUpdate() { + this.recordTime(); + } + + constructor(props: IProps, context: any) { + super(props, context); + // 监听以下事件,当变化时更新自己 + this.initOnPropsChangeEvent(); + this.initOnChildrenChangeEvent(); + this.initOnVisibleChangeEvent(); + this.initLangChangeEvent(); + this.state = { + nodeChildren: null, + childrenInState: false, + }; + } + + /** 由于内部属性变化,在触发渲染前,会执行该函数 */ + beforeRender(type: string, node?: Node): void { + this.recordInfo.startTime = Date.now(); + this.recordInfo.type = type; + this.recordInfo.node = node; + } + + shouldComponentUpdate() { + if (whitelist.includes(schema.componentName)) { + __debug(`${schema.componentName} is in leaf Hoc whitelist`); + container.rerender(); + return false; + } + + return true; + } + + /** 监听参数变化 */ + initOnPropsChangeEvent(): void { + const dispose = this.leaf?.onPropChange?.((propChangeInfo: PropChangeOptions) => { + const { + key, + } = propChangeInfo; + const node = this.leaf; + + // 如果循坏条件变化,从根节点重新渲染 + // 目前多层循坏无法判断需要从哪一层开始渲染,故先粗暴解决 + if (key === '___loop___') { + container.rerender(); + return; + } + + this.beforeRender(RerenderType.PropsChanged); + __debug(`${this.leaf?.componentName} component trigger onPropsChange event`); + const nextProps = getProps(node?.export?.(TransformStage.Render) as types.ISchema, Comp, componentInfo); + this.setState(nextProps.children ? { + nodeChildren: nextProps.children, + nodeProps: nextProps, + } : { + nodeProps: nextProps, + }); + }); + + dispose && this.disposeFunctions.push(dispose); + } + + /** + * 监听显隐变化 + */ + initOnVisibleChangeEvent() { + const dispose = this.leaf?.onVisibleChange?.((flag: boolean) => { + if (this.state.visible === flag) { + return; + } + + __debug(`${this.leaf?.componentName} component trigger onVisibleChange event`); + this.beforeRender(RerenderType.VisibleChanged); + this.setState({ + visible: flag, + }); + }); + + dispose && this.disposeFunctions.push(dispose); + } + + /** + * 监听子元素变化(拖拽,删除...) + */ + initOnChildrenChangeEvent() { + const dispose = this.leaf?.onChildrenChange?.((param): void => { + const { + type, + node, + } = param || {}; + this.beforeRender(`${RerenderType.ChildChanged}-${type}`, node); + __debug(`${this.leaf} component trigger onChildrenChange event`); + const nextChild = getChildren(this.leaf?.export?.(TransformStage.Render) as types.ISchema, Comp); + this.setState({ + nodeChildren: nextChild, + childrenInState: true, + }); + }); + + dispose && this.disposeFunctions.push(dispose); + } + + /** + * 监听语言切换 + */ + initLangChangeEvent() { + if (this.leaf?.componentName !== 'Page') { + return; + } + const dispose = (window as any).VisualEngine.Env.onEnvChange(() => { + this.beforeRender(RerenderType.LangChanged); + const nextProps = getProps(this.leaf?.export?.(TransformStage.Render) as types.ISchema, Comp, this.componentInfo); + const nextChildren = getChildren(this.leaf?.export?.(TransformStage.Render) as types.ISchema, Comp); + this.setState({ + nodeChildren: nextChildren, + nodeProps: nextProps, + }); + }); + + dispose && this.disposeFunctions.push(dispose); + } + + componentWillUnmount() { + this.disposeFunctions.forEach(fn => fn()); + } + + get hasChildren(): boolean { + if (this.state.childrenInState) { + return !!this.state.nodeChildren?.length; + } + + if (Array.isArray(this.props.children)) { + return Boolean(this.props.children && this.props.children.length); + } + + return Boolean(this.props.children); + } + + get children(): any { + if (this.state.nodeChildren) { + return this.state.nodeChildren; + } + if (this.props.children && !Array.isArray(this.props.children)) { + return [this.props.children]; + } + if (this.props.children && this.props.children.length) { + return this.props.children; + } + return []; + } + + get leaf(): Node | undefined { + return this.props._leaf; + } + + get childrenMap(): any { + const map = new Map(); + + if (!this.hasChildren) { + return map; + } + + this.children.forEach((d: any) => { + if (Array.isArray(d)) { + map.set(d[0].props.componentId, d[0]); + return; + } + map.set(d.props.componentId, d); + }); + + return map; + } + + get visible(): boolean { + if (typeof this.state.visible === 'boolean') { + return this.state.visible; + } + if (typeof this.leaf?.schema?.hidden === 'boolean') { + return !this.leaf?.schema?.hidden; + } + return true; + } + + render() { + if (!this.visible) { + return null; + } + + const compProps = { + ...this.props, + ...(this.state.nodeProps || {}), + children: [], + __id: this.props.componentId, + }; + + return engine.createElement(Comp, compProps, this.hasChildren ? this.children : null); + } + } + + if (typeof Comp === 'object') { + const compExtraPropertyNames = Object.getOwnPropertyNames(Comp).filter(d => !compDefaultPropertyNames.includes(d)); + + compExtraPropertyNames.forEach((d: string) => { + (LeafWrapper as any)[d] = Comp[d]; + }); + } + + LeafWrapper.displayName = (Comp as any).displayName; + + return LeafWrapper; +} diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index f0ef23d2a..a31d35fec 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -30,6 +30,7 @@ import { } from '../utils'; import { IRendererProps, ISchema, IInfo, ComponentModel, IRenderer } from '../types'; import { compWrapper } from '../hoc'; +import { IComponentConstruct, IComponentHoc, leafWrapper } from '../hoc/leaf'; export default function baseRenererFactory() { const { BaseRenderer: customBaseRenderer } = adapter.getRenderers(); @@ -53,7 +54,7 @@ export default function baseRenererFactory() { let scopeIdx = 0; return class BaseRenderer extends Component implements IRenderer { - static dislayName = 'base-renderer'; + static displayName = 'base-renderer'; static defaultProps = { __schema: {}, @@ -61,8 +62,12 @@ export default function baseRenererFactory() { static contextType = AppContext; + __hoc_components: any = {}; + __namespace = 'base'; + _self: any = null; + constructor(props: IRendererProps, context: any) { super(props, context); this.__beforeInit(props); @@ -332,10 +337,21 @@ export default function baseRenererFactory() { const { __schema, __ctx, __components = {} } = this.props; const self: any = {}; self.__proto__ = __ctx || this; + if (!this._self) { + this._self = self; + } const _children = this.getSchemaChildren(__schema); + let Comp = __components[__schema.componentName]; + this.componentHoc.forEach((ComponentConstruct: IComponentConstruct) => { + Comp = ComponentConstruct(Comp || Div, { + schema: __schema, + componentInfo: {}, + baseRenderer: this, + }); + }); return this.__createVirtualDom(_children, self, ({ schema: __schema, - Comp: __components[__schema.componentName], + Comp, } as IInfo)); }; @@ -352,7 +368,7 @@ export default function baseRenererFactory() { // self 为每个渲染组件构造的上下文,self是自上而下继承的 // parentInfo 父组件的信息,包含schema和Comp // idx 若为循环渲染的循环Index - __createVirtualDom = (schema: any, self: any, parentInfo: IInfo, idx: string | number = ''): any => { + __createVirtualDom = (schema: ISchema, self: any, parentInfo: IInfo, idx: string | number = ''): any => { const { engine } = this.context || {}; try { if (!schema) return null; @@ -376,7 +392,7 @@ export default function baseRenererFactory() { } if (typeof schema === 'string') return schema; if (typeof schema === 'number' || typeof schema === 'boolean') { - return schema.toString(); + return String(schema); } if (Array.isArray(schema)) { if (schema.length === 1) return this.__createVirtualDom(schema[0], self, parentInfo); @@ -394,9 +410,15 @@ export default function baseRenererFactory() { return schema; } if (!isSchema(schema)) return null; - let Comp = components[schema.componentName] || engine.getNotFoundComponent(); + let Comp = components[schema.componentName] || this.props.__container?.components?.[schema.componentName]; - if (schema.hidden && engine?.props?.designMode) { + if (!Comp) { + console.error(`${schema.componentName} is not found! component list is:`, this.props.__container?.components); + Comp = engine.getNotFoundComponent(); + } + + if (schema.hidden && !this._designModeIsDesign) { + // designMode 为 design 情况下,需要进入 leaf Hoc,进行相关事件注册 return null; } @@ -457,21 +479,34 @@ export default function baseRenererFactory() { otherProps.__designMode = engine.props.designMode; } const componentInfo: any = {}; - const props: any = - this.__parseProps(schema.props, self, '', { - schema, - Comp, - componentInfo: { - ...componentInfo, - props: transformArrayToMap(componentInfo.props, 'name'), - }, - }) || {}; + const props: any = this.__getComponentProps(schema, Comp, { + ...componentInfo, + props: transformArrayToMap(componentInfo.props, 'name'), + }) || {}; + + if (!this.__hoc_components[schema.componentName]) { + this.componentHoc.forEach((ComponentConstruct: IComponentConstruct) => { + Comp = ComponentConstruct(Comp, { + schema, + componentInfo, + baseRenderer: this, + }); + }); + } + // 对于可以获取到ref的组件做特殊处理 - if (!acceptsRef(Comp)) { + if (!acceptsRef(Comp) && !this.__hoc_components[schema.componentName]) { Comp = compWrapper(Comp); components[schema.componentName] = Comp; } + + if (!this.__hoc_components[schema.componentName]) { + this.__hoc_components[schema.componentName] = Comp; + } else { + Comp = this.__hoc_components[schema.componentName]; + } + otherProps.ref = (ref: any) => { this.$(props.fieldId || props.ref, ref); // 收集ref const refProps = props.ref; @@ -500,17 +535,7 @@ export default function baseRenererFactory() { props.key = props.__id; } - let child: any = null; - if (/*! isFileSchema(schema) && */_children) { - child = this.__createVirtualDom( - isJSExpression(_children) ? parseExpression(_children, self) : _children, - self, - { - schema, - Comp, - }, - ); - } + let child: any = this.__getSchemaChildrenVirtualDom(schema, Comp); const renderComp = (props: any) => engine.createElement(Comp, props, child); // 设计模式下的特殊处理 if (engine && [DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(engine.props.designMode)) { @@ -550,7 +575,69 @@ export default function baseRenererFactory() { } }; - __createLoopVirtualDom = (schema: any, self: any, parentInfo: IInfo, idx: number | string) => { + get componentHoc(): IComponentConstruct[] { + const componentHoc: IComponentHoc[] = [ + { + designMode: 'design', + hoc: leafWrapper, + }, + ]; + + return componentHoc + .filter((d: IComponentHoc) => { + if (Array.isArray(d.designMode)) { + return d.designMode.includes(this.props.designMode); + } + + return d.designMode === this.props.designMode; + }) + .map((d: IComponentHoc) => d.hoc); + } + + __getSchemaChildrenVirtualDom = (schema: ISchema, Comp: any) => { + let _children = this.getSchemaChildren(schema); + + let children: any = []; + if (/*! isFileSchema(schema) && */_children) { + if (!Array.isArray(_children)) { + _children = [_children]; + } + + _children.forEach((_child: any) => { + const _childVirtualDom = this.__createVirtualDom( + isJSExpression(_child) ? parseExpression(_child, self) : _child, + self, + { + schema, + Comp, + }, + ); + + children.push(_childVirtualDom); + }); + } + + if (children && children.length) { + return children; + } + return null; + }; + + __getComponentProps = (schema: ISchema, Comp: any, componentInfo?: any) => { + if (!schema) { + return {}; + } + return this.__parseProps(schema?.props, this.self, '', { + schema, + Comp, + componentInfo: { + ...(componentInfo || {}), + props: transformArrayToMap((componentInfo || {}).props, 'name'), + }, + }) || {}; + }; + + __createLoopVirtualDom = (schema: ISchema, self: any, parentInfo: IInfo, idx: number | string) => { if (isFileSchema(schema)) { console.warn('file type not support Loop'); return null; @@ -576,6 +663,11 @@ export default function baseRenererFactory() { }); }; + get _designModeIsDesign() { + const { engine } = this.context || {}; + return engine?.props?.designMode === 'design'; + } + __parseProps = (props: any, self: any, path: string, info: IInfo): any => { const { schema, Comp, componentInfo = {} } = info; const propInfo = getValue(componentInfo.props, path); @@ -721,6 +813,7 @@ export default function baseRenererFactory() { __renderContextProvider = (customProps?: object, children?: any) => { customProps = customProps || {}; children = children || this.__createDom(); + this.__hoc_components = {}; return createElement(AppContext.Provider, { value: { ...this.context, @@ -742,13 +835,21 @@ export default function baseRenererFactory() { Comp, componentInfo: {}, }); + this.__hoc_components = {}; const { className } = data; const { engine } = this.context || {}; if (!engine) { return null; } + this.componentHoc.forEach((ComponentConstruct: IComponentConstruct) => { + Comp = ComponentConstruct(Comp || Div, { + schema: __schema, + componentInfo: {}, + baseRenderer: this, + }); + }); const child = engine.createElement( - Comp || Div, + Comp, { ...data, ...this.props, diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index 09794f341..cd61de6bb 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -38,7 +38,6 @@ export default function rendererFactory() { class NotFoundComponent extends PureComponent { render() { - console.error('component not found', this.props); return createElement(Div, this.props, this.props.children || 'Component Not Found'); } } diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index 123d9e114..e5e68cf3b 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -1,3 +1,10 @@ +import { BuiltinSimulatorHost } from '@ali/lowcode-designer'; +import { baseRendererFactory } from '../renderer'; +import baseRenererFactory from '../renderer/base'; + +export type IBaseRenderer = ReturnType; +export type IBaseRendererInstance = InstanceType>; + export interface IProps { schema: ISchema; components: { [key: string]: any }; @@ -17,15 +24,22 @@ export interface IProps { export interface IState { engineRenderError?: boolean; error?: Error + onCompGetRef: (schema: ISchema, ref: any) => void; + onCompGetCtx: (schema: ISchema, ref: any) => void; + customCreateElement: (...args: any) => any; + notFoundComponent: any; + faultComponent: any; + [key: string]: any; } - export interface IRendererProps { - locale: string; + locale?: string; messages: object; __appHelper: object; __components: object; __ctx: object; __schema: ISchema; + __host?: BuiltinSimulatorHost; + __container?: any; [key: string]: any; } @@ -46,7 +60,7 @@ export interface ISchema { } export interface IInfo { - schema: any; + schema: ISchema; Comp: any; componentInfo?: any; } @@ -77,7 +91,7 @@ export interface DataSource { dataHandler: JSExpression; } -type Constructor = new(...args: any) => any; +export type Constructor = new(...args: any) => any; export interface IRuntime { Component: Constructor; @@ -90,7 +104,7 @@ export interface IRuntime { } export interface IRendererModules { - BaseRenderer?: any; + BaseRenderer?: new(...args: any) => IRenderer; PageRenderer: any; ComponentRenderer: any; BlockRenderer?: any, @@ -102,10 +116,11 @@ export interface IRendererModules { export interface IRenderer { props?: IRendererProps; context?: any; + reloadDataSource: () => Promise; + getSchemaChildren: (schema: ISchema) => any; __beforeInit: (props: IRendererProps) => any; __init: (props: IRendererProps) => any; __afterInit: (props: IRendererProps) => any; - reloadDataSource: () => Promise; __setLifeCycleMethods: (method: string, args?: any) => any; __bindCustomMethods: (props: IRendererProps) => any; __generateCtx: (ctx: object) => any; @@ -113,7 +128,8 @@ export interface IRenderer { __initDataSource: (props: IRendererProps) => any; __render: () => any; __getRef: (ref: any) => any; - getSchemaChildren: (schema: ISchema) => any; + __getSchemaChildrenVirtualDom: (schema: ISchema, Comp: any, nodeChildrenMap?: Map) => any; + __getComponentProps: (schema: ISchema, Comp: any, componentInfo: any) => any; __createDom: () => any; __createVirtualDom: (schema: any, self: any, parentInfo: IInfo, idx: string | number) => any; __createLoopVirtualDom: (schema: any, self: any, parentInfo: IInfo, idx: number | string) => any; diff --git a/packages/types/package.json b/packages/types/package.json index 430ee0e18..7c8eb6d2d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -13,8 +13,10 @@ }, "dependencies": { "@ali/lowcode-datasource-types": "^1.0.21", + "@ali/lowcode-designer": "^1.0.65", "power-di": "^2.2.4", - "react": "^16" + "react": "^16", + "strict-event-emitter-types": "^2.0.0" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", diff --git a/packages/types/src/editor.ts b/packages/types/src/editor.ts index 5d6cca2d7..fa6e1cdb1 100644 --- a/packages/types/src/editor.ts +++ b/packages/types/src/editor.ts @@ -1,7 +1,9 @@ import { EventEmitter } from 'events'; +import StrictEventEmitter from 'strict-event-emitter-types'; import { ReactNode, ComponentType } from 'react'; -import { NpmInfo } from './npm'; import { RegisterOptions } from 'power-di'; +import { NpmInfo } from './npm'; +import * as GlobalEvent from './event'; export type KeyType = (new (...args: any[]) => any) | symbol | string; export type ClassType = new (...args: any[]) => any; @@ -17,7 +19,7 @@ export type GetReturnType = T extends undefined : any : T; -export interface IEditor extends EventEmitter { +export interface IEditor extends StrictEventEmitter { get(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType | undefined; has(keyOrType: KeyType): boolean; diff --git a/packages/types/src/event/index.ts b/packages/types/src/event/index.ts new file mode 100644 index 000000000..6cba1be3e --- /dev/null +++ b/packages/types/src/event/index.ts @@ -0,0 +1,10 @@ +import * as Node from './node'; + +export interface EventConfig { + [Node.Prop.Change]: (options: Node.Prop.ChangeOptions) => any; + [Node.Prop.InnerChange]: (options: Node.Prop.ChangeOptions) => any; + [Node.Rerender]: (options: Node.RerenderOptions) => void; + [eventName: string]: any; +} + +export * as Node from './node'; \ No newline at end of file diff --git a/packages/types/src/event/node.ts b/packages/types/src/event/node.ts new file mode 100644 index 000000000..af14dbd42 --- /dev/null +++ b/packages/types/src/event/node.ts @@ -0,0 +1,10 @@ +export * as Prop from './prop'; + +export interface RerenderOptions { + time: number; + componentName?: string; + type?: string; + nodeCount?: number; +} + +export const Rerender = 'node.edit.rerender.time'; diff --git a/packages/types/src/event/prop.ts b/packages/types/src/event/prop.ts new file mode 100644 index 000000000..66e5aa208 --- /dev/null +++ b/packages/types/src/event/prop.ts @@ -0,0 +1,18 @@ +import { IPropParent, SettingEntry } from '@ali/lowcode-designer'; + +export interface ChangeOptions { + key?: string | number; + prop?: SettingEntry | IPropParent; + node: Node; + newValue: any; + oldValue: any; +} + +/** + * Node Prop 变化事件 + * @Deprecated Please Replace With InnerPropChange + */ +export const Change = 'node.prop.change'; + +/** Node Prop 变化事件 */ +export const InnerChange = 'node.innerProp.change'; \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2c284bf21..ca45efdc6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -19,3 +19,4 @@ export * from './transform-stage'; export * from './code-intermediate'; export * from './code-result'; export * from './assets'; +export * as GlobalEvent from './event'; diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 2672b317e..08133d26a 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -55,14 +55,6 @@ export function waitForThing(obj: any, path: string): Promise { return _innerWaitForThing(obj, path); } -/** - * 判断当前 meta 是否从 vc prototype 转换而来 - * @param meta - */ -export function isFromVC(meta: ComponentMeta) { - return !!meta?.getMetadata()?.experimental; -} - export function arrShallowEquals(arr1: any[], arr2: any[]): boolean { if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false; if (arr1.length !== arr2.length) return false; diff --git a/packages/vision-polyfill/package.json b/packages/vision-polyfill/package.json index fc70373a5..f86ab3e8e 100644 --- a/packages/vision-polyfill/package.json +++ b/packages/vision-polyfill/package.json @@ -22,6 +22,7 @@ "@ali/lowcode-editor-core": "1.0.67", "@ali/lowcode-editor-skeleton": "1.0.67", "@ali/lowcode-utils": "1.0.67", + "@ali/lowcode-types": "1.0.67", "@ali/ve-i18n-util": "^2.0.0", "@ali/ve-icons": "^4.1.9", "@ali/ve-less-variables": "2.0.3", diff --git a/packages/vision-polyfill/src/bus.ts b/packages/vision-polyfill/src/bus.ts index 2faead23a..9830d05d0 100644 --- a/packages/vision-polyfill/src/bus.ts +++ b/packages/vision-polyfill/src/bus.ts @@ -1,7 +1,7 @@ import logger from '@ali/vu-logger'; import { EventEmitter } from 'events'; import { editor } from '@ali/lowcode-engine'; -import { isJSExpression } from '@ali/lowcode-types'; +import { GlobalEvent, isJSExpression } from '@ali/lowcode-types'; /** * Bus class as an EventEmitter @@ -87,7 +87,7 @@ function triggerUseVariableChange(data: any) { propConfig.useVariableChange.call(prop, { isUseVariable: true }); } } -editor?.on('node.prop.change', (data) => { +editor?.on(GlobalEvent.Node.Prop.Change, (data) => { bus.emit('node.prop.change', data); triggerUseVariableChange(data); diff --git a/packages/vision-polyfill/src/i18n-util/index.d.ts b/packages/vision-polyfill/src/i18n-util/index.d.ts index d258fe9d2..254e4ed65 100644 --- a/packages/vision-polyfill/src/i18n-util/index.d.ts +++ b/packages/vision-polyfill/src/i18n-util/index.d.ts @@ -1,3 +1,5 @@ +import { Node } from '@ali/lowcode-designer'; + declare enum LANGUAGES { zh_CN = 'zh_CN', en_US = 'en_US' @@ -62,7 +64,10 @@ export interface II18nUtil { * @param key * @param lang */ - get(key: string, lang: string): string | I18nRecord; + get(key: string, lang: string, info?: { + node?: Node, + path?: string, + }): string | I18nRecord; getFromRemote(key: string): Promise; getItem(key: string, forceData?: boolean): any; getItems(): I18nRecord[]; diff --git a/packages/vision-polyfill/src/i18n-util/index.js b/packages/vision-polyfill/src/i18n-util/index.js index 46aabed87..97efdf4ad 100644 --- a/packages/vision-polyfill/src/i18n-util/index.js +++ b/packages/vision-polyfill/src/i18n-util/index.js @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; - -import { editorCabin } from '@ali/lowcode-engine'; +import { editorCabin, editor } from '@ali/lowcode-engine'; +import lodashGet from 'lodash.get'; +import { GlobalEvent } from '@ali/lowcode-types'; const { obx } = editorCabin; @@ -23,6 +24,33 @@ class DocItem { ...strings, }); this.emitter = new EventEmitter(); + this.nodeList = new Map(); + this.onChange((doc, oldDoc) => { + if (this.nodeList.size <= 0) { + return; + } + this.nodeList.forEach(({ + node, + path, + }) => { + const prop = node.settingEntry.getProp(path); + const changeInfo = { + key: prop?.key, + prop, + newValue: doc, + oldValue: oldDoc, + }; + node.emitPropChange(changeInfo); + editor.emit(GlobalEvent.Node.Prop.Change, { + node, + ...changeInfo, + }); + editor.emit(GlobalEvent.Node.Prop.InnerChange, { + node, + ...changeInfo, + }); + }); + }); this.inited = unInitial !== true; } @@ -34,17 +62,18 @@ class DocItem { if (lang) { return this.doc[lang]; } - return this.doc; + return Object.assign({}, this.doc); } setDoc(doc, lang, initial) { + const oldValue = Object.assign({}, this.doc); if (lang) { this.doc[lang] = doc; } else { const { use, strings } = doc || {}; - Object.assign(this.doc, strings); + Object.assign(this.doc, doc, strings); } - this.emitter.emit('change', this.doc); + this.emitter.emit('change', this.doc, oldValue); if (initial) { this.inited = true; @@ -53,6 +82,14 @@ class DocItem { } } + collectNode = (nodeInfo) => { + const key = lodashGet(nodeInfo, 'node.id'); + if (!key) { + return; + } + this.nodeList.set(key, nodeInfo); + }; + remove() { if (!this.inited) return Promise.reject('not initialized'); @@ -135,6 +172,13 @@ class I18nUtil { item = this.fullList.find(doc => doc.getKey() === key); } + if (!item && (this.maps[key] || this.fullMap[key])) { + // 如果从 items 和 fullList 里面找到 DocItem,就从 maps/fullMap 里面取 + item = this.maps[key] || this.fullMap[key]; + this.items.unshift(item); + this.fullList.unshift(item); + } + if (item) { item.setDoc(dict); } else { @@ -228,9 +272,10 @@ class I18nUtil { return this._load(configs); } - get(key, lang) { + get(key, lang, nodeInfo) { const item = this.getItem(key); if (item) { + item.collectNode(nodeInfo); return item.getDoc(lang); } return null; diff --git a/packages/vision-polyfill/src/props-reducers/deep-value-reducer.ts b/packages/vision-polyfill/src/props-reducers/value-parser.ts similarity index 73% rename from packages/vision-polyfill/src/props-reducers/deep-value-reducer.ts rename to packages/vision-polyfill/src/props-reducers/value-parser.ts index bb15f078f..d748f30bb 100644 --- a/packages/vision-polyfill/src/props-reducers/deep-value-reducer.ts +++ b/packages/vision-polyfill/src/props-reducers/value-parser.ts @@ -8,7 +8,21 @@ import { isVariable } from '../utils'; // FIXME: 表达式使用 mock 值,未来live 模式直接使用原始值 // TODO: designType -export function deepValueParser(obj: any, node: Node): any { +export function valueParser(obj: any, node: Node): any { + return deepValueParser(obj, { + node, + path: '', + }); +} + +function deepValueParser(obj: any, info: { + node: Node; + path?: string; +}): any { + const { + path = '', + node, + } = info; // 如果不是 vc 体系,不做这个兼容处理 if (!node.componentMeta.prototype) { return obj; @@ -34,14 +48,17 @@ export function deepValueParser(obj: any, node: Node): any { return obj; } if (Array.isArray(obj)) { - return obj.map((item) => deepValueParser(item, node)); + return obj.map((item) => deepValueParser(item, { node })); } if (isPlainObject(obj)) { if (isI18nData(obj)) { // FIXME! use editor.get let locale = Env.getLocale(); if (obj.key && i18nUtil.get(obj.key, locale)) { - return i18nUtil.get(obj.key, locale); + return i18nUtil.get(obj.key, locale, { + node, + path, + }); } if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) { locale = 'en_US'; @@ -54,7 +71,10 @@ export function deepValueParser(obj: any, node: Node): any { } const out: any = {}; Object.keys(obj).forEach((key) => { - out[key] = deepValueParser(obj[key], node); + out[key] = deepValueParser(obj[key], { + node, + path: path ? `${path}.${key}` : key, + }); }); return out; } diff --git a/packages/vision-polyfill/src/reducers.ts b/packages/vision-polyfill/src/reducers.ts index 5bc04e35d..1df10fbf2 100644 --- a/packages/vision-polyfill/src/reducers.ts +++ b/packages/vision-polyfill/src/reducers.ts @@ -2,7 +2,7 @@ import { editor, designer, designerCabin } from '@ali/lowcode-engine'; import bus from './bus'; import { VE_EVENTS } from './base/const'; -import { deepValueParser } from './props-reducers/deep-value-reducer'; +import { valueParser } from './props-reducers/value-parser'; import { liveEditingRule, liveEditingSaveHander } from './vc-live-editing'; import { compatibleReducer, @@ -49,7 +49,7 @@ designer.addPropsReducer(upgradePageLifeCyclesReducer, TransformStage.Save); // 设计器组件样式处理 designer.addPropsReducer(stylePropsReducer, TransformStage.Render); // 国际化 & Expression 渲染时处理 -designer.addPropsReducer(deepValueParser, TransformStage.Render); +designer.addPropsReducer(valueParser, TransformStage.Render); // Init 的时候没有拿到 dataSource, 只能在 Render 和 Save 的时候都调用一次,理论上执行时机在 Init // Render 和 Save 都要各调用一次,感觉也是有问题的,是不是应该在 Render 执行一次就行了?见上 filterReducer 也是一样的处理方式。