07 拓展能力 - 深入分析,流程图库的架构设计,该从哪些方面持续增强

253 阅读14分钟

引言

在前两篇中,我们构建了一个最简易的流程图库模型,并逐步拆解和实现了其核心能力,包括数据视图模型分离、节点高效管理、动态连线管理、统一事件中心和优化图形渲染。尽管这些基础功能已经具备了基本的流程图绘制和交互能力,但要将流程图库发展成为一个功能完善、用户友好的工具,仍需在多个方面进行拓展和优化。本章将深入分析如何在已有基础上,进一步增强流程图库的能力,使其具备更强的扩展性和实用性。

目标

在已有最简模型和核心功能的基础上,逐步完善流程图库的核心能力,提升其实用性和灵活性。具体目标包括:

  • 设计和实现插件系统,支持用户自定义功能扩展。
  • 增加配置选项,允许用户自定义画布、节点和连线的样式与行为。
  • 进行性能优化,确保库在处理大量节点和复杂交互时依然高效。
  • 拓展多端支持,兼容移动端设备,增加手势操作等功能。
  • 设计清晰的架构层次,采用合适的设计模式,提升代码的可维护性和扩展性。
  • 提供丰富的API接口,便于与其他系统集成或进行二次开发。

拓展方向

为了实现上述目标,我们将流程图库的拓展能力拆解为以下几个方向:

  1. 插件系统
  2. 可配置性
  3. 性能优化
  4. 多端支持

实现解析

1. 插件系统

设计思路

插件系统的设计旨在为流程图库提供可扩展的机制,允许用户在不修改核心代码的情况下,添加自定义功能。通过插件,用户可以轻松集成导入导出功能、自动布局算法、模板支持等扩展功能。

实现步骤

1. 定义插件接口

插件需要遵循一定的接口规范,以确保与流程图库的兼容性。我们定义一个基本的插件接口,包含初始化和销毁方法。

// Plugin Interface
class Plugin {
  constructor(flowChartInstance) {
    this.flowChart = flowChartInstance;
  }

  init() {
    // 初始化插件
  }

  destroy() {
    // 清理插件
  }
}

2. 插件管理

在FlowChart类中添加插件管理功能,允许加载、卸载插件。

class FlowChart extends EventEmitter {
  constructor(container) {
    super();
    this.canvas = new Canvas(container, 800, 600);
    this.nodes = [];
    this.edges = [];
    this.plugins = [];
    // ...其他初始化代码
  }

  loadPlugin(pluginClass) {
    const plugin = new pluginClass(this);
    plugin.init();
    this.plugins.push(plugin);
  }

  unloadPlugin(pluginClass) {
    const pluginIndex = this.plugins.findIndex(p => p instanceof pluginClass);
    if (pluginIndex !== -1) {
      this.plugins[pluginIndex].destroy();
      this.plugins.splice(pluginIndex, 1);
    }
  }

  // ...其他方法
}

3. 示例插件:导出功能

下面是一个示例插件,实现将流程图导出为PNG图像的功能。

// ExportPlugin.js
class ExportPlugin extends Plugin {
  init() {
    // 创建导出按钮
    this.exportButton = document.createElement('button');
    this.exportButton.textContent = '导出PNG';
    this.exportButton.style.position = 'absolute';
    this.exportButton.style.top = '10px';
    this.exportButton.style.right = '10px';
    this.exportButton.style.padding = '8px 12px';
    this.exportButton.style.backgroundColor = '#007BFF';
    this.exportButton.style.color = '#fff';
    this.exportButton.style.border = 'none';
    this.exportButton.style.borderRadius = '4px';
    this.exportButton.style.cursor = 'pointer';
    this.exportButton.style.zIndex = '1000';
    this.exportButton.addEventListener('click', () => this.exportAsPNG());

    // 添加到画布容器
    this.flowChart.canvas.element.appendChild(this.exportButton);
  }

  exportAsPNG() {
    const svg = this.flowChart.canvas.svg;
    const serializer = new XMLSerializer();
    const source = serializer.serializeToString(svg);

    const svgBlob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
    const url = URL.createObjectURL(svgBlob);
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      canvas.width = svg.width.baseVal.value;
      canvas.height = svg.height.baseVal.value;
      const context = canvas.getContext('2d');
      context.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);
      const imgURL = canvas.toDataURL('image/png');

