feat: improve the editor's performance

avoid the unnecessary render and add the lazy load to some widgets
This commit is contained in:
MrWindlike 2022-05-12 13:58:10 +08:00
parent 24e7b87c89
commit 67fb8a0065
6 changed files with 191 additions and 127 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo, useCallback } from 'react';
import { css } from '@emotion/css';
import { IconButton, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
@ -27,13 +27,74 @@ const TableRowStyle = css`
}
`;
type Props = WidgetProps & {
type ArrayTableProps = WidgetProps & {
itemSpec: JSONSchema7;
};
type RowProps = ArrayTableProps & {
itemValue: any;
itemIndex: number;
};
export const ArrayTable: React.FC<Props> = props => {
const { value, itemSpec, spec, level, path, children, onChange } = props;
const DEFAULT_KEYS = ['index'];
const TableRow: React.FC<RowProps> = props => {
const { value, itemSpec, spec, level, path, children, itemValue, itemIndex, onChange } =
props;
const { expressionOptions, displayedKeys = [] } = spec.widgetOptions || {};
const keys = displayedKeys.length ? displayedKeys : DEFAULT_KEYS;
const mergedSpec = useMemo(
() =>
mergeWidgetOptionsIntoSpec(
{
...itemSpec,
title: '',
},
{
expressionOptions,
}
),
[itemSpec, expressionOptions]
);
const nextPath = useMemo(() => path.concat(String(itemIndex)), [path, itemIndex]);
const onPopoverWidgetChange = useCallback(
(newItemValue: any) => {
const newValue = [...value];
newValue[itemIndex] = newItemValue;
onChange(newValue);
},
[itemIndex, onChange, value]
);
return (
<Tr className={TableRowStyle}>
<Td key="setting">
<PopoverWidget
{...props}
value={itemValue}
spec={mergedSpec}
path={nextPath}
level={level + 1}
onChange={onPopoverWidgetChange}
>
{typeof children === 'function' ? children(props, itemValue, itemIndex) : null}
</PopoverWidget>
</Td>
{keys.map((key: string) => {
const propertyValue =
key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key];
return <Td key={key}>{propertyValue}</Td>;
})}
<Td key="button">
<ArrayButtonGroup index={itemIndex} value={value} onChange={onChange} />
</Td>
</Tr>
);
};
export const ArrayTable: React.FC<ArrayTableProps> = props => {
const { value, itemSpec, spec, onChange } = props;
const { displayedKeys = [] } = spec.widgetOptions || {};
const keys = displayedKeys.length ? displayedKeys : ['index'];
return (
@ -63,43 +124,12 @@ export const ArrayTable: React.FC<Props> = props => {
</Thead>
<Tbody>
{value.map((itemValue: any, itemIndex: number) => (
<Tr key={itemIndex} className={TableRowStyle}>
<Td key="setting">
<PopoverWidget
{...props}
value={itemValue}
spec={mergeWidgetOptionsIntoSpec(
{
...itemSpec,
title: '',
},
{
expressionOptions,
}
)}
path={path.concat(String(itemIndex))}
level={level + 1}
onChange={(newItemValue: any) => {
const newValue = [...value];
newValue[itemIndex] = newItemValue;
onChange(newValue);
}}
>
{typeof children === 'function'
? children(props, itemValue, itemIndex)
: null}
</PopoverWidget>
</Td>
{keys.map((key: string) => {
const propertyValue =
key === 'index' ? itemValue[key] ?? itemIndex : itemValue[key];
return <Td key={key}>{propertyValue}</Td>;
})}
<Td key="button">
<ArrayButtonGroup index={itemIndex} value={value} onChange={onChange} />
</Td>
</Tr>
<TableRow
{...props}
key={itemIndex}
itemValue={itemValue}
itemIndex={itemIndex}
/>
))}
</Tbody>
</Table>

View File

@ -93,6 +93,10 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
return params;
}, [formik.values.method.name]);
const parametersPath = useMemo(()=> path.concat('method', 'parameters'), [path]);
const parametersSpec = useMemo(()=> mergeWidgetOptionsIntoSpec(paramsSpec, { onlySetValue: true }), [paramsSpec]);
const disabledPath = useMemo(()=> path.concat('disabled'), [path]);
const disabledSpec = useMemo(()=> Type.Boolean({ widgetOptions: { isShowAsideExpressionButton: true } }), []);
const updateMethods = useCallback(
(componentId: string) => {
@ -127,18 +131,29 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
}
}, [value, updateMethods]);
const onTargetComponentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const onTargetComponentChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
updateMethods(e.target.value);
formik.handleChange(e);
formik.setFieldValue('method', { name: '', parameters: {} });
};
}, [updateMethods, formik]);
const onSubmit = useCallback(() => {
formik.submitForm();
}, [formik]);
const onParametersChange = useCallback(json => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
}, [formik]);
const onDisabledChange = useCallback(value => {
formik.setFieldValue('disabled', value);
formik.submitForm();
}, [formik]);
const typeField = (
<FormControl>
<FormLabel>Event Type</FormLabel>
<Select
name="type"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
placeholder="Select Event Type"
value={formik.values.type}
@ -156,7 +171,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Target Component</FormLabel>
<Select
name="componentId"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={onTargetComponentChange}
placeholder="Select Target Component"
value={formik.values.componentId}
@ -174,7 +189,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Method</FormLabel>
<Select
name="method.name"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
placeholder="Select Method"
value={formik.values.method.name}
@ -193,15 +208,12 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Parameters</FormLabel>
<RecordWidget
component={component}
path={path.concat('method', 'parameters')}
path={parametersPath}
level={level + 1}
spec={mergeWidgetOptionsIntoSpec(paramsSpec, { onlySetValue: true })}
spec={parametersSpec}
services={services}
value={formik.values.method.parameters}
onChange={json => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
}}
onChange={onParametersChange}
/>
</FormControl>
);
@ -211,7 +223,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Wait Type</FormLabel>
<Select
name="wait.type"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
value={formik.values.wait?.type}
>
@ -227,7 +239,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Wait Time</FormLabel>
<Input
name="wait.time"
onBlur={() => formik.submitForm()}
onBlur={onSubmit}
onChange={formik.handleChange}
value={formik.values.wait?.time}
/>
@ -239,14 +251,11 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
<FormLabel>Disabled</FormLabel>
<SpecWidget
{...props}
spec={Type.Boolean({ widgetOptions: { isShowAsideExpressionButton: true } })}
spec={disabledSpec}
level={level + 1}
path={['disabled']}
path={disabledPath}
value={formik.values.disabled}
onChange={value => {
formik.setFieldValue('disabled', value);
formik.submitForm();
}}
onChange={onDisabledChange}
/>
</FormControl>
);

