作者:来自 Elastic Anton Dosov, Ryan Keairns, Krzysztof Kowalczyk, Alex Marhaba

我们为 Kibana 的共享外壳定义了 类 型化契约,这让设计系统治理成为默认方式,也解释了为什么新的页面页眉不再包含面包屑导航。
通过单一解决方案观察、保护和搜索你的数据。从应用监控到威胁检测,Kibana 都是满足关键使用场景的多用途平台。立即开始你的14 天免费试用。
我们将 Kibana 中每个页面页眉(header)背后的开放式 React API 替换为类型化契约(typed contracts),从而将设计系统治理直接融入共享外壳。现在,几十个团队不再需要手动确保每个页眉都正确。页面只需要声明某个控件的含义,而共享外壳则决定它的外观以及位置。我们也在这个过程中移除了面包屑导航,因为重新设计后的导航已经能够告诉你当前所在的位置。新的界面框架目前已经在 Elastic Cloud Serverless 中上线,并将在 9.6 中发布到 Elastic Cloud Hosted 和自管理部署。
什么是 Kibana 界面框架?
界面框架是 Kibana 中构成应用外围框架的一切内容:全局页眉和导航,以及你当前所在页面的页眉。每个应用都会在其中进行渲染。由于界面框架是共享的,它的 API 决定了整个产品的一致性,以及下一次重新设计的难易程度。
用户看到的不一致页面页眉是什么样的
要了解这个问题,最直接的方法就是看看用户实际看到的内容:

