简介: 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…
后续会继续写其他模块的内容。