深入 Kibana 14 - 插件体系:New Platform 的 setup/start 生命周期
上一篇解决了 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 | |
平台在启动时读取所有插件的 kibana.jsonc,构建依赖图,做拓扑排序,按顺序初始化插件。循环依赖在排序阶段就会报错退出,而不是在运行时随机崩溃。
requiredPlugins 和 optionalPlugins 的区别在运行时体现:前者在 setup 和 start 的参数里保证非 undefined,后者是 T | undefined,插件必须自己做判断。
生命周期:setup 和 start
NP 插件有两个主要的生命周期阶段和一个清理阶段。
1 | |
setup 阶段是配置阶段。在这里,插件注册路由(core.http.createRouter())、注册 Saved Object 类型、注册 UI 应用入口。此时 Elasticsearch 连接还没有完全就绪,所以在 setup 里不应该发送查询请求。
start 阶段是运行阶段。ES 客户端可用,savedObjects 客户端可用,HTTP server 开始接受外部请求。插件在这里订阅 Observable、启动后台轮询、开始实际的业务逻辑。
这个两阶段划分解决了旧平台最常见的错误:在配置时就访问尚未就绪的运行时资源。
Contract 模式
插件之间通过 contract 传递能力,而不是互相持有引用。setup 函数的返回值就是这个插件的 setup contract,start 函数的返回值是 start contract。
1 | |
另一个插件要使用 alerting 的能力,只需在自己的 kibana.jsonc 里声明 requiredPlugins: ["alerting"],平台就会把 AlertingSetup 和 AlertingStart 分别注入到对应阶段的 deps 参数里。类型系统保证调用方知道自己拿到的是什么。
contract 是单向的、版本化的接口。插件暴露什么,由自己控制;调用方看到的只有 contract,看不到插件内部。这比旧平台里两个插件互相持有对方全部 instance 要干净得多。
Core Services
平台自身的能力(core services)也用相同的方式注入给插件。插件在 setup 和 start 里收到的第一个参数 core 就是 Core 的 contract。
1 | |
getStartServices() 是 setup 阶段访问 start 资源的合法途径:它返回一个 Promise,在 start 阶段完成后 resolve。路由 handler 在 setup 阶段注册,但在 start 阶段才会被调用,所以在 handler 里用 getStartServices() 是标准做法。
前后端对称结构
NP 插件在结构上是对称的。服务端插件(server/plugin.ts)和前端插件(public/plugin.ts)遵循完全相同的接口:都实现 Plugin<SetupContract, StartContract, SetupDeps, StartDeps>,都有 setup 和 start 方法,都通过 deps 接收其他插件的 contract。
1 | |
前端的 CoreSetup 和 CoreStart 包含的服务不同(没有 http.createRouter,有 application.register 等),但 lifecycle 完全一致。Discover、Lens、Alerting、Maps 这些大型功能模块,都是这套对称结构的具体实例。
实验:追踪一个插件的依赖声明与 contract 注入
在 Kibana 源码或已安装的 Kibana 中,找到 alerting 插件的 manifest 和 plugin class。
1 | |
在已运行的 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()。大部分插件不需要。
练习
-
在 Kibana 源码中找到
discover插件的kibana.jsonc,列出它的requiredPlugins。再找到discover/server/plugin.ts的setup()方法,确认它从 deps 里解构了哪些 contract。 -
写一个最小的 NP 服务端插件:
kibana.jsonc声明一个requiredPlugin,plugin.ts在setup里注册一条路由,在路由 handler 里用getStartServices()访问 savedObjects 客户端。 -
比较
requiredPlugins和optionalPlugins在 TypeScript 类型签名上的差异:找两个分别用了强依赖和弱依赖的插件,看它们如何处理各自的 deps 类型。
系列导航
- 上一篇:深入 Kibana 13 - Spaces 与 RBAC:平台级多租户模型
- 下一篇:深入 Kibana 15 - 性能模型:bundle、bootstrap 与异步搜索会话
