plugin / 插件系统总览
文档:
vjcad/doc/plugin/index.md、vjcad/doc/plugin/plugin-basics.md示例:vjcad/example/plugin/01basic.js、04pluginmanager.js
1. 插件系统能做什么
vjcad 插件系统让你以 可热插拔 的方式扩展引擎:
- 新增命令、SVG 图标
- 添加菜单项 / Ribbon 标签 / Ribbon 组 / Ribbon 按钮
- 监听引擎事件(实体增删 / 命令执行 / 文档生命周期)
- 持久化插件设置(localStorage 风格 API)
- 引入新的实体类型(CustomEntityBase 派生)和服务类
官方插件示例:annotation-plugin(批注)、architecture-plugin(建筑)、insert-plugin(图片矢量化)、view3d-plugin(三维视图,最小骨架)、aiassist-plugin(AI 助手)、network-graph-plugin(网络拓扑)。
2. 两种写法
| 写法 | 入口 | 适合场景 |
|---|---|---|
| 标准插件(推荐) | 源码 export default { manifest, onLoad, onActivate, onDeactivate, onUnload },按 URL 加载须打包成 IIFE(见 §3 下方说明) |
需要 PluginManager 管理 / 跨项目复用 |
| 手写注册类 | 自己 class MyPlugin { install() / uninstall() },调用 CommandRegistry / IconRegistry / MenuRegistry / Engine.view.ribbonBar |
快速试验、内嵌业务代码 |
3. 标准插件骨架
// my-plugin.js — 标准结构
export default {
manifest: {
id: 'my-plugin',
name: '我的插件',
version: '1.0.0',
author: '...',
description: '...',
keywords: ['demo'],
dependencies: [],
minEngineVersion: '1.0.0'
},
async onLoad(context) { /* 资源准备,不暴露功能 */ },
async onActivate(context) {
context.registerIcon('MYCMD', '<svg>...</svg>');
context.registerCommand('MYCMD', '我的命令', MyCommandClass);
context.addMenuItem('tool', { command: 'MYCMD' });
context.addRibbonGroup('plugins', { id: 'my', label: '我的', primaryButtons: [...] });
},
async onDeactivate(context) {
context.removeRibbonGroup('plugins', 'my');
context.removeMenuItem('tool', 'MYCMD');
context.unregisterCommand('MYCMD');
},
async onUnload(context) { /* 最终清理 */ }
};
模块格式:上面的
export default是源码写法。loadFromUrl / loadFromPath / loadByName以及new MainView({ plugins })用经典<script>(失败再回退 fetch +<script>文本)执行文件,再扫描新增的全局变量(带manifest对象的变量或vjcadPlugin*变量)找插件——直接给它一个 ES module 文件会因export语法失败(Plugin did not export a valid object)。所以 URL 加载的文件必须是 IIFE 产物并挂全局,官方插件统一用 vite:build.lib: { formats: ['iife'], name: 'vjcadPluginXxx' }、rollupOptions: { external: ['vjcad'], output: { globals: { vjcad: 'vjcad' } } }。裸export default源码字符串只适用于loadFromContent(检测到import/export时走 Blob URL +import())。
4. 插件管理器(PluginManager)
const { PluginManager } = vjcad;
const pm = PluginManager.getInstance();
// 加载方式(常用 4 种;另有 loadFromCacheEntry(entry) 与通用 load(source) 两个公开入口)
await pm.loadFromUrl('./plugins/vcad-plugin-insert.js', './plugins/vcad-plugin-insert.css'); // (js, css?, options?)
await pm.loadFromPath('/static/plugins/vcad-plugin-architecture.js');
await pm.loadFromContent(pluginCode /* 字符串源码 */);
await pm.loadByName('vcad-plugin-architecture'); // 包名约定路径:默认 baseUrl './plugins/',只加载 JS
// 状态查询
pm.getLoadedPlugins(); // [{ manifest, state, loadedAt, activatedAt, error, source, origin, registeredCommands }, ...](无 id 字段,用 info.manifest.id)
pm.isLoaded('my-plugin');
pm.isActive('my-plugin');
pm.getPluginInfo('my-plugin'); // 返回 { ..., state } —— 没有独立 getPluginState;状态经 info.state 取
// 生命周期控制
await pm.activate('my-plugin');
await pm.deactivate('my-plugin');
await pm.unload('my-plugin');
完整代码:
vjcad/example/plugin/04pluginmanager.js
加载选项(loadFromUrl/loadFromPath/loadFromContent 的第 3 参、loadByName/load 的第 2 参):{ autoActivate = true, timeout = 30000, forceReload, origin };loadByName 另收 baseUrl。
loadByName(pkg)只拼${baseUrl}${pkg}.js,从不加载 CSS;自带样式的官方插件(insert/view3d/aiassist的 dist 都有.css)请用loadFromUrl(js, css)- 同
manifest.id已加载时直接返回已有 id(不重跑onLoad);要重载传{ forceReload: true }(先unload再注册) - 插件激活/停用/卸载时 RibbonBar 会自动刷新;激活之后再改
RibbonConfigManager.getConfig()(或用RibbonConfigManager.patchConfig({...})做增量修改)要调RibbonConfigManager.refresh()才会重绘
5. MainView 中预声明插件
const cadView = new MainView({
appname: '唯杰CAD',
serviceUrl: '...',
accessToken: '...',
plugins: [
{ url: './plugins/vcad-plugin-annotation.js' },
{ url: './plugins/vcad-plugin-architecture.js' },
{ url: './plugins/vcad-plugin-insert.js', cssUrl: './plugins/vcad-plugin-insert.css', autoActivate: true }
]
});
MainView 会在 onLoad 阶段自动按顺序加载(内部即 pm.loadFromUrl(url, cssUrl, { autoActivate }),autoActivate 默认 true)、激活这些插件;单个失败只 console.error,不中断其余。文件同样必须是 IIFE 产物(见 §3 说明)。
6. 子文档
- 生命周期 / Plugin 接口 / Manifest / 事件 → lifecycle
- PluginContext API(命令、图标、菜单、Ribbon、事件、存储) → context-api
- 菜单与 Ribbon 详解(默认 ID、增量/全量、MainView 配置) → menu-ribbon
- 官方插件指南(annotation / architecture / view3d / insert;另有 aiassist / network-graph,无 "tools" 插件) → builtin-plugins
7. 心智模型
flowchart LR
subgraph PluginManager
P[Plugin] --> L[Loaded]
L --> A[Active]
A -.-> D[Deactivated]
D --> A
D --> U[Unloaded]
end
A -->|context.registerXxx| Eng[Engine\nIconRegistry / CommandRegistry / MenuRegistry / RibbonBar / EventManager]
D -->|removeXxx| Eng
激活态才会在 UI 露出;停用即可隐藏,无需重新加载。
红旗
- 生命周期与函数对照:注册放
onActivate,注销放onDeactivate,加载/卸载阶段不要碰 UI - 多实例 / 多次激活:插件
id必须唯一(同 id 再 load 直接复用旧实例);addMenu / addRibbonTab重复 ID 会被忽略(addMenu只对addMenu建的动态菜单去重,细节见 context-api 红旗) - 资源清理顺序遵循"添加的逆序":先移按钮,再移组,再移标签
- 命令名规范全大写、图标名小写、菜单/组/标签 ID 用 kebab-case
- 用
loadFromContent时插件代码内用全局vjcad.<导出名>,不能 直接import 'vjcad'(沙箱里没有打包器) - 顶层没有
registerPlugin/pluginManager导出:唯一入口是PluginManager.getInstance();PluginSourceType枚举已导出(Url/Path/Content),pm.load({type: PluginSourceType.Content, js})可加载export default {manifest, ...}的 ES module 字符串 deactivate后context._cleanup()确实自动注销命令/事件/菜单/Ribbon(executerWithOp不再执行已注销命令;不含once、registerIcon,侧栏面板仅unload时移除);但注意executerWithOp对不存在命令是静默 resolve,无法用抛错判断注销与否
参考(官方文档 MCP)
- 文档:
vjcad/doc/plugin/index.md、vjcad/doc/plugin/plugin-basics.md - 示例:
vjcad/example/plugin/01basic.js、04pluginmanager.js