      const downloadLink = document.createElement('a');
      downloadLink.href = imgURL;
      downloadLink.download = 'flowchart.png';
      document.body.appendChild(downloadLink);
      downloadLink.click();
      document.body.removeChild(downloadLink);
    };
    img.src = url;
  }

  destroy() {
    // 移除导出按钮
    if (this.exportButton) {
      this.flowChart.canvas.element.removeChild(this.exportButton);
    }
  }
}

4. 加载插件

在使用流程图库时,用户可以通过调用loadPlugin方法加载所需的插件。

// 使用流程图库并加载导出插件
const flowChart = new FlowChart(document.getElementById('canvas'));

// 加载导出插件
flowChart.loadPlugin(ExportPlugin);

// 添加节点和连线
const node1 = flowChart.addNode('node1', 100, 100, '开始', 'start');
const node2 = flowChart.addNode('node2', 300, 100, '步骤1', 'default');
const node3 = flowChart.addNode('node3', 500, 100, '结束', 'end');

flowChart.addEdge('edge1', node1, node2, 'default');
flowChart.addEdge('edge2', node2, node3, 'dashed');

flowChart.render();

效果展示

加载导出插件后,画布右上角将出现“导出PNG”按钮,用户点击该按钮即可将当前流程图导出为PNG图像。

2. 可配置性

设计思路

为了满足不同用户的需求,流程图库需要具备高度的可配置性。通过增加配置选项,用户可以自定义画布、节点和连线的样式与行为,从而实现个性化的流程图展示。

实现步骤

1. 配置选项定义

在FlowChart类的构造函数中,接受一个配置对象,用于定义各种样式和行为。

class FlowChart extends EventEmitter {
  constructor(container, config = {}) {
    super();
    this.canvas = new Canvas(container, config.canvasWidth || 800, config.canvasHeight || 600);
    this.nodes = [];
    this.edges = [];
    this.plugins = [];
    this.selectedNode = null;
    this.connecting = false;
    this.config = config;

    // 绑定画布点击事件,用于取消选择
    this.canvas.element.addEventListener('click', () => {
      this.selectedNode = null;
      this.connecting = false;
      this.nodes.forEach(node => node.group.classList.remove('selected'));
      this.emit('canvasClick');
    });
  }

  // ...其他方法
}

2. 节点样式自定义

在 Node 类中,应用来自配置的样式。

class Node {
  constructor(id, x, y, label, type = 'default', style = {}, config = {}) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.label = label;
    this.type = type;
    this.style = { ...config.defaultNodeStyle, ...style };
    this.group = null; // SVG群组元素
  }

  render(svgContainer) {
    this.group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    this.group.setAttribute('class', `node ${this.type}`);
    this.group.setAttribute('transform', `translate(${this.x}, ${this.y})`);
    svgContainer.appendChild(this.group);

    // 根据节点类型创建不同形状
    let shape;
    switch (this.type) {
      case 'start':
      case 'end':
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        shape.setAttribute('r', this.style.radius || '50');
        shape.setAttribute('fill', this.style.color || '#2196F3');
        break;
      case 'decision':
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
        shape.setAttribute('points', '50,0 100,50 50,100 0,50');
        shape.setAttribute('fill', this.style.color || '#FFC107');
        break;
      default:
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        shape.setAttribute('width', this.style.width || '120');
        shape.setAttribute('height', this.style.height || '50');
        shape.setAttribute('rx', this.style.rx || '5');
        shape.setAttribute('ry', this.style.ry || '5');
        shape.setAttribute('fill', this.style.color || '#4CAF50');
    }

    this.group.appendChild(shape);

    // 添加文本
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    text.setAttribute('x', this.style.textX || '60');
    text.setAttribute('y', this.style.textY || '30');
    text.setAttribute('text-anchor', 'middle');
    text.setAttribute('dominant-baseline', 'middle');
    text.setAttribute('fill', this.style.textColor || '#fff');
    text.textContent = this.label;
    this.group.appendChild(text);

    // 添加交互事件
    this.initEvents();
  }

  // ...其他方法
}

3. 连线样式自定义

在 Edge 类中,应用来自配置的样式

