LogicFlow core解析(一)

1,597 阅读5分钟

简介: LogicFlow是使用Monorepo进行项目代码管理的,在项目仓库(repo)中管理多个模块/包(package),不同于常见的每个模块建一个repo,同时使用lerna处理发布相关的问题。

1、环境设置与启动项目

从git下载代码,进入到packages/core文件夹,cnpm install(或者yarn)安装相关依赖包。npm run dev启动项目,访问http://localhost:9090/ 至此LogicFlow core项目已经正常启动了。

2、代码流程梳理

说明: LogicFlow core的代码使用了preact(和react几乎没有区别),mobx-react,mobx,EventEmitter(基于订阅发布模式开发管理事件派发相关的工具)等,了解这些内容后更有助于代码阅读,代码中牵涉到的地方会做详细说明。本章节先对代码流程进行梳理。

(0)、起步
import LogicFlow from '@logicflow/core';
const config = {
  isSilentMode: true,
  stopScrollGraph: true,
  stopZoomGraph: true,
  style: {
    rect: {
      width: 100,
      height: 50
    }
  }
}
const data = {
  nodes: [
    {
      id: 10,
      type: 'rect',
      x: 150,
      y: 70,
      text: '矩形1'
    }
  ]
};
const lf = new LogicFlow({
  ...config,
  container: document.querySelector('#graph') as HTMLElement
});
lf.render(data);

从上面的使用方法可以看出,入口是LogicFlow,先new LogicFlow({})调用LogicFlow的构造函数,之后lf.render(data)绘制data数据到页面中用svg展示。

(1)、LogicFlow.tsx

这是项目的入口文件,文件中定义了class LogicFlow, 其包含需多的方法,根据例子中代码的执行到lf.render(data);,从LogicFlow的render开始(构造函数会初始化一些参数,用到的参数会详细说明)。

import { render, h } from 'preact';
export default class LogicFlow {
    constructor(options: Options.Definition) {}
    render(graphData = {}) {//代码有省略
        this.graphModel.graphDataToModel(graphData);
        render((
          <Provider
            graphModel={this.graphModel}
          >
            <Graph
              eventCenter={this.eventCenter}
              getView={this.getView}
              tool={this.tool}
              options={this.options}
              dnd={this.dnd}
              snaplineModel={this.snaplineModel}
              components={this.components}
            />
          </Provider>
        ), this.container);
  }
}

外层的render函数是LogicFlow中的,内层的render是preact中的。 render中执行this.graphModel.graphDataToModel(graphData),代码调用GraphModel.ts中的graphDataToModel方法,设置nodes和edges,代码如下

graphDataToModel(graphData) {//代码有省略
    this.nodes = map(graphData.nodes, node => {
        const Model = this.getModel(node.type);
        return new Model(node, this);
    });
    this.edges = map(graphData.edges, edge => {
        const Model = this.getModel(edge.type);
        return new Model(edge, this);
    });
}

getModel(type: string) {
    return this.modelMap.get(type);
}

其中调用this.getModel(type: string)获取Model,函数this.getModel中的this.modelMap的内容是在LogicFlow.tsx中调用 this.graphModel.setModel(type, ModelClass);进行写入的。

代码new Model(node, this);中的Model最终可以找到RectNodeModel.ts中。

import { computed, observable } from 'mobx';
class RectNodeModel extends BaseNodeModel {
    modelType = ModelType.RECT_NODE;
    @observable width = defaultTheme.rect.width;
    @observable height = defaultTheme.rect.height;
    @observable radius = defaultTheme.rect.radius;
}
export default class BaseNodeModel implements IBaseModel {}

RectNodeModel继承了BaseNodeModel,两个类中的很多属性前添加了@observable注解(observable表示属性是可观测的,当属性被改变时,将会触发组件的重新渲染展示在页面中)。

到这里,执行第二个render函数把this.graphModel传递给Provider了。

<Provider graphModel={this.graphModel}>
    <Graph
    />
</Provider>

RectNodeModel类中没有定义@action用于修改width,height,radius属性。RectNodeModel中继承了BaseNodeModel中的@action moveTo(x, y): void {}方法,用于修改x,y属性引起rect矩形位置的变化。

(2)、Graph.tsx

LogicFlow的第二个render函数执行渲染,会进入到Graph.tsx组件中。

import { observer, inject } from 'mobx-react';
@inject('graphModel')
@observer
class Graph extends Component<IProps> {//这是一个组件
    render() {
        return (<div>
            <CanvasOverlay>
                <g className="lf-base">
                    {
                        map(graphModel.sortElements, (nodeModel) => (
                            this.getComponent(nodeModel, graphModel, eventCenter)
                        ))
                    }
                </g>
            </CanvasOverlay>
        </div>)
    }
}

