跳到主要内容

PluginManager API

PluginManager 负责管理插件的注册、卸载和生命周期协调。

Plugin 接口

所有插件都应实现 Plugin 接口:

interface Plugin {
// 唯一标识符
id: string;

// 可选:安装时调用
setup?(engine: CanvasEngine): void;

// 可选:卸载时调用
dispose?(): void;

// 生命周期钩子(可选)
beforeRender?(ctx: CanvasRenderingContext2D): void;
afterRender?(ctx: CanvasRenderingContext2D): void;
renderNodeLabels?(ctx: CanvasRenderingContext2D, node: NodeData): void;
renderEdgeLabels?(ctx: CanvasRenderingContext2D, edge: EdgeData, graph: Graph): void;
}

构造函数

constructor(engine: CanvasEngine)

PluginManager 由 CanvasEngine 自动创建,通常不需要手动实例化。

方法

use()

use(...plugins: Plugin[]): void

注册一个或多个插件。如果插件已存在(相同 ID),则忽略。

参数:

  • plugins: 要注册的插件(支持多个)
import { GridPlugin, DragPlugin, PanZoomPlugin } from '@fnt-agilejs/core';

// 注册单个插件
engine.plugins.use(new GridPlugin({ size: 20 }));

// 批量注册
engine.plugins.use(
new DragPlugin(),
new PanZoomPlugin(),
new BoxSelectPlugin()
);

eject()

eject(id: string): void

卸载插件。会调用插件的 dispose() 方法(如果存在)。

参数:

  • id: 插件 ID
// 卸载网格插件
engine.plugins.eject('grid');

get()

get<T extends Plugin = Plugin>(id: string): T | undefined

根据 ID 获取插件实例(带类型推断)。

参数:

  • id: 插件 ID

返回:

  • 插件实例或 undefined
const gridPlugin = engine.plugins.get<GridPlugin>('grid');
if (gridPlugin) {
console.log('Grid size:', gridPlugin.options.size);
}

// 修改插件配置
const dragPlugin = engine.plugins.get<DragPlugin>('drag');
if (dragPlugin) {
dragPlugin.enabled = false;
}

emitHook()

emitHook(hook: keyof Plugin, ...args: any[]): void

触发插件生命周期钩子。按注册顺序依次调用所有插件的对应钩子方法。

参数:

  • hook: 钩子名称
  • args: 传递给钩子的参数
// 引擎内部使用示例
engine.plugins.emitHook('beforeRender', ctx);
engine.plugins.emitHook('renderNodeLabels', ctx, node);
engine.plugins.emitHook('afterRender', ctx);

插件生命周期

安装阶段

class MyPlugin implements Plugin {
id = 'my-plugin';

setup(engine: CanvasEngine): void {
console.log('Plugin installed');

// 注册事件监听器
engine.events.on('graph:change', this.onGraphChanged);

// 保存引用以便后续使用
this.engine = engine;
}
}

渲染钩子

beforeRender()

在主渲染前调用(屏幕空间),可用于绘制背景层。

beforeRender(ctx: CanvasRenderingContext2D): void {
// 绘制网格、背景等
ctx.save();
// ... 绘制逻辑
ctx.restore();
}

afterRender()

在主渲染后调用(屏幕空间),可用于绘制覆盖层。

afterRender(ctx: CanvasRenderingContext2D): void {
// 绘制选择框、工具提示等
ctx.save();
// ... 绘制逻辑
ctx.restore();
}

renderNodeLabels()

在每个节点渲染后调用(世界空间),用于绘制节点标签。

renderNodeLabels(ctx: CanvasRenderingContext2D, node: NodeData): void {
if (node.data?.label) {
ctx.save();
ctx.fillStyle = '#000';
ctx.font = '12px sans-serif';
ctx.fillText(node.data.label, node.position.x, node.position.y);
ctx.restore();
}
}

renderEdgeLabels()

在边渲染后调用(世界空间),用于绘制边标签。