class Edge {
  constructor(id, sourceNode, targetNode, type = 'default', style = {}, config = {}) {
    this.id = id;
    this.source = sourceNode;
    this.target = targetNode;
    this.type = type;
    this.style = { ...config.defaultEdgeStyle, ...style };
    this.element = null; // SVG路径元素引用
  }

  render(svgContainer) {
    this.element = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    this.element.classList.add(`edge ${this.type}`);
    this.element.setAttribute('stroke', this.style.stroke || '#000');
    this.element.setAttribute('stroke-width', this.style.strokeWidth || (this.type === 'bold' ? '4' : '2'));
    this.element.setAttribute('fill', 'none');
    if (this.style.strokeDasharray) {
      this.element.setAttribute('stroke-dasharray', this.style.strokeDasharray);
    }
    this.updatePosition();
    svgContainer.appendChild(this.element);
  }

  // ...其他方法
}

4. 配置示例

用户可以在初始化流程图库时,通过配置对象自定义样式和行为。

const config = {
  canvasWidth: 1000,
  canvasHeight: 800,
  defaultNodeStyle: {
    color: '#4CAF50',
    width: '150',
    height: '60',
    rx: '10',
    ry: '10',
    textX: '75',
    textY: '30',
    textColor: '#fff'
  },
  defaultEdgeStyle: {
    stroke: '#000',
    strokeWidth: '2',
    strokeDasharray: '0'
  }
};

const flowChart = new FlowChart(document.getElementById('canvas'), config);

// 添加节点和连线
const node1 = flowChart.addNode('node1', 100, 100, '开始', 'start', { radius: '40', color: '#2196F3' });
const node2 = flowChart.addNode('node2', 300, 100, '步骤1', 'default');
const node3 = flowChart.addNode('node3', 500, 100, '结束', 'end', { radius: '40', color: '#f44336' });

flowChart.addEdge('edge1', node1, node2, 'default');
flowChart.addEdge('edge2', node2, node3, 'dashed');
flowChart.render();

效果描述

通过配置选项,用户可以灵活地自定义节点和连线的样式,实现个性化的流程图展示。

3. 性能优化

设计思路

随着流程图库功能的增强,尤其是在处理大量节点和复杂交互时,性能问题变得尤为重要。为确保流程图库在各种场景下均能保持高效运行,我们需要从以下几个方面进行性能优化:

  1. 虚拟滚动
  2. 大量节点的渲染优化
  3. 减少重绘和重排

实现步骤

1. 虚拟滚动

虚拟滚动是一种优化技术,旨在仅渲染视图中可见的节点和连线,减少不必要的DOM操作。对于大型流程图,这可以显著提升渲染性能。

class Canvas {
  constructor(element, width, height) {
    this.element = element;
    this.width = width;
    this.height = height;
    this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    this.svg.setAttribute('width', width);
    this.svg.setAttribute('height', height);
    this.svg.style.position = 'absolute';
    this.svg.style.top = '0';
    this.svg.style.left = '0';
    this.svg.style.pointerEvents = 'none'; // 让SVG不阻挡下方元素的鼠标事件
    this.element.appendChild(this.svg);
  }

  draw() {
    // 可添加画布的基础渲染逻辑,如背景网格
  }

  addEdge(edgeElement) {
    this.svg.appendChild(edgeElement);
  }

  removeEdge(edgeElement) {
    this.svg.removeChild(edgeElement);
  }

  // 虚拟滚动相关方法
  // 这里只是示例,具体实现需要根据项目需求进行调整
}

注意:虚拟滚动的具体实现较为复杂,需要根据流程图库的具体需求和布局方式进行优化。可以考虑引入现有的虚拟化库,如react-virtualized,或自行实现基于可视区域的节点渲染逻辑。

2. 大量节点的渲染优化

批量DOM操作:尽量减少DOM的频繁插入和修改,采用批量操作提升渲染效率。

class FlowChart extends EventEmitter {
  // ...之前的代码

  renderBatch(nodesToRender, edgesToRender) {
    const fragment = document.createDocumentFragment();

    nodesToRender.forEach(node => {
      node.render(this.canvas.svg);
      fragment.appendChild(node.group);
    });

    edgesToRender.forEach(edge => {
      edge.render(this.canvas.svg);
      fragment.appendChild(edge.element);
    });

    this.canvas.svg.appendChild(fragment);
  }
}

