唯杰地图 logo唯杰地图
VJMAP
EN

plugin / 插件系统总览

vjcad/references/plugin/overview.md 8.1 KB147 行
原文

文档: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