跳到主要内容

Spark Workbench Dashboard Gadget 插件开发指南

本篇面向需要二次扩展 Spark Workbench Dashboard Gadget 的开发者。目标是把一个 Gadget 插件做成一个自包含工程:后端扩展点、前端 Vue 组件、前端构建产物都放在同一个插件模块中,平台主工程只负责加载和运行。

1. Gadget 插件模型​

Spark Workbench 的 Gadget 由两部分组成:

  • 后端扩展点:通过 Lattice Ability 注册 Gadget 描述信息,并在运行期渲染数据。
  • 前端组件:通过 Vue 组件展示数据,打包成 UMD 资源后由 Workbench 动态加载。

默认插件位于:

spark-plugins/spark-workbench-default-plugin/

这个模块同时包含 Java 与前端代码。第三方插件也建议采用同样结构,避免把具体 Gadget 代码散落到平台主工程。

2. 推荐工程结构​

my-workbench-gadget-plugin/
pom.xml
src/main/java/
.../MyGadgetRegistryExt.java
src/main/frontend/
package.json
vite.config.js
src/
index.js
GadgetMyChart.vue
GadgetMyList.vue

其中:

  • MyGadgetRegistryExt.java 负责声明 Gadget 类型、标题、分类、图标、前端资源 URL,并返回运行期数据。
  • src/main/frontend/src/index.js 是前端入口,负责把 Vue 组件注册给 Workbench。
  • vite.config.js 使用 library 模式构建 UMD 文件,并把产物输出到插件 jar 的静态资源目录。

3. 后端扩展点​

插件需要实现 SparkEraTaskGadgetRegistryExt,通常继承空实现:

@Realization(codes = "my.workbench.gadget")
public class MyGadgetRegistryExt extends BlankSparkEraTaskGadgetRegistryExt {

private static final String FRONTEND_RESOURCE =
"/spark-gadgets/my-workbench-gadget/index.umd.js";

@Override
public List<SparkEraTaskGadgetDescriptor> listGadgetDescriptors(SparkEraTaskGadgetCatalogContext context) {
SparkEraTaskGadgetDescriptor descriptor = new SparkEraTaskGadgetDescriptor();
descriptor.setCode("myChart");
descriptor.setTitle("My Chart");
descriptor.setCategory("charts");
descriptor.setIcon("bar");
descriptor.setComponent("spark$gadget-my-chart");
descriptor.setFrontendResource(FRONTEND_RESOURCE);
descriptor.setConfigurable(true);
return List.of(descriptor);
}

@Override
public SparkEraTaskGadgetRenderResult renderGadget(SparkEraTaskGadgetRenderContext context) {
if (!StringUtils.equals(context.getGadgetType(), "myChart")) {
return null;
}
SparkEraTaskGadgetRenderResult result =
SparkEraTaskGadgetRenderResult.of("myChart", "spark$gadget-my-chart");
result.getData().put("bars", List.of(Map.of("label", "Open", "value", 12)));
return result;
}
}

关键字段:

  • code:Gadget 类型编码,要求全局唯一。
  • component:前端组件注册 key,建议使用 spark$gadget-xxx。
  • frontendResource:插件前端入口 JS 的运行期 URL。
  • configurable:是否允许在 Dashboard 中打开配置面板。

4. 前端自注册入口​

Gadget 前端入口负责在脚本加载后注册组件:

import GadgetMyChart from "./GadgetMyChart.vue";

const COMPONENTS = {
"spark$gadget-my-chart": GadgetMyChart
};

const api = typeof window === "undefined" ? null : window.__SPARK_DASHBOARD__;
if (api && typeof api.registerGadgetComponent === "function") {
Object.entries(COMPONENTS).forEach(([key, component]) => {
api.registerGadgetComponent(key, component);
});
}

Workbench 在加载 Dashboard catalog 后,会收集所有 frontendResource,去重后动态注入 <script>。脚本执行完成后,组件进入宿主注册表,后续 render result 中的 component 就能解析到对应 Vue 组件。

5. Vite 构建配置​

插件前端应使用 Vite library 模式,并把 Vue 设为 external。这样插件不会打包自己的 Vue,运行期使用宿主提供的 window.__SPARK_VUE__。