使用SVG Symbols:对于重复的图形元素,使用SVG的<symbol><use>标签,减少DOM节点数量。

// 定义符号
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const rectSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol');
rectSymbol.setAttribute('id', 'rectSymbol');
rectSymbol.setAttribute('viewBox', '0 0 120 50');
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '120');
rect.setAttribute('height', '50');
rect.setAttribute('rx', '5');
rect.setAttribute('ry', '5');
rect.setAttribute('fill', '#4CAF50');
rectSymbol.appendChild(rect);
defs.appendChild(rectSymbol);
this.canvas.svg.appendChild(defs);

// 使用符号
class Node {
  render(svgContainer) {
    this.group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    this.group.setAttribute('class', `node ${this.type}`);
    this.group.setAttribute('transform', `translate(${this.x}, ${this.y})`);
    svgContainer.appendChild(this.group);

    const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
    use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#rectSymbol');
    use.setAttribute('fill', this.style.color || '#4CAF50');
    this.group.appendChild(use);

    // 添加文本和事件绑定
    // ...
  }

  // ...其他方法
}

  1. 减少重绘和重排

主要是通过以下的几个途径实现:

避免频繁修改样式:尽量减少对 DOM 元素样式的频繁修改,合并样式变更操作。

使用 CSS 类:通过添加或移除 CSS 类来批量应用样式,而不是逐个设置样式属性。

使用 requestAnimationFrame:将动画和频繁的 DOM 更新操作封装在requestAnimationFrame 中,确保浏览器优化渲染过程。

class Node {
  // ...之前的代码

  updatePosition(x, y) {
    this.x = x;
    this.y = y;
    // 使用 requestAnimationFrame 优化位置更新
    window.requestAnimationFrame(() => {
      this.group.setAttribute('transform', `translate(${x}, ${y})`);
    });
  }

  // ...其他方法
}

性能优化的效果

通过上述优化措施,流程图库在处理大量节点和复杂交互时,能够显著提升渲染性能,确保用户体验流程。

 

4. 多端支持

设计思路

随着移动设备的普及,支持多端(桌面和移动端)已成为现代应用的重要需求。为使流程图库兼容移动端,我们需要:

  • 响应式设计:确保流程图库在不同屏幕尺寸下均能良好展示和操作。
  • 手势操作:支持触摸手势,如拖拽、缩放等。
  • 优化触控目标:增大交互元素的触控区域,提升移动端的使用体验。

实现步骤

1. 响应式设计

通过CSS媒体查询和灵活的布局,确保流程图库在不同设备和屏幕尺寸下均能良好展示。

/* 流程图库容器 */
#canvas {
  width: 100%;
  height: 100vh;
  border: 1px solid #000;
  position: relative;
  background-color: #f9f9f9;
}

/* 节点样式调整 */
@media (max-width: 600px) {
  .node.start,
  .node.end {
    width: 80px;
    height: 80px;
  }

  .node.default {
    width: 100px;
    height: 40px;
  }

  .edge {
    stroke-width: 1.5;
  }
}

2. 手势操作

为流程图库添加触摸事件监听,支持触摸拖拽和缩放等手势操作。

class FlowChart extends EventEmitter {
  constructor(container, config = {}) {
    super();
    this.canvas = new Canvas(container, config.canvasWidth || 800, config.canvasHeight || 600);
    this.nodes = [];
    this.edges = [];
    this.plugins = [];
    this.selectedNode = null;
    this.connecting = false;
    this.config = config;

    // 缩放参数
    this.scale = 1;
    this.minScale = 0.5;
    this.maxScale = 2;

    // 绑定画布点击事件,用于取消选择
    this.canvas.element.addEventListener('click', () => {
      this.selectedNode = null;
      this.connecting = false;
      this.nodes.forEach(node => node.group.classList.remove('selected'));
      this.emit('canvasClick');
    });

    // 绑定触摸事件
    this.initTouchEvents();
  }

