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

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

前置文章:前端

背景

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

先看一下大的设计图java

设计思路.png

先看效果

业务注册入口

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

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


设计模式原则

依赖反转原则

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

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

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


单一职责原则

这部分实际上是根据固定的产品设计而来的。设计组件的时候,每一个组件具备本身固定的功能,彼此之间的数据交互能够经过相关的设计模式去进行弱关联。
数组

image.png

开闭原则

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

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


里式替换原则

这里个人应用规则是,全部的业务组件须要按照既定的规则来开发,须要经过的特定的方法进行和框架层次的数据交互。
antd


设计思路

用户行为分析

店铺装修对应的用户操做基本以下:

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区域,这样将公共展现部分暴露个开发者。
相关文章
相关标签/搜索