Day 18 / 共 20 天 · 第 4 周 前端/部署

动态插件配置表单(技术高峰)

这是整个前端最精华的一讲。43 个插件各有不同配置项,前端不可能为每个插件手写表单——它从后端返回的 JSON Schema 动态生成表单,还能和 YAML 源码双向同步。今天拆透这套机制。

📍 你在整门课的位置 · 第 4 周 前端/部署(Day 16-20)· 本讲是前端技术高峰
D16 前端技术栈 D17 前端结构 D18 动态表单 D19 部署 D20 收官串讲
L01

问题:插件配置千变万化

🤔 痛点:43 个插件、每个配置都不同 限流插件要填"QPS 数字",鉴权插件要填"允许的消费者列表",AI 插件要填"模型映射"……而且以后还会加新插件。要是为每个插件手写一个表单页,工作量爆炸,加新插件还得改前端上线。
💡 本质:照着"说明书"自动拼表单 后端用 JSON Schema 描述"这个插件有哪些配置项、什么类型"(像一张乐高说明书),前端读说明书自动"拼"出表单。加新插件只要后端给张新说明书,前端零改动。这就是"schema 驱动的动态表单"——把"为每个插件写表单"变成"写一个能读任意说明书的通用拼装器"。
📝 举个例子(schema → 控件) 后端返回片段 {type:"object", properties:{qps:{type:"integer", title:"每秒请求数"}}} → 前端 generateFields 看到 integer → 渲染出一个"每秒请求数"数字输入框。你填 100 → 存回 qps: 100
为什么不能给每个插件写死表单? 限流插件要填"QPS 数字",鉴权插件要填"允许的消费者列表",AI 插件要填"模型映射"……43 个插件、每个配置项都不同,而且以后还会加新插件。如果为每个插件手写一个表单页,工作量爆炸,加新插件还得改前端。解法:让后端用 JSON Schema 描述"这个插件有哪些配置项、什么类型",前端读 Schema 自动"画"出表单。加新插件只需后端给个 Schema,前端零改动。这就是"schema 驱动的动态表单"。
L02

插件端点

services/plugin.ts(88 行):

GET /v1/wasm-plugins/{name}/config     :31-33  // ← 返回配置 Schema(动态表单数据源)
GET /v1/wasm-plugins/{name}/readme     :84-88
// 三种作用域的实例 CRUD:
/v1/global/plugin-instances[/{name}]           :36-48
/v1/routes/{name}/plugin-instances/{pluginName} :51-82
/v1/domains/{name}/plugin-instances/{pluginName}
读法:/config 就是 Day 12 后端从 spec.yaml 加载的那个 openAPIV3Schema。前端拿它渲染表单。三种作用域对应后端 Day 09 的 ROUTE/DOMAIN/GLOBAL。
L03

抽屉分发

pages/plugin/components/PluginDrawer/index.tsx:12-17 有个 PLUGIN_COMP_MAPrewrite/headerModify/cors/retries 四个内置策略用专用组件,其余(普通 wasm 插件)走 schema 驱动的通用表单 GlobalPluginDetail:70-90)。

为什么有的插件用专用表单? rewrite/cors 这类"路由原生能力"(不是真正的 Wasm 插件)交互特殊,用手写的专用组件体验更好。其余几十个真正的 Wasm 插件,全部走同一个通用动态表单。专用的少数、通用的多数——务实的取舍。
L04

数据流

GlobalPluginDetail.tsx(767 行,最核心)的数据流:

getData拉实例配置
(rawConfigurations YAML)
getConfig拉 Schema
setSchema
yamlToFormValues
YAML 反解成表单值
generateFields
递归渲染表单

getConfig:173-198):若 schema 没有 jsonSchema.properties 则强制切到 YAML 编辑 tab(:178-180)——即无 schema 的插件只能裸写 YAML;取 x-example-raw 作示例默认值。

读法:这就回收了 Day 12 的伏笔——后端 schema 校验虽是空 TODO,但 schema 在前端派了大用场:驱动整个表单渲染。没 schema 就退化成纯 YAML 编辑器。
L05

递归渲染 generateFields

