用最簡單的方式畫拓撲圖!!!

git地址css

前言

前段時間重構了下面這樣一個頁面(產品頁面不方便截圖):html

相似於拓撲圖的配置,原來是使用go.js實現的,相似的庫還有antv g6。重構主要是爲了提升代碼質量,下降維護成本,產品上須要更強的定製化能力(對付產品經理的變態需求),因此通過一番研究以後,最後決定放棄使用現成的庫。緣由以下:node

  1. 維護成本高:相似的庫(antv/g6, go.js)都是基於canvas實現,也都大同小異的定義了一套組件,有必定的學習成本,同時基於這樣的庫寫出來的代碼都相對複雜;
  2. 靈活性差:由於是canvas實現,元素通常須要指定尺寸,因此在一些須要元素大小自適應的地方並無DOM元素好實現;
  3. 定製化能力差。只能使用庫裏定義的api和事件,遇到一些比較極端的需求時無能爲力。

固然,以上兩個庫仍是至關強大的,不過基於這些緣由,本身基於(DOM + SVG)擼一個拓撲圖配置的工具庫topology-byfereact

demo演示

源代碼

import React from 'react';
import { Topology, topologyWrapper, TemplateWrapper } from 'topology-byfe';
import { ITopologyNode, ITopologyData, IWrapperOptions } from 'topology-byfe/lib/declare';
import './index.less';

interface FlowState {
    data: ITopologyData;
}
class Flow extends React.Component<{}, FlowState> {
    state: FlowState = {
        data: { lines: [], nodes: [] },
    };

    generatorNodeData = (isBig: boolean) => ({
        id: `${Date.now()}`,
        name: isBig ? '寬節點' : '窄節點',
        content: isBig ? '這是一個寬節點' : '這是一個窄節點',
        branches: isBig ? ['錨點1', '錨點2', '錨點3'] : ['錨點1'],
    });

    handleSelect = (data: ITopologyData) => {
        console.log(data);
    }

    renderTreeNode = (data: ITopologyNode, { anchorDecorator }: IWrapperOptions) => {
        const {
            name = '',
            content = '',
            branches = [],
        } = data;
        return (
            <div className="topology-node">
                <div className="node-header">{name}</div>
                <p className="node-content">{content}</p>
                {branches.length > 0 && (
                    <div className="flow-node-branches-wrapper">
                        {branches.map(
                            (item: string, index: number) => anchorDecorator({
                                anchorId: `${index}`,
                            })(<div className="flow-node-branch">{item}</div>),
                        )}
                    </div>
                )}
            </div>
        );
    };

    onChange = (data: ITopologyData, type: string) => {
        this.setState({ data });
        console.log('change type:', type);
    };

    render() {
        const { data } = this.state;
        return (
            <div className="topology">
                <div className="topology-templates">
                    <TemplateWrapper generator={() => this.generatorNodeData(true)}>
                        <div className="topology-templates-item">寬節點</div>
                    </TemplateWrapper>
                    <TemplateWrapper generator={() => this.generatorNodeData(false)}>
                        <div className="topology-templates-item">窄節點</div>
                    </TemplateWrapper>
                </div>
                <div style={{ width: '100%', height: 800 }}>
                    <Topology
                        data={data}
                        autoLayout
                        onChange={this.onChange}
                        onSelect={this.handleSelect}
                        renderTreeNode={this.renderTreeNode}
                    />
                </div>
            </div>
        );
    }
}

export default topologyWrapper(Flow);

複製代碼

效果圖

能夠看到,包裏只提供了極少的api,文檔幾分鐘就能看完,之因此少,是由於庫只負責將節點放到正確的位置,連上線就行了,其餘的「概不負責」,修改樣式能夠在你的div上加個class,添加事件就再上個onXXX,想作啥作啥。git

爲何選擇DOM + SVG?

最主要的緣由就是爲了「簡單」!對於拓撲圖這樣 的場景,畫一個簡單的節點,用DOM實現只須要簡單的幾行代碼,用canvas的話就要寫一大堆了,別人來看估計就是「一大坨」了。同時相對於須要先學習一套組件,而後用那些「奇奇怪怪」的api去寫交互和樣式,直接上手就開始擼本身最熟悉的div + css豈不是很開心?對於開發而言,調試DOM可以看到每個元素的細節,而canvas就無能爲力了。github

