flowchart TD
A[VTJ Application] --> B[Extension System]
A --> E[Runtime Plugins]
B --> C[Remote Block Plugins]
B --> D[Engine Extensions]
E --> F[Access Plugin]
E --> G[Custom Runtime Plugins]
C --> H[Dynamic Component Loading]
C --> I[Material Schema]
D --> J[Engine Options]
D --> K[Provider Extensions]
F --> L[Authentication]
F --> M[Authorization]
F --> N[Route Guards]
H --> O[loadScriptUrl]
H --> P[loadCssUrl]
flowchart TD
A[Project Init] --> B[Extract Extension Config]
B --> C[Create Extension Instance]
C --> D{Has Library?}
D -- No --> E[Return Base Options]
D -- Yes --> F[Load CSS URLs]
F --> G[Load JS URLs]
G --> H[Execute Extension Factory]
H --> I[Merge with Engine Options]
I --> J[Initialize Engine]
参考资料
开源代码仓库:gitee.com/newgateway/…
VTJ 插件系统开发指南
VTJ 插件系统提供了一个灵活、可扩展的架构,用于将自定义组件、身份验证逻辑和运行时增强功能集成到低代码应用程序中。这份综合指南涵盖了插件架构模式、开发工作流以及面向扩展 VTJ 平台的高级开发者的集成技术。
插件架构概述
VTJ 实现了一个多层插件架构,支持三种主要的插件类别:区块插件(远程组件)、扩展插件(引擎增强)和运行时插件(框架级功能)。该系统利用依赖注入、动态加载和工厂模式,在不修改核心框架代码的情况下实现无缝集成。
插件系统建立在核心协议定义之上,该协议确立了所有插件的契约。
@vtj/core中的BlockPlugin接口规定插件必须提供 Vue 组件和可选的 CSS 依赖。这种最小化的契约在保持类型安全的同时实现了最大的灵活性。插件加载流水线
扩展系统通过一个复杂的流水线编排插件加载:
区块插件开发
区块插件允许将自定义组件作为远程包分发,并可以被 VTJ 应用程序动态加载。这些插件遵循物料架构契约,定义属性、事件、插槽和默认代码片段。
插件结构
一个完整的区块插件需要三个核心文件:
vtj-block-example/ ├── src/ │ ├── component/ │ │ ├── Example.vue │ │ ├── types.ts # TypeScript 类型定义 │ │ ├── index.ts # 组件导出 │ │ └── style.scss # 组件样式 │ ├── material.json # 物料架构定义 │ └── index.ts # 插件入口点 ├── package.json # 包配置 └── vite.config.ts # 构建配置组件实现
Vue 组件遵循标准的组合式 API 模式,并为 props 和 emits 提供显式类型定义:
类型定义模式
独立的类型定义文件确保 TypeScript 支持和文档生成:
export type ComponentPropsType<T> = Readonly<Partial<ExtractPropTypes<T>>>; export const exampleProps = { stringProp: { type: String }, booleanProp: { type: Boolean }, numberProp: { type: Number }, selectProp: { type: String }, objectProp: { type: Object }, arrayProp: { type: Array }, iconProp: { type: String }, colorProp: { type: String }, modelValue: { type: String }, syncProp: { type: String }, }; export type ExamplePropsProps = ComponentPropsType<typeof exampleProps>; export type ExampleEmits = { click: [props: ExamplePropsProps]; submit: [props: ExamplePropsProps]; change: [data: any]; "update:modelValue": [value?: string]; "update:syncProp": [value?: string]; };物料架构配置
material.json文件定义了插件的设计器界面:{ "name": "VtjBlockPlugin", "label": "区块测试插件", "props": [ { "name": "booleanProp", "label": "布尔值", "setters": "BooleanSetter", "title": "提示说明文本", "defaultValue": true }, { "name": "selectProp", "setters": "SelectSetter", "defaultValue": "default", "options": ["default", "primary", "success", "warning", "danger", "info"] } ], "events": [ { "name": "click", "params": ["props"] }, { "name": "submit", "params": ["props"] }, { "name": "change", "params": ["data"] } ], "slots": [ { "name": "default", "params": ["props", "data"] }, { "name": "extra", "params": ["props", "data"] } ], "snippet": { "props": {} } }插件注册
在
package.json的vtj.plugins字段中注册插件:{ "vtj": { "plugins": [ { "id": "v-test", "name": "VTest", "library": "VTest", "title": "测试", "urls": "xxx.json,xxx.css,xxx.js" } ] } }扩展系统开发
扩展通过提供与基础 VTJ 设置合并的配置选项来修改引擎行为。扩展可以是静态对象,也可以是接收 VTJConfig 的工厂函数。
扩展工厂模式
ExtensionFactory类型支持对象和函数格式:export type ExtensionFactory = | Partial<EngineOptions> | ((config: VTJConfig, ...args: any) => Partial<EngineOptions>);动态扩展加载
Extension类处理远程插件加载,支持 CSS 和 JavaScript:async load(): Promise<ExtensionOutput> { let options: Partial<EngineOptions> = {}; if (this.library) { const base = this.__BASE_PATH__; const css = this.urls .filter((n) => renderer.isCSSUrl(n)) .map((n) => `${base}${n}`); const scripts: string[] = this.urls .filter((n) => renderer.isJSUrl(n)) .map((n) => `${base}${n}`); renderer.loadCssUrl(css); if (scripts.length) { const output: ExtensionFactory = await renderer .loadScriptUrl(scripts, this.library) .catch(() => null); if (output && typeof output === 'function') { options = output.call(output, this.config, ...this.params); } else { options = output || {}; } } } return Object.assign({}, this.getEngineOptions(), options); }扩展集成流程
访问插件(身份验证与授权)
访问插件提供了全面的身份验证、授权和路由保护功能。它与 Vue Router 和请求拦截器集成以执行安全策略。
访问配置
export interface AccessOptions { session: boolean; // Token 存储在 cookie (session) 还是 localStorage authKey: string; // 请求头/cookie token 名称 storageKey: string; // 本地存储键前缀 storagePrefix: string; // 本地存储键 whiteList?: string[] | ((to: RouteLocationNormalized) => boolean); unauthorized?: string | (() => void); auth?: string | ((search: string) => void); isAuth?: (to: RouteLocationNormalized) => boolean; redirectParam?: string; unauthorizedCode?: number; alert?: (message: string, options?: Record<string, any>) => Promise<any>; unauthorizedMessage?: string; noPermissionMessage?: string; privateKey?: string; // RSA 解密密钥 appName?: string; statusKey?: string; // 响应状态字段名 }访问集成模式
import { Access, ACCESS_KEY } from "@vtj/renderer"; const access = new Access({ session: false, authKey: "Authorization", storageKey: "ACCESS_STORAGE", whiteList: ["/login", "/public"], unauthorized: "/#/unauthorized", auth: "/#/login", unauthorizedCode: 401, }); access.connect({ mode: ContextMode.Runtime, router: router, request: requestInstance, }); app.provide(ACCESS_KEY, access);身份验证流程
请求拦截
访问插件自动拦截 HTTP 请求以注入身份验证 token:
this.request?.interceptors.request.use((config) => { if (this.data && this.data.token) { config.headers[this.options.authKey] = this.data.token; } return config; }); this.request?.interceptors.response.use( (response) => response, (error) => { const status = error.response?.data?.[this.options.statusKey]; if (status === this.options.unauthorizedCode && this.interceptResponse) { this.handleUnauthorized(); } return Promise.reject(error); }, );插件加载工具
VTJ 在
@vtj/renderer/utils中提供了用于动态插件加载的工具函数。CSS 加载
export function loadCssUrl(urls: string[], global: any = window) { const doc = global.document; const head = global.document.head; for (const url of urls) { const el = doc.getElementById(url); if (!el) { const link = doc.createElement("link"); link.rel = "stylesheet"; link.id = url; link.href = url; head.appendChild(link); } } }JavaScript 加载
export async function loadScriptUrl( urls: string[], library: string, global: any = window, ) { const doc = global.document; const head = global.document.head; let module = global[library]; if (module) return module.default || module; return new Promise((resolve, reject) => { for (const url of urls) { const el = doc.createElement("script"); el.src = url; el.onload = () => { module = global[library]; if (module) { resolve(module.default || module); } else { reject(null); } }; el.onerror = (e: any) => reject(e); head.appendChild(el); } }); }URL 类型检测
export function isCSSUrl(url: string): boolean { return /\.css(\?.*)?$/.test(url); } export function isJSUrl(url: string): boolean { return /\.js(\?.*)?$/.test(url); }最佳实践
插件设计原则
插件分发策略
性能优化
错误处理模式
export class PluginError extends Error { constructor( public pluginId: string, public originalError: Error, ) { super(`Plugin [${pluginId}] failed: ${originalError.message}`); this.name = "PluginError"; } } // 在插件加载器中的使用 try { const plugin = await loadPlugin(config); return plugin; } catch (error) { console.error("Plugin loading failed:", error); throw new PluginError(config.id, error as Error); }迁移路径
对于从其他插件系统迁移的开发者:
下一步
参考实现
完整的插件示例可在 Monorepo 中找到:
参考资料