  initTouchEvents() {
    let lastTouchEnd = 0;
    let initialPinchDistance = null;
    let initialScale = this.scale;

    this.canvas.element.addEventListener('touchstart', (e) => {
      if (e.touches.length === 2) {
        initialPinchDistance = this.getPinchDistance(e.touches[0], e.touches[1]);
        initialScale = this.scale;
      }
    });

    this.canvas.element.addEventListener('touchmove', (e) => {
      if (e.touches.length === 2 && initialPinchDistance) {
        const currentDistance = this.getPinchDistance(e.touches[0], e.touches[1]);
        const scaleChange = currentDistance / initialPinchDistance;
        this.scale = Math.min(this.maxScale, Math.max(this.minScale, initialScale * scaleChange));
        this.canvas.svg.setAttribute('transform', `scale(${this.scale})`);
      }
    });

    this.canvas.element.addEventListener('touchend', (e) => {
      if (e.touches.length < 2) {
        initialPinchDistance = null;
      }
    });
  }

  getPinchDistance(touch1, touch2) {
    const dx = touch2.clientX - touch1.clientX;
    const dy = touch2.clientY - touch1.clientY;
    return Math.sqrt(dx * dx + dy * dy);
  }

  // ...其他方法
}

3. 优化触控目标

增大节点和连线的触控区域,确保在移动端的操作便捷性。

/* 增大节点的点击区域 */
.node {
  cursor: grab;
}

.node:active {
  cursor: grabbing;
}

/* 增大连线的点击区域 */
.edge {
  pointer-events: stroke;
}

多端支持效果

通过响应式设计和手势操作,流程图库能够在桌面和移动端设备上均提供良好的使用体验。用户可以在移动设备上通过触摸手势进行节点拖拽和画布缩放,实现便捷的流程图操作。

5. 架构设计

设计思路

为了确保流程图库具备良好的可维护性和扩展性,需设计清晰的架构层次,并引入合适的设计模式如 MCV(模型-视图-控制器)或 MVVM(模型-视图-视图模型)能够有效的分离数据、视图和逻辑,提高代码的可读性和可复用性。

实现步骤

1. 架构层次划分

  • 数据层(Model): 负责数据的管理和操作,包括节点和连线的数据结构
  • 视图层(View): 负责图形的渲染和现实,包括节点和连线的 SVG 元素
  • 控制层(Controller): 负责处理用户交互和业务逻辑,如节点拖拽、连线创建等
FlowChart
├── Model
│   ├── Node
│   └── Edge
├── View
│   ├── Canvas
│   ├── NodeView
│   └── EdgeView
└── Controller
    ├── EventHandler
    └── PluginManager

2. 引入 MVC 设计模式

通过 MVC 模式,将数据管理、视图渲染和用户交互逻辑分离,提升代码的结构化和可维护性。

// Model - NodeModel.js
class NodeModel {
  constructor(id, x, y, label, type = 'default', style = {}) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.label = label;
    this.type = type;
    this.style = style;
  }
}

// Model - EdgeModel.js
class EdgeModel {
  constructor(id, sourceId, targetId, type = 'default', style = {}) {
    this.id = id;
    this.sourceId = sourceId;
    this.targetId = targetId;
    this.type = type;
    this.style = style;
  }
}

// View - NodeView.js
class NodeView {
  constructor(model, svgContainer) {
    this.model = model;
    this.svgContainer = svgContainer;
    this.group = null;
    this.initView();
  }

  initView() {
    this.group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    this.group.setAttribute('class', `node ${this.model.type}`);
    this.group.setAttribute('transform', `translate(${this.model.x}, ${this.model.y})`);
    this.svgContainer.appendChild(this.group);

    // 根据节点类型创建不同形状
    let shape;
    switch (this.model.type) {
      case 'start':
      case 'end':
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        shape.setAttribute('r', this.model.style.radius || '50');
        shape.setAttribute('fill', this.model.style.color || '#2196F3');
        break;
      case 'decision':
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
        shape.setAttribute('points', '50,0 100,50 50,100 0,50');
        shape.setAttribute('fill', this.model.style.color || '#FFC107');
        break;
      default:
        shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        shape.setAttribute('width', this.model.style.width || '120');
        shape.setAttribute('height', this.model.style.height || '50');
        shape.setAttribute('rx', this.model.style.rx || '5');
        shape.setAttribute('ry', this.model.style.ry || '5');
        shape.setAttribute('fill', this.model.style.color || '#4CAF50');
    }

    this.group.appendChild(shape);

    // 添加文本
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    text.setAttribute('x', this.model.style.textX || '60');
    text.setAttribute('y', this.model.style.textY || '30');
    text.setAttribute('text-anchor', 'middle');
    text.setAttribute('dominant-baseline', 'middle');
    text.setAttribute('fill', this.model.style.textColor || '#fff');
    text.textContent = this.model.label;
    this.group.appendChild(text);
  }

