VTJ 插件系统开发指南
VTJ 插件系统提供了一个灵活、可扩展的架构,用于将自定义组件、身份验证逻辑和运行时增强功能集成到低代码应用程序中。这份综合指南涵盖了插件架构模式、开发工作流以及面向扩展 VTJ 平台的高级开发者的集成技术。
插件架构概述
VTJ 实现了一个多层插件架构,支持三种主要的插件类别:区块插件(远程组件)、扩展插件(引擎增强)和运行时插件(框架级功能)。该系统利用依赖注入、动态加载和工厂模式,在不修改核心框架代码的情况下实现无缝集成。
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]
插件系统建立在核心协议定义之上,该协议确立了所有插件的契约。@vtj/core 中的 BlockPlugin 接口规定插件必须提供 Vue 组件和可选的 CSS 依赖。这种最小化的契约在保持类型安全的同时实现了最大的灵活性。
插件加载流水线
扩展系统通过一个复杂的流水线编排插件加载:
- 配置解析:从项目架构中提取扩展配置
- 依赖加载:动态注入 CSS 和 JavaScript 资源
- 工厂执行:插件工厂接收配置并生成引擎选项
- 集成合并:插件选项与基础引擎配置合并
platforms/pro中的Extension类实现了核心加载逻辑,处理基于对象和基于函数的插件工厂。基于函数的插件接收完整的 VTJConfig 对象和附加参数,从而支持上下文感知的初始化。
区块插件开发
区块插件允许将自定义组件作为远程包分发,并可以被 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 提供显式类型定义:
<script lang="ts" setup>
import { computed, ref } from "vue";
import { exampleProps, type ExampleEmits } from "./types";
defineOptions({
name: "VtjBlockExample",
});
const props = defineProps(exampleProps);
const emit = defineEmits<ExampleEmits>();
// 响应式状态和计算属性
const data = ref("default inner data");
const currentModelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
});
// 对外暴露的方法
defineExpose({
click,
submit,
data,
change,
});
</script>
类型定义模式
独立的类型定义文件确保 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);
}
扩展集成流程
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]
访问插件(身份验证与授权)
访问插件提供了全面的身份验证、授权和路由保护功能。它与 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);
身份验证流程
sequenceDiagram
participant User
participant Router
participant Access Plugin
participant Storage
participant API Service
User->>Router: 导航到受保护路由
Router->>Access Plugin: BeforeEach 守卫
Access Plugin->>Storage: 检查 token
alt [Token 存在]
Access Plugin->>API Service: 在请求头中包含 token
API Service-->>Access Plugin: 响应
alt [401 未授权]
Access Plugin->>User: 显示登录跳转
Access Plugin->>Router: 导航到认证页面
else [其他状态]
Access Plugin->>Router: 允许导航
end
else [无 token]
Access Plugin->>Router: 检查白名单
alt [不在白名单]
Access Plugin->>Router: 导航到认证页面
else [在白名单]
Access Plugin->>Router: 允许导航
end
end
请求拦截
访问插件自动拦截 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);
}
最佳实践
插件设计原则
- 隔离性:插件不应直接修改全局状态或 VTJ 内部
- 类型安全:始终为 props、emits 和 options 导出 TypeScript 类型
- 懒加载:仅在需要时加载插件依赖
- 错误恢复:优雅地处理插件加载失败
在开发远程区块插件时,请确保组件导出遵循默认导出模式以匹配插件加载器的预期。使用
defineOptions设置显式组件名称以便于调试和 Tree-shaking。
插件分发策略
| 分发方式 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
| NPM 包 | 稳定的公共插件 | 版本控制、类型定义、CDN 支持 | 需要构建流程、npm registry 访问 |
| 远程 URL | 私有插件、快速迭代 | 无构建步骤、即时更新 | 网络依赖、无类型安全 |
| 本地路径 | 开发、Monorepos | 快速反馈、完全控制 | 部署复杂性 |
性能优化
- 代码分割:将插件包拆分为核心功能和可选特性
- CSS 隔离:使用作用域样式或 CSS-in-JS 防止冲突
- Tree Shaking:仅导出需要的组件和工具
- 缓存:利用浏览器缓存远程插件资源
错误处理模式
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);
}
迁移路径
对于从其他插件系统迁移的开发者:
| 功能 | VTJ 实现 | 传统替代方案 |
|---|---|---|
| 组件注册 | 物料架构 + 插件入口 | 全局组件注册 |
| 依赖注入 | 引擎选项 + 提供者系统 | 原型链继承 |
| 动态加载 | Extension 类 + loadScriptUrl | require/import() |
| 类型安全 | TypeScript + 物料架构 | PropTypes / 运行时验证 |
下一步
- 自定义设置器和属性编辑器:了解如何使用自定义输入组件扩展属性配置系统
- 自定义小部件和设计器面板:构建设计器界面扩展以增强编辑能力
- 扩展提供者系统:深入研究跨组件状态共享的提供者模式
- 集成第三方库:整合外部 UI 库和工具的策略
参考实现
完整的插件示例可在 Monorepo 中找到:
- 区块插件:apps/plugin - 包含物料架构的功能齐全的示例组件
- 扩展系统:platforms/pro/src/extension.ts - 远程插件加载基础设施
- 访问插件:packages/renderer/src/plugins/access.ts - 身份验证/授权实现
- 加载工具:packages/renderer/src/utils/util.ts - 核心动态加载函数
参考资料
- 开源代码仓库:gitee.com/newgateway/…