AI 驱动的 Vue3 应用开发平台 深入探究(十四):扩展与定制之插件系统开发指南

0 阅读7分钟

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 依赖。这种最小化的契约在保持类型安全的同时实现了最大的灵活性。

插件加载流水线

扩展系统通过一个复杂的流水线编排插件加载:

  1. 配置解析:从项目架构中提取扩展配置
  2. 依赖加载:动态注入 CSS 和 JavaScript 资源
  3. 工厂执行:插件工厂接收配置并生成引擎选项
  4. 集成合并:插件选项与基础引擎配置合并

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.jsonvtj.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);
}

最佳实践

插件设计原则

  1. 隔离性:插件不应直接修改全局状态或 VTJ 内部
  2. 类型安全:始终为 props、emits 和 options 导出 TypeScript 类型
  3. 懒加载:仅在需要时加载插件依赖
  4. 错误恢复:优雅地处理插件加载失败

在开发远程区块插件时,请确保组件导出遵循默认导出模式以匹配插件加载器的预期。使用 defineOptions 设置显式组件名称以便于调试和 Tree-shaking。

插件分发策略

分发方式使用场景优点缺点
NPM 包稳定的公共插件版本控制、类型定义、CDN 支持需要构建流程、npm registry 访问
远程 URL私有插件、快速迭代无构建步骤、即时更新网络依赖、无类型安全
本地路径开发、Monorepos快速反馈、完全控制部署复杂性

性能优化

  1. 代码分割:将插件包拆分为核心功能和可选特性
  2. CSS 隔离:使用作用域样式或 CSS-in-JS 防止冲突
  3. Tree Shaking:仅导出需要的组件和工具
  4. 缓存:利用浏览器缓存远程插件资源

错误处理模式

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 类 + loadScriptUrlrequire/import()
类型安全TypeScript + 物料架构PropTypes / 运行时验证

下一步

  • 自定义设置器和属性编辑器:了解如何使用自定义输入组件扩展属性配置系统
  • 自定义小部件和设计器面板:构建设计器界面扩展以增强编辑能力
  • 扩展提供者系统:深入研究跨组件状态共享的提供者模式
  • 集成第三方库:整合外部 UI 库和工具的策略

参考实现

完整的插件示例可在 Monorepo 中找到:

  • 区块插件:apps/plugin - 包含物料架构的功能齐全的示例组件
  • 扩展系统:platforms/pro/src/extension.ts - 远程插件加载基础设施
  • 访问插件:packages/renderer/src/plugins/access.ts - 身份验证/授权实现
  • 加载工具:packages/renderer/src/utils/util.ts - 核心动态加载函数

参考资料