renderEdgeLabels(
ctx: CanvasRenderingContext2D,
edge: EdgeData,
graph: Graph
): void {
if (edge.data?.label) {
const source = graph.getNode(edge.source);
const target = graph.getNode(edge.target);
if (source && target) {
const midX = (source.position.x + target.position.x) / 2;
const midY = (source.position.y + target.position.y) / 2;
ctx.save();
ctx.fillStyle = '#666';
ctx.font = '11px sans-serif';
ctx.fillText(edge.data.label, midX, midY);
ctx.restore();
}
}
}

卸载阶段

class MyPlugin implements Plugin {
id = 'my-plugin';
private engine?: CanvasEngine;
private listener?: () => void;

setup(engine: CanvasEngine): void {
this.engine = engine;
this.listener = () => console.log('Graph changed');
engine.events.on('graph:change', this.listener);
}

dispose(): void {
console.log('Plugin disposed');

// 清理事件监听器
if (this.engine && this.listener) {
this.engine.events.off('graph:change', this.listener);
}

// 清理其他资源
this.engine = undefined;
this.listener = undefined;
}
}

自定义插件示例

提示

可参考既有插件实现,如:BlinkPlugin

简单插件

import { Plugin, CanvasEngine, NodeData } from '@fnt-agilejs/core';

class HelloPlugin implements Plugin {
id = 'hello';

setup(engine: CanvasEngine): void {
console.log('Hello Plugin installed');
}

beforeRender(ctx: CanvasRenderingContext2D): void {
// 在左上角显示文本
ctx.save();
ctx.fillStyle = '#000';
ctx.font = '14px sans-serif';
ctx.fillText('Hello from Plugin!', 10, 20);
ctx.restore();
}

dispose(): void {
console.log('Hello Plugin disposed');
}
}

// 使用
engine.plugins.use(new HelloPlugin());

带配置的插件

interface MyPluginOptions {
color?: string;
size?: number;
}

class MyPlugin implements Plugin {
id = 'my-plugin';
private options: Required<MyPluginOptions>;

constructor(options: MyPluginOptions = {}) {
this.options = {
color: options.color ?? '#000',
size: options.size ?? 10
};
}

afterRender(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.fillStyle = this.options.color;
ctx.fillRect(0, 0, this.options.size, this.options.size);
ctx.restore();
}

// 允许动态修改配置
setOptions(options: Partial<MyPluginOptions>): void {
Object.assign(this.options, options);
}
}

// 使用
const plugin = new MyPlugin({ color: '#ff0000', size: 20 });
engine.plugins.use(plugin);

// 动态修改
plugin.setOptions({ color: '#00ff00' });

交互插件

class ClickPlugin implements Plugin {
id = 'click';
private engine?: CanvasEngine;

setup(engine: CanvasEngine): void {
this.engine = engine;
engine.canvas.addEventListener('click', this.onClick);
}

private onClick = (e: MouseEvent): void {
if (!this.engine) return;

const rect = this.engine.canvas.getBoundingClientRect();
const screenX = e.clientX - rect.left;
const screenY = e.clientY - rect.top;
const world = this.engine.toWorld({ x: screenX, y: screenY });

console.log('Clicked at world:', world);

// 查找点击的节点
const nodes = this.engine.graph.getNodes();
for (const node of nodes) {
const { x, y } = node.position;
const { width, height } = node.size;

if (
world.x >= x && world.x <= x + width &&
world.y >= y && world.y <= y + height
) {
console.log('Clicked node:', node.id);
break;
}
}
};

dispose(): void {
if (this.engine) {
this.engine.canvas.removeEventListener('click', this.onClick);
}
}
}

// 使用
engine.plugins.use(new ClickPlugin());

状态管理插件

