上一篇解决了 Spaces 与 RBAC 如何在平台层实施访问控制。这一篇进入 New Platform 的插件体系,核心问题是:一个 Kibana 插件如何声明依赖、如何向外暴露 contract、前后端插件的对称结构是什么样的,以及为什么要重构掉旧平台。

旧平台的问题

Kibana 5 到 7 早期,插件系统是有机生长出来的,没有统一的生命周期模型。每个插件在 init 函数里随意注册路由、随意拉取其他插件的引用、随意访问全局 server 对象。这带来三个结构性问题。

第一是循环依赖。插件 A 拉 B,B 拉 C,C 再拉 A。因为没有强制的声明式依赖,运行时才能发现环。第二是缺乏生命周期。所有插件的 init 在服务器启动时同步跑完,但没有清晰的"配置阶段"和"运行阶段"之分,导致在配置阶段就能访问运行时状态(反过来也是)。第三是没有类型安全。插件暴露给外部的 API 是任意 JavaScript 对象,没有类型约束,外部调用者无法静态感知接口变化。

New Platform(NP)从 Kibana 7.0 开始引入,7.x 期间与旧平台并存,8.x 中旧平台被彻底移除。NP 的目标是三条:显式声明依赖、typed contract、两阶段生命周期(setup → start)。

插件清单:kibana.jsonc

每个 NP 插件的根目录有一个 kibana.jsonc 文件,这是插件对平台做出的静态声明。

1
2
3
4
5
6
7
kibana.jsonc
├── id 插件唯一标识符,全局不能重复
├── version 插件版本,通常与 Kibana 版本对齐
├── requiredPlugins 强依赖列表:这些插件不存在时,本插件不会启动
├── optionalPlugins 弱依赖列表:这些插件存在时注入,不存在时注入 undefined
├── requiredBundles 构建时需要的其他插件 bundle(前端按需加载用)
└── server / ui 分别声明是否有服务端代码和前端代码

平台在启动时读取所有插件的 kibana.jsonc,构建依赖图,做拓扑排序,按顺序初始化插件。循环依赖在排序阶段就会报错退出,而不是在运行时随机崩溃。

requiredPluginsoptionalPlugins 的区别在运行时体现:前者在 setupstart 的参数里保证非 undefined,后者是 T | undefined,插件必须自己做判断。

生命周期:setup 和 start

NP 插件有两个主要的生命周期阶段和一个清理阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Kibana 启动序列

├─ [setup phase]
│ ├── Core setup(http.createRouter, savedObjects.registerType 等)
│ ├── Plugin A.setup(core, deps) ← deps 是其他插件的 setup contract
│ ├── Plugin B.setup(core, deps)
│ └── ...(拓扑顺序)

├─ [start phase]
│ ├── Core start(savedObjects client 可用,es client 可用)
│ ├── Plugin A.start(core, deps) ← deps 是其他插件的 start contract
│ ├── Plugin B.start(core, deps)
│ └── ...(拓扑顺序)

└─ [stop phase]
└── 逆序调用各插件的 stop()(如果实现了的话)

setup 阶段是配置阶段。在这里,插件注册路由(core.http.createRouter())、注册 Saved Object 类型、注册 UI 应用入口。此时 Elasticsearch 连接还没有完全就绪,所以在 setup 里不应该发送查询请求。

start 阶段是运行阶段。ES 客户端可用,savedObjects 客户端可用,HTTP server 开始接受外部请求。插件在这里订阅 Observable、启动后台轮询、开始实际的业务逻辑。

这个两阶段划分解决了旧平台最常见的错误:在配置时就访问尚未就绪的运行时资源。

Contract 模式

插件之间通过 contract 传递能力,而不是互相持有引用。setup 函数的返回值就是这个插件的 setup contract,start 函数的返回值是 start contract。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// alerting 插件的服务端 plugin.ts(简化)
export class AlertingPlugin {
setup(core: CoreSetup, deps: AlertingPluginSetupDeps): AlertingSetup {
// 注册路由、注册 savedObject 类型
const router = core.http.createRouter();
// ...
return {
registerType: (alertType) => { /* ... */ },
};
}

start(core: CoreStart, deps: AlertingPluginStartDeps): AlertingStart {
return {
getAlertsClient: (request) => new AlertsClient(request, /* ... */),
};
}
}

另一个插件要使用 alerting 的能力,只需在自己的 kibana.jsonc 里声明 requiredPlugins: ["alerting"],平台就会把 AlertingSetupAlertingStart 分别注入到对应阶段的 deps 参数里。类型系统保证调用方知道自己拿到的是什么。

contract 是单向的、版本化的接口。插件暴露什么,由自己控制;调用方看到的只有 contract,看不到插件内部。这比旧平台里两个插件互相持有对方全部 instance 要干净得多。

Core Services

平台自身的能力(core services)也用相同的方式注入给插件。插件在 setupstart 里收到的第一个参数 core 就是 Core 的 contract。

1
2
3
4
5
6
7
8
9
10
11
12
CoreSetup(setup 阶段可用)
├── http 注册路由、中间件、自定义响应头
├── savedObjects 注册新的 savedObject 类型
├── uiSettings 注册前端可配置的设置项
├── elasticsearch 获取 ES 客户端工厂(start 阶段才能真正发请求)
└── getStartServices() 异步获取 start 阶段的 core 和 deps