在Graph.tsx中,@inject('graphModel')会引入Provider组件中提供的graphModel参数,至此Graph组件中的this.props属性中含有了graphModel。

Graph.tsx中render的return中使用了CanvasOverlay组件

class CanvasOverlay extends Component<IProps, Istate> {
    render() {
    const { transformStyle: { transform } } = this.InjectedProps;
    const { children, dnd } = this.props;
    const { isDraging } = this.state;
        return (
            <svg
                xmlns="http://www.w3.org/2000/svg"
                width="100%"
                height="100%"
                name="canvas-overlay"
                onWheel={this.zoomHandler}
                onMouseDown={this.mouseDownHandler}
                onContextMenu={this.handleContextMenu}
                className={isDraging ? 'lf-dragging' : 'lf-drag-able'}
                {...dnd.eventMap()}
            >
                <g style={{ transform }}>
                    {children}
                </g>
            </svg>
        );
    }
}

CanvasOverlay组件中使用的children,就是Graph提供的。

<g className="lf-base">
    {
        map(graphModel.sortElements, (nodeModel) => (
            this.getComponent(nodeModel, graphModel, eventCenter)
        ))
    }
</g>

this.getComponent函数还在Graph.tsx中。

getComponent() {
    const { getView } = this.props;
    const View = getView(model.type);//'rect'
    return (
        <View
            key={model.id}
            model={model}
            graphModel={graphModel}
            overlay={overlay}
            eventCenter={eventCenter}
        />
    );
}

getView(model.type)函数在LogicFlow.tsx中。

getView = (type: string) => this.viewMap.get(type);

this.viewMap中的数据内容是通过this.setView(type, observer(ViewClass as IReactComponent));写入的。 最终可以找到RectNode.tsx中。

(3)、RectNode.tsx
export default class RectNode extends BaseNode {
    getShape() {
        const attributes = this.getAttributes();
        return (
            <Rect
                {...attributes}
            />
        );
    }
}

export default abstract class BaseNode extends Component<IProps, Istate> {
    render() {
        const nodeShapeInner = (
            <g className="lf-node-content">
                {this.getShape()}
                {this.getText()}
            </g>
        );
        let nodeShape;
        if (!isHitable) {
            nodeShape = (
                <g className={this.getStateClassName()} id={model.id}>
                    { nodeShapeInner }
                </g>
            );
        } else {
            nodeShape = (
                <g
                    className={this.getStateClassName()}
                    id={model.id}
                    onMouseDown={this.handleMouseDown}
                    onMouseUp={this.handleClick}
                    onMouseEnter={this.setHoverON}
                    onMouseLeave={this.setHoverOFF}
                    onContextMenu={this.handleContextMenu}
                >
                    { nodeShapeInner }
                </g>
            );
        }
        return nodeShape;
    }
}

RectNode继承自BaseNode,变成了是一个组件,继承了BaseNode中的render函数。在render函数中调用this.getShape()<Rect {...attributes} />组件。

(4)、Rect.tsx
export default function Rect(props: IProps) {
    return (
        <rect {...attrs} />
    );
}

这是一个函数组件,是最终渲染到页面中svg标签内的rect标签。

至此上面实例中rect矩形被成功渲染到了页面中。

3、绘制和交互

页面内有多个节点和边时,牵涉到复杂的组件通信是用mobx,和mobx-react来解决的。项目中的model文件夹中的model数据被view文件夹中的视图使用,。视图中包括了node节点,edge边等相关组件。

(0)、view使用model数据

在LogicFlow.tsx中使用了Graph.tsx组件,在Graph.tsx中调用getComponent()函数中根据model.type找到对应的view组件(如RectNode.tsx),同时把graphModel,model传递进去,model中包含了组件需要的数据。

getComponent(model: BaseEdgeModel | BaseNodeModel, graphModel: GraphModel, eventCenter: EventEmitter, overlay = 'canvas-overlay') {
    const { getView } = this.props;
    const View = getView(model.type);//'rect'
    return (
        <View
            key={model.id}
            model={model}
            graphModel={graphModel}
            overlay={overlay}
            eventCenter={eventCenter}
        />
    );
}

RectNode.tsx继承自BaseNode.tsx,也是一个组件。在BaseNode组件中可以找到render函数,同时定义鼠标操作。render函数中调用getShape()获取对应的组件。

