This is the 13th day of my participation in Gwen Challenge

Precursors:

  • What you need to know before you understand the front-end design pattern
  • JS design pattern – Observer pattern versus publish/subscribe pattern

background

The company has done a related project in the past, shop decoration. At that time, the design was relatively simple. As the project got bigger and bigger, the redundancy of the upper construction and the bottom code was relatively high, and it was difficult to maintain. There was no clear dividing line between each other. Therefore, at that time, I wanted to redesign a shop decoration, which could divide the boundary clearly and maintain a relatively simple model. Let’s take a look at the big blueprints.

Look at the effect

Business Registration Entry

The entry point for business components is unified here, independent of the system framework, to separate the two. New business components only need to be registered in this way.

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);
});
Copy the code



Function display







Design pattern principles

Dependency inversion principle

This design principle states that high-level strategic code should not rely on code that implements low-level details; rather, code that implements low-level details should rely on high-level strategic code.

The most important problem with the dependency inversion principle is to ensure that the major components of an application or framework are decoupled from the non-essential low-level component implementation details. This will ensure that the most important parts of the application are not affected by low-level component changes.



The whole store decoration is divided into two parts: frame layer and business component layer. There is a division of labor and data interaction.



Single responsibility principle

This part is actually based on fixed product design. When designing components, each component has its own fixed function, and the data interaction between each other can be weakly correlated by related design patterns.

The open closed principle

Here the open closed principle applies, the addition and modification of business components are all controlled by the user (handled by the developer himself, no changes to the framework layer are involved).

  • Open: Extend [Business component additions and Modifications]
  • Close: Modify [frame layer changes]



Omega substitution principle

The rule I apply here is that all business components need to be developed according to established rules and need to interact with data at the framework level through specific methods.



Design ideas

User behavior analysis

The corresponding user operations for store decoration are basically as follows:





Development Angle analysis

For developers, as more and more components are built, more and more developers want to only worry about the corresponding components, don’t let me do too much.





Architecture Design analysis

First of all, the functionality must satisfy user behavior, and then the needs of developers. Of course, in terms of design, we also want to make the maintenance and expansion of the framework layer as simple as possible, and separate the design of the framework layer from that of the business layer.

What’s the key point here? Reference and loading of specific business components, the list of components on the left needs to be brought in. Component rendering requires the corresponding component to be loaded. Component properties also need to import corresponding component properties files and load them dynamically.

The key here is to introduce the loading dynamics of components. The introduction of business components is no longer introduced through the important method.





Design specification



This is a bit of a hassle to explain. So let’s introduce the monomer, introduce the self function, the interaction function with the outside world.

On the left side of the component

Data section

NodeRegistry: class

  • NodeTypes: Internal private property that stores how many shop-fitting components are registered with the current system
  • NodePropertyTypes: Internal private property that stores the property component corresponding to the shop component.
  • RegisterNode: Registers a component
  • RenderNode: Render component
  • RegisterNodeProperty: Registers the property component
  • RenderNodeProperty: Render property component

Page section

The page iterates through the components registered within the current nodeTypes and displays the list of shop decoration components on the left.

Behavior & related to other components

Business code writing

The developer invokes the register** method to register the component.

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);
});
Copy the code

Middle page render area

RenderNode components are called for rendering components. The corresponding attribute data is also passed in.

 {renderNode(item.type, getCurrentNodeContent(item.key))}
Copy the code

RenderNode method

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

Properties render area on the right

The renderNodeProperty method is called when the right property component is rendering. The three properties, the key, and the method to update the property are used for the component written by the developer to communicate with the system. The content is the current latest property value.

<div key={property.key}>{renderNodeProperty(property.type, {
                                    keyString: property.key, 
                                    onValuesChange: store.updateNodeContent.bind(store), 
                                    content: JSON.parse(property.content || '{}')
                                  })}
                    </div>;
Copy the code

renderNodeProperty

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



Intermediate component rendering

Data section

TempStoreData: Used to update data, to re-render pages, but my idea here needs to be optimized. Updates of page data are processed through HOC.

Page section

We loop through tempStoreData and call the renderNode method to render the component. Gets property data for the current component and synchronously passes it in.

Behavior & related to other components

  • Call renderNode to render the page
  • Subscribe to StoreData for data updates to re-render pages
  • Call StoreData’s setCurrentNode method to set the node to be displayed on the right side of the property.



Render component properties on the right

Data section

The private property is used to update the properties page on the right.

Page section

Gets the type and key of the current component to be displayed, and gets the latest property value synchronized in. Call the renderProperty method

<div key={property.key}>{renderNodeProperty(property.type, {
                                    keyString: property.key, 
                                    onValuesChange: store.updateNodeContent.bind(store), 
                                    content: JSON.parse(property.content || '{}')
                                  })}
                    </div>;
Copy the code

Behavior & related to other components

  • Subscribe to updates of the currently selected node in the middle area of storeData



StoreData Data interaction

Data section

  • StoreData: Data used for Data interaction between components in the entire system
  • CurrentNode: The currently selected component node in the middle area
  • SubscriptionNodeArray: An array of events subscribed to currentNode changes
  • SubscriptionStoreDataArray: change subscription storeData data array
  • SetCurrentNode: Sets the currently selected component node in the middle region
  • UpdateStoreData: Updates SotrData
  • UpdateNodeContent: Updates currentNode data
  • SubscriptionNodeChange: Method to add currentNode change subscriptions
  • SubscriptionStoreDataChange: add storeData change subscription method

Behavior & related to other components

  • Drag the left component to the middle area: call updateStoreData
  • The intermediate content component subscribes to storeData updates
  • Intermediate content component, toggle to select component properties to edit: call setCurrentNode method
  • The component properties on the right subscribe to currentNode updates
  • The property component update on the right invokes the updateNodeContent method



Substitution in rules – business component development rules

Registration way

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);
});
Copy the code

Logical processing of business components themselves

  • If a component has a name attribute, the developer needs to obtain the name attribute of props to display the corresponding attribute on the Node node.
  • An update to a property component requires a call to the props onValuesChange method to update all data of the current component.

Code implementation

I’m still using Dumi to create projects.

yarn create @umijs/dumi-lib --site
Copy the code



The code structure





The core code

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;
Copy the code

On the left side of the component

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>
  );
};
Copy the code

Intermediate component area

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

Copy the code

Right side property render component

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>;
};
Copy the code

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;

Copy the code

The component registration

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);
});
Copy the code

To optimize

Due to time problem, I have no time to write today, because the function is not perfect enough, I will not upload the code.

  • Delete function, middle content area can be dragged up and down to adjust the position
  • As for the data interaction part and the re-rendering of the intermediate content, I want to do a layer of processing through the HOC higher-order function to achieve the purpose that the update of attributes will only re-render the part of the currently selected component, not all components.
  • You can also add a Wrapper area that can be configured by the developer itself to the component rendering area and the property component area, exposing the public presentation part to the developer.