mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-12 17:08:14 +00:00
refactor: 设计态下画布渲染模块性能优化
This commit is contained in:
parent
76c5acdc38
commit
f0cb1cd8ea
@ -441,7 +441,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
// TODO: Thinkof move events control to simulator renderer
|
||||
// just listen special callback
|
||||
// because iframe maybe reload
|
||||
this.setupRendererChannel();
|
||||
this.setupDragAndClick();
|
||||
this.setupDetecting();
|
||||
this.setupLiveEditing();
|
||||
@ -452,81 +451,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
|
||||
this.emitter.emit(eventName, ...data);
|
||||
}
|
||||
|
||||
onActivityEvent(cb: (activity: ActivityData, ctx?: any) => 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<BuiltinSimulatorProp
|
||||
const inst = instances
|
||||
? instances.length > 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
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<Schema extends NodeSchema = NodeSchema> {
|
||||
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<Schema extends NodeSchema = NodeSchema> {
|
||||
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<Schema extends NodeSchema = NodeSchema> {
|
||||
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<GlobalEvent.Node.Prop.ChangeOptions, 'node'>;
|
||||
|
||||
export type SlotNode = ParentalNode<SlotSchema>;
|
||||
export type PageNode = ParentalNode<PageSchema>;
|
||||
export type ComponentNode = ParentalNode<ComponentSchema>;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -152,21 +152,9 @@ export interface ISimulatorHost<P = object> 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;
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
|
||||
@ -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<EventEmitter, GlobalEvent.EventConfig> {
|
||||
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<string | symbol>;
|
||||
}
|
||||
|
||||
export class Editor extends EventEmitter implements IEditor {
|
||||
export class Editor extends (EventEmitter as any) implements IEditor {
|
||||
/**
|
||||
* Ioc Container
|
||||
*/
|
||||
|
||||
@ -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
|
||||
<RaxRenderer
|
||||
schema={documentInstance.schema}
|
||||
components={renderer.components}
|
||||
@ -183,6 +202,8 @@ class Renderer extends Component<{
|
||||
device={device}
|
||||
designMode={renderer.designMode}
|
||||
key={this.key}
|
||||
__host={host}
|
||||
__container={container}
|
||||
suspended={documentInstance.suspended}
|
||||
self={documentInstance.scope}
|
||||
onCompGetRef={(schema: any, ref: any) => {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
# Ali Low-Code React Renderer
|
||||
|
||||
> 低代码引擎渲染模块。
|
||||
|
||||
|
||||
👉 [使用文档](https://yuque.antfin-inc.com/docs/share/755c6899-4739-46a1-8d7e-3f5a94d910b8?# 《渲染模块》)
|
||||
|
||||
|
||||
@ -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);
|
||||
// }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<string, ReactInstance[]>();
|
||||
|
||||
@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 = {};
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
@ -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",
|
||||
|
||||
300
packages/renderer-core/src/hoc/leaf.tsx
Normal file
300
packages/renderer-core/src/hoc/leaf.tsx
Normal file
@ -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<EngineOptions, 'designMode'>['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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import { BuiltinSimulatorHost } from '@ali/lowcode-designer';
|
||||
import { baseRendererFactory } from '../renderer';
|
||||
import baseRenererFactory from '../renderer/base';
|
||||
|
||||
export type IBaseRenderer = ReturnType<typeof baseRenererFactory>;
|
||||
export type IBaseRendererInstance = InstanceType<ReturnType<typeof baseRendererFactory>>;
|
||||
|
||||
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<any>;
|
||||
getSchemaChildren: (schema: ISchema) => any;
|
||||
__beforeInit: (props: IRendererProps) => any;
|
||||
__init: (props: IRendererProps) => any;
|
||||
__afterInit: (props: IRendererProps) => any;
|
||||
reloadDataSource: () => Promise<any>;
|
||||
__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<string, any>) => 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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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, ClsType> = T extends undefined
|
||||
: any
|
||||
: T;
|
||||
|
||||
export interface IEditor extends EventEmitter {
|
||||
export interface IEditor extends StrictEventEmitter<EventEmitter, GlobalEvent.EventConfig> {
|
||||
get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType<T, KeyOrType> | undefined;
|
||||
|
||||
has(keyOrType: KeyType): boolean;
|
||||
|
||||
10
packages/types/src/event/index.ts
Normal file
10
packages/types/src/event/index.ts
Normal file
@ -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';
|
||||
10
packages/types/src/event/node.ts
Normal file
10
packages/types/src/event/node.ts
Normal file
@ -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';
|
||||
18
packages/types/src/event/prop.ts
Normal file
18
packages/types/src/event/prop.ts
Normal file
@ -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';
|
||||
@ -19,3 +19,4 @@ export * from './transform-stage';
|
||||
export * from './code-intermediate';
|
||||
export * from './code-result';
|
||||
export * from './assets';
|
||||
export * as GlobalEvent from './event';
|
||||
|
||||
@ -55,14 +55,6 @@ export function waitForThing(obj: any, path: string): Promise<any> {
|
||||
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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<I18nRecord>;
|
||||
getItem(key: string, forceData?: boolean): any;
|
||||
getItems(): I18nRecord[];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 也是一样的处理方式。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user