阅读 2697

js设计模式运用 - 设计一个简单的店铺装修

这是我参与更文挑战的第13天,活动详情查看更文挑战

前置文章:

背景

公司以前的项目当中做过一个相关项目,店铺装修。当时的设计比较简陋,随着项目越来越大,上层建设和底层代码冗余度较高,维护起来比较麻烦,彼此之间没有清晰的分界线。所以那个时候就想着重新设计一个店铺装修,可以将边界划分清楚,维护起来相对简单的模式。

先看一下大的设计图

设计思路.png

先看效果

业务注册入口

业务组件的入口统一在这里,和系统框架无关,将二者进行区分开。新的业务组件只需要按照这种方式注册就可以。

import React from 'react';
import ShopDecorationNode from '../LeftNode/Nodes';
import { Yihangsitu, YihangsituProperty } from './Yihangsitu';
import Yihangyitu from './Yihangyitu';

const { registerNode, registerNodeProperty } = ShopDecorationNode;

registerNode('yihangsitu', (config: any) => {
  return React.createElement((Yihangsitu as unknown) as React.FC<{}>, config);
});

registerNodeProperty('yihangsitu', (config: any) => {
  return React.createElement((YihangsituProperty as unknown) as React.FC<{}>, config);
});

registerNode('yihangyitu', (config: any) => {
  return React.createElement(Yihangyitu, config);
});
复制代码


功能展示

屏幕录制2021-06-13 下午8.07.22.2021-06-13 20_09_30.gif


设计模式原则

依赖反转原则

该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。

依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会因为低层次组件的变化修改而受影响。

image.png
整个店铺装修从设计原则来看:分为两大部分,框架层,业务组件层。他们存在各自的分工,以及数据交互。


单一职责原则

这部分其实是根据固定的产品设计而来的。设计组件的时候,每个组件具有自己固定的功能,彼此之间的数据交互可以通过相关的设计模式去进行弱关联。

image.png

开闭原则

这里的开闭原则应用,对于业务组件的新增和修改全部由用户控制【开发者自己处理,不涉及到框架层改动】。

  • 打开:扩展【业务组件新增和修改】
  • 关闭:修改【框架层架的改动】


里式替换原则

这里我的应用规则是,所有的业务组件需要按照既定的规则来开发,需要通过的特定的方法进行和框架层次的数据交互。


设计思路

用户行为分析

店铺装修对应的用户操作基本如下:

image.png

开发角度分析

针对开发人员,当随着组建的增多,开发人员更多的想只需要操心对应的组件就行了,不要让我操作太多。

image.png

架构设计分析

首先功能上肯定要满足用户行为,然后兼顾开发人员的需求。当然从设计上来讲,我们也是想在后期的维护和扩展上面尽量简单,将框架层设计,和业务层设计做到分离。

这里的关键点在哪里呢?
具体业务组件的引用和加载,左侧组件列表需要引入进来。组件渲染需要加载对应的组件。组件属性也需要引入对应的组件属性文件,然后动态加载。

这里核心的关键就是将组件的加载动态【注册的概念】引入进来。业务组件的引入不是通过important的方式引入进来了。
image.png


设计说明

设计思路.png

这个文字说明起来有点麻烦啊。那就通过单体来介绍,介绍自我功能,和外界交互功能。

左侧组件

数据部分

NodeRegistry:类

  • nodeTypes:内部私有属性,存储当前系统注册多少了店铺装修组件
  • nodePropertyTypes:内部私有属性,存储店铺组件对应的属性组件。
  • registerNode:注册组件
  • renderNode:渲染组件
  • registerNodeProperty:注册属性组件
  • renderNodeProperty:渲染属性组件

页面部分

页面遍历当前nodeTypes内部注册的组件,显示左侧店铺装修组件列表。

行为 & 与其他组件相关

业务代码书写

开发人员调用register**方法,注册组件。

registerNode('yihangsitu', (config: any) => {
  return React.createElement((Yihangsitu as unknown) as React.FC<{}>, config);
});

registerNodeProperty('yihangsitu', (config: any) => {
  return React.createElement((YihangsituProperty as unknown) as React.FC<{}>, config);
});
复制代码

中间页面渲染区域

组件的渲染会调用renderNode组件。同时传入对应的属性数据。

 {renderNode(item.type, getCurrentNodeContent(item.key))}
复制代码

renderNode方法

  public renderNode = (name: string, config: any) => {
    return this.nodeTypes[name](config);
  };
复制代码

右侧属性渲染区域

右侧属性组件渲染的时候会调用:renderNodeProperty方法,三个属性,key,更新属性的方法,用于开发人员书写的组件,和系统进行数据通信,content是当前最新的属性值。

<div key={property.key}>{renderNodeProperty(property.type, {
                                    keyString: property.key, 
                                    onValuesChange: store.updateNodeContent.bind(store), 
                                    content: JSON.parse(property.content || '{}')
                                  })}
                    </div>;
复制代码