  updateView() {
    this.group.setAttribute('transform', `translate(${this.model.x}, ${this.model.y})`);
    const text = this.group.querySelector('text');
    if (text) {
      text.textContent = this.model.label;
    }
    const shape = this.group.children[0];
    if (shape) {
      shape.setAttribute('fill', this.model.style.color || '#4CAF50');
    }
  }

  removeView() {
    this.svgContainer.removeChild(this.group);
  }
}

// View - EdgeView.js
class EdgeView {
  constructor(model, svgContainer) {
    this.model = model;
    this.svgContainer = svgContainer;
    this.path = null;
    this.initView();
  }

  initView() {
    this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    this.path.setAttribute('class', `edge ${this.model.type}`);
    this.path.setAttribute('stroke', this.model.style.stroke || '#000');
    this.path.setAttribute('stroke-width', this.model.style.strokeWidth || (this.model.type === 'bold' ? '4' : '2'));
    this.path.setAttribute('fill', 'none');
    if (this.model.style.strokeDasharray) {
      this.path.setAttribute('stroke-dasharray', this.model.style.strokeDasharray);
    }
    this.updateView();
    this.svgContainer.appendChild(this.path);
  }

  updateView(nodesMap) {
    const sourceNode = nodesMap.get(this.model.sourceId);
    const targetNode = nodesMap.get(this.model.targetId);
    if (sourceNode && targetNode) {
      const x1 = sourceNode.model.x;
      const y1 = sourceNode.model.y;
      const x2 = targetNode.model.x;
      const y2 = targetNode.model.y;

      // 计算控制点,实现贝塞尔曲线
      const ctrlX = x1 + (x2 - x1) / 2;
      const ctrlY = y1 - 50; // 控制曲线的弯曲程度

      const pathData = `M${x1},${y1} Q${ctrlX},${ctrlY} ${x2},${y2}`;
      this.path.setAttribute('d', pathData);
    }
  }

  removeView() {
    this.svgContainer.removeChild(this.path);
  }
}

// Controller - FlowChartController.js
class FlowChartController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.initController();
  }

  initController() {
    // 绑定模型与视图的同步
    this.model.nodes.forEach(nodeModel => {
      const nodeView = new NodeView(nodeModel, this.view.canvas.svg);
      this.view.nodesMap.set(nodeModel.id, nodeView);
    });

    this.model.edges.forEach(edgeModel => {
      const edgeView = new EdgeView(edgeModel, this.view.canvas.svg);
      this.view.edgesMap.set(edgeModel.id, edgeView);
    });

    // 监听模型变化
    this.model.on('nodeAdded', (nodeModel) => {
      const nodeView = new NodeView(nodeModel, this.view.canvas.svg);
      this.view.nodesMap.set(nodeModel.id, nodeView);
    });

    this.model.on('nodeRemoved', (nodeId) => {
      const nodeView = this.view.nodesMap.get(nodeId);
      if (nodeView) {
        nodeView.removeView();
        this.view.nodesMap.delete(nodeId);
      }
    });

    this.model.on('edgeAdded', (edgeModel) => {
      const edgeView = new EdgeView(edgeModel, this.view.canvas.svg);
      this.view.edgesMap.set(edgeModel.id, edgeView);
    });

    this.model.on('edgeRemoved', (edgeId) => {
      const edgeView = this.view.edgesMap.get(edgeId);
      if (edgeView) {
        edgeView.removeView();
        this.view.edgesMap.delete(edgeId);
      }
    });

    // ...其他控制逻辑
  }

  // ...其他控制方法
}

// FlowChart 类综合架构
class FlowChart extends EventEmitter {
  constructor(container, config = {}) {
    super();
    this.model = new FlowChartModel();
    this.view = new FlowChartView(container, config);
    this.controller = new FlowChartController(this.model, this.view);
    // ...其他初始化代码
  }

  // ...方法调用模型与视图的交互
}

