基于实际项目代码的深度分析
本项目采用组件化配置系统而非标准JSON Schema,核心设计理念:
标准JSON Schema方式:
// 声明式Schema
{
schema: { type: 'object', properties: {...} },
uiSchema: { 'ui:groups': [...] }
}
// 自动生成表单
本项目方式:
// 命令式组件
<Text config={config} onConfigUpdate={handleUpdate} />
// 组件内部定义属性面板结构
优势:
劣势:
interface ComponentConfig {
id: string; // 唯一标识,如 'panel-uuid'
type: string; // 组件类型:'text' | 'image' | 'button' | 'swiper' | 'panel'
name?: string; // 组件名称
css: CSSConfig; // 样式配置
props: PropsConfig; // 组件属性
elements?: Element[]; // 子元素(容器类组件)
mapping?: MappingConfig; // 数据映射
action?: ActionConfig; // 动作配置
content?: string; // 内容(文本组件)
}
interface CSSConfig {
width: number;
height: number;
left: number;
top: number;
zIndex: number;
backgroundColor?: string;
backgroundImage?: string;
backgroundPosition?: string;
backgroundRepeat?: string;
backgroundSize?: string;
// ... 更多CSS属性
}
interface PropsConfig {
sceneType?: string; // 场景类型
[key: string]: any; // 其他业务属性
}
容器初始化:
// src/models/shop-decoration/container.jsx
export function init(config) {
const result = {
id: createID(), // 'panel-uuid'
type: 'panel',
name: '组合容器',
css: {
width: 375,
height: 250,
zIndex: 1,
left: 0,
top: 0,
},
props: {
sceneType: 'container',
},
elements: [], // 子元素数组
};
if (!config) return result;
return merge(result, config);
}
文本组件配置:
// Text组件的config结构
{
id: 'text-uuid',
type: 'text',
content: '文本内容',
css: {
width: 100,
height: 20,
fontSize: 14,
color: '#000000',
// ...
},
props: {
sceneType: 'normal', // 'normal' | 'floortext' | 'marquee'
line: 1, // 行数
},
mapping: { // 数据关联
'fn::model': ['product', 'name']
},
showSwitch: true // 是否显示(商品楼层场景)
}
轮播图组件配置:
// Swiper组件的config结构
{
id: 'swiper-uuid',
type: 'swiper',
css: {
width: 375,
height: 200,
},
props: {
duration: 500, // 停留时间(毫秒)
interval: 3000, // 轮播时间(毫秒)
indicatorDots: true, // 显示指示器
indicatorColor: '#ffffff', // 指示器颜色
indicatorActiveColor: '#ff0000', // 选中颜色
},
items: [ // 轮播项数组
{
id: 'item-uuid',
css: { width: 375, height: 200 },
elements: [ // 每个轮播项的子元素
{ type: 'image', src: '...' },
{ type: 'text', content: '...' }
]
}
]
}
1. 扁平化CSS配置
2. 类型化Props
type字段区分组件类型props.sceneType区分使用场景3. 嵌套结构支持
elements数组支持容器类组件的嵌套Properties组件注册:
// src/components/shop-decoration/properties/index.jsx
export { default as image } from './basic/Image';
export { default as text } from './Text';
export { default as button } from './basic/Button';
export { default as swiper } from './Swiper';
export { default as container } from './Container';
// ...
// 使用
import * as PropertiesMap from '../properties';
const PropertyComponent = PropertiesMap[config.type];
<PropertyComponent config={config} onConfigUpdate={handleUpdate} />
Text组件属性面板:
// src/components/shop-decoration/properties/Text.jsx
class Text extends Component {
render() {
return (
<div className="prop-edit-pane">
<h3 className="component-title">文字组件</h3>
<PropertyCollapse defaultActiveKey={['content', 'style', 'link']}>
{this.renderContent()} {/* 组件内容 */}
{this.renderLink()} {/* 跳转设置 */}
{this.renderStyle()} {/* 组件样式 */}
{this.renderCommon()} {/* 通用设置 */}
</PropertyCollapse>
</div>
);
}
renderContent() {
return (
<Panel header="组件内容" key="content">
<TextArea
value={config.content}
onChange={this.changeContent}
onBlur={this.ckeckContent}
/>
</Panel>
);
}
}
createFormControl工具函数:
// src/components/shop-decoration/properties/utils.jsx
export function createFormControl(options) {
const { tip, wrapClassName, wrapStyle, component } = options;
return (
<div className={`item-control-wrap ${wrapClassName || ''}`} style={wrapStyle}>
<div className="item-control-tip">{tip}</div>
{component}
</div>
);
}
// 使用
{createFormControl({
tip: '轮播时间(秒)',
wrapClassName: 'flex-1',
component: (
<InputNumber
value={interval}
onChange={value => this.changeProps('interval', value * 1000)}
/>
),
})}
设计优势:
用户操作
↓
Properties组件处理
↓
调用changeProps/changeCss等方法
↓
修改config对象
↓
调用onConfigUpdate(config)
↓
父组件更新state
↓
触发重新渲染
Text组件的更新方法:
class Text extends Component {
// 更新props
changeProps(key, value) {
const { props } = this.props.config;
props[key] = value;
this.updateConfig();
}
// 更新CSS
changeCss = (key, value) => {
const { css } = this.props.config;
css[key] = value;
this.setAutoHeight(key); // 自动计算高度
this.updateConfig();
}
// 更新内容
changeContent = e => {
this.props.config.content = e.target.value;
if (this.props.config.type === 'text') {
// 自动计算文本尺寸
const rect = calcText(this.props.config);
if (rect && rect.width && rect.height) {
this.props.config.css.width = rect.width;
this.props.config.css.height = rect.height;
}
}
this.updateConfig();
}
// 统一更新
updateConfig() {
const { onConfigUpdate, config } = this.props;
if (!onConfigUpdate) return;
onConfigUpdate(config);
}
}
文本自动高度计算:
// src/components/shop-decoration/properties/basic/constants.js
export const AFFECT_HEIGHT_KEYS = ['fontSize', 'lineHeight', 'line', 'width'];
export function getAutoHeight(css, props) {
// 根据fontSize、lineHeight、line等计算高度
const fontSize = css.fontSize || 14;
const lineHeight = css.lineHeight || 1.5;
const line = props.line || 1;
return fontSize * lineHeight * line;
}
// 使用
setAutoHeight = key => {
const { css, props } = this.props.config;
if (AFFECT_HEIGHT_KEYS.indexOf(key) === -1) return;
css.height = getAutoHeight(css, props);
this.updateConfig();
}
轮播图高度自动计算:
// Swiper组件
updateConfig() {
const { onConfigUpdate, config } = this.props;
if (!onConfigUpdate) return;
// 计算最大子项高度
const childHeights = config?.items?.map(item => item?.css?.height);
const maxHeight = max(childHeights);
config.css.height = maxHeight;
onConfigUpdate(config);
}
Text组件的条件渲染:
class Text extends Component {
// 根据场景类型决定是否显示样式设置
get showByFloorText() {
const { config = {} } = this.props;
const { showSwitch, props } = config;
const { sceneType } = props;
if (sceneType !== 'floortext') {
return true; // 非商品楼层场景,都显示
}
return showSwitch; // 商品楼层场景,根据开关决定
}
render() {
return (
<PropertyCollapse>
{this.renderContent()}
{this.renderLink()}
{/* 条件渲染样式面板 */}
{this.showByFloorText && (
<Panel header="组件样式" key="style">
<TextBasicStyle {...this.props} />
</Panel>
)}
{/* 条件渲染通用设置 */}
{this.showByFloorText && (
<Panel header="通用设置" key="4">
<Common {...this.props} />
</Panel>
)}
</PropertyCollapse>
);
}
}
Swiper组件的动态属性:
class Swiper extends Component {
renderStyle() {
const { css, props } = this.props.config;
let { duration, interval } = props;
// 毫秒转秒(显示用)
duration = duration / 1000;
interval = interval / 1000;
return (
<Panel header="播放设置">
<InputNumber
value={interval}
onChange={value => this.changeProps('interval', value * 1000)}
/>
<InputNumber
value={duration}
onChange={value => this.changeProps('duration', value * 1000)}
/>
</Panel>
);
}
}
基于authKey的权限控制:
<Panel
authKey="propertyConfig.text.content.visiable"
header="组件内容"
key="content"
>
{/* 内容 */}
</Panel>
// PropertyCollapse内部处理权限
if (!hasAuth(authKey)) {
return fallback || null;
}
mapping字段的作用:
mapping字段用于数据关联与映射,将组件的内容(如文本、图片)关联到外部数据源(如商品数据、导航数据等),实现动态数据绑定。在编辑时,组件显示静态内容;在预览/运行时,组件会根据mapping配置从数据源获取实际数据并显示。
核心价值:
文本组件的mapping结构:
{
content: {
'fn::model': [modelName, propName, productId, '']
// [模型名, 属性名, 商品ID(运行时填充), 预留字段]
},
_locked: false // 是否锁定,锁定后不可修改
}
// 实际示例
{
content: {
'fn::model': ['ProductModel', 'title', '', '']
// 关联商品模型的title属性
},
_locked: false
}
图片组件的mapping结构:
{
src: {
'fn::model': [modelName, propName, productData, '']
// productData是JSON字符串,包含productId和source
},
_locked: false
}
// 实际示例
{
src: {
'fn::model': ['ProductModel', 'headImg', '', '']
// 关联商品模型的headImg属性
},
_locked: false
}
fn::model数组说明:
[0] - 模型名:数据模型名称,如 'ProductModel'、'NavModel'、'CouponModel'[1] - 属性名:模型中的属性名,如 'title'、'minPrice'、'headImg'[2] - 运行时数据:运行时填充,如商品ID、商品数据JSON等[3] - 预留字段:目前未使用商品模型(ProductModel):
// 文本组件可关联的属性
{
title: '商品标题',
minPrice: '最低价格',
actPrice: '活动价格',
subTitle: '副标题',
saleType: '销售类型'
}
// 图片组件可关联的属性
{
headImg: '商品头图'
}
导航模型(NavModel):
// 文本组件
{
linkName: '导航名称'
}
// 图片组件
{
linkImage: '导航图片'
}
优惠券模型(CouponModel):
{
type: '优惠券类型',
value: '优惠金额',
label: '单位',
condition: '使用条件'
}
创建mapping:
// src/models/shop-decoration/text.jsx
export function createMapping(propName = 'title', modelName = 'ProductModel', _locked = false) {
return {
content: {
'fn::model': [modelName, propName, '', ''],
},
_locked,
};
}
// 使用示例
const mapping = createMapping('title', 'ProductModel', false);
// 结果:
// {
// content: { 'fn::model': ['ProductModel', 'title', '', ''] },
// _locked: false
// }
设置mapping属性:
// src/models/shop-decoration/text.jsx
export function setMappingProp(mapping, propName, modelName, _locked) {
if (!propName) return undefined; // 如果属性名为空,返回undefined(清除mapping)
// 如果mapping不存在,创建新的
if (!mapping) {
mapping = createMapping(propName, modelName, _locked);
return mapping;
}
// 更新现有mapping
if (modelName) mapping.content['fn::model'][0] = modelName;
if (propName) mapping.content['fn::model'][1] = propName;
if (_locked !== undefined) mapping._locked = _locked;
return mapping;
}
获取mapping属性:
// src/models/shop-decoration/text.jsx
export function getMappingProp(mapping) {
if (!mapping) return undefined;
const modelName = mapping.content['fn::model'][0];
const propName = mapping.content['fn::model'][1];
return [modelName, propName]; // 返回 [模型名, 属性名]
}
// 使用示例
const [modelName, propName] = getMappingProp(mapping);
// 如果 mapping = { content: { 'fn::model': ['ProductModel', 'title', '', ''] } }
// 返回:['ProductModel', 'title']
Common组件的映射设置UI:
// src/components/shop-decoration/properties/basic/Common.jsx
class Common extends Component {
renderMappding() {
const { config, toConnect } = this.props;
const { mapping, type } = config;
const model = MODELS_MAP[type]; // textM 或 imageM
return (
<Cascader
allowClear
onChange={toConnect} // 选择时触发
options={model.getMappingOptions(config)} // 获取可选项
placeholder="请选择关联的模型/属性"
value={model.getMappingProp(mapping)} // 显示当前值
/>
);
}
// toConnect在父组件中定义
toConnect = prop => {
if (!prop) {
// 清空mapping
config.mapping = undefined;
onConfigUpdate(config);
return;
}
const [modelName, propName] = prop;
const model = MODELS_MAP[config.type];
// 设置映射关系
config.mapping = model.setMappingProp(
config.mapping,
propName,
modelName
);
onConfigUpdate(config);
}
}
可选项配置:
// src/models/shop-decoration/text.jsx
export const DEFAULT_MAPPING_OPTIONS = [
{
value: 'ProductModel',
label: '商品',
children: [
{ label: '标题', value: 'title' },
{ label: '价格', value: 'minPrice' },
{ label: '副标题', value: 'subTitle' },
],
},
];
// 根据场景类型返回不同选项
export function getMappingOptions(textM) {
const { sceneType } = textM.props;
if (sceneType === 'coupon') {
return COUPON_MAPPING_OPTIONS; // 优惠券相关选项
}
if (sceneType === 'nav') {
return NAV_MAPPING_OPTIONS; // 导航相关选项
}
return DEFAULT_MAPPING_OPTIONS; // 默认商品选项
}
商品楼层场景的应用:
// src/commons/model.js
export function autoFillProductForBasicElement(element, product) {
const { mapping, type } = element;
// 如果没有设置mapping,则不处理
if (!mapping) return;
if (!product) return;
// 文本组件的处理
if (type === 'text') {
// 填充商品ID到mapping的第三个位置
mapping.content['fn::model'][2] = product.productId;
// 获取映射的属性名
const mappingProp = getMappingProp(mapping, type);
let value = product[mappingProp.key]; // 从商品数据中获取值
// 价格特殊处理:单位转换(分 -> 元)
if (['minPrice', 'actPrice'].includes(mappingProp.key) && value) {
value = `¥ ${value / 100}`;
}
// 活动价格为0时,最低价格显示为红色
if (product.actPrice === 0 && mappingProp.key === 'minPrice') {
element.css.color = '#f74e5c';
}
// 将值填充到组件内容
element.content = value;
}
// 图片组件的处理
if (type === 'image') {
// 填充商品数据到mapping
mapping.src['fn::model'][2] = JSON.stringify({
productId: product.productId,
source: product.source,
});
// 从商品头图数组中取第一张
[element.src] = product.headImg;
}
}
导航场景的应用:
// src/commons/model.js
export function autoFillNavForBasicElement(element, nav) {
const { mapping, type } = element;
if (!mapping) return;
if (type === 'text') {
const mappingProp = getMappingProp(mapping, type);
element.content = nav[mappingProp.key]; // 如 nav.linkName
}
if (type === 'image') {
element.src = nav.linkImage;
}
}
完整的应用流程:
// 1. 编辑时:设置mapping
config.mapping = {
content: { 'fn::model': ['ProductModel', 'title', '', ''] }
};
// 2. 预览/运行时:应用mapping
function applyMapping(config, product) {
if (!config.mapping) return config;
const [modelName, propName] = config.mapping.content['fn::model'];
// 根据组件类型应用映射
if (config.type === 'text') {
// 从商品数据中获取值
const value = product[propName];
config.content = value || config.content; // 有值则替换,无值保留默认
// 同时填充商品ID
config.mapping.content['fn::model'][2] = product.productId;
} else if (config.type === 'image') {
config.src = product[propName] || config.src;
config.mapping.src['fn::model'][2] = JSON.stringify({
productId: product.productId,
source: product.source
});
}
return config;
}
// 3. 批量应用(递归处理嵌套结构)
function applyMappingsRecursive(config, dataSource) {
const newConfig = applyMapping(config, dataSource);
if (newConfig.elements) {
newConfig.elements = newConfig.elements.map(element =>
applyMappingsRecursive(element, dataSource)
);
}
return newConfig;
}
场景1:商品楼层
// 商品楼层中的文本组件配置
{
id: 'text-uuid',
type: 'text',
content: '商品标题', // 默认显示文本
mapping: {
content: { 'fn::model': ['ProductModel', 'title', '', ''] }
}
}
// 运行时,传入商品数据
const product = {
productId: '123',
title: 'iPhone 15 Pro',
minPrice: 799900, // 单位:分
headImg: ['https://...']
};
// 应用mapping后
applyMapping(config, product);
// 结果:
// {
// content: 'iPhone 15 Pro', // 被替换
// mapping: {
// content: { 'fn::model': ['ProductModel', 'title', '123', ''] }
// }
// }
场景2:导航栏
// 导航栏中的图片组件配置
{
id: 'image-uuid',
type: 'image',
src: 'https://placeholder.png',
mapping: {
src: { 'fn::model': ['NavModel', 'linkImage', '', ''] }
}
}
// 运行时,传入导航数据
const nav = {
linkName: '首页',
linkImage: 'https://home-icon.png'
};
// 应用mapping后
applyMapping(config, nav);
// 结果:
// {
// src: 'https://home-icon.png', // 被替换
// mapping: { ... }
// }
场景3:优惠券组件
// 优惠券价格显示
{
type: 'text',
content: '¥0',
mapping: {
content: { 'fn::model': ['CouponModel', 'value', '', ''] }
}
}
// 运行时
const coupon = { value: 50 };
applyMapping(config, coupon);
// 结果:content = '50'
1. 锁定机制(_locked):
// 某些场景下,mapping会被锁定,不允许修改
mapping = {
content: { 'fn::model': ['ProductModel', 'title', '', ''] },
_locked: true // 锁定后,属性面板中不允许修改
};
2. 价格格式化:
// 价格需要特殊处理:分 -> 元
if (['minPrice', 'actPrice'].includes(propName) && value) {
value = `¥ ${value / 100}`;
}
3. 图片数组处理:
// 商品头图是数组,取第一张
if (type === 'image' && Array.isArray(product.headImg)) {
[element.src] = product.headImg;
}
4. 销售类型特殊处理:
// saleType属性需要特殊处理
if (propName === 'saleType') {
if (product.typeText === '普通' || !product.typeText) {
element.content = ''; // 普通商品不显示
} else {
element.content = product.typeText; // 显示类型文本
}
}
1. 解耦设计:
2. 动态渲染:
3. 类型安全:
4. 扩展性强:
getMappingOptions动态配置选项5. 运行时优化:
完整的CSS配置:
interface CSSConfig {
// 尺寸
width: number;
height: number;
left: number;
top: number;
// 层级
zIndex: number;
// 文字样式
fontSize?: number;
fontFamily?: string;
fontWeight?: string;
color?: string;
lineHeight?: number;
textAlign?: string;
// 背景
backgroundColor?: string;
backgroundImage?: string;
backgroundPosition?: string;
backgroundRepeat?: string;
backgroundSize?: string;
// 边框
border?: string;
borderRadius?: number;
// 间距
padding?: string;
margin?: string;
// 其他
opacity?: number;
transform?: string;
}
ContainerCommon通用样式组件:
// src/components/shop-decoration/properties/basic/ContainerCommon.jsx
class ContainerCommon extends Component {
render() {
const { css, onChange, showPadding = true } = this.props;
return (
<div>
{/* 尺寸设置 */}
<InputNumber
label="宽度"
value={css.width}
onChange={value => onChange({ ...css, width: value }, 'width')}
/>
<InputNumber
label="高度"
value={css.height}
onChange={value => onChange({ ...css, height: value }, 'height')}
/>
{/* 位置设置 */}
<InputNumber label="X坐标" value={css.left} />
<InputNumber label="Y坐标" value={css.top} />
{/* 层级设置 */}
<InputNumber label="层级" value={css.zIndex} />
{/* 内边距(可选) */}
{showPadding && (
<Input label="内边距" value={css.padding} />
)}
</div>
);
}
}
文本样式计算:
// src/components/shop-decoration/calcText.js
export function calcText(config) {
// 创建临时DOM元素
const tempDiv = document.createElement('div');
tempDiv.style.cssText = `
position: absolute;
visibility: hidden;
white-space: pre-wrap;
word-wrap: break-word;
font-size: ${config.css.fontSize}px;
font-family: ${config.css.fontFamily};
line-height: ${config.css.lineHeight};
width: ${config.css.width}px;
`;
tempDiv.textContent = config.content;
document.body.appendChild(tempDiv);
// 计算实际尺寸
const rect = {
width: tempDiv.offsetWidth,
height: tempDiv.offsetHeight
};
document.body.removeChild(tempDiv);
return rect;
}
Panel容器结构:
{
id: 'panel-uuid',
type: 'panel',
css: { width: 375, height: 250 },
props: { sceneType: 'container' },
elements: [ // 子元素数组
{
id: 'text-uuid',
type: 'text',
content: '文本',
css: { ... }
},
{
id: 'image-uuid',
type: 'image',
src: '...',
css: { ... }
}
]
}
FreeCombinationEditor的嵌套渲染:
// src/components/shop-decoration/FreeCombinationEditor/index.jsx
class FreeCombinationEditor extends Component {
renderElements() {
const { config } = this.props;
const { elements } = config;
return elements.map(item => {
// 根据类型渲染不同组件
switch (item.type) {
case 'text':
return <TextElement key={item.id} config={item} />;
case 'image':
return <ImageElement key={item.id} config={item} />;
case 'button':
return <ButtonElement key={item.id} config={item} />;
default:
return null;
}
});
}
}
级联更新处理:
// 更新子元素时,需要更新父容器
const updateElement = (containerId, elementId, newConfig) => {
const container = findContainer(containerId);
const elementIndex = container.elements.findIndex(e => e.id === elementId);
if (elementIndex !== -1) {
container.elements[elementIndex] = newConfig;
// 更新容器尺寸(如果需要)
updateContainerSize(container);
// 通知父组件更新
onConfigUpdate(container);
}
};
文本输入防抖:
class Text extends Component {
constructor(props) {
super(props);
this.debouncedUpdate = debounce(this.updateConfig, 300);
}
changeContent = e => {
this.props.config.content = e.target.value;
// 立即更新UI(本地状态)
this.forceUpdate();
// 防抖更新(避免频繁调用API)
this.debouncedUpdate();
}
}
按需渲染属性面板:
// 只渲染展开的面板
<PropertyCollapse
defaultActiveKey={['content']}
onChange={activeKeys => {
// 只渲染activeKeys中的面板
this.setState({ activeKeys });
}}
>
{activeKeys.includes('content') && this.renderContent()}
{activeKeys.includes('style') && this.renderStyle()}
</PropertyCollapse>
文本尺寸计算缓存:
const textSizeCache = new Map();
function calcText(config) {
const cacheKey = `${config.content}-${config.css.fontSize}-${config.css.width}`;
if (textSizeCache.has(cacheKey)) {
return textSizeCache.get(cacheKey);
}
const rect = doCalcText(config);
textSizeCache.set(cacheKey, rect);
return rect;
}
添加新组件类型:
// 1. 创建Properties组件
// src/components/shop-decoration/properties/NewComponent.jsx
class NewComponent extends Component {
render() {
return (
<div className="prop-edit-pane">
<PropertyCollapse>
{this.renderContent()}
{this.renderStyle()}
</PropertyCollapse>
</div>
);
}
}
// 2. 注册组件
// src/components/shop-decoration/properties/index.jsx
export { default as newComponent } from './NewComponent';
// 3. 使用
const PropertyComponent = PropertiesMap[config.type];
动态添加属性:
// 通过props.sceneType扩展属性
class Text extends Component {
render() {
const { sceneType } = this.props.config.props;
return (
<PropertyCollapse>
{this.renderContent()}
{/* 根据场景类型渲染不同属性 */}
{sceneType === 'marquee' && this.renderMarqueeProps()}
{sceneType === 'floortext' && this.renderFloorTextProps()}
</PropertyCollapse>
);
}
}
答案:
这是一个架构选择问题,我们选择了组件化配置,主要考虑:
选择组件化的原因:
业务复杂度高:
开发效率:
类型安全:
权衡:
优势:
劣势:
改进方向:
我们正在考虑混合方案:
答案:
设计考虑:
关注点分离:
{
css: { ... }, // 样式相关
props: { ... }, // 业务逻辑相关
content: '...', // 内容相关
}
序列化友好:
更新优化:
// CSS更新不影响Props
changeCss('width', 100);
// Props更新不影响CSS
changeProps('sceneType', 'marquee');
实际案例:
在文本组件中:
分开设计使得这些逻辑互不干扰。
答案:
实现机制:
// 1. 定义影响高度的属性
const AFFECT_HEIGHT_KEYS = ['fontSize', 'lineHeight', 'line', 'width'];
// 2. 高度计算函数
function getAutoHeight(css, props) {
const fontSize = css.fontSize || 14;
const lineHeight = css.lineHeight || 1.5;
const line = props.line || 1;
return fontSize * lineHeight * line;
}
// 3. 变更时自动计算
setAutoHeight = key => {
const { css, props } = this.props.config;
if (AFFECT_HEIGHT_KEYS.indexOf(key) === -1) return;
css.height = getAutoHeight(css, props);
this.updateConfig();
}
文本尺寸计算:
// 使用临时DOM元素计算
function calcText(config) {
const tempDiv = document.createElement('div');
tempDiv.style.cssText = `
position: absolute;
visibility: hidden;
font-size: ${config.css.fontSize}px;
width: ${config.css.width}px;
`;
tempDiv.textContent = config.content;
document.body.appendChild(tempDiv);
const rect = {
width: tempDiv.offsetWidth,
height: tempDiv.offsetHeight
};
document.body.removeChild(tempDiv);
return rect;
}
性能优化:
计算缓存:
const cache = new Map();
const cacheKey = `${content}-${fontSize}-${width}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
防抖处理:
const debouncedCalc = debounce(calcText, 300);
按需计算:
// 只在特定属性变更时计算
if (AFFECT_HEIGHT_KEYS.indexOf(key) !== -1) {
calcText(config);
}
性能数据:
答案:
嵌套结构:
{
type: 'panel',
elements: [
{
type: 'text',
content: '...'
},
{
type: 'panel', // 嵌套容器
elements: [...]
}
]
}
更新机制:
向上冒泡更新:
const updateElement = (container, elementId, newConfig) => {
const index = container.elements.findIndex(e => e.id === elementId);
if (index !== -1) {
container.elements[index] = newConfig;
// 通知父容器更新
onConfigUpdate(container);
}
};
级联更新:
// 更新子元素时,更新父容器尺寸
const updateContainerSize = (container) => {
const childHeights = container.elements.map(e => e.css.height);
container.css.height = Math.max(...childHeights);
};
深度克隆:
// 避免直接修改原对象
const newConfig = cloneDeep(config);
newConfig.elements[index] = newElement;
onConfigUpdate(newConfig);
数据一致性保证:
单一数据源:
不可变更新:
// 使用immutable方式更新
const newConfig = {
...config,
css: {
...config.css,
width: 100
}
};
事务性更新:
// 多个更新合并为一个
undoManager.startGroup(() => {
updateElement1();
updateElement2();
updateContainer();
});
答案:
实现方式:
计算属性:
get showByFloorText() {
const { config } = this.props;
const { showSwitch, props } = config;
const { sceneType } = props;
if (sceneType !== 'floortext') return true;
return showSwitch;
}
条件渲染:
render() {
return (
<PropertyCollapse>
{this.renderContent()}
{this.showByFloorText && (
<Panel header="组件样式">
<TextBasicStyle />
</Panel>
)}
</PropertyCollapse>
);
}
性能优化:
按需渲染:
// 只渲染展开的面板
const [activeKeys, setActiveKeys] = useState(['content']);
return (
<PropertyCollapse activeKeys={activeKeys}>
{activeKeys.includes('content') && this.renderContent()}
{activeKeys.includes('style') && this.renderStyle()}
</PropertyCollapse>
);
Memo优化:
const MemoizedPanel = React.memo(({ config }) => {
return <Panel>{/* ... */}</Panel>;
});
计算缓存:
const showByFloorText = useMemo(() => {
return calculateShowByFloorText(config);
}, [config.showSwitch, config.props.sceneType]);
性能数据:
答案:
映射结构:
// mapping结构
{
'fn::model': ['product', 'name'] // [模型名, 属性名]
}
设置映射:
// Common组件
toConnect = prop => {
const [modelName, propName] = prop;
config.mapping = textM.setMappingProp(
config.mapping,
propName,
modelName
);
onConfigUpdate(config);
}
运行时绑定:
// 预览时应用映射
function applyMapping(config, dataSource) {
if (!config.mapping) return config;
const [modelName, propName] = config.mapping['fn::model'];
const value = dataSource[modelName]?.[propName];
// 根据组件类型应用映射
switch (config.type) {
case 'text':
return { ...config, content: value || config.content };
case 'image':
return { ...config, src: value || config.src };
default:
return config;
}
}
批量应用:
// 递归应用所有元素的映射
function applyMappingsRecursive(config, dataSource) {
const newConfig = applyMapping(config, dataSource);
if (newConfig.elements) {
newConfig.elements = newConfig.elements.map(element =>
applyMappingsRecursive(element, dataSource)
);
}
return newConfig;
}
性能考虑:
答案:
更新策略:
防抖处理:
// 文本输入防抖
const debouncedUpdate = debounce(this.updateConfig, 300);
changeContent = e => {
config.content = e.target.value;
this.forceUpdate(); // 立即更新UI
debouncedUpdate(); // 防抖更新数据
}
批量更新:
// 合并多个更新
const updates = [];
const batchUpdate = (update) => {
updates.push(update);
if (updates.length > 10) {
flushUpdates();
}
};
const flushUpdates = () => {
// 一次性应用所有更新
updates.forEach(update => update());
updates.length = 0;
};
按需更新:
// 只更新变化的组件
const shouldUpdate = (prevConfig, nextConfig) => {
return prevConfig.id !== nextConfig.id ||
prevConfig.content !== nextConfig.content ||
// ... 其他关键属性
};
大量组件优化:
虚拟滚动:
// 属性面板列表虚拟滚动
<VirtualList
height={600}
itemCount={components.length}
itemSize={50}
>
{({ index, style }) => (
<ComponentItem
style={style}
config={components[index]}
/>
)}
</VirtualList>
懒加载:
// 只加载可见的属性面板
const [visiblePanels, setVisiblePanels] = useState(['content']);
<PropertyCollapse
activeKeys={visiblePanels}
onChange={setVisiblePanels}
>
{visiblePanels.includes('content') && this.renderContent()}
</PropertyCollapse>
计算缓存:
// 缓存计算结果
const cache = new WeakMap();
const getCachedValue = (config, key) => {
if (!cache.has(config)) {
cache.set(config, {});
}
const cached = cache.get(config);
if (cached[key]) {
return cached[key];
}
const value = calculateValue(config, key);
cached[key] = value;
return value;
};
性能数据:
答案:
扩展步骤:
创建Properties组件:
// src/components/shop-decoration/properties/NewComponent.jsx
class NewComponent extends Component {
render() {
return (
<div className="prop-edit-pane">
<PropertyCollapse>
{this.renderContent()}
{this.renderStyle()}
</PropertyCollapse>
</div>
);
}
}
注册组件:
// src/components/shop-decoration/properties/index.jsx
export { default as newComponent } from './NewComponent';
添加渲染逻辑:
// FreeCombinationEditor
switch (item.type) {
case 'newComponent':
return <NewComponentElement config={item} />;
}
扩展成本:
时间成本:
代码量:
优化方向:
组件模板:
// 提供组件模板
const createComponentTemplate = (type, schema) => {
return {
Properties: createPropertiesComponent(schema),
Render: createRenderComponent(schema),
};
};
Schema驱动:
// 通过Schema自动生成
const schema = {
type: 'object',
properties: {
content: { type: 'string' },
style: { type: 'object' }
}
};
const Component = generateFromSchema(schema);
答案:
序列化:
// 1. 直接JSON序列化
const serialized = JSON.stringify(config);
// 2. 处理特殊值
const serializeConfig = (config) => {
return JSON.stringify(config, (key, value) => {
// 处理undefined
if (value === undefined) {
return null;
}
// 处理函数(如果有)
if (typeof value === 'function') {
return value.toString();
}
return value;
});
};
反序列化:
// 1. 基本反序列化
const config = JSON.parse(serialized);
// 2. 恢复默认值
const deserializeConfig = (serialized) => {
const config = JSON.parse(serialized);
// 恢复默认值
if (!config.css) {
config.css = getDefaultCSS(config.type);
}
if (!config.props) {
config.props = getDefaultProps(config.type);
}
return config;
};
注意事项:
循环引用:
// 避免循环引用
const serializeSafe = (config) => {
const visited = new Set();
return JSON.stringify(config, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (visited.has(value)) {
return '[Circular]';
}
visited.add(value);
}
return value;
});
};
数据校验:
// 反序列化后校验
const validateConfig = (config) => {
if (!config.id || !config.type) {
throw new Error('Invalid config');
}
if (!config.css || typeof config.css.width !== 'number') {
throw new Error('Invalid CSS');
}
return config;
};
版本兼容:
// 处理版本迁移
const migrateConfig = (config, version) => {
if (version < '2.0.0') {
// 迁移逻辑
config.css = migrateCSS(config.css);
}
return config;
};
答案:
最佳实践:
统一接口:
// 所有Properties组件统一接口
interface PropertyComponentProps {
config: ComponentConfig;
onConfigUpdate: (config: ComponentConfig) => void;
mode?: string;
}
工具函数复用:
// 统一的工具函数
export function createFormControl(options) { ... }
export function verifyConfig(config) { ... }
export function getDefaultConfig(type) { ... }
类型定义:
// TypeScript类型定义
interface ComponentConfig {
id: string;
type: ComponentType;
css: CSSConfig;
props: PropsConfig;
}
文档规范:
/**
* 文本组件配置
* @param {Object} config - 组件配置
* @param {string} config.content - 文本内容
* @param {Object} config.css - 样式配置
* @param {number} config.css.fontSize - 字体大小
*/
单元测试:
describe('Text Component', () => {
it('should update content', () => {
const config = createTextConfig();
const wrapper = mount(<Text config={config} />);
wrapper.find('TextArea').simulate('change', { target: { value: 'new' } });
expect(config.content).toBe('new');
});
});
代码组织:
properties/
├── index.jsx # 组件导出
├── Text.jsx # 文本组件
├── Swiper.jsx # 轮播组件
├── basic/ # 基础组件
│ ├── Image.jsx
│ ├── Button.jsx
│ └── Common.jsx # 通用组件
├── utils.jsx # 工具函数
└── constants.js # 常量定义
维护建议:
文档版本: v1.0
最后更新: 2024年
基于项目: creative-cloud-web-designer