renderNodeProperty

  public renderNodeProperty = (name: string, config?: any) => {
    const callback = this.nodePropertyTypes[name];
    return callback ? callback(config) : '';
  };
复制代码


中间组件渲染

数据部分

tempStoreData:用于数据的更新,进行页面的重新渲染,这里其实我的想法还需要继续优化一下。通过HOC处理页面数据的更新。

页面部分

循环遍历tempStoreData,然后调用renderNode方法渲染组件。获取当前组件的属性数据,同步传入进去。

行为 & 与其他组件相关

  • 调用renderNode渲染页面
  • 订阅StoreData的数据更新,重新渲染页面
  • 调用StoreData的setCurrentNode方法设置右侧属性需要展示的节点。


右侧组件属性渲染

数据部分

私有属性property,用来进行右侧属性页面的更新。

页面部分

获取当前需要展示的组件的type类型和key,并且获取到最新的属性值同步传入。调用renderProperty方法

<div key={property.key}>{renderNodeProperty(property.type, {
                                    keyString: property.key, 
                                    onValuesChange: store.updateNodeContent.bind(store), 
                                    content: JSON.parse(property.content || '{}')
                                  })}
                    </div>;
复制代码

行为 & 与其他组件相关

  • 订阅storeData的中间区域当前选择节点的更新


StoreData 数据交互

数据部分

  • storeData:整个系统各组件进行数据交互的数据Data
  • currentNode:中间区域当前选择的组件node
  • subscriptionNodeArray:订阅currentNode变化的事件数组
  • subscriptionStoreDataArray:订阅storeData数据变化的事件数组
  • setCurrentNode:设置中间区域当前选择的组件node
  • updateStoreData:更新SotrData
  • updateNodeContent:更新currentNode的数据数据
  • subscriptionNodeChange:添加currentNode变化订阅的方法
  • subscriptionStoreDataChange:添加storeData变化订阅的方法

行为 & 与其他组件相关

  • 左侧组件拖拽到中间区域:调用updateStoreData
  • 中间内容组件订阅storeData的更新
  • 中间内容组件,切换选择需要编辑的组件属性:调用setCurrentNode方法
  • 右侧组件属性订阅currentNode更新
  • 右侧属性组件更新调用updateNodeContent方法


里式替换原则 - 业务组件开发规则

注册方式

import React from 'react';
import ShopDecorationNode from '../LeftNode/Nodes';
import { Yihangsitu, YihangsituProperty } from './Yihangsitu';
import Yihangyitu from './Yihangyitu';

const { registerNode, registerNodeProperty } = ShopDecorationNode;

registerNode('yihangsitu', (config: any) => {
  return React.createElement((Yihangsitu as unknown) as React.FC<{}>, config);
});

registerNodeProperty('yihangsitu', (config: any) => {
  return React.createElement((YihangsituProperty as unknown) as React.FC<{}>, config);
});

registerNode('yihangyitu', (config: any) => {
  return React.createElement(Yihangyitu, config);
});
复制代码

业务组件自身逻辑处理

  • 组件内部的属性值开发人员自己对应上,就是属性组件有一个name属性,那在node节点想展示对应的属性值,也需要去获取props的name属性值。
  • 属性组件的更新需要调用props的onValuesChange方法进行数据更新,更新的是当前组件所有的数据。

代码实现

我这里还是使用的是dumi来创建项目。

yarn create @umijs/dumi-lib --site
复制代码


代码结构

image.png

核心代码

NodeRegistry

class NodeRegistry {
  public nodeTypes: Record<string, (config: any) => {}> = Object.create(null);
  public nodePropertyTypes: Record<string, (config: any) => {}> = Object.create(null);

  public registerNode = (name: string, callback: any) => {
    this.nodeTypes[name] = callback;
  };

  public renderNode = (name: string, config: any) => {
    return this.nodeTypes[name](config);
  };

  public registerNodeProperty = (name: string, callback: any) => {
    this.nodePropertyTypes[name] = callback;
  };

  public renderNodeProperty = (name: string, config?: any) => {
    const callback = this.nodePropertyTypes[name];
    return callback ? callback(config) : '';
  };
}

const ShopDecoration = new NodeRegistry();

export default ShopDecoration;
复制代码

左侧组件

import React from 'react';
import { Button } from 'antd';
import styles from './index.less';
import ShopDecorationNode from './Nodes';

export default () => {
  const ondragstart = (event: any, text: string) => {
    event.dataTransfer.setData('Text', text);
  };

  const { nodeTypes } = ShopDecorationNode;
  const nodes = Object.keys(nodeTypes);

  return (
    <div className={styles.left_node}>
      {nodes.map((item) => (
        <Button
          type="primary"
          draggable={true}
          onDragStart={(event) => {
            ondragstart(event, item);
          }}
        >
          {item}
        </Button>
      ))}
    </div>
  );
};
复制代码

中间组件区域

