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區域,這樣將公共展現部分暴露個開發者。
相關文章
相關標籤/搜索