如何使用

核心部分代碼:npm

<Topology
    data={data}
    autoLayout
    onChange={this.onChange}
    onSelect={this.handleSelect}
    renderTreeNode={this.renderTreeNode}
 />
複製代碼

data

data = {
    nodes: [
        { id: '1', position: { x: 0, y: 0 } },
        { id: '2', position: { x: 100, y: 100 } }
    ],
    lines: [
        { start: '1-0', end: '2' }
    ]
}
複製代碼

data.nodes

data包含兩個屬性:nodes和lines,nodes記錄節點信息,每一個node含有id和position屬性,id是必選的,position記錄了節點的位置,若是不包含position,點擊自動佈局,將會自動生成。canvas

data.lines

lines記錄節點與節點間的關係,start記錄起點信息,格式爲:'起點id-錨點id',end記錄終點信息,格式爲:'終點id'。api

autoLayout

上面的效果圖能夠看到右下角第二個圖標點擊自動佈局功能,爲了方便排版,自動佈局會根據樹結構計算節點的位置。當初始數據沒有position字段時,若是autoLayout爲true,組件會自動觸發佈局功能,至關於點擊了自動佈局按鈕。bash

onChange

組件使用相似input或者select,當有新增節點或者連線發生時,觸發onChange,onChange帶有兩個參數,newData和changeType, 整個過程徹底受控,你能夠在onChange中作一些校驗,決定數據是否更新。

onSelect

當選擇節點或者線段時觸發,參數selectData格式同data。

renderTreeNode

renderTreeNode接收兩個參數:nodeData, decorators,返回節點的DOM。

renderTreeNode = (data: ITopologyNode, { anchorDecorator }) => {
        // name、content、branches都是自定義的字段,經過模板節點生成,詳見TemplateWrapper
        const {
            name = '',
            content = '',
            branches = [],
        } = data;
        return (
            <div className="topology-node">
                <div className="node-header">{name}</div>
                <p className="node-content">{content}</p>
                {branches.length > 0 && (
                    <div className="flow-node-branches-wrapper">
                        {branches.map(
                            (item: string, index: number) => anchorDecorator({
                                anchorId: `${index}`,
                            })(<div className="flow-node-branch">{item}</div>),
                        )}
                    </div>
                )}
            </div>
        );
    };
複製代碼

錨點,decorators.anchorDecorator

anchorDecorator({ anchorId: `${index}` })(
    <div className="flow-node-branch">
        {item}
    </div>
)
複製代碼

anchorDecorator是一個裝飾器函數,接受一個options,目前只包含一個anchorId屬性,即錨點id,若是不傳的話,內部會自動生成一個自增id。能夠看到,錨點長什麼樣,放到哪兒徹底由你本身決定。

templateWrapper

<div className="topology-templates">
        <TemplateWrapper generator={() => this.generatorNodeData(true)}>
            <div className="topology-templates-item">寬節點</div>
        </TemplateWrapper>
        <TemplateWrapper generator={() => this.generatorNodeData(false)}>
            <div className="topology-templates-item">窄節點</div>
        </TemplateWrapper>
</div>
複製代碼

經過templateWrapper包裝生成一個模板節點,接收一個generator函數,當添加節點時,會調用這個函數,生成節點的初始數據,裏面包含什麼值由你決定,但必須包含一個惟一的id值。

topologyWrapper

export default topologyWrapper(Flow);
複製代碼

包含拖拽部分的最上層組件必需要用topologyWrapper包一下,這是由於使用了react-dnd須要設置backend,這裏只是作了一個簡單的導出,方便使用:

export const topologyWrapper = DragDropContext(HTML5BackEnd);
複製代碼

總結

從需求出發來看,須要擁有更好的定製化能力和靈活性,從重構的角度來看,須要讓代碼更簡單明瞭。go.js功能十分強大,但對於拓撲圖這種相對較簡單的場景而言,並不須要那麼多複雜的能力,反而可能出現上面說的問題,全部canvas的實現應該都會有相似的問題。因此充分利用DOM的能力,在能實現需求的狀況下,本庫或許是更好的選擇。

最後

項目只實現了簡單的功能,存在不足歡迎大佬們提issue,pr。另外我司招人,歡迎大佬們加入:招聘連接!!!

相關文章
相關標籤/搜索