news 2026/9/18 23:15:37

Headlamp 插件开发:基于 sidebar 示例插件深入理解侧边栏与路由注册体系

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Headlamp 插件开发:基于 sidebar 示例插件深入理解侧边栏与路由注册体系

Headlamp 插件开发:基于 sidebar 示例插件深入理解侧边栏与路由注册体系

【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlamp

Headlamp 是一款可扩展的 Kubernetes Web UI,其左侧边栏(Sidebar)和 URL 路由体系是插件介入 UI 的两个核心扩展点。本文以仓库中的官方示例插件 sidebar 为主体,完整讲解如何通过registerSidebarEntryregisterRoute及其过滤器 API 增删 Headlamp 侧边栏菜单项与页面路由,并结合 frontend/src/plugin/registry.tsx 与 Redux slice 源码,说明这些注册在底层是如何被存储、过滤和转换为/c/<集群名>/...形式 URL 的。读完本文,你将能够独立编写、运行并调试一个自定义 Headlamp 插件,并理解侧边栏条目与路由之间的绑定关系。

一、运行示例插件

示例插件位于 plugins/examples/sidebar,按文档说明,启动方式非常直接:

cd plugins/examples/sidebar npm install npm start # 此时观察 Headlamp 左侧边栏,会发现菜单已经发生变化

npm start背后调用的是 plugins/examples/sidebar/package.json 中定义的headlamp-plugin start脚本。该插件的 package.json 展示了 Headlamp 插件工程的典型骨架:

  • 所有脚本(startbuildformatlinttscstorybooktesti18n)都委托给headlamp-pluginCLI,由插件工具链统一管理构建;
  • 唯一的开发依赖是@kinvolk/headlamp-plugin(当前版本约束为^0.13.1),插件运行时 API 正是从该包导入;
  • 通过overrides将 typescript 固定在5.6.2,并沿用@headlamp-k8sESLint 与 Prettier 配置。

插件主代码只有一个文件:plugins/examples/sidebar/src/index.tsx,另有一个 src/headlamp-plugin.d.ts,其内容仅为/// <reference types="@kinvolk/headlamp-plugin" />,用于把工具链的类型声明挂到工程里。

从该插件当前版本的代码看,它完成的能力比 README 摘要所述更多:除了放置"Feedback"菜单项、删除 Namespaces 菜单外,它还演示了子菜单、自定义侧边栏、HOME 侧边栏、非集群路由等全部侧边栏 API 形态。下文按 API 逐个拆解。

二、registerSidebarEntry:添加侧边栏菜单项

示例插件中几乎每个registerSidebarEntry调用都值得逐行阅读。API 从@kinvolk/headlamp-plugin/lib导入,最终实现位于 frontend/src/plugin/registry.tsx:

export function registerSidebarEntry({ parent, name, label, url, useClusterURL = true, icon, sidebar, entryType, sx, }: SidebarEntryProps) { store.dispatch(setSidebarItem({ name, label, url, parent, useClusterURL, icon, sidebar, entryType, sx })); }

可以看到注册动作本质上是一次 Redux dispatch。各字段含义(依据 frontend/src/components/Sidebar/sidebarSlice.ts 中的SidebarEntry接口注释):