👶 小白 vs 👨‍🏫 老师 👶:"递归"到底是什么?为什么处理表单非它不可?
👨‍🏫:配置能层层嵌套——一个 object 里还有 object,里面可能又有 object。就像俄罗斯套娃,你不知道套了几层。
👶:那怎么全部拆开?
👨‍🏫:让函数调用它自己generateFields 遇到 object 类型,就再调一次自己去处理它的子字段,无论套多深都能一层层展开。这就是递归——处理"树状结构"的标准手法,动态表单能应对任意复杂 schema 全靠它。

generateFields(scm, prefix):230-341)遍历 schema 的 properties,按类型映射组件:

string  → Input
integer → Input[number] + parseInt
number  → Input[number] step=any + parseFloat
boolean → Select(true/false)
object 且有子 properties → 递归(用 Card 包成嵌套子表单)  :244-258
array   → (先 validateArrayTypes 校验元素类型)  :302-323

getLocalizedText:225-228)读 x-{title/description}-i18n[语言]——正是 Day 12/16 那套多语言约定。

"递归"为什么关键? 配置可以嵌套(一个 object 里还有 object)。递归 = 函数调用自己:遇到 object 类型就再调一次 generateFields 处理它的子字段,无论嵌套多深都能展开。这是处理"树状结构"的标准手法,动态表单能应对任意复杂的 schema 全靠它。
L06

表单 ↔ YAML 双向同步

表单 tab点选控件(新手) YAML tab直接写源码(专家) schemaToYaml / fieldChange yamlToFormValues(扁平化 a.b.c) 底层数据只有一份 rawConfigurations,两个 tab 是它的两种视图
表单和 YAML 是同一份数据的两种视图,靠一组扁平化/还原函数实时互译——像同声传译,说中文和说英文的听到的是同一个意思。

用户可以在"表单 tab"和"YAML 源码 tab"间切换,两边数据必须实时同步。这套转换是本讲的技术核心:

yamlToFormValues :426-462  // YAML → 扁平化成 a.b.c 点号键,数组包成 {uid,data}
formValuesToSchema :343-380 // 点号键 → 还原成嵌套对象
schemaToYaml :382-416       // 对象 → 手写 YAML 序列化(跳过 enabled,字符串加引号)
fieldChange :594-603        // 表单任意字段变化 → 实时转回 YAML 存 rawConfigurations
为什么要双向? 有的用户喜欢点表单(直观),有的高级用户喜欢直接写 YAML(快)。Console 两种都给,但底层数据只有一份。所以要在两种表示间实时互转:改表单立刻同步到 YAML、改 YAML 也能反解回表单。fieldChange 保证切 tab 不丢数据——这是最容易出 bug 的地方,作者用一套严谨的扁平化/还原逻辑搞定。
L07

ArrayForm 与 Monaco

  • ArrayFormArrayForm/index.tsx,302 行):可编辑表格(antd Table + 行内 Form),每行 {uid,data},可增删,用于渲染 schema 里 type=array 的字段。
  • CodeEditorcomponents/CodeEditor/index.tsx:26-58):封装 Monaco,loader.config({monaco}) 用本地 monaco(配合 Day 16 拷贝的 vs 目录),autoHeight 时用 ResizeObserver 自适应高度。

渲染骨架(:614-763):enabled 开关 + 三个 Tab:form(动态表单)/ yaml(Monaco)/ readme(ReactMarkdown + remarkGfm,支持全屏)。提交 onSubmit:515-528)拼 {...pluginData, enabled, rawConfigurations}

读法:三个 tab 完整覆盖三类用户:新手用表单、专家写 YAML、想了解插件就看 README。这个 767 行的组件是整个前端工程能力的集中体现。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么不能给每个插件手写表单?schema 驱动怎么解决?
  • 数据流四步(getData→getConfig→yamlToFormValues→generateFields)?
  • 递归渲染怎么处理嵌套 object?各类型映射成什么组件?
  • 表单↔YAML 双向同步为什么必要?谁保证切 tab 不丢数据?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress-console/frontend/src/pages/plugin
cat ../../services/plugin.ts
sed -n '12,17p' components/PluginDrawer/index.tsx
sed -n '230,341p' components/PluginDrawer/GlobalPluginDetail.tsx
sed -n '426,462p' components/PluginDrawer/GlobalPluginDetail.tsx
明天预告 · Day 19部署——前后端一体化 Maven 打包、SPA 回退再回顾、Dockerfile、Helm Chart,以及"Grafana/Prometheus/Loki 可观测栈"的集成闭环。
← Day 17 Day 19 · 部署 →