class SelectionPlugin implements Plugin {
id = 'selection';
private engine?: CanvasEngine;
private selectedNodes = new Set<string>();

setup(engine: CanvasEngine): void {
this.engine = engine;
engine.canvas.addEventListener('click', this.onClick);
}

private onClick = (e: MouseEvent): void {
// ... 检测点击的节点

// 清除之前的选中
this.clearSelection();

// 选中新节点
this.selectNode(nodeId);
};

selectNode(nodeId: string): void {
const node = this.engine?.graph.getNode(nodeId);
if (node) {
node.selected = true;
this.selectedNodes.add(nodeId);
this.engine?.graph.markDirty();
}
}

clearSelection(): void {
for (const id of this.selectedNodes) {
const node = this.engine?.graph.getNode(id);
if (node) node.selected = false;
}
this.selectedNodes.clear();
this.engine?.graph.markDirty();
}

getSelectedNodes(): string[] {
return Array.from(this.selectedNodes);
}

afterRender(ctx: CanvasRenderingContext2D): void {
if (!this.engine) return;

// 绘制选中框
ctx.save();
ctx.strokeStyle = '#2196F3';
ctx.lineWidth = 2;

for (const id of this.selectedNodes) {
const node = this.engine.graph.getNode(id);
if (node) {
const screen = this.engine.toScreen(node.position);
const width = node.size.width * this.engine.getScale();
const height = node.size.height * this.engine.getScale();

ctx.strokeRect(screen.x, screen.y, width, height);
}
}

ctx.restore();
}

dispose(): void {
if (this.engine) {
this.engine.canvas.removeEventListener('click', this.onClick);
}
this.clearSelection();
}
}

插件管理最佳实践

1. 插件按需加载

// 根据模式加载不同插件
if (mode === 'edit') {
engine.plugins.use(
new DragPlugin(),
new SelectionPlugin(),
new BoxSelectPlugin()
);
} else {
engine.plugins.use(
new PanZoomPlugin()
);
}

2. 插件热插拔

function toggleGrid(enabled: boolean) {
if (enabled) {
engine.plugins.use(new GridPlugin({ size: 20 }));
} else {
engine.plugins.eject('grid');
}
}

3. 插件通信

class PluginA implements Plugin {
id = 'plugin-a';

setup(engine: CanvasEngine): void {
// 触发自定义事件
engine.events.emit('pluginA:ready', { data: 'something' });
}
}

class PluginB implements Plugin {
id = 'plugin-b';

setup(engine: CanvasEngine): void {
// 监听插件A的事件
engine.events.on('pluginA:ready', (payload) => {
console.log('Plugin A is ready:', payload);
});
}
}

4. 插件配置持久化

提示

根据自身需要实现,通常来说必要性不大。

class ConfigurablePlugin implements Plugin {
id = 'configurable';
private options: any;

constructor(options: any = {}) {
// 尝试从本地存储加载
const saved = localStorage.getItem('plugin-config');
this.options = saved ? JSON.parse(saved) : options;
}

setOptions(options: any): void {
Object.assign(this.options, options);
// 保存到本地存储
localStorage.setItem('plugin-config', JSON.stringify(this.options));
}

dispose(): void {
// 可选:清理本地存储
localStorage.removeItem('plugin-config');
}
}

内置插件参考

AgileJS 提供了丰富的内置插件:

  • GridPlugin - 网格背景
  • DragPlugin - 节点拖拽
  • PanZoomPlugin - 平移和缩放
  • SelectionOverlayPlugin - 选择框
  • BoxSelectPlugin - 框选
  • ConnectPlugin - 连线
  • PortOverlayPlugin - 端口显示
  • KeyboardPlugin - 键盘快捷键
  • LabelOverlayPlugin - 标签显示
  • ClipboardPlugin - 复制粘贴
  • SnapToGridPlugin - 对齐网格
  • GuidesPlugin - 参考线
  • ResizeRotatePlugin - 缩放旋转
  • GroupResizeRotatePlugin - 组缩放旋转
  • GroupPlugin - 分组
  • EdgeEditPlugin - 边编辑
  • PolylineNodeEditPlugin - 折线节点编辑
  • FlowDashPlugin - 流动动画
  • NodeFlowDashPlugin - 节点描边流动
  • MinimapPlugin - 迷你地图
  • BlinkPlugin - 闪烁效果
  • HoverCursorPlugin - 悬停光标
  • DataTooltipPlugin - 数据提示

详见各插件的专门文档。