CoreStart(start 阶段可用)
├── savedObjects 可发起实际的 CRUD 请求
├── elasticsearch 可发起实际的 ES 请求
├── http HTTP server 正在运行
└── uiSettings 可读取用户的设置值

getStartServices() 是 setup 阶段访问 start 资源的合法途径:它返回一个 Promise,在 start 阶段完成后 resolve。路由 handler 在 setup 阶段注册,但在 start 阶段才会被调用,所以在 handler 里用 getStartServices() 是标准做法。

前后端对称结构

NP 插件在结构上是对称的。服务端插件(server/plugin.ts)和前端插件(public/plugin.ts)遵循完全相同的接口:都实现 Plugin<SetupContract, StartContract, SetupDeps, StartDeps>,都有 setupstart 方法,都通过 deps 接收其他插件的 contract。

1
2
3
4
5
6
my-plugin/
├── kibana.jsonc
├── server/
│ └── plugin.ts 实现 Plugin 接口(服务端)
└── public/
└── plugin.ts 实现 Plugin 接口(浏览器端)

前端的 CoreSetup 和 CoreStart 包含的服务不同(没有 http.createRouter,有 application.register 等),但 lifecycle 完全一致。Discover、Lens、Alerting、Maps 这些大型功能模块,都是这套对称结构的具体实例。

实验:追踪一个插件的依赖声明与 contract 注入

在 Kibana 源码或已安装的 Kibana 中,找到 alerting 插件的 manifest 和 plugin class。

1
2
3
4
5
6
# 在 Kibana 源码中
cat x-pack/plugins/alerting/kibana.jsonc
# 重点看 requiredPlugins 和 optionalPlugins

grep -n "setup\|start" x-pack/plugins/alerting/server/plugin.ts | head -30
# 看 setup 返回什么 contract,start 返回什么 contract

在已运行的 Kibana 中,访问 GET /api/status 查看所有已加载插件的列表。对比 kibana.jsonc 中的 requiredPlugins 和实际运行时的插件列表,确认依赖关系。

追踪 contract 的传递路径:alerting 的 setup contract 中有 registerType,找到调用它的插件(如 stack_alerts),确认这个调用发生在 stack_alerts.setup() 里,而不是 start()

模式提炼

插件声明式依赖 + 拓扑排序启动,把循环依赖从运行时问题变成启动期报错,故障点前移。

contract 作为接口边界,使插件间耦合只发生在 contract 类型上,而不是实现细节上。这是依赖倒置原则在大型前端应用中的具体落地。

两阶段生命周期(setup 配置、start 运行)强制分离"配置什么"和"用什么",避免在配置时访问尚未就绪的资源这一类时序 bug。

getStartServices() 是一个有意思的设计:它把跨阶段访问的需求封装成一个 Promise,而不是打破两阶段边界。这个模式在其他需要延迟初始化的场景里可以直接借鉴。

工程迁移表

旧平台问题 NP 解法 可迁移的模式
循环依赖运行时爆 kibana.jsonc 声明 + 拓扑排序 把依赖关系显式化,构建期检测环
任意时序访问全局对象 两阶段 setup/start 隔离 配置阶段和运行阶段的资源访问分开
插件间通过 instance 耦合 contract 接口注入 模块间只暴露接口,不暴露实现
无类型安全 TypeScript typed contract 用类型系统做接口契约
可选依赖不明确 optionalPlugins + T undefined

常见误解

setup 阶段可以发 ES 查询。不可以。setup 阶段 ES 客户端的连接还没有完全就绪。需要在 setup 里拿到 ES 能力,应该用 core.getStartServices() 包装成 Promise,在路由 handler 里等待。

requiredPlugins 里的插件必须和当前插件在同一个 Kibana 发行版里。不是。requiredPlugins 只要求插件在运行时存在,可以来自 x-pack、开源版或自定义插件,只要 Kibana 启动时能找到。

NP 只适用于服务端代码。不是。前端 public/plugin.ts 用完全相同的接口,ApplicationService、ChromeService 等前端 core service 也在这套框架里。

stop 阶段是必须实现的。不是。只有有清理工作的插件才需要实现 stop()。大部分插件不需要。

练习

  1. 在 Kibana 源码中找到 discover 插件的 kibana.jsonc,列出它的 requiredPlugins。再找到 discover/server/plugin.tssetup() 方法,确认它从 deps 里解构了哪些 contract。

  2. 写一个最小的 NP 服务端插件:kibana.jsonc 声明一个 requiredPluginplugin.tssetup 里注册一条路由,在路由 handler 里用 getStartServices() 访问 savedObjects 客户端。

  3. 比较 requiredPluginsoptionalPlugins 在 TypeScript 类型签名上的差异:找两个分别用了强依赖和弱依赖的插件,看它们如何处理各自的 deps 类型。

系列导航

  • 上一篇:深入 Kibana 13 - Spaces 与 RBAC:平台级多租户模型
  • 下一篇:深入 Kibana 15 - 性能模型:bundle、bootstrap 与异步搜索会话

参考资料