render() {
    const { model, graphModel } = this.props;
    const nodeShapeInner = (
        <g className="lf-node-content">
            {this.getShape()}
            {this.getText()}
            {
                hideAnchors ? null : this.getAnchors()
            }
        </g>
    );
    let nodeShape;
    nodeShape = (
        <g
            className={this.getStateClassName()}
            id={model.id}
            onMouseDown={this.handleMouseDown}
            onMouseUp={this.handleClick}
            onMouseEnter={this.setHoverON}
            onMouseLeave={this.setHoverOFF}
            onContextMenu={this.handleContextMenu}
        >
            { nodeShapeInner }
        </g>
    );
    return nodeShape;
  }
getShape() {
    const attributes = this.getAttributes();
    return (
        <Rect
            {...attributes}
        />
    );
}

在getAttributes()函数中解析model中的数据,提供给Rect组件使用。

getAttributes() {
    const {
        model: {
            id,
            properties = {},
            type,
            x,
            y,
            isSelected,
            isHovered,
            text,
        },
    } = this.props;
    const style = this.getShapeStyle();
    return {
      id,
      properties: {
          ...properties,
      },
      type,
      x,
      y,
      isSelected,
      isHovered,
      text: {
          ...text,
      },
      ...style,
    };
  }

Rect来自于../basic-shape/Rect,至此Rect可以绘制在页面中了,其他组件和边的绘制也是类似的。

(1)、鼠标操作改变model数据

在BaseNode.tsx的render函数中,为元素注册了onMouseDown={this.handleMouseDown}事件。

handleMouseDown = (ev: MouseEvent) => {
    const { model, graphModel } = this.props;
    graphModel.toFront(model.id);
    this.startTime = new Date().getTime();
    this.stepDrag && this.stepDrag.handleMouseDown(ev);
};

函数handleMouseDown中使用this.stepDrag的handleMouseDown处理点击。 在/util/drag中可以找到class StepDrag,类中使用handleMouseDown处理点击事件。

handleMouseDown = (e: MouseEvent) => {
    if (e.button !== LEFT_MOUSE_BUTTON_CODE) return;
    if (this.isStopPropagation) e.stopPropagation();
    this.isDraging = true;
    this.startX = e.x;
    this.startY = e.y;

    DOC.addEventListener('mousemove', this.handleMouseMove, false);
    DOC.addEventListener('mouseup', this.handleMouseUp, false);
    this.onDragStart({ event: e });
    const elementData = this.model?.getData();
    this.eventCenter?.emit(EventType[`${this.eventType}_MOUSEDOWN`], { e, data: elementData });
};

函数handleMouseDown中注册了mousemove事件,调用this.handleMouseMove方法。在this.handleMouseMove中调用 this.onDraging({ deltaX, deltaY, event: e }); 又回到了BaseNode中的onDraging函数中。

onDraging = ({ deltaX, deltaY }) => {
    const { model, graphModel } = this.props;
    const { isDraging } = this.state;
    if (!isDraging) {
      this.setState({
          isDraging: true,
      });
    }
    const { transformMatrix } = graphModel;

    const [curDeltaX, curDeltaY] = transformMatrix.fixDeltaXY(deltaX, deltaY);
    if (model.modelType.indexOf('node') > -1) {
        graphModel.moveNode(model.id, curDeltaX, curDeltaY);
    }
};

在onDraging函数中,调用graphModel中的moveNode方法,用来改变node的位置(graphModel是在getComponent方法中和model一起传入其中的)。

@action
moveNode(nodeId: BaseNodeModelId, deltaX: number, deltaY: number) {
    // 1) 移动节点
    const nodeModel = this.nodesMap[nodeId].model;
    nodeModel.move(deltaX, deltaY);
    // 2) 移动连线
    this.moveEdge(nodeId, deltaX, deltaY);
}

函数moveNode中调用model文件夹下对应的node或者是edge的方法修改属性。 调用到了RectNodeModel中的move方法,RectNodeModel继承了BaseNodeModel,可以在BaseNodeModel中找到move方法。

@action
move(deltaX, deltaY): void {
    this.x += deltaX;
    this.y += deltaY;
    this.text && this.moveText(deltaX, deltaY);
}

由于@observable x = defaultConfig.x;`` @observable y = defaultConfig.y;属性被observable修饰,可以引起页面中的变化。 在moveNode方法中运动节点之后,还需要移动和节点有关的边,调用moveEdge方法。

至此从node绘制到页面中和鼠标操作node交互代码分析完了。

4、感谢

感谢LogicFlow!

相关链接: 专注流程可视化的前端框架 juejin.cn/post/693341…

后续会继续写其他模块的内容。