import { resolve } from "node:path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
plugins: [vue()],
build: {
outDir: resolve(__dirname, "../../../target/classes/META-INF/resources/spark-gadgets/my-workbench-gadget"),
lib: {
entry: resolve(__dirname, "src/index.js"),
name: "MyWorkbenchGadget",
formats: ["umd"],
fileName: () => "index.umd.js",
cssFileName: "index"
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "__SPARK_VUE__"
}
}
}
}
});

构建后会得到:

target/classes/META-INF/resources/spark-gadgets/my-workbench-gadget/index.umd.js
target/classes/META-INF/resources/spark-gadgets/my-workbench-gadget/index.css

Spring Boot 会自动服务 META-INF/resources 下的静态资源,所以运行期 URL 就是:

/spark-gadgets/my-workbench-gadget/index.umd.js

6. Maven 打包​

插件 pom.xml 可以通过 frontend-maven-plugin 在 prepare-package 阶段构建前端:

<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<workingDirectory>src/main/frontend</workingDirectory>
<nodeVersion>v18.19.1</nodeVersion>
<npmVersion>9.2.0</npmVersion>
</configuration>
<executions>
<execution>
<id>install-node-and-npm</id>
<phase>prepare-package</phase>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm-ci</id>
<phase>prepare-package</phase>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>ci</arguments>
</configuration>
</execution>
<execution>
<id>npm-build</id>
<phase>prepare-package</phase>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>

完成后,插件 jar 同时包含 Java 扩展点和前端资源。平台只需要依赖这个插件 jar。

7. 组件开发约定​

  • 组件只接收 data 和 t 等宿主传入的 props,不直接请求平台接口。
  • 样式使用 scoped 和 Spark 主题变量,例如 --text-main、--card-bg、--primary。
  • 不要在插件里重新创建 Vue app,也不要打包独立 Vue 运行时。
  • 数据结构由 renderGadget 返回,前端组件只负责展示。
  • 可配置项写入 Gadget instance 的 config,由后端 render 时读取。

一个简单 Vue 组件示例:

<template>
<div class="my-gadget">
<div v-for="item in rows" :key="item.label" class="my-row">
<span>{{ item.label }}</span>
<b>{{ item.value }}</b>
</div>
</div>
</template>

<script setup>
import { computed } from "vue";

const props = defineProps({
data: { type: Object, default: () => ({}) },
t: { type: Function, default: (key) => key }
});

const rows = computed(() => Array.isArray(props.data?.rows) ? props.data.rows : []);
</script>

8. 本地验证清单​

开发完成后建议按下面顺序验证:

cd src/main/frontend
npm ci
npm run build

然后在插件模块执行:

mvn -DskipTests package

检查 jar 中是否包含:

META-INF/resources/spark-gadgets/<plugin-code>/index.umd.js
META-INF/resources/spark-gadgets/<plugin-code>/index.css

启动 Spark Console 后,在浏览器访问:

/spark-gadgets/<plugin-code>/index.umd.js

如果能返回 JS,说明静态资源已经由插件 jar 成功注册。

9. 常见问题​

Gadget 显示 Unsupported

通常是 component key 和 index.js 注册 key 不一致,或者 frontendResource 没有被正确加载。

样式没有生效

确认 Vite 是否生成了 index.css,并在入口脚本中注入了同路径 CSS。默认插件采用从 document.currentScript.src 推导 CSS URL 的方式。

运行期 Vue 报错

确认 Vite 配置中 vue 已 external,并且 output.globals.vue 指向 __SPARK_VUE__。插件不应打包第二份 Vue。

插件 catalog 中没有出现

确认插件 jar 已被 Spark Console 依赖,且 @Realization(codes = "...") 的业务身份已经被 Lattice 扫描。

10. 交付原则​

Dashboard Gadget 插件应该遵循一个 artifact 自包含原则:

  • Java 扩展点在插件模块。
  • Vue 组件在插件模块。
  • 前端构建产物进入插件 jar。
  • 平台主工程只保留注册表、动态加载器、卡片外壳和兜底组件。

这样新增一个业务 Gadget 时,平台不需要改核心代码,只需引入插件依赖即可完成扩展。