字段类型/默认值说明
namestring(必填)条目唯一标识,作为 Redux 中entries的键
labelstring(必填)显示文案
parentstring | null父条目namenull表示顶级
urlstring点击后跳转的 URL(通常配合registerRoute
useClusterURLboolean,默认true是否把 URL 写成/c/<集群名>/...形式
iconiconify 图标字符串'mdi:comment-quote',取值参考 iconify 的 MDI 图标集
sidebarstring条目所属的侧边栏,默认为集群侧边栏(IN_CLUSTER),也可取HOME或自定义字符串
entryType'link' \| 'subheader'默认可点击链接;subheader渲染为不可点击的分节标题
sxMUI SxPropssubheader条目的样式覆盖

2.1 顶级条目与配套路由

// 顶级条目。生成的链接 URL 为: /c/mycluster/feedback registerSidebarEntry({ parent: null, name: 'feedback', label: 'Feedback', url: '/feedback', icon: 'mdi:comment-quote', }); // 与 URL 路径配套的组件,渲染于 /c/mycluster/feedback registerRoute({ path: '/feedback', sidebar: 'feedback', name: 'feedback', exact: true, component: () => ( <SectionBox title="Feedback" textAlign="center" paddingTop={2}> <Typography>Embed your feedback forms here</Typography> </SectionBox> ), });

这段代码揭示了侧边栏与路由的绑定关系:条目里的url是"相对路径",真正的完整 URL 由运行时加上当前集群前缀得到(例如集群名是mycluster时为/c/mycluster/feedback,集群名为minikube时则是/c/minikube/feedback);而registerRoute中的sidebar: 'feedback'反向声明"访问该路由时应高亮哪个侧边栏条目"。

2.2 嵌套子菜单

// 另一个顶级菜单项,链接 URL 为: /c/mycluster/feedback2 registerSidebarEntry({ parent: null, name: 'feedback2', label: 'Diff Feedback', url: '/feedback2', icon: 'mdi:comment-quote' }); // 挂在 feedback2 下的两个子菜单 registerSidebarEntry({ parent: 'feedback2', name: 'feedback3', label: 'More Feedback', url: '/feedback3' }); registerSidebarEntry({ parent: 'feedback2', name: 'feedback4', label: 'Other Feedback', url: '/feedback4' });

parent指向任意已注册的条目名即可形成多级菜单,子菜单项同样可以省略icon

2.3 非集群路由与 subheader

// 挂在 cluster 下的二级条目,但 URL 不带集群前缀 registerSidebarEntry({ parent: 'cluster', name: 'no-cluster-sublevel-link', label: 'No cluster link', url: '/no-cluster-link', icon: 'mdi:airplane', useClusterURL: false, }); // 不可点击的分节标题 registerSidebarEntry({ parent: null, name: 'feedback-section', label: 'Feedback Tools', entryType: 'subheader', sx: { fontSize: '0.8rem', textTransform: 'none' }, });

useClusterURL: false表示该条目的链接就是/no-cluster-link本身,与集群上下文无关。配套的 src/index.tsx 中还有更完整的形态:

registerRoute({ path: '/no-cluster-link', sidebar: null, // 不激活任何侧边栏条目 name: 'no-cluster-link', exact: true, useClusterURL: false, // URL 中不包含 "/c/mycluster/" noAuthRequired: true, // 查看该页面无需已认证 hideAppBar: true, // 隐藏顶部 AppBar component: () => ( <SectionBox title="No Cluster Link" textAlign="center" paddingTop={2}> <Typography>Your component here</Typography> </SectionBox> ), });

noAuthRequired: true使该页面在未选择集群、未登录时也可访问;hideAppBar: true则隐藏页面顶部的应用栏,适合做全屏或独立视图。

2.4 创建全新侧边栏与 HOME 侧边栏

sidebar字段不仅可以取值,还可以"造"一个新侧边栏——当某个字符串值首次出现时,Headlamp 会创建对应的全新侧边栏:

// 因 "myplugin" 这个侧边栏不存在,此调用直接创建了一个全新侧边栏 registerSidebarEntry({ name: 'backtoclusters', label: 'Back to Clusters', url: '/', icon: 'mdi:hexagon', sidebar: 'myplugin', }); // 再往刚创建的 "myplugin" 侧边栏里追加条目 registerSidebarEntry({ name: 'mypluginarea', label: 'Special Area', url: '/mypluginarea', icon: 'mdi:comment-quote', sidebar: 'myplugin', });

对应的路由则通过对象形式的sidebar字段同时指定"哪个侧边栏 + 哪个条目",并把页面挂到 HOME 侧边栏下:

// 加到 HOME 侧边栏(不在集群内)的条目 registerSidebarEntry({ name: 'mypluginsidebar', label: 'Special Plugin Area', url: '/mypluginarea', icon: 'mdi:comment-quote', sidebar: 'HOME', }); registerRoute({ path: '/mypluginarea', sidebar: { item: 'mypluginarea', sidebar: 'myplugin' }, useClusterURL: false, noAuthRequired: true, name: 'mypluginarea', exact: true, component: () => ( <SectionBox title="Special Plugin Area" textAlign="center" paddingTop={2}> <Typography>See how the home sidebar is completely new?</Typography> </SectionBox> ), });

内置侧边栏的取值在 sidebarSlice.ts 中定义为枚举:

export enum DefaultSidebars { HOME = 'HOME', IN_CLUSTER = 'IN-CLUSTER', }

即"集群侧边栏"(选中集群后显示的菜单)与"HOME 侧边栏"(集群选择页等场景显示的菜单)是两条独立的菜单树。

2.5 运行时动态移除条目

示例中有一个"点一下就消失"的条目,它演示了过滤器也可以延迟注册——在路由组件的useEffect里再调用一次registerSidebarEntryFilter

registerSidebarEntry({ parent: null, name: 'nothing-to-see-here', label: 'Click me and I will disappear', url: '/feedback2', icon: 'mdi:glasses', }); registerRoute({ path: '/feedback2', sidebar: 'feedback2', name: 'feedback2', exact: true, component: () => { React.useEffect(() => { // 该过滤器把 "nothing-to-see-here" 条目从侧边栏移除 registerSidebarEntryFilter(entry => (entry.name === 'nothing-to-see-here' ? null : entry)); }, []); return ( <SectionBox title="Diff Feedback" textAlign="center" paddingTop={2}> <Typography>Different feedback forms go here.</Typography> </SectionBox> ); }, });

三、registerRoute:完整的路由注册

frontend/src/lib/router/Route.tsx 定义了Route接口的全部字段:

字段默认值说明
path必填URL 路径,支持path-to-regexp语法(如/namespaces/:name
exact-true时仅当路径精确匹配才命中
name-人类可读名称,供createRouteURL按名查找
useClusterURLtrueURL 是否带集群前缀(旧字段noCluster已标记废弃)
noAuthRequired-无需认证即可访问该路由
sidebar必填路由命中时激活的侧边栏条目名;null不激活任何条目;对象形式{ item, sidebar }可跨侧边栏定位
component必填渲染的 React 组件
hideAppBar-隐藏顶部 AppBar
isFullWidth-全宽渲染

以示例中的/feedback4为例,它展示了子菜单路由的典型写法(src/index.tsx):

registerRoute({ path: '/feedback4', sidebar: 'feedback4', name: 'feedback4', exact: true, component: () => ( <SectionBox title="Other Feedback" textAlign="center" paddingTop={2}> <Typography>Other feedback forms go here.</Typography> </SectionBox> ), hideAppBar: true, // 该路由下隐藏顶部 AppBar });

另外注意:条目名与路由名建议保持一致(如feedback/feedback),因为路由的sidebar字段要精确引用条目的name,否则高亮会错位。

四、过滤器 API:移除内置菜单项与路由

Headlamp 的默认路由表(Nodes、Namespaces、Workloads 等)集中在 frontend/src/lib/router/index.tsx 中定义。插件无需修改前端源码,即可用三个过滤器裁剪用户可见的 UI,这正是 README 所述"移除 Namespaces 侧边栏条目和路由"的实现方式(src/index.tsx):

// 移除 "Workloads" 顶级侧边栏菜单项 registerSidebarEntryFilter(entry => (entry.name === 'workloads' ? null : entry)); // 移除 "/workloads" 路由 registerRouteFilter(route => (route.path === '/workloads' ? null : route)); // 移除二级侧边栏菜单项 "Namespaces" registerSidebarEntryFilter(entry => (entry.name === 'namespaces' ? null : entry)); // 移除 "/namespaces" 路由 registerRouteFilter(route => (route.path === '/namespaces' ? null : route)); // 从 HOME 侧边栏移除 "settings" registerHomeSidebarEntryFilter(entry => (entry.name === 'settings' ? null : entry));

三个过滤器的分工(实现见 frontend/src/plugin/registry.tsx):

  • registerSidebarEntryFilter:过滤/修改IN_CLUSTER 集群侧边栏条目,返回null即删除该条目,返回修改后的条目即改写它;
  • registerHomeSidebarEntryFilter:同样语义,但作用于HOME 侧边栏
  • registerRouteFilter:过滤/修改路由表,返回null即从路由中删除——菜单项删了但路由没删,用户仍可通过手输 URL 访问,所以两者要成对出现。

五、源码视角:注册数据如何被存储与消费

5.1 侧边栏状态:sidebarSlice

frontend/src/components/Sidebar/sidebarSlice.ts 中的 reducer 说明了存储结构:

setSidebarItem(state, action: PayloadAction<SidebarEntry>) { state.entries[action.payload.name] = castDraft(action.payload); }, setSidebarItemFilter(state, action: PayloadAction<(entry) => SidebarEntry | null>) { state.filters.push(action.payload); }, setHomeSidebarItemFilter(state, action: PayloadAction<(entry) => SidebarEntry | null>) { state.homeFilters.push(action.payload); },

由此可以确认两点:

  1. 侧边栏条目按name存为字典,同名注册会覆盖先前的条目——插件自定义条目应避免与内置条目(如nodespods)重名;
  2. 集群侧边栏与 HOME 侧边栏的过滤器分别累积在filtershomeFilters两个数组中,渲染时各自应用,这就是两套 Filter API 分离的原因。

5.2 路由状态:routesSlice

frontend/src/redux/routesSlice.ts 的结构类似,但按path作键:

setRoute(state, action: PayloadAction<Route>) { state.routes[action.payload.path] = action.payload; }, setRouteFilter(state, action: PayloadAction<(route) => Route | null>) { state.routeFilters.push(action.payload); },

也就是说registerRoute时若path已存在会直接覆盖旧路由;registerRouteFilter则按注册顺序对所有路由(包括内置路由与插件路由)逐一执行。

5.3 URL 生成:createRouteURL 与集群前缀

前端跳转普遍使用createRouteURL(routeName, params)。frontend/src/lib/router/createRouteURL.tsx 的处理逻辑是:先在 Redux 中存储的插件路由里按 name 查找,找不到再按 path 查找(此时会打印按路径匹配的弃用警告),最后回退到内置路由表;随后若路由useClusterURL为真,则取当前集群参数拼入路径,通过generatePath产出最终 URL。

这段实现解释了示例中所有 URL 形态的来源:

侧边栏条目/路由useClusterURL实际访问 URL
feedbacktrue(默认)/c/<集群名>/feedback
feedback2/3/4子菜单true(默认)/c/<集群名>/feedback2
no-cluster-sublevel-link/no-sidebar-linkfalse/no-cluster-link
mypluginarea(HOME/自定义侧边栏)false/mypluginarea
table-props测试路由-/table-props

/table-props是该插件顺带保留的一个 Table 组件测试页面,与侧边栏主题无关,可忽略。)

六、小结与工程建议

  • 加菜单registerSidebarEntry定义条目(name/label/parent/url/icon/sidebar/entryType),配合registerRoute提供页面组件,两者通过route.sidebar === entry.name绑定高亮;
  • 做层级parent挂子菜单,sidebar挂自定义侧边栏(首次出现的字符串即新侧边栏),sidebar: 'HOME'进入 HOME 菜单树;
  • 去菜单registerSidebarEntryFilter+registerRouteFilter成对删除内置条目与路由,HOME 侧边栏用registerHomeSidebarEntryFilter
  • 控 URLuseClusterURL: false剥离集群前缀,noAuthRequired: true开放未认证访问,hideAppBar: true做沉浸式页面;
  • 避坑:条目名与路由 path 分别是侧边栏字典和路由字典的主键,重复注册会覆盖旧值;noCluster字段已废弃,应使用useClusterURL

所有 API 的权威定义与 JSDoc 示例可继续参阅 frontend/src/plugin/registry.tsx(registerSidebarEntry见 L327 起、registerRoute见 L471 起),路由默认表见 frontend/src/lib/router/index.tsx,完整可运行代码见 plugins/examples/sidebar/src/index.tsx。

【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlamp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 22:15:56

C# Modbus RTU读取RS485温湿度变送器:CRC与字节序处理

上一篇把 SerialPort 的打开、关闭、参数配置和基本收发捋了一遍&#xff0c;但真把一支 RS485 温湿度变送器接到电脑上&#xff0c;很多人会卡在同一个地方&#xff1a;串口明明打开了&#xff0c;命令也发出去了&#xff0c;返回的要么是一串看不懂的01 03 04 00 FA 01 2C XX…

作者头像 李华