View File

@ -1,4 +1,10 @@
import React, { useState, useEffect, useCallback, useImperativeHandle } from 'react';
import React, {
useState,
useEffect,
useMemo,
useCallback,
useImperativeHandle,
} from 'react';
import {
Popover,
PopoverTrigger,
@ -38,10 +44,20 @@ export const PopoverWidget = React.forwardRef<
>((props, ref) => {
const { spec, path, children } = props;
const isObjectChildren = children && typeof children === 'object';
const [isInit, setIsInit] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const mergedSpec = useMemo(
() => ({
...spec,
widget:
spec.widget === `${CORE_VERSION}/${CoreWidgetName.Popover}` ? '' : spec.widget,
}),
[spec]
);
const handleOpen = useCallback(() => {
setIsOpen(true);
setIsInit(true);
emitter.emit('other-popover-close', path);
}, [path]);
const handleClickTrigger = useCallback(event => {
@ -132,17 +148,13 @@ export const PopoverWidget = React.forwardRef<
<PopoverContent onClick={handleClickContent}>
<PopoverArrow />
<PopoverBody maxHeight="400px" overflow="auto">
{isObjectChildren && 'body' in children ? (
(children as Children).body
) : (
<SpecWidget
{...props}
spec={{
...spec,
widget: spec.widget === `${CORE_VERSION}/${CoreWidgetName.Popover}` ? '' : spec.widget,
}}
/>
)}
{isInit ? (
isObjectChildren && 'body' in children ? (
(children as Children).body
) : (
<SpecWidget {...props} spec={mergedSpec} />
)
) : null}
</PopoverBody>
</PopoverContent>
</Portal>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { EventWidget } from '@sunmao-ui/editor-sdk';
import {
Box,
@ -44,9 +44,13 @@ export const EventHandlerForm: React.FC<Props> = props => {
onUp,
onDown,
} = props;
const [hasOpened, setHasOpened] = React.useState<boolean>(false);
const onAccordionChange = useCallback(() => {
setHasOpened(true);
}, []);
return (
<Accordion width="100%" allowMultiple>
<Accordion width="100%" allowMultiple onChange={onAccordionChange}>
<AccordionItem>
<h2>
<AccordionButton>
@ -57,45 +61,47 @@ export const EventHandlerForm: React.FC<Props> = props => {
</AccordionButton>
</h2>
<AccordionPanel pb={4} pt={2} padding={0}>
<Box position="relative" width="100%">
<VStack className={formWrapperCSS}>
<EventWidget
component={component}
spec={spec}
value={handler}
path={[]}
level={1}
services={services}
onChange={onChange}
/>
</VStack>
<Box position="absolute" right="4" top="4">
<IconButton
aria-label="up event handler"
icon={<ArrowUpIcon />}
size="xs"
variant="ghost"
disabled={index === 0}
onClick={onUp}
/>
<IconButton
aria-label="down event handler"
icon={<ArrowDownIcon />}
size="xs"
variant="ghost"
disabled={index === size - 1}
onClick={onDown}
/>
<IconButton
aria-label="remove event handler"
colorScheme="red"
icon={<CloseIcon />}
onClick={onRemove}
size="xs"
variant="ghost"
/>
{hasOpened ? (
<Box position="relative" width="100%">
<VStack className={formWrapperCSS}>
<EventWidget
component={component}
spec={spec}
value={handler}
path={[]}
level={1}
services={services}
onChange={onChange}
/>
</VStack>
<Box position="absolute" right="4" top="4">
<IconButton
aria-label="up event handler"
icon={<ArrowUpIcon />}
size="xs"
variant="ghost"
disabled={index === 0}
onClick={onUp}
/>
<IconButton
aria-label="down event handler"
icon={<ArrowDownIcon />}
size="xs"
variant="ghost"
disabled={index === size - 1}
onClick={onDown}
/>
<IconButton
aria-label="remove event handler"
colorScheme="red"
icon={<CloseIcon />}
onClick={onRemove}
size="xs"
variant="ghost"
/>
</Box>
</Box>
</Box>
) : null}
</AccordionPanel>
</AccordionItem>
</Accordion>

View File

@ -145,6 +145,12 @@ export const Editor: React.FC<Props> = observer(
}
}, [isDisplayApp]);
const onPreview = useCallback(() => setPreview(true), []);
const onSelectComponent = useCallback(id => {
editorStore.setSelectedComponentId(id);
}, [editorStore]);
const onRightTabChange = useCallback(activatedTab => {
setToolMenuTab(activatedTab);
}, []);
const renderMain = () => {
const appBox = (
@ -216,9 +222,7 @@ export const Editor: React.FC<Props> = observer(
<StructureTree
components={components}
selectedComponentId={selectedComponentId}
onSelectComponent={id => {
editorStore.setSelectedComponentId(id);
}}
onSelectComponent={onSelectComponent}
services={services}
/>
</TabPanel>
@ -257,9 +261,8 @@ export const Editor: React.FC<Props> = observer(
display="flex"
flexDirection="column"
index={toolMenuTab}
onChange={activatedTab => {
setToolMenuTab(activatedTab);
}}
onChange={onRightTabChange}
isLazy
>
<TabList background="gray.50">
<Tab>Inspect</Tab>

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentItemView } from './ComponentItemView';
@ -110,14 +110,20 @@ export const ComponentTree: React.FC<Props> = props => {
isExpanded,
]);
const onClickRemove = () => {
const onClickRemove = useCallback(() => {
eventBus.send(
'operation',
genOperation(registry, 'removeComponent', {
componentId: component.id,
})
);
};
}, [component.id, eventBus, registry]);
const onClickItem = useCallback(() => {
onSelectComponent(component.id);
}, [component.id, onSelectComponent]);
const onToggleExpanded = useCallback(() => setIsExpanded(prev => !prev), []);
const onDragStart = useCallback(() => setIsDragging(true), []);
const onDragEnd = useCallback(() => setIsDragging(false), []);
return (
<VStack
@ -141,15 +147,13 @@ export const ComponentTree: React.FC<Props> = props => {
id={component.id}
title={component.id}
isSelected={component.id === selectedComponentId}
onClick={() => {
onSelectComponent(component.id);
}}
onClick={onClickItem}
onClickRemove={onClickRemove}
noChevron={slots.length === 0}
isExpanded={isExpanded}
onToggleExpanded={() => setIsExpanded(prev => !prev)}
onDragStart={() => setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
onToggleExpanded={onToggleExpanded}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
depth={depth}
/>
</DropComponentWrapper>