插件架构(微内核架构)
支持软件能力可扩展、可定制
核心概念:
- 内核:作为应用入口(主体),提供基础服务、插件调度、事件中心等等
- 插件:实现标准的初始化、启动、关闭等标准接口,实现插件的生命周期管理
- 插件接口(插件注册机制):插件接入内核的方式
- 上下文信息:在插件间传递的共享数据
三种插件模式
常用以下三种插件模式:管道式、洋葱式和事件式,
管道式插件设计
- 比如 命令行
cat file.txt | grep "keyword"
- 比如 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);
特点:
- 可实现操作历史,支持恢复到任意一步操作
- 牺牲了空间,数据量太大,需要考虑性能问题
命令模式:
命令模式一般包含以下角色:
- CommandManager:命令管理者,调用接收者对应接口处理发布者的请求。
- Receiver:接收者,执行命令的具体操作。
- 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();
特点:
- 不需要存储大量数据结构,可节省空间
- 无法恢复到任意步操作,只能一步步操作
关于数据存储
在 kernel 中接管了数据存储,当节点添加到画布之后,kernel 全托管了节点(特别节点需要数据回显的情况),所以数据也需要托管到 kernel 中,同样地,节点的表单数据也需要托管到 kernel 中,因此,用动态表单比普通表单也会更合适。