插件架构实践

发布于:2025-04-18 ⋅ 阅读:(22) ⋅ 点赞:(0)

插件架构(微内核架构)

支持软件能力可扩展、可定制

核心概念:

  1. 内核:作为应用入口(主体),提供基础服务、插件调度、事件中心等等
  2. 插件:实现标准的初始化、启动、关闭等标准接口,实现插件的生命周期管理
  3. 插件接口(插件注册机制):插件接入内核的方式
  4. 上下文信息:在插件间传递的共享数据

三种插件模式

常用以下三种插件模式:管道式、洋葱式和事件式,

管道式插件设计

  1. 比如 命令行cat file.txt | grep "keyword"
  2. 比如 Gulp:
gulp.task("css", [], function () {
  return gulp
    .src(["src/css/**/*.less"])
    .pipe(less())
    .pipe(minifyCss())
    .pipe(concat("app.css"));
});

缺点:容易出现单点故障。顺序设计复杂,确保数据流的正确和有效需要仔细管理。

洋葱式插件设计

比如 Nodejs 中间件,参考 koa-compose

特点: 可处理输入流和输出流,但层次嵌套设计提高了应用复杂度。

事件式插件设计

比如 vscode 插件系统、chrome 扩展插件、webpack 插件机制等

事件模式插件实践

flow 编辑器设计示例

Kernel 设计
//节点schema定义
interface NodeSchema{
  //基础属性,比如name,icon等
  id,
  //节点组件、表单schema、表单数据预检、转换等部分
  node,
  //edge组件、表单schema等
  edge,
}
// 操作schema定义
interface OperationSchema {
  id,
  //基础属性,比如name,icon等
  meta: {
    name,
    icon,
    visibility: ["toolbar", "contextmenu"],
    execute: ({ selectedNodes, kernel,...rest }) => void,
  }
  // 插件激活方法,注册时调用
  activate(kernel) {},
  deactivate(kernel) {},
}
// 内核
class FlowKernel {
  //节点
  nodes: NodeTypeRegistry;
  //操作
  ops: OperationRegistry;
  //设置
  config: ConfigManager;
  //事件中心
  events: EventBus;
  //快捷键
  hotkeys:HotkeyManager;
  //省略其他属性...

  constructor() {
    this.nodes = new NodeTypeRegistry(this);
    this.ops = new OperationRegistry(this);
    this.config = new ConfigManager();
    this.events = new EventBus();
    this.hotkeys = new HotkeyManager();

    // 加载基础节点/插件 结构示例
    this.registerNode(DefaultNode);
    this.registerOperation(DefaultOperation);
    //...
  }

  registerNode(node: NodeMeta) {
    this.nodes.register(node);
  }

  registerOperation(plugin: Plugin) {
    this.ops.register(plugin);
    plugin.activate?.(this);
  }
}
class NodeTypeRegistry {
  private types = new Map<string, NodeMeta>();

  constructor(private kernel: FlowKernel) {}

  register(type: NodeMeta) {
    this.types.set(type.id, {
      component: type.component,
      defaultConfig: type.defaultConfig,
      formSchema: type.formSchema // 动态表单定义
    });
  }

  getFormSchema(nodeType: string) {
    return this.types.get(nodeType)?.formSchema;
  }
}

class OperationRegistry {
  private operations = new Map<string, OperationMeta>();

  register(op: OperationMeta) {
    this.operations.set(op.id, {
      ...op.meta,
      visibility: op.meta.visibility||['toolbar','contextmenu']
    });
  }

  getVisibleOperations(location: 'toolbar' | 'contextmenu') {
    return Array.from(this.operations.values())
      .filter(op => op.visibility.includes(location));
  }
}

class EventBus {
    private listeners = new Map<string, (...args: any[]) => void>();
    on(event: string, listener: (...args: any[]) => void) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, listener);
        }
    }
    emit(event: string, ...args: any[]) {
        if (this.listeners.has(event)) {
            this.listeners.get(event)(...args);
        }
    }
}

class ConfigManager {
  private config = {
    //主题色
    theme:{}
    operations: {
      //比如:控制菜单是否在上下文可见
      'copy': { visibleInContextMenu: true }
    },
  };

  updateOperationConfig(opId: string, config: object) {
    this.config.operations[opId] = config;
  }

