Merge branch feat/mobx-rendererPerformanceOptimization into feat/0.16.12

Title: refactor: 设计态下画布渲染模块性能优化 

Link: https://code.aone.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/codereview/6495497
This commit is contained in:
lihao.ylh 2021-09-22 17:10:58 +08:00
commit b54eb8ade7
34 changed files with 709 additions and 353 deletions

View File

@ -28,6 +28,8 @@ module.exports = {
'eol-last': 0,
'react/no-find-dom-node': 0,
'no-case-declarations': 0,
'@typescript-eslint/indent': 0
'@typescript-eslint/indent': 0,
"indent": "off",
"@typescript-eslint/indent": ["error", 2]
}
};

View File

@ -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

View File

@ -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) => {

View File

@ -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() {

View File

@ -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) {

View File

@ -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);

View File

@ -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';
@ -558,7 +559,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
return !this.getExtraProp('hidden')?.getValue();
}
onVisibleChange(func: (flag: boolean) => any) {
onVisibleChange(func: (flag: boolean) => any): () => void {
this.emitter.on('visibleChange', func);
return () => {
this.emitter.removeListener('visibleChange', func);
@ -918,7 +919,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);
}
@ -1111,6 +1112,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 {
@ -1134,6 +1146,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>;

View File

@ -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);
}
}

View File

@ -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;
/**
*
*/

View File

@ -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
*/

View File

@ -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) => {

View File

@ -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),

View File

@ -1,3 +1,7 @@
# Ali Low-Code React Renderer
> 低代码引擎渲染模块。
👉 [使用文档](https://yuque.antfin-inc.com/docs/share/755c6899-4739-46a1-8d7e-3f5a94d910b8?# 《渲染模块》)

View File

@ -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);
// }}
/>
);
}

View File

@ -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 = {};

View File

@ -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();
});
}

View File

@ -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",

View File

@ -0,0 +1,296 @@
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;
} = {};
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];
});
}
return LeafWrapper;
}

View File

@ -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();
@ -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,

View File

@ -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');
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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;

View 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';

View 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';

View 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';

View File

@ -19,3 +19,4 @@ export * from './transform-stage';
export * from './code-intermediate';
export * from './code-result';
export * from './assets';
export * as GlobalEvent from './event';

View File

@ -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;

View File

@ -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",

View File

@ -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);

View File

@ -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[];

View File

@ -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;

View File

@ -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;
}

View File

@ -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 也是一样的处理方式。