3. 引入设计模式

采用 MVC (模型-视图-控制器)设计模式,将数据管理、视图渲染那和用户交互逻辑分离,提升代码的结构化和可维护性。

模型层(Model)

负责数据的存储和管理,如节点和连线的信息。

// FlowChartModel.js
class FlowChartModel extends EventEmitter {
  constructor() {
    super();
    this.nodes = [];
    this.edges = [];
  }

  addNode(nodeModel) {
    this.nodes.push(nodeModel);
    this.emit('nodeAdded', nodeModel);
  }

  removeNode(nodeId) {
    this.nodes = this.nodes.filter(node => node.id !== nodeId);
    this.edges = this.edges.filter(edge => edge.sourceId !== nodeId && edge.targetId !== nodeId);
    this.emit('nodeRemoved', nodeId);
  }

  addEdge(edgeModel) {
    this.edges.push(edgeModel);
    this.emit('edgeAdded', edgeModel);
  }

  removeEdge(edgeId) {
    this.edges = this.edges.filter(edge => edge.id !== edgeId);
    this.emit('edgeRemoved', edgeId);
  }

  // ...更多数据操作方法
}

视图层(View)

负责图形的渲染和现实,如节点和连线的 svg 元素。

// FlowChartView.js
class FlowChartView {
  constructor(container, config) {
    this.container = container;
    this.config = config;
    this.canvas = new Canvas(container, config.canvasWidth || 800, config.canvasHeight || 600);
    this.nodesMap = new Map();
    this.edgesMap = new Map();
  }
  
  // ...视图相关方法
}

控制层(Controller)

负责处理用户交互和业务逻辑,如节点拖拽、连线创建等

// FlowChartController.js
class FlowChartController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.initController();
  }

  initController() {
    // 绑定模型与视图的同步
    this.model.on('nodeAdded', (nodeModel) => {
      const nodeView = new NodeView(nodeModel, this.view.canvas.svg);
      this.view.nodesMap.set(nodeModel.id, nodeView);
    });

    this.model.on('nodeRemoved', (nodeId) => {
      const nodeView = this.view.nodesMap.get(nodeId);
      if (nodeView) {
        nodeView.removeView();
        this.view.nodesMap.delete(nodeId);
      }
    });

    this.model.on('edgeAdded', (edgeModel) => {
      const edgeView = new EdgeView(edgeModel, this.view.canvas.svg, this.view.nodesMap);
      this.view.edgesMap.set(edgeModel.id, edgeView);
    });

    this.model.on('edgeRemoved', (edgeId) => {
      const edgeView = this.view.edgesMap.get(edgeId);
      if (edgeView) {
        edgeView.removeView();
        this.view.edgesMap.delete(edgeId);
      }
    });

    // 绑定用户交互事件
    this.view.canvas.element.addEventListener('click', () => {
      // 处理画布点击事件
    });

    // ...更多控制逻辑
  }

  // ...其他控制方法
}

4. 提供 API 接口

为了便于与其他系统集成后进行二次开发,流程图库需要提供丰富的 API 接口。通过公开的方法和事件,用户可以自定义流程图库的行为和外观。

API 设计示例

class FlowChart extends EventEmitter {
  constructor(container, config = {}) {
    super();
    this.model = new FlowChartModel();
    this.view = new FlowChartView(container, config);
    this.controller = new FlowChartController(this.model, this.view);
    // ...其他初始化代码
  }

  // 节点管理API
  addNode(id, x, y, label, type = 'default', style = {}) {
    const nodeModel = new NodeModel(id, x, y, label, type, style);
    this.model.addNode(nodeModel);
    return nodeModel;
  }

  removeNode(nodeId) {
    this.model.removeNode(nodeId);
  }

  updateNode(nodeId, newData) {
    const node = this.model.getNodeById(nodeId);
    if (node) {
      if (newData.x !== undefined && newData.y !== undefined) {
        node.x = newData.x;
        node.y = newData.y;
      }
      if (newData.label !== undefined) {
        node.label = newData.label;
      }
      if (newData.style !== undefined) {
        node.style = { ...node.style, ...newData.style };
      }
      this.emit('nodeUpdated', nodeId, newData);
    }
  }

