唯杰地图 logo唯杰地图
VJMAP
EN

插件生命周期与 Plugin 接口

vjcad/references/plugin/lifecycle.md 7.1 KB162 行
原文

文档: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.registerCommandIcon
  • CommandRegistry.regist
  • MenuRegistry.getInstance().addMenuItem('tool', { command })
  • Engine.view.ribbonBar.addGroup(...)

uninstall 时按相同顺序逆向 unregist / removeMenuItem / removeGroup。

红旗

  • onActivate / onDeactivate 抛异常会让插件进入 Error,再 activate 直接抛 PluginError,必须先 unload 再重新 load;onLoad 抛错则插件根本不登记(见 §1 表)
  • dependencies 没自动安装;先确保依赖插件已 loadXxx + activate
  • autoActivate=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