以最基本的主要操作为例,也就是页面最希望你点击的那个按钮,例如 创建索引(Create index)。在这项工作之前,主要操作根据页面的不同,会出现在三个不同的位置:
-
旧的应用页眉中。
-
页面模板页眉中。
-
页面正文的某个位置。
常用链接也存在同样的问题,分享、反馈和文档链接会出现在不同的位置,而且不同应用的样式也各不相同。许多页面的面包屑导航配置也存在问题。例如,有些页面会为页面中的每个标签页显示一个面包屑,有些面包屑无法点击。还有一些则会重复显示你已经所在页面的标题。
这些情况无一例外,都是一个出发点良好的团队使用灵活 API 时通常会产生的结果。用户不得不重新学习每个页面的布局,而每次尝试重新设计界面框架时,都必须考虑数百种局部差异。
页面页眉重新设计的目标
重新设计的目标很简单:页面应该表达它们需要什么(例如标题、徽章或主要操作),而共享外壳应该决定这些内容的外观和位置。常用链接和主要操作应该在所有地方都有统一的位置。如果它是主要操作,就始终位于相同的位置,并采用相同的样式。而一致性也不应该永远成为几十个团队需要手动确保正确的事情。
要实现这一目标,并不是简单地重新设计样式;它需要改变应用与平台之间的契约。
类型化属性如何取代 EuiPageHeader 开放式的 React 节点
每个页面页眉都很容易单独构建,而这些局部选择不断累积,最终形成了整个产品中肉眼可见的差异:不同页面之间的间距、标题样式和操作位置都不一致。
无论哪里出现这种问题,解决方法都是同一种机制:更严格的 API。控件不再接受任意的 React 节点,而是通过类型化属性声明其含义,由共享外壳决定它的外观和位置。这一项改变同时带来了两方面的好处:一致性,以及对共享界面允许包含哪些内容的治理能力。
以页面页眉为例。使用 EuiPageHeader 时,API 暴露了布局设置,并为几乎每个可见区域都接受 React 节点:
ini
`
1. <EuiPageHeader
2. pageTitle={
3. <>
4. Index Management
5. <EuiBadge color="accent">Beta</EuiBadge>
6. </>
7. }
8. description={
9. <>
10. View and manage your Elasticsearch indices.{' '}
11. <EuiLink href={docsUrl}>Learn more</EuiLink>
12. </>
13. }
14. bottomBorder
15. alignItems="top"
16. responsive={false}
17. tabs={tabs}
18. rightSideItems={[
19. <RefreshButton onClick={onRefresh} />,
20. <CreateButton onClick={onCreate} />,
21. ]}
22. />
`AI写代码
由于 pageTitle、description 和 rightSideItems 都接受任意的 React 节点,因此每个团队都以不同的方式填充它们。一个页面会在标题旁边放置一个徽章,而另一个页面则自行设计一个标签。一个团队的操作是按某种顺序排列的普通按钮;下一个团队的操作则是另一种组合、另一种顺序,有些会折叠到菜单中,有些则不会。所有人都使用同一个外壳组件,但这些页眉看起来和使用起来却像是来自不同的产品。共享组件保证了外部包装,却无法保证其中包含的内容。
AppHeader API 则关闭了这种开放性。同一个页眉现在通过类型化属性来表达:
css
`
1. <AppHeader
2. title="Index Management"
3. badges={[4. {5. label: 'Beta',6. color: 'accent',7. tooltip: 'This feature is in beta.',8. },9. ]}
10. description={{
11. text: 'View and manage your Elasticsearch indices.',
12. learnMoreUrl: docsUrl,
13. }}
14. tabs={tabs}
15. menu={{
16. primaryActionItem: {
17. id: 'create',
18. label: 'Create index',
19. iconType: 'plusInCircle',
20. run: onCreate,
21. },
22. items: [
23. {
24. id: 'refresh',
25. label: 'Refresh',
26. iconType: 'refresh',
27. run: onRefresh,
28. },
29. ],
30. }}
31. />
`AI写代码
现在,每个部分都只有一种表达方式,因此每个页面的渲染方式也都一致。徽章位于独立的类型化集合中,与标题分开。描述包含其文本,并可以选择提供一个"了解更多"URL。操作会声明自己是主要操作还是次要操作,而组件则决定它们的外观以及何时折叠。此外,更丰富的控件也遵循相同的结构。可编辑标题或收藏切换按钮现在都是结构化配置,而不是自定义的 React 树:
ini
`
1. <AppHeader
2. title={{
3. text: indexName,
4. onSave: renameIndex,
5. }}
6. favorite={{
7. status: favoriteStatus,
8. onToggle: toggleFavorite,
9. }}
10. />
`AI写代码
这种职责划分在整个系统中都是一致的;应用提供状态和行为(当前名称、重命名时执行什么操作、如何切换收藏状态),而页眉负责呈现。它负责渲染控件、处理键盘交互、显示验证错误,以及反映待处理的更改。由于这些职责都是明确的,布局和响应式行为就可以保留在共享组件内部,而应用不需要在每次页眉发生变化时都进行协调更新。
为什么共享 UI 外壳需要一组封闭的控件
全局外壳也存在类似的问题,只不过开放程度更高了一层。任何插件都可以在左侧或右侧注册任意内容,并通过一个数字选择其位置,而且无需与平台团队或设计师进行任何沟通:
markdown
`
1. chrome.navControls.registerLeft({
2. content: <ProjectPicker />,
3. });
5. chrome.navControls.registerRight({
6. order: 10,
7. content: <AiAssistantButton />,
8. });
10. chrome.navControls.registerRight({
11. order: 20,
12. content: <FeedbackButton />,
13. });
`AI写代码
这个 registerRight API 就像一扇敞开的门。任何团队都可以添加控件,而页眉最终会堆满没有经过统一设计的元素。这些控件会争夺空间,并各自携带自己的样式;它们的顺序则由选择了更大数字的人决定。外壳只知道一个 React 节点排在另一个 React 节点之后;它无法判断一个控件用于打开 AI 助手,而另一个用于收集反馈,因此也无法对它们进行合理处理或保持整体一致。
新的外壳通过 chrome.controls 和 chrome.help 下的一组封闭的命名角色,取代了这个开放画布。旧的 注册机制 目前还没有消失,因为在应用迁移期间,经典页眉仍然与新页眉并行运行,但新外壳中的任何内容都无法通过它访问:
ini
`
1. chrome.controls.projectPicker.set(<ProjectPicker />);
2. chrome.controls.aiButton.register({ content: <AiAssistantButton /> });
3. chrome.controls.globalSearch.set({ onClick: openGlobalSearch });
4. chrome.help.registerFeedbackHandler(openFeedback);
`AI写代码
全局 chrome 页眉与应用页面页眉
旧版界面框架之所以难以演进,部分原因在于 "页眉" 实际上是多个相互纠缠的东西。重新设计后,我们明确区分了两个由不同所有者负责的界面:
| 界面框架(全局)页眉 | 应用页眉 | |
|---|---|---|
| 所有者 | 平台 | 页面 |
| 范围 | Kibana 中任何地方都成立的内容 | 当前页面上的内容 |
| 内容 | 导航、项目或部署选择器、搜索、帮助、AI 助手、反馈 | 标题、徽章、描述、标签页、页面操作 |
| 如何填充 | chrome.controls 和 chrome.help 下的命名插槽 |
AppHeader 类型化属性,直接渲染或通过 chrome.appHeader.set() 设置 |
由于每个界面都有唯一的所有者,并且两者之间通过类型化契约进行连接,平台就可以在不审查数百个页面的情况下重新设计界面框架,而应用也可以在不与全局控件发生冲突的情况下演进自己的页眉。设置页眉配置会返回一个清理回调,因此应用在卸载时可以清理自己添加的内容。
严格 API 强制产生的设计决策
更严格的 API 强制我们做出了一些设计决策,而灵活的 API 让每个人都可以一直推迟这些决定。其中有两个特别值得说明。
为什么我们从 Kibana 导航中移除了面包屑导航
从界面框架中移除面包屑导航是争议较大的决定之一,而实际数据让这件事比预期更容易。在实践中,Kibana 中的面包屑导航存在大量配置错误:
-
有些页面会为页面中的每个标签页生成一个面包屑。
-
有些面包屑无法点击。
-
有些会重复显示当前页面的标题。
它们增加了视觉负担,却无法可靠地帮助用户确定当前位置。
与此同时,重新设计后的导航已经能够传达层级关系。主导航和次级导航会告诉你当前所在的位置,并提供直接返回上层的方式。保留面包屑意味着需要维护同一信息的第三种(而且经常是错误的)表达方式,因此我们停止绘制这条路径,让导航承担这项工作。
面包屑 API 本身并没有消失。应用仍然可以注册面包屑,而新外壳会读取这些面包屑来生成返回按钮;如果应用没有提供页面标题,它也会使用面包屑回退到页面标题。数据的职责发生了变化,而不是直接消失,这也是为什么将应用迁移到新页眉时,通常不需要一开始就把面包屑全部移除:
Before:

After:


页面页眉应该显示多少个操作按钮
另一个反复出现的争论是优先级。现在每个页面的操作都会通过同一个菜单结构呈现,那么哪些按钮应该直接显示,以及应该按照什么顺序显示?随着产品不断演进,优先级也会发生变化,因此这场讨论仍在继续。
在正式发布时,我们做了一个有意的简化:限制应用菜单中可以显示的按钮数量。一个页面可以有一个主要操作和数量受限的次要操作,剩余操作则折叠到菜单中。这一限制避免单个页面通过一整排按钮分散用户的注意力,同时也让优先级问题变得明确,而不是由下一个添加按钮的人自行决定。
我们经过了多次迭代才最终确定这个方案。最初,我们允许显示很多操作。一个页面最多可以有三个按钮,而且主要按钮左侧还可以有一个次要操作。这占用了太多空间,因此我们逐步简化布局,移除了次要操作,并减少了可见按钮的数量。我们还让溢出菜单变得更加结构化。例如,反馈和文档等一些项目现在会固定显示在溢出菜单的页脚中。
严格的设计系统治理给团队带来了什么成本
在旧 API 下,插件可以通过选择一侧、指定顺序并挂载一个 React 树来添加新的 UI。在更严格的 API 下,一种新的控件类型可能需要先具备共享能力,插件才能添加它。这会增加前期工作,并迫使团队做出过去可以回避的设计决策。
我们接受了严格 API 带来的成本,因为另一种选择只是把成本推迟到之后的每一次重新设计中。只要任意 React 树可以挂载到任意位置,界面框架的布局或无障碍能力每发生一次变化,就必须考虑所有这些内容。
这就是我们做出的权衡,而且它在两个方面都能带来回报。一致性不再是每个团队都必须手动确保正确的事情;表达一个控件只有一种方式,因此页面默认就是一致的。同时,共享界面也始终受到治理。它所包含的控件集合是一项有意做出的设计决策,而不是边缘位置不断累积的结果。应用描述其控件的含义,而外壳可以自由决定它们的呈现方式,无论是今天还是下一次重新设计。
重新设计的 Kibana 界面框架在哪里可用
重新设计的界面框架目前已经在 Elastic Cloud Serverless 中可用。打开任何项目,你就已经在使用它。对于 Elastic Cloud Hosted 和自管理用户,它将在 9.6 中发布。
本文所述任何功能或特性的发布及时间安排均由 Elastic 自行决定。目前尚未提供的任何功能或特性可能无法按时交付,也可能根本不会交付。
原文:Design system governance: Rebuilding Kibana's page headers | Elasticsearch Labs