  // 连线管理API
  addEdge(id, sourceId, targetId, type = 'default', style = {}) {
    const edgeModel = new EdgeModel(id, sourceId, targetId, type, style);
    this.model.addEdge(edgeModel);
    return edgeModel;
  }

  removeEdge(edgeId) {
    this.model.removeEdge(edgeId);
  }

  // 插件管理API
  loadPlugin(pluginClass) {
    this.controller.flowChartInstance.loadPlugin(pluginClass);
  }

  unloadPlugin(pluginClass) {
    this.controller.flowChartInstance.unloadPlugin(pluginClass);
  }

  // 导出与导入API
  exportData() {
    return this.model.exportData();
  }

  loadData(jsonData) {
    this.model.loadData(jsonData);
  }

  // ...更多API方法
}

架构设计效果

通过清晰的架构层次和设计模式的应用,流程图库的代码结构更加模块化和可维护。丰富的 API 接口使得流程图库更易于集成和扩展,满足不同用户和项目的需求。

总结

本章深入分析并实现了流程图库的拓展能力,涵盖插件系统、可配置性、性能优化和多端支持等关键方面。通过引入插件机制,流程图库具备了高度的可扩展性,允许用户根据需求自定义和扩展功能。增加配置选项和样式自定义,提升了流程图库的灵活性和个性化程度。性能优化措施确保了流程图库在处理大规模数据和复杂交互时的高效运行。同时,拓展多端支持,兼容移动设备,提升了用户的使用体验。

 

未来寄语

尽管FlowChart库在本章中已经取得了显著进展,但为了打造一个真正功能完善、广泛适用的流程图库工具,未来仍有多个方面需要进一步优化和扩展:

  1. 节点内容的直接编辑

目标:允许用户在界面上直接双击节点,实时编辑其内容,无需弹出提示框或对话框。

实现方式:在节点视图中嵌入可编辑的文本元素,监听编辑事件并同步更新数据模型。

  1. 多种连线样式的支持

目标:丰富连线的表现形式,支持曲线、折线、带箭头等多种样式,以满足不同的流程需求。

实现方式:扩展连线类,增加不同类型的路径计算算法,并在配置中提供选择项。

  1. 缩放与平移功能

目标:实现画布的缩放和平移,提升用户在大规模流程图中的操作体验。

实现方式:添加缩放工具和拖动画布的功能,优化SVG视图的变换矩阵,实现流畅的缩放和平移效果。

  1. 保存与加载功能

目标:支持将流程图保存为JSON格式,便于数据的持久化和共享。

实现方式:完善数据导出与导入的API接口,确保流程图的所有属性和状态能够准确保存和恢复。

  1. 撤销与重做功能

目标:为用户提供撤销和重做操作,允许轻松回退或重做之前的操作,提高操作的灵活性和安全性。

实现方式:引入命令模式,维护操作历史记录,实现在用户界面上的撤销与重做按钮。

  1. 多用户协作

目标:支持多人实时协作编辑同一流程图,满足团队协作的需求。

实现方式:集成实时通信技术,如WebSocket,管理多个用户的编辑权限和实时同步数据。

  1. 高级性能优化

目标:进一步提升流程图库在处理复杂和大规模数据时的性能,确保高效运行。

实现方式:优化渲染算法,采用更高效的数据结构,利用Web Workers进行多线程处理,减少主线程的负担。

  1. 全面的测试覆盖

目标:通过单元测试和集成测试,确保库的稳定性和可靠性,降低潜在的Bug风险。

实现方式:使用测试框架,如Jest或Mocha,编写全面的测试用例,覆盖各个功能模块和边界情况。

  1. 详尽的文档与示例

目标:提供详细的使用文档和丰富的示例,帮助用户快速上手和深入理解库的功能。

实现方式:撰写清晰的API文档,制作互动式的教程和示例项目,利用工具如Storybook展示组件用法。

  1. 增强的可定制性

目标:允许用户通过主题、样式配置等方式,进一步个性化流程图库的外观和行为。

实现方式:引入主题系统,支持CSS变量和自定义样式钩子,提供更多的配置选项和灵活的API接口。

后续我们会继续丰富 FowChart 库的功能,希望这三节课程能给大家提供一些实现一个流程图库的思路。后续我们会对比一下几个流行的流程图库关键功能的实现区别,欢迎大家持续关注~