  getOperationVisibility(opId: string) {
    return this.config.operations[opId]?.visibleInContextMenu ?? false;
  }
}
UI 集成:
  • 节点面板:
function NodePalette({ kernel }: { kernel: FlowKernel }) {
  const nodes = useMemo(
    () => Array.from(kernel.nodes.types.values()),
    [kernel]
  );

  return (
    <div className="node-palette">
      {nodes.map((node) => (
        <DraggableNode nodeSchema={node} />
      ))}
    </div>
  );
}
  • 工具栏
function Toolbar({ kernel }: { kernel: FlowKernel }) {
  const operations = useMemo(
    () => kernel.ops.getVisibleOperations("toolbar"),
    [kernel]
  );

  return (
    <div className="toolbar">
      {operations.map((op) => (
        <OpButton
          key={op.id}
          onClick={() => op.execute()}
          icon={op.icon}
          name={op.name}
        />
      ))}
    </div>
  );
}
  • 上下文菜单
function useContextMenu(kernel: FlowKernel) {
  const [menuItems, setItems] = useState<OperationMeta[]>([]);

  useEffect(() => {
    const handler = (position: { x: number; y: number }) => {
      setItems(kernel.ops.getVisibleOperations('contextmenu'));
    };
    //每次显示菜单时,更新菜单项
    kernel.events.on('contextmenu', handler);
    return () => kernel.events.off('contextmenu', handler);
  }, [kernel]);

  return menuItems;
}


function ConfigPanel({ kernel }) {
  return (
    <div>
      {Array.from(kernel.ops.operations).map(([id, op]) => (
        <label key={id}>
          <input
            type="checkbox"
            checked={kernel.config.getOperationVisibility(id)}
            onChange={e => kernel.config.updateOperationConfig(id, {
              visibleInContextMenu: e.target.checked
            })}
          />
          {op.name}
        </label>
      ))}
    </div>
  );
}

  • 节点表单
function PropertyPanel({ node, kernel }) {
  const schema = kernel.nodes.getFormSchema(node.type);

  return (
    <ConfigForm
      schema={schema}
      formData={node.data}
      onChange={({ formData }) => {
        kernel.events.emit("nodeUpdate", {
          id: node.id,
          data: formData,
        });
      }}
    />
  );
}

使用示例

  • 自定义页面基础布局
function App() {
  const [kernel, setKernel] = useState(null);
  useEffect(() => {
    const kernel = new FlowKernel();
    // 自定义节点/插件
    kernel.registerNode(CustomNode);
    kernel.registerOperation(CopyOperation);
    kernel.registerOperation(PasteOperation);
    setKernel(kernel);
  }, []);
  return (
    <div className="flow-editor">
      <div className="sidebar">
        <NodePalette kernel={kernel} />
        <ConfigPanel kernel={kernel} />
      </div>

      <div className="main">
        <Toolbar kernel={kernel} />
        <ReactFlow
          nodes={nodes}
          edges={edges}
          nodeTypes={kernel.nodes.getComponents()}
          onNodeClick={handleNodeClick}
        />
        <PropertyPanel kernel={kernel} />
      </div>
    </div>
  );
}
  • 自定义节点
const CustomNodeComponent = ({ data }) => {};
export const CustomNode = {
  id: "custom-node",
  node: {
    component: CustomNodeComponent,
    formSchema: {},
    defaultConfig: {},
  },
  edge: {
    component: CustomEdgeComponent,
    formSchema: {},
    defaultConfig: {},
  },
};
  • 复制/粘贴操作插件
const CopyOperation = {
  type: "operation",
  meta: {
    id: "copy-op",
    name: "复制",
    icon: <CopyIcon />,
    visibility: ["toolbar", "contextmenu"],
    execute: ({ selectedNodes, kernel }) => {
      kernel.clipboard.set(clipboardData);
      kernel.events.emit("copy", selectedNodes);
    },
  },
  activate(kernel) {
    kernel.hotkeys.register("ctrl+c", () => {
      if (kernel.selection.hasSelection) {
        this.meta.execute({
          selectedNodes: kernel.selection.selectedNodes,
          kernel,
        });
      }
    });
    kernel.events.on("doubleclick", ({ node }) => {
      this.meta.execute({ selectedNodes: [node], kernel });
    });
  },
  deactivate(kernel) {
    kernel.hotkeys.unregister("ctrl+c");
  },
};