/*
 * @Description: 
 * @Author: rodchen
 * @Date: 2021-06-13 14:14:46
 * @LastEditTime: 2021-06-13 18:20:11
 * @LastEditors: rodchen
 */

import React, { useState } from 'react';
import styles from './index.less';
import store from '../Utils/store';
import ShopDecorationNode from '../LeftNode/Nodes';
import { NodeClass } from '../Type/interface';
import { NodeClassType } from '../Type/type';

export default () => {
  const [tempStoreData, setTempStoreData] = useState<NodeClassType[]>([])
  const { renderNode } = ShopDecorationNode;

  const onDrop = (event: any) => {
    const data: string = event.dataTransfer.getData('Text');
    event.preventDefault();
    const newNode = new NodeClass(data)
    store.updateStoreData(store.storeData.concat([newNode]))
    store.setCurrentNode(newNode);
  };

  store.subscriptionStoreDataChange((storeData: NodeClassType[]) => {
    setTempStoreData(storeData)
  })

  const allowDrop = (ev: any) => {
    ev.preventDefault();
  };

  const onClickForHanldeProperty = (item: NodeClassType) => {
    store.setCurrentNode(item);
  };

  const getCurrentNodeContent = (key: string) => {
    const content = tempStoreData.filter(innerItem => innerItem.key === key)[0].content;
    
    try {
      return JSON.parse(content as string)
    } catch (e) {
      return ''
    }
  }
  
  return (
    <div onDrop={onDrop} onDragOver={allowDrop} className={styles.content_render}>
      {tempStoreData.map((item) => (
        <div
          key={item.key}
          className={styles.content_node}
          onClick={() => {
            onClickForHanldeProperty(item);
          }}
        >
          {renderNode(item.type, getCurrentNodeContent(item.key))}
        </div>
      ))}
    </div>
  );
};

复制代码

右侧属性渲染组件

import React, { useState } from 'react';
import store from '../Utils/store';
import ShopDecorationNode from '../LeftNode/Nodes';
import { NodeClassType } from '../Type/type';

export default () => {
  const { renderNodeProperty } = ShopDecorationNode;
  const [property, setProperty] = useState<NodeClassType>({type: '', key: ''});

  store.subscriptionNodeChange((item: any) => {
    setProperty(item);
  });
  
  return <div key={property.key}>{renderNodeProperty(property.type, {
                                    keyString: property.key, 
                                    onValuesChange: store.updateNodeContent.bind(store), 
                                    content: JSON.parse(property.content || '{}')
                                  })}
                    </div>;
};
复制代码

StoreData

import { NodeClass } from '../Type/interface';
import { NodeClassType } from '../Type/type';

class Store {
  public storeData: NodeClassType[] = [];
  public currentNode: NodeClassType = new NodeClass('');
  public subscriptionNodeArray: any[] = [];
  public subscriptionStoreDataArray: any[] = [];


  public setCurrentNode(node: NodeClassType) {
    this.currentNode = node;
    this.subscriptionNodeArray.forEach((item) => {
      item(node);
    });
  }

  public updateStoreData(storeData: NodeClassType[]) {
    this.storeData = storeData;
    this.subscriptionStoreDataArray.forEach((item) => {
      item(storeData);
    });
  }

  public updateNodeContent({key, content}: {key: string, content: Object}) {
    this.storeData = this.storeData.map(item => item.key === key ? ((item.content = JSON.stringify(content)), item) : item);
    this.subscriptionStoreDataArray.forEach((item) => {
      item(this.storeData);
    });
  }

  public subscriptionNodeChange(callback: Function) {
    this.subscriptionNodeArray.push(callback);
  }

  public subscriptionStoreDataChange(callback: Function) {
    this.subscriptionStoreDataArray.push(callback);
  }
}

const store = new Store();

export default store;

复制代码

组件注册

import React from 'react';
import ShopDecorationNode from '../LeftNode/Nodes';
import { Yihangsitu, YihangsituProperty } from './Yihangsitu';
import Yihangyitu from './Yihangyitu';

const { registerNode, registerNodeProperty } = ShopDecorationNode;

registerNode('yihangsitu', (config: any) => {
  return React.createElement((Yihangsitu as unknown) as React.FC<{}>, config);
});

registerNodeProperty('yihangsitu', (config: any) => {
  return React.createElement((YihangsituProperty as unknown) as React.FC<{}>, config);
});

registerNode('yihangyitu', (config: any) => {
  return React.createElement(Yihangyitu, config);
});
复制代码

待优化

时间问题,今天没空书写了,因为功能还不够完善,就不上传代码了。

  • 删除功能,中间内容区域上下可拖动调整位置
  • 数据交互部分,中间内容的重新渲染,我想通过HOC高阶函数做一层处理,想达到的目的是:属性的更新只会造成当前选择的组件这部分重新渲染,不是造成所有的组件都重新渲染。
  • 还可以将组件渲染区域以及属性组件区域添加一个可以由开发自己配置的wrapper区域,这样将公共展示部分暴露个开发者。
文章分类
前端