插件生命周期与 Plugin 接口
文档:
vjcad/doc/plugin/plugin-basics.md示例:vjcad/example/plugin/04pluginmanager.js
1. Plugin 接口
import type { Plugin, PluginManifest, PluginContext } from 'vjcad';
const myPlugin: Plugin = {
manifest: PluginManifest,
async onLoad(context): Promise<void> { /* 加载完成(未激活) */ },
async onActivate(context): Promise<void> { /* 激活:注册命令/UI/事件 */ },
async onDeactivate(context): Promise<void> { /* 停用:注销/清理 */ },
async onUnload(context): Promise<void> { /* 最终清理 */ }
};
export default myPlugin;
四个钩子都是可选的,签名 Promise<void> | void(同步函数也行,管理器统一 await)。抛错后果按阶段不同:
| 抛错钩子 | 结果 |
|---|---|
onLoad |
插件不登记(getPluginInfo 查不到)、已注入的 CSS 移除,load* 抛 PluginError;修好后直接重新 load |
onActivate |
状态 Error 且仍留在列表中;再 activate 会抛 PluginError("in state error, cannot activate"),须先 unload 再重新 load |
onDeactivate |
状态 Error,且跳过 context._cleanup()——命令/菜单/Ribbon/事件仍挂着;unload 可完成清理 |
onUnload |
照样从列表移除、清理侧栏面板,但 _cleanup() 与 CSS 移除被跳过,unload 抛 PluginError |
URL 加载(
loadFromUrl/loadFromPath/loadByName与new MainView({ plugins }))的文件必须是打包好的 IIFE(挂全局变量),上面的export default是源码写法,详见 overview §3 说明。
2. PluginManifest
interface PluginManifest {
id: string; // 必填,唯一
name: string; // 必填,显示名
version: string; // 必填,semver
author?: string;
description?: string;
keywords?: string[];
dependencies?: string[]; // 依赖的其他插件 id
minEngineVersion?: string; // 最低 vjcad 版本
homepage?: string;
icon?: string; // 插件自身图标
}
dependencies 只是声明性元数据:PluginManager 既不会自动安装依赖,激活时也不做校验(bundle 中 activate() 只检查状态并调用 onActivate)。需要自行先 loadXxx + activate 依赖插件。
3. 状态枚举
enum PluginState {
Unloaded = 'unloaded',
Loading = 'loading',
Loaded = 'loaded',
Activating = 'activating',
Active = 'active',
Deactivating = 'deactivating',
Unloading = 'unloading',
Error = 'error'
}
stateDiagram-v2
[*] --> Unloaded
Unloaded --> Loading: load / loadByName / loadFromUrl
Loading --> Loaded: onLoad
Loaded --> Activating: activate
Activating --> Active: onActivate
Active --> Deactivating: deactivate
Deactivating --> Loaded: onDeactivate
Loaded --> Unloading: unload
Unloading --> Unloaded: onUnload
Loading --> Error: 加载失败
Activating --> Error: 激活失败
Deactivating --> Error: 停用失败
Unloading --> Error: 卸载失败
Error --> Unloaded: 重置
4. 钩子用途与边界
| 钩子 | 触发时机 | 推荐做的事 | 不建议做的事 |
|---|---|---|---|
onLoad |
pm.load* 解析完插件 |
校验依赖、初始化无副作用变量 | 注册命令 / 改 UI(此时还未激活) |
onActivate |
pm.activate(id) |
context.registerCommand / registerIcon / addMenuItem / addRibbon* / on(event) |
长时间阻塞(改用 setTimeout) |
onDeactivate |
pm.deactivate(id) |
与 onActivate 对称的 unregister / remove |
抛错(会进入 Error 状态且跳过自动清理) |
onUnload |
pm.unload(id) |
释放最终资源(关闭 worker / socket) | 重新注册命令 |
5. 自动激活
loadFromUrl / loadFromContent 默认 autoActivate = true:加载完即自动激活。要显式手动控制,在加载选项里传 { autoActivate: false }:
await pm.load({
type: PluginSourceType.Url,
js: 'plugins/my-plugin.js', // 必须是 IIFE 产物(挂全局变量),不能是 ES module 文件
css: 'plugins/my-plugin.css'
}, { autoActivate: false }); // 放第 2 参;写在 source 对象里会被丢弃(加载器重建 source 时不带 autoActivate)
await pm.activate('my-plugin');
// 等价:await pm.loadFromUrl('plugins/my-plugin.js', 'plugins/my-plugin.css', { autoActivate: false });
补充说明:pm.load({type: PluginSourceType.Content, js: code}) 对含 export 的 ES module 字符串走 Blob URL + import(),取 default 导出;不含 import/export 的 IIFE 代码则扫描全局 vjcadPlugin* 变量找带 manifest 的对象。autoActivate 默认 true,load 返回插件 id,getPluginInfo(id).state 激活后为 "active"(枚举值是 Active="active",不是 "activated")。
6. 生命周期事件
CadEvents 暴露了一组插件事件,可在外部监听:
| 事件 | 触发 |
|---|---|
PluginLoading / PluginLoaded |
加载前 / 后 |
PluginActivating / PluginActivated |
激活前 / 后 |
PluginDeactivating / PluginDeactivated |
停用前 / 后 |
PluginUnloading / PluginUnloaded |
卸载前 / 后 |
PluginError |
任何阶段失败 |
Engine.eventManager.on(CadEvents.PluginLoaded, args => {
console.log('插件已加载:', args.pluginId);
});
Engine.eventManager.on(CadEvents.PluginError, args => {
console.error('插件错误:', args.pluginId, args.error);
});
7. 没有 PluginManager 的"轻量版"
class MyPlugin {
install() { /* 注册 */ }
uninstall() { /* 注销 */ }
}
new MyPlugin().install();
完整可运行例子在 vjcad/example/plugin/01basic.js:
IconRegistry.registerCommandIconCommandRegistry.registMenuRegistry.getInstance().addMenuItem('tool', { command })Engine.view.ribbonBar.addGroup(...)
uninstall 时按相同顺序逆向 unregist / removeMenuItem / removeGroup。
红旗
onActivate / onDeactivate抛异常会让插件进入Error,再activate直接抛PluginError,必须先unload再重新 load;onLoad抛错则插件根本不登记(见 §1 表)dependencies没自动安装;先确保依赖插件已loadXxx + activateautoActivate=false时记得自己调pm.activate(id),否则插件不可见- 不要在
onActivate里阻塞(数据库 / fetch),用setTimeout/Promise.then异步推进 PluginManager按manifest.id去重:同 id 已加载时load*直接返回已有 id,不会再跑onLoad(loadFromUrl等仍会先把脚本再执行一次,loadByName则提前按 id 短路);要换新版本传{ forceReload: true }(内部先unload再注册)
参考(官方文档 MCP)
- 文档:
vjcad/doc/plugin/plugin-basics.md - 示例:
vjcad/example/plugin/04pluginmanager.js