const PasteOperation = {
  type: "operation",
  meta: {
    id: "paste-op",
    name: "粘贴",
    icon: <PasteIcon />,
    visibility: ["toolbar", "contextmenu"],
    execute: ({ kernel }) => {
      const clipboardData = kernel.clipboard.get();
      if (!clipboardData) {
        return;
      }
      // 计算偏移量
      const offset = 50;
      const newNodes = clipboardData.nodes.map((node, index) => ({
        ...node,
        id: `copied-${node.id}-${clipboardData.timestamp}-${index}`,
        position: {
          x: node.position.x + offset,
          y: node.position.y + offset,
        },
      }));
      kernel.graph.addNode(newNodes);
    },
  },
  activate: (kernel) => {
    kernel.hotkeys.register("ctrl+v", () => {
      this.meta.execute({ kernel });
    });
  },
  deactivate: (kernel) => {
    kernel.hotkeys.unregister("ctrl+v");
  },
};

undo/redo 操作

两种实现方式:记录数据和命令模式

记录数据

基础数据结构:

interface OperationRecord {
  meta,//操作元数据,比如操作类型、操作参数等
  data,//该次操作后的节点快照
  timestamp
}

使用两个栈来记录操作历史,一个栈 undoStack 用于存储撤销记录,一个栈 history 用于存储历史记录。(或者用一个 history 数组+一个指针也可实现)

每次操作都记录到 history 栈中:

//需要找到pointer的位置在其后插入
kernel.history.push(record);

撤销操作:

kernel.history.pop();
const record = kernel.history[currentPointer];
kernel.undoStack.push(record);
kernel.graph.set(record.data);

重做操作:

const record = kernel.undoStack.pop();
kernel.history.push(record);
kernel.graph.set(record.data);

移动到任意一步:

const record = kernel.history[step];
//省略将后面的数据从history中逆序移动到undoStack中过程
kernel.graph.set(record.data);

特点:

  1. 可实现操作历史,支持恢复到任意一步操作
  2. 牺牲了空间,数据量太大,需要考虑性能问题
命令模式:

命令模式一般包含以下角色:

  1. CommandManager:命令管理者,调用接收者对应接口处理发布者的请求。
  2. Receiver:接收者,执行命令的具体操作。
  3. Invoker:调用者,负责调用命令对象执行命令。
    在 js 中可以简化一下,只需要定义一个 CommandManager 和 Receiver 即可,Invoker 即为具体调用的操作
    使用一个 history 数组保存操作记录,使用一个指针指向当前操作
//举例:添加节点命令,作为Receiver,kernel.ops类比CommandManager
const AddNodeOperation = {
  ...
  meta:{
    ...
    execute: function({node,kernel}) {
      const newNode={...}
      this.node=newNode;
      kernel.graph.addNode(newNode);
    },
    undo:function({kernel}){
      kernel.graph.removeNode(this.node.id);
    }
  },
  activate:function({kernel}){
  }
}

class CommandHistoryManager{
  constructor(){
    this.history=[];
    this.pointer=-1;
  }
  execute(command){
    command.execute();
    this.history.push(command);
    this.pointer++;
  }
  undo(){
    if(this.pointer>0){
      const command=this.history[this.pointer];
      command.undo();
      this.pointer--;
    }
  }
}

每次操作都记录到 history 栈中:

kernel.history.push(new CommandManager(AddNodeOperation));

撤销操作:

const command = history[pointer];
command.undo();

重做操作:

const command = history[pointer + 1];
command.execute();

特点:

  1. 不需要存储大量数据结构,可节省空间
  2. 无法恢复到任意步操作,只能一步步操作

关于数据存储

在 kernel 中接管了数据存储,当节点添加到画布之后,kernel 全托管了节点(特别节点需要数据回显的情况),所以数据也需要托管到 kernel 中,同样地,节点的表单数据也需要托管到 kernel 中,因此,用动态表单比普通表单也会更合适。

【参考】从 VS Code 看优秀插件系统的设计思路


网站公告

今日签到

点亮在社区的每一天
去签到