编辑此页面

开发用户界面

概述

Quarkus Dev UI 是一个开发者友好的用户界面,当您在开发模式下运行应用程序时(./mvnw quarkus:dev)会激活。它充当一个强大的门户,用于探索、调试和与您的应用程序交互——所有这些都在应用程序运行时进行,无需任何代码更改或重启。

什么是 Dev UI?

Dev UI 旨在通过使 Quarkus 的内部运作可观察和可扩展来提升开发者的乐趣。它提供:

  • 对可用扩展及其功能的洞察

  • 访问运行时元数据和工具

  • 与热重载紧密集成的响应式前端

  • 扩展公开配置、文档和操作的灵活方式

无论您是探索项目的开发者还是增强体验的扩展作者,Dev UI 都是您的入口点。

布局概览

在开发模式下运行时,Quarkus 提供一个动态的、模块化的 Web 界面,可通过 https://:8080/q/dev-ui 访问。您也可以在控制台日志中单击 d,这将在您的默认浏览器中打开 Dev UI。

基本布局包含以下部分:

  • 菜单

  • 页面

  • 页脚

  • 卡片

以下是布局的可视化概述:

Dev UI Layout

左侧的菜单提供对内置页面的结构化访问:

菜单项 描述

扩展

以卡片形式显示所有 Quarkus 扩展(当前应用程序使用)。每个卡片可以提供配置、文档或工具。

配置

显示所有配置选项(包含当前、默认和文档信息)。包括一个配置编辑器(文本和表单模式)。

工作区

项目文件浏览器。文件可以内联打开和编辑。扩展可以添加工作区操作。

端点

列出所有 HTTP 端点(REST、GraphQL、内部等)。帮助您检查应用程序中可用的路由。

持续测试

监视和控制持续测试。查看测试结果、重新运行测试或切换测试状态。

开发服务

显示有关自动启动的服务的信息(例如,数据库、Kafka 代理)。

构建指标

提供来自上次构建和重新加载周期的性能指标。

自述文件

呈现您的项目的 README.md,如果可用。对于入门或项目上下文很有用。

依赖项

显示所有运行时和部署依赖项,具有依赖项路径探索和搜索功能。

您可以将扩展卡片中的页面拖到菜单中以进行书签,以便快速访问。

页面区域(主要内容)

主面板显示所选菜单项或扩展的内容。根据所选页面,它可能呈现:

  • 扩展特定的工具和 UI(例如,GraphQL UI、Swagger UI)

  • JSON 查看器、代码编辑器或指标图表

  • 交互式操作(例如,按钮、切换)

  • 文档和链接

页面是模块化的并且动态加载。扩展可以使用 Web Components 编写自定义页面,并通过 JSON-RPC 与后端交互。

底部的页脚主要用于在应用程序以开发模式运行时显示日志。

默认情况下,选择 Server 日志(标准 Quarkus 日志输出),但是根据存在的扩展,可能会出现额外的日志选项卡。例如:

  • 使用相关扩展时,可能会出现 HTTPGraphQLScheduler 等。

  • 来自启动的服务(如数据库、Kafka 代理等)的 Dev Services 日志输出。

此实时日志视图帮助开发者跟踪行为、调试问题和监视 Dev Services,而无需切换到终端。

卡片

每个贡献给 Dev UI 的 Quarkus 扩展都以卡片的形式表示在扩展页面中。这些卡片提供对扩展提供的功能、文档、配置和运行时工具的快速访问。

Extension Card Anatomy

每个卡片可能包括以下元素:

元素 描述

徽标(可选)

一个可选的徽标,代表扩展或其领域。

标题

Dev UI 中识别的扩展的名称。

收藏

将卡片标记为收藏。收藏的卡片始终首先显示在扩展视图中,以便于访问。

指南(可选)

链接到扩展的在线 Quarkus 指南(如果可用)。

描述

简要说明扩展的作用或启用的功能。

页面链接(可选)

导航到扩展贡献的自定义页面的交互式条目。页面链接还可以选择包含我们稍后讨论的标签。

配置

一个到配置编辑器的快捷方式,过滤到与此扩展相关的设置。

底层库(可选)

显示为扩展提供支持的主要库的版本(如果有)。

更多细节

打开一个包含所有信息的对话框。

单击卡片内的页面链接将导航到该功能的专用 UI 页面。这些页面可以被书签到菜单中以便快速访问。

扩展可以自由地自定义它们的卡片并根据它们在运行时提供的内容添加交互行为。

扩展开发者指南

Quarkus 扩展可以通过贡献给 Dev UI 来增强开发者体验。本指南的其余部分概述了如何将您的扩展集成到 Dev UI 中,涵盖元数据配置、将页面添加到卡片、菜单和页脚,以及动态内容的最佳实践。

quarkus-extension.yaml

要使您的扩展显示在 Dev UI 中,请确保 quarkus-extension.yaml 文件存在于扩展的运行时模块的 src/main/resources/META-INF 目录中。此文件提供 Dev UI 用于生成扩展卡片的元数据。

示例

name: "Hibernate ORM"
description: "Define your persistent model with Hibernate ORM and Jakarta Persistence"
guide: "https://quarkus.net.cn/guides/hibernate-orm"
metadata:
  categories:
    - "data"
  config:
    - "quarkus.hibernate-orm"

关键字段:

  • name:在 Dev UI 卡片中显示为扩展标题。

  • description:显示为卡片的摘要。

  • guide:扩展指南的 URL;用于在卡片上呈现指南图标。

  • metadata.config:在单击卡片上的“配置”时筛选显示的配置键。

将页面添加到 Dev UI

您的扩展需要在其部署模块中具有以下依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx-http-dev-ui-spi</artifactId>
</dependency>

这可以是传递的或直接的,并且仅在部署模块中需要。这使您的处理器可以访问相关的 Build Items。

扩展可以将交互式页面贡献给 Dev UI 的以下区域:

  • 卡片:直接在扩展卡片上添加页面的链接。

  • 页脚:向页脚添加选项卡,用于日志或其他运行时信息。

  • 菜单:将页面添加到 Dev UI 的左侧菜单。

卡片

贡献 Dev UI 最常见的方式是通过扩展页面中的扩展卡片中的页面。

要将链接添加到扩展的卡片,请在 @BuildStep 中生成一个 CardPageBuildItem

    @BuildStep(onlyIf = IsLocalDevelopment.class) (1)
    void createJokesPageOnCard(BuildProducer<CardPageBuildItem> cardsProducer) {

        CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); (2)
        cardPageBuildItem.setLogo("clown.svg", "clown.svg"); (3)

        cardPageBuildItem.addPage(Page.webComponentPageBuilder() (4)
                .title("Joke List") (5)
                .icon("font-awesome-solid:cubes") (6)
                .componentLink("qwc-jokes-list.js")); (7)

        cardsProducer.produce(cardPageBuildItem);
    }
1 始终确保此构建步骤仅在本地开发模式下运行。
2 要在卡片上添加任何内容,您必须返回/生成一个 CardPageBuildItem
3 您可以选择在 deployment/src/main/resources/dev-ui/ 中添加徽标(深色和浅色模式)。
4 要在卡片上添加链接,您可以使用 addPage 方法,因为所有链接都转到“页面”。Page 有一些构建器可以帮助构建页面。最常见的是 webComponentPageBuilder - 但我们稍后将讨论其他一些。
5 您可以(可选地)添加标题,否则它将从组件链接派生。
6 您可以添加一个图标。所有免费的 font-awesome 图标都可用。
7 deployment/src/main/resources/dev-ui/ 中添加 Web 组件源 (js) 文件的链接。这必须采用 qwc-extensionname-pagename.js 的格式。

稍后我们将讨论如何创建此 Web 组件页面。

关于图标的说明
如果您在 Font awesome 中找到您的图标,您可以按如下方式映射:示例 <i class="fa-solid fa-house"></i> 将映射到 font-awesome-solid:house,因此 fa 变为 font-awesome,对于图标名称,删除 fa-
可选:库版本

您可以卡片上添加一个底层库徽章。这对于扩展公开一个知名的库的情况很有用。例如,quarkus-hibernate-orm 扩展提供了对 Hibernate 的访问。显示底层的 Hibernate 详细信息可能对用户很有用。

Dev UI Layout

这可以使用 addLibraryVersion 方法完成:

cardPageBuildItem.addLibraryVersion("org.hibernate.orm", "hibernate-core", "Hibernate", "https://hibernate.com.cn/orm/");

您提供 groupId 和 artifactId(以便我们可以查找版本)以及名称,还可以选择一个 url。

可选:构建时数据

您可以将构建期间收集的数据传递到页面 (js)。

cardPageBuildItem.addBuildTimeData("jokes", jokesBuildItem.getJokes());

您可以为所有您在构建时知道的、并且需要在页面上的数据添加多个键值对。

密钥将自动限定范围,因此您不必添加任何命名空间前缀。稍后我们将讨论如何在 Web 组件(页面)中访问此数据。

可选:标签

您可以使用页面构建器上的构建器方法之一向卡片中的链接添加可选标签。这些标签可以是:

  • 静态的(在构建时已知).staticLabel("staticLabelValue")

  • 动态的(在运行时加载).dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")

  • 流式的(在运行时连续流式传输更新的值).streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")

对于动态和流式标签,方法名称是 JsonRPC 方法的名称,我们将在稍后讨论。

Dev UI card labels

除了添加卡片和页面之外,扩展还可以向页脚添加选项卡。这对于连续发生的事情很有用。页面在导航离开该页面(或 Web 组件)时会从 DOM(以及可能的后端)断开连接,而页脚中的日志将永久连接到 DOM,因为它始终是视图(或应用程序)的一部分。

有两种方法可以将内容添加到页脚。最简单的方法是在 BuildItem 中公开日志。在这里,我们期望一个提供者(函数)返回 Flow.Publisher<String> 或 RuntimeValue<SubmissionPublisher<String>>。

@BuildStep(onlyIf = { IsLocalDevelopment.class }) (1)
public void createFooterLog(BuildProducer<FooterLogBuildItem> footerLogProducer){
    footerLogProducer.produce(new FooterLogBuildItem("My Extension Log", () -> {(2)
        return createLogPublisher();(3)
    }));
}
1 始终确保此构建步骤仅在本地开发模式下运行。
2 您必须返回/生成一个 FooterLogBuildItem
3 在这里返回一个 Flow.Publisher<String>,它将流式传输日志。

如果日志仅在运行时可用,则 RuntimeValue 很有用。如果可以在部署模块中获取日志,则可以使用提供者。

或者您可以完全控制 UI,并生成一个 FooterPageBuildItem,并在 js 文件中提供一个自定义 Web 组件。

    @BuildStep(onlyIf = IsLocalDevelopment.class)(1)
    void createJokesLog(BuildProducer<FooterPageBuildItem> footerProducer) {

            FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem();(2)

            footerPageBuildItem.addPage(Page.webComponentPageBuilder()(3)
                    .title("Joke Log")(4)
                    .icon("font-awesome-regular:face-grin-tongue-wink")(5)
                    .componentLink("qwc-jokes-log.js"));(6)

            footerProducer.produce(footerPageBuildItem);
    }
1 始终确保此构建步骤仅在本地开发模式下运行。
2 要在页脚上添加任何内容,您必须返回/生成一个 FooterPageBuildItem
3 要在页脚中添加选项卡,您可以使用 addPage 方法,因为所有选项卡都呈现一个“页面”。Page 有一些构建器可以帮助构建页面。最常见的是 webComponentPageBuilder - 但我们稍后将讨论其他一些。
4 您可以(可选地)添加标题,否则它将从组件链接派生。
5 您可以添加一个图标。所有免费的 font-awesome 图标都可用。
6 deployment/src/main/resources/dev-ui/ 中添加 Web 组件源 (js) 文件的链接。这必须采用 qwc-extensionname-pagename.js 的格式。
可选:构建时数据

您可以将构建期间收集的数据传递到页面 (js)。

footerPageBuildItem.addBuildTimeData("jokes", jokesBuildItem.getJokes());

您可以为所有您在构建时知道的、并且需要在页面上的数据添加多个键值对。

密钥将自动限定范围,因此您不必添加任何命名空间前缀。稍后我们将讨论如何在 Web 组件(页面)中访问此数据。

要将页面添加到 Dev UI 菜单,请生成一个 MenuPageBuildItem

    @BuildStep(onlyIf = IsLocalDevelopment.class) (1)
    void createJokesMenu(BuildProducer<MenuPageBuildItem> menuProducer) {

        MenuPageBuildItem menuPageBuildItem = new MenuPageBuildItem();  (2)

        menuPageBuildItem.addPage(Page.webComponentPageBuilder() (3)
                .title("One Joke") (4)
                .icon("font-awesome-regular:face-grin-tongue-wink") (5)
                .componentLink("qwc-jokes-menu.js")); (6)

        menuProducer.produce(menuPageBuildItem);
    }
}
1 始终确保此构建步骤仅在本地开发模式下运行。
2 要在菜单上添加任何内容,您必须返回/生成一个 MenuPageBuildItem
3 要在菜单中添加链接,您可以使用 addPage 方法,因为所有链接都转到“页面”。Page 有一些构建器可以帮助构建页面。最常见的是 webComponentPageBuilder - 但我们稍后将讨论其他一些。
4 您可以(可选地)添加标题,否则它将从组件链接派生。
5 您可以添加一个图标。所有免费的 font-awesome 图标都可用。
6 deployment/src/main/resources/dev-ui/ 中添加 Web 组件源 (js) 文件的链接。这必须采用 qwc-extensionname-pagename.js 的格式。

此页面将与内置菜单项(如“配置”或“工作区”)一起显示。

可选:构建时数据

您可以将构建期间收集的数据传递到页面 (js)。

menuPageBuildItem.addBuildTimeData("jokes", jokesBuildItem.getJokes());

您可以为所有您在构建时知道的、并且需要在页面上的数据添加多个键值对。

密钥将自动限定范围,因此您不必添加任何命名空间前缀。稍后我们将讨论如何在 Web 组件(页面)中访问此数据。

Dev UI Jokes

构建 Web 组件

Dev UI 使用 Lit 来简化这些 Web 组件的构建。您可以阅读有关 Web Components 和 Lit 的更多信息:

Web 组件页面的基本结构

Web 组件页面只是一个 JavaScript 类,用于创建一个新的 HTML 元素。

import { LitElement, html, css} from 'lit'; (1)
import { jokes } from 'build-time-data'; (2)

export class QwcJokesList extends LitElement { (3)

    static styles = css` (4)
        .buttonBar {
            display: flex;
            justify-content: space-between;
            gap: 10px;
            align-items: center;
            width: 90%;
            color: var(--lumo-primary-text-color); (5)
        }

        .buttonBar .button {
            width: 100%;
        }
        `;

    static properties = {
        _jokes: {state: true},
        _numberOfJokes: {state: true},
        _message: {state: true},
        _isStreaming: {state: true} (6)
    };

    constructor() { (7)
        super();
        this._jokes = [];
        this._numberOfJokes = 0;
        this._isStreaming = false;
    }

    connectedCallback() { (8)
        super.connectedCallback();
        jokes.forEach((joke) =>{
            var item = this._toJokeItem(joke);
            this._jokes.push(item);
        });
        this._numberOfJokes = this._jokes.length;
    }

    disconnectedCallback() { (9)
        if(this._isStreaming){
            this._observer.cancel();
        }
        super.disconnectedCallback()
    }

    render() { (10)
         return html`<h3>Here are ${this._numberOfJokes} jokes</h3> (11)
            <vaadin-message-list .items="${this._jokes}"></vaadin-message-list>

            ${this._renderLoadingMessage()}
            <div class="buttonBar">
                <vaadin-button class="button" theme="success" @click=${() => this._fetchMoreJokes()}>
                    <vaadin-icon icon="font-awesome-solid:comment"></vaadin-icon> Tell me more jokes
                </vaadin-button>
                <vaadin-checkbox class="button" label="Stream new jokes continuously" @input=${(e) =>this._startStopStreaming(e)}></vaadin-checkbox>
            </div>
            `;
    }

    // ... more private methods
}
customElements.define('qwc-jokes-list', QwcJokesList); (12)
1 您可以从其他库导入类和/或函数。在本例中,我们使用 LitElement 类和来自 Lithtml & css 函数。
2 构建时数据是在构建步骤中定义的,可以使用键从 build-time-data 导入。在构建步骤中添加的所有键都将可用。
3 组件的命名应遵循以下格式:Qwc(代表 Quarkus Web 组件),然后是扩展名称,然后是页面标题,所有内容都用 Camel Case 连接。这也将匹配前面描述的文件名格式。组件还应扩展 LitComponent
4 可以使用 css 函数添加 CSS 样式,并且这些样式仅适用于您的组件。
5 样式可以引用全局定义的 CSS 变量,以确保您的页面正确呈现,尤其是在浅色和深色模式之间切换时。您可以在 Vaadin 文档中找到所有 CSS 变量(颜色尺寸和间距等)。
6 可以添加属性。如果在属性前使用 _,则该属性是私有的。属性通常注入到 HTML 模板中,并且可以定义为具有状态,这意味着如果该属性发生更改,则组件(或其一部分)应重新呈现。在本例中,笑话是我们构建时收集的构建时数据。
7 构造函数(可选)应始终首先调用 super,然后设置属性的默认值。
8 connectedCallback 是 Lit 中的一个方法,当此组件连接到 DOM 时调用。这是执行“页面加载”类型操作的好地方,例如从后端获取数据(我们将在稍后讨论)。
9 disconnectedCallback 是 Lit 中的一个方法,当此组件与 DOM 断开连接时调用。这是执行任何清理工作的好地方。
10 render 是 Lit 中的一个方法,将调用该方法来呈现页面。在此方法中,您将返回您想要的页面的标记。
11 您可以使用来自 Lithtml 函数,它为您提供了一种模板语言来输出您想要的 HTML。创建模板后,您只需设置或更改属性即可重新呈现页面内容。阅读有关 Lit html 的更多信息
12 您必须始终使用唯一标记将您的 Web 组件注册为自定义元素。在这里,标记将遵循与文件名相同的格式(qwc 短横线 extension name 短横线 page title);
热重载

您可以在发生热重载时自动更新屏幕。为此,请将您的 Webcomponent 扩展的 LitElement 替换为 QwcHotReloadElement

QwcHotReloadElement 扩展了 LitElement,因此您的组件仍然是一个 Lit 元素。

当扩展一个 QwcHotReloadElement 时,您必须使用 hotReload 方法。(您还必须提供来自 Lit 的 render 方法)

import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';

// ...

export class QwcMyExtensionPage extends QwcHotReloadElement {

    render(){
        // ...
    }

    hotReload(){
        // ..
    }

}
UI 组件
Vaadin Web 组件

Vaadin Web 组件:Dev UI 大量使用 Vaadin Web 组件作为 UI 构建块。

Qomponent

Qomponent:一些可以使用的自定义构建组件。

目前,以下 UI 组件可用:

  • qui-dot - 渲染 dot 文件。

  • qui-code-block - 渲染代码。(参见下面的代码块部分)

  • qui-directory-tree - 渲染目录树(如工作区中)。

  • qui-alert - 显示警报。

  • qui-card - 卡片组件。

  • qui-switch - 开关按钮。

  • qui-badge - 徽章组件。

代码块

创建一个代码块(包含标记代码)。这也可以是可编辑的。此组件使用来自 qomponent 的上述代码块,该代码块使用 code-mirror 构建,但在切换主题时添加了自动主题状态。

代码可以远程提供 (src) 或作为属性 (content) 或作为 slotted 值(如下例)。

import 'qui-themed-code-block';
<qui-themed-code-block mode="properties">
      <slot>
foo = bar
      </slot>
</qui-themed-code-block>

目前支持以下模式:

  • xml

  • javascript

  • php

  • cpp

  • go

  • rust

  • python

  • json

  • java

  • sql

  • yaml

  • html

  • css

  • sass

  • less

  • markdown

  • asciidoc

  • properties

  • asciiArmor

  • powerShell

  • shell

  • protobuf

  • dockerFile

  • diff

有关更多详细信息,请参阅 @qomponent/qui-code-block

创建一个指向资源的链接(如 Java 源代码文件),该链接可以在用户的 IDE 中打开(如果我们能检测到 IDE)。

import 'qui-ide-link';
<qui-ide-link title='Source full class name'
                        class='text-source'
                        fileName='${sourceClassNameFull}'
                        lineNumber='${sourceLineNumber}'>[${sourceClassNameFull}]</qui-ide-link>;
使用内部控制器

一些 内部控制器 可用于使某些事情更容易

  • 通知器

  • 存储

  • 日志

  • 路由器

通知器

这是一种显示 toast 消息的简单方法。toast 可以放置在屏幕上(默认为左下角),并且可以具有一个级别(信息、成功、警告、错误)。任何级别也可以是主要的,这将创建一个更突出的 toast 消息。

参见此控制器的源代码 此处

使用示例

Dev UI Notifier
import { notifier } from 'notifier';
<a @click=${() => this._info()}>Info</a>;
_info(position = null){
    notifier.showInfoMessage("This is an information message", position);
}

您可以在 此处 找到所有有效位置。

存储

一种以安全方式访问本地存储的简单方法。这将在本地存储中存储值,该值限定为您的扩展。这样,您不必担心可能会与其他扩展冲突。

本地存储对于记住用户偏好或状态很有用。例如,页脚会记住底部抽屉的状态(打开/关闭)和打开时的大小。

import { StorageController } from 'storage-controller';

// ...

storageControl = new StorageController(this); // Passing in this will scope the storage to your extension

// ...

const storedHeight = this.storageControl.get("height"); // Get some value

// ...

this.storageControl.set('height', 123); // Set some val
日志

日志控制器用于向(页脚)日志添加控制按钮。参见 页脚

Dev UI Log control
import { LogController } from 'log-controller';

// ...

logControl = new LogController(this); // Passing in this will scope the control to your extension

// ...
this.logControl
                .addToggle("On/off switch", true, (e) => {
                    this._toggleOnOffClicked(e);
                }).addItem("Log levels", "font-awesome-solid:layer-group", "var(--lumo-tertiary-text-color)", (e) => {
                    this._logLevels();
                }).addItem("Columns", "font-awesome-solid:table-columns", "var(--lumo-tertiary-text-color)", (e) => {
                    this._columns();
                }).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "var(--lumo-tertiary-text-color)", (e) => {
                    this._zoomOut();
                }).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "var(--lumo-tertiary-text-color)", (e) => {
                    this._zoomIn();
                }).addItem("Clear", "font-awesome-solid:trash-can", "var(--lumo-error-color)", (e) => {
                    this._clearLog();
                }).addFollow("Follow log", true , (e) => {
                    this._toggleFollowLog(e);
                }).done();
路由器

路由器主要在内部使用。这在底层使用 Vaadin Router 将 URL 路由到 SPA 中的正确页面/部分。它将更新导航并允许历史记录(后退按钮)。这也会创建扩展上可用的子菜单,这些扩展具有多个页面。

有关一些可能有用的方法,请参见 控制器

全局状态

Web 组件页面上的属性是该页面/Web 组件的范围。还有一些全局状态可用,可能在您的组件中很有用。Dev UI 使用 LitState 为此。当它们使用的共享应用程序状态变量发生更改时,LitState 会自动重新呈现您的 LitElement 组件。它类似于 LitElement 的属性,但然后在多个组件之间共享。

Dev UI 具有以下内置状态:

  • 连接

  • 主题

  • 助手

  • 开发用户界面

连接状态

这将为您提供与后端的连接状态。Dev UI 通过 Web Socket 连接到后端。在某些情况下,UI 可能会失去与后端的连接,例如正在进行热重载或用户实际上停止了服务器。

要在您的页面中使用此状态:

import { observeState } from 'lit-element-state'; (1)
import { connectionState } from 'connection-state'; (2)

export class QwcExtentionPage extends observeState(LitElement) {  (3)
1 从 LitState 库导入 observeState。
2 导入您感兴趣的状态,在本例中为连接状态。
3 将 LitElement 包装在 observerState

现在您可以在页面的任何位置访问连接状态,并且当该状态更改时,它的作用与本地状态完全相同 - 重新呈现页面的相关部分

render() {
    return html`<vaadin-icon title="${connectionState.current.message}" style="color:${connectionState.current.color}" icon="font-awesome-solid:${connectionState.current.icon}"></vaadin-icon>`;
    }

您可以在 此处 查看连接状态的所有属性

主题状态

这将使您可以访问当前主题,用户可以随时更改。

要在您的页面中使用此状态:

import { observeState } from 'lit-element-state'; (1)
import { themeState } from 'theme-state'; (2)

export class QwcExtentionPage extends observeState(LitElement) {  (3)
1 从 LitState 库导入 observeState。
2 导入您感兴趣的状态,在本例中为主题状态。
3 将 LitElement 包装在 observerState

现在您可以在页面的任何位置访问主题状态,并且当该状态更改时,它的作用与本地状态完全相同 - 重新呈现页面的相关部分

render() {
    return html`<div class="codeBlock">
                        <qui-code-block
                            mode='json'
                            content='${json}'
                            theme='${themeState.theme.name}'
                            showLineNumbers>
                        </qui-code-block>`;
    }

您可以在 此处 查看主题状态的所有属性

助手状态

此状态包含有关 Quarkus Assistant 的信息,如果它可用并且已配置并准备好使用。如果您扩展提供了助手功能并且需要知道助手的状态,这将非常有用。

要在您的页面中使用此状态:

import { observeState } from 'lit-element-state'; (1)
import { assistantState } from 'assistant-state'; (2)

export class QwcExtentionPage extends observeState(LitElement) {  (3)
1 从 LitState 库导入 observeState。
2 导入您感兴趣的状态,在本例中为助手状态。
3 将 LitElement 包装在 observerState

现在您可以在页面的任何位置访问助手状态,并且当该状态更改时,它的作用与本地状态完全相同 - 重新呈现页面的相关部分

render() {
    if(assistantState.current.isConfigured){
        return html`<div class="assistantfeature">
                        <span> Magic happends here</span>
                    </div>`;
    }
}

您可以在 此处 查看助手状态的所有属性

Dev UI 状态

此状态是一个通用状态,包含 Dev UI 中使用的全局属性,主要在内部使用。它的工作方式与之前讨论的任何状态完全相同。

此状态中的许多属性实际上是在发生热重载时自动重新加载的构建时数据。

您可以在 此处 查看 dev ui 状态的所有属性

创建页面的其他方法

如前所述,在 Dev UI 中创建页面还有其他一些方法(而不是使用 Web 组件)。

这些链接引用其他(Dev UI 外部的)数据。此数据可以是 HTML 页面、文本或其他数据。

一个很好的例子是 SmallRye OpenAPI 扩展,其中包含指向以 JSON 和 YAML 格式生成的 OpenAPI 模式的链接,以及指向 Swagger UI 的链接

Dev UI extension card

指向这些外部引用的链接在构建时已知。因此,要在您的卡片上获得这样的链接,您可以在您的扩展中添加以下 Build Step:

@BuildStep(onlyIf = IsLocalDevelopment.class)(1)
public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {

    CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); (2)

    cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") (3)
            .url(nonApplicationRootPathBuildItem.resolvePath("openapi")) (4)
            .isYamlContent() (5)
            .icon("font-awesome-solid:file-lines")); (6)

    cardPageBuildItem.addPage(Page.externalPageBuilder("Schema json")
            .url(nonApplicationRootPathBuildItem.resolvePath("openapi") + "?format=json")
            .isJsonContent()
            .icon("font-awesome-solid:file-code"));

    cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI")
            .url(nonApplicationRootPathBuildItem.resolvePath("swagger-ui"))
            .isHtmlContent()
            .icon("font-awesome-solid:signs-post"));

    return cardPageBuildItem;
}
1 始终确保此构建步骤仅在本地开发模式下运行
2 要在卡片上添加任何内容,您必须返回/生成一个 CardPageBuildItem
3 要添加链接,您可以使用 addPage 方法,因为所有链接都转到“页面”。Page 有一些构建器可以帮助构建页面。对于 external 链接,使用 externalPageBuilder
4 添加外部链接的 url(在本例中,我们使用 NonApplicationRootPathBuildItem 来创建此链接,因为此链接位于可配置的非应用程序路径下,默认值为 /q)。如果您的链接在 /q 下可用,请始终使用 NonApplicationRootPathBuildItem
5 您可以(可选地)提示您要导航到的内容的 content type。如果没有提示,将进行 header 调用以确定 MediaType
6 您可以添加一个图标。所有免费的 font-awesome 图标都可用。
嵌入外部内容

默认情况下,即使是外部链接也将在 Dev UI 中内部呈现(嵌入)。对于 HTML,将呈现该页面,任何其他内容将使用 code-mirror 显示以标记媒体类型。例如,YAML 格式的 OpenAPI 模式文档

Dev UI embedded page

如果您不想嵌入内容,您可以使用 Page Builder 上的 .doNotEmbed(),这将在新选项卡中打开链接。

上面的示例假设您知道构建时要使用的链接。在某些情况下,您可能只在运行时才知道这一点。在这种情况下,您可以使用一个 JsonRPC 方法(稍后讨论)返回要添加的链接,并在创建链接时使用它。而不是在页面构建器上使用 .url 方法,使用 .dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")

原始数据页面

如果您有一些构建时已知的数据(构建时数据),您想要显示,您可以使用 Page 中的以下构建器之一:

标记数据

这将在其原始(序列化)JSON 值中显示您的数据

cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") (1)
                .icon("font-awesome-brands:js")
                .buildTimeDataKey("someKey")); (2)
1 使用 rawDataPageBuilder
2 链接回在 Page BuildItem 上 addBuildTimeData 时使用的键。

这将创建一个链接,指向呈现 JSON 中的原始数据的页面

Dev UI raw page
表格数据

如果结构允许,您还可以在表格中显示您的构建时数据

cardPageBuildItem.addPage(Page.tableDataPageBuilder("Table data") (1)
                .icon("font-awesome-solid:table")
                .showColumn("timestamp") (2)
                .showColumn("user") (2)
                .showColumn("fullJoke") (2)
                .buildTimeDataKey("someKey")); (3)
1 使用 tableDataPageBuilder
2 可以选择仅显示某些字段。
3 链接回在 Page BuildItem 上 addBuildTimeData 时使用的键。

这将创建一个链接,指向呈现表格中数据的页面

Dev UI table page
Qute 数据

您还可以使用 Qute 模板显示您的构建时数据。所有构建时数据键都可以在模板中使用

cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") (1)
                .icon("font-awesome-solid:q")
                .templateLink("qute-jokes-template.html")); (2)
1 使用 quteDataPageBuilder
2 链接到 /deployment/src/main/resources/dev-ui/ 中的 Qute 模板。

使用任何 Qute 模板来显示数据,例如 qute-jokes-template.html

<table>
    <thead>
        <tr>
            <th>Timestamp</th>
            <th>User</th>
            <th>Joke</th>
        </tr>
    </thead>
    <tbody>
        {#for joke in jokes} (1)
        <tr>
            <td>{joke.timestamp}</td>
            <td><span><img src="{joke.profilePic}" height="30px"></img> {joke.user}</span></td>
            <td>{joke.fullJoke}</td>
        </tr>
        {/for}
    </tbody>
</table>
1 jokes 作为 Page Build Item 上的构建时数据键添加。

与后端通信

与后端的所有通信都通过 Web Socket 使用 JsonRPC 进行。Dev UI 使扩展开发者可以轻松使用,您实际上不需要深入了解 JsonRPC 或 Web Sockets 的细节。

在运行时(当用户以开发模式运行其应用程序时),与后端通信有 3 个阶段:

  • 针对运行时类路径执行某些方法

  • 针对部署类路径执行某些方法

  • 从某些记录的值返回数据

针对运行时类路径的 JsonRPC

您可以获取或流式传输运行时数据(而不是前面讨论的构建时数据)或针对运行时类路径执行方法(而不是部署类路径)。在运行时获取数据有两个部分。运行时或 runtime-dev 模块中的 Java 端,然后在 Web 组件中使用(我们将在稍后讨论)。

在您的 Runtime 或 Runtime-dev 模块中,创建 JsonRPC 服务。除非您显式地限定 bean 的范围,否则此类将默认为应用程序范围的 bean。所有返回内容的公共方法都可以从 Web 组件 Javascript 中调用。

这些方法中的返回对象可以是:

  • primitives 或 String

  • io.vertx.core.json.JsonArray

  • io.vertx.core.json.JsonObject

  • 任何其他可以序列化为 JSON 的 POJO

上述所有内容都可以是阻塞的(POJO)或非阻塞的(@NonBlockingUni)。或者,可以使用 Multi 流式传输数据。

(1)
public class JokesJsonRPCService {

    private final BroadcastProcessor<Joke> jokeStream = BroadcastProcessor.create();
    private final BroadcastProcessor<Joke> jokeLog = BroadcastProcessor.create();
    private static int numberOfJokesTold = 10;

    @PostConstruct
    void init() {
        Multi.createFrom().ticks().every(Duration.ofMinutes(1)).subscribe().with((item) -> {
            jokeStream.onNext(getJoke());
        });
    }

    public Multi<Joke> streamJokes() { (2)
        return jokeStream;
    }

    @NonBlocking (3)
    public Joke getJoke() {
        numberOfJokesTold++;
        Joke joke = fetchRandomJoke();
        jokeLog.onNext(joke);
        return joke;
    }

    public Multi<Joke> jokeLog() {
        return jokeLog;
    }

    // Some more private methods
}
1 非范围类将默认为 Application Scope
2 可以使用 Multi 流式传输数据
3 此示例以非阻塞方式运行。我们也可以返回 Uni<Joke>

此代码负责使数据可用于在 UI 上显示。

您必须在部署模块的处理器中注册 JsonPRCService

@BuildStep
JsonRPCProvidersBuildItem createJokesJsonRPCService() {(1)
    return new JsonRPCProvidersBuildItem(JokesJsonRPCService.class);(2)
}
1 生成或返回 JsonRPCProvidersBuildItem
2 在运行时或 runtime-dev 模块中定义将包含使数据在 UI 中可用的方法的类

针对部署类路径的 JsonRPC

在某些情况下,您可能需要针对部署类路径执行方法和/或获取数据。这也通过 JsonRPC 通信进行,但在这种情况下,您不会在运行时模块中创建 JsonRPC 服务,您可以只提供在部署模块中的提供者中运行的代码。为此,您将生成一个 BuildTimeActionBuildItem,例如:

    @BuildStep(onlyIf = IsLocalDevelopment.class)
    BuildTimeActionBuildItem createBuildTimeActions() { (1)
        BuildTimeActionBuildItem generateManifestActions = new BuildTimeActionBuildItem();(2)
        generateManifestActions.addAction("generateManifests", params -> { (3)
            try {
                List<Manifest> manifests = holder.getManifests();
                // Avoid relying on databind.
                Map<String, String> map = new LinkedHashMap<>();
                for (Manifest manifest : manifests) {
                    map.put(manifest.getName(), manifest.getContent());
                }
                return map;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        return generateManifestActions;
    }
1 返回或使用 BuildProducer 创建一个 BuildTimeActionBuildItem
2 BuildTimeActionBuildItem 会自动限定您的扩展命名空间
3 在这里,我们添加一个操作,该操作与请求-响应方法相同。方法名称(可以从 js 中调用,与任何 json-rpc 服务一样)是 generateManifests。如果有任何参数,这些参数将在地图中可用(参数)

您还可以返回一个 CompletableFuture/CompletionStage 作为操作,如果您想流式传输数据,则需要使用 addSubscription(而不是 addAction)并返回一个 Flow.Publisher。在这里,您不能使用 Uni 和 Multi,因为我们需要在部署和运行时类路径之间传递数据,因此坚持使用 JDK 类是安全的选择。

针对记录值的 JsonRPC

将记录的数据传递给 UI 的工作方式与上述部署类路径相同,除了您传递 RuntimeValue(从您的记录器返回)而不是函数

@BuildStep(onlyIf = IsLocalDevelopment.class)
BuildTimeActionBuildItem createBuildTimeActions() {
    BuildTimeActionBuildItem actionBuildItem = new BuildTimeActionBuildItem();
    actionBuildItem.addAction("getMyRecordedValue", runtimeValue); (1)

    return actionBuildItem;
}
1 将 RuntimeValue 设置为从您的记录器返回。

Web 组件(页面)中的 JsonRPC

您可以使用内置的 JsonRPC 控制器来访问您定义的任何方法。运行时、部署和记录的工作方式在 WebComponent 中都相同。

import { JsonRpc } from 'jsonrpc';

// ...

jsonRpc = new JsonRpc(this); // Passing in this will scope the RPC calls to your extension

// ...

connectedCallback() {
    super.connectedCallback();
    this.jsonRpc.getJoke().then(jsonRpcResponse => { (1)
        this._addToJokes(jsonRpcResponse.result); (2)
    });
}
1 请注意,方法 getJoke 对应于您的 Java 服务中的方法。此方法返回一个 Promise,其中包含 JsonRPC 结果。
2 在本例中,结果是一个对象,所以我们只是将其添加到我们的笑话列表中。如果服务器返回了某些集合,这也可以是一个数组。

JsonArray(或任何 Java 集合),无论是阻塞的还是非阻塞的,都将返回一个数组;否则,将返回一个 JsonObject。

您还可以在被调用的方法中传递参数,例如:(在运行时 Java 代码中)

public Uni<JsonObject> clear(String name) { (1)
    Optional<Cache> cache = manager.getCache(name);
    if (cache.isPresent()) {
        return cache.get().invalidateAll().map((t) -> getJsonRepresentationForCache(cache.get()));
    } else {
        return Uni.createFrom().item(new JsonObject().put("name", name).put("size", -1));
    }
}
1 clear 方法接受一个名为 name 的参数

在 Webcomponent (Javascript) 中

_clear(name) {
    this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { (1)
        this._updateCache(jsonRpcResponse.result)
    });
}
1 传入 name 参数。

流式传输数据

您可以通过连续向屏幕流式传输数据,使 UI 屏幕保持最新的数据更新。这可以使用 Multi(Java 端)和 Observer(Javascript 端)完成

流式传输数据的 Java 端

public class JokesJsonRPCService {

    private final BroadcastProcessor<Joke> jokeStream = BroadcastProcessor.create();

    @PostConstruct
    void init() {
        Multi.createFrom().ticks().every(Duration.ofHours(4)).subscribe().with((item) -> {
            jokeStream.onNext(getJoke());
        });
    }

    public Multi<Joke> streamJokes() { (1)
        return jokeStream;
    }

    // ...
}
1 返回将流式传输笑话的 Multi

流式传输数据的 Javascript 端

this._observer = this.jsonRpc.streamJokes().onNext(jsonRpcResponse => { (1)
    this._addToJokes(jsonRpcResponse.result);
    this._numberOfJokes = this._numberOfJokes++;
});

// ...

this._observer.cancel(); (2)
1 您可以调用该方法(可选地传入参数),然后提供将在下一个事件上调用的代码。
2 确保保留观察者的实例,以便稍后在需要时取消。

工作区

扩展可以向工作区中的项目贡献 actions

Dev UI workspace

操作实际上是一个 JsonRPC 方法,它将工作区项目作为输入。

为此,您可以在处理器中返回/生成一个 WorkspaceActionBuildItem

    @BuildStep(onlyIf = IsLocalDevelopment.class)
    WorkspaceActionBuildItem createWorkspaceActions() {
        ActionBuilder actionBuilder = Action.actionBuilder() (1)
                .label("Joke") (2)
                .function((t) -> { (3)
                    // Here do something with the input and return something
                    String content = t.content;
                    // ....
                    return t;
                })
                .display(Display.split) (4)
                .displayType(DisplayType.markdown) (5)
                .filter(Patterns.ANY_JAVA); (6)

        return new WorkspaceActionBuildItem(actionBuilder);
    }
1 使用 actionBuilder 创建一个新操作。
2 标签将显示在工作区页面中的操作下拉列表中
3 这是如果用户选择此操作将执行的代码。您将收到一些输入(参见下面的 Input 部分)
4 应如何显示结果(参见下面的 Display 部分)
5 结果类型将是什么(参见下面的 DisplayType 部分)
6 可选过滤器,用于限定此操作仅适用于特定项目。接受正则表达式作为输入,并且在 Patterns 类中存在一些预定义的正则表达式。

输入

您的函数接收的输入是

  • actionId: 唯一的(自动作用域)操作 ID

  • name: 项目(或文件)名称

  • path: 该项目(或文件)的完整路径

  • content: 该项目(或文件)的内容

  • type: 类型(例如 text/plain)

显示

您可以在这里设置响应在工作区页面上的显示方式。选项包括

  • nothing: 不显示任何内容

  • dialog: 内容将显示在对话框弹出窗口中

  • replace: 内容将替换原始(输入)内容

  • split: 内容将显示在分屏中(左/右)

  • notification: 内容将在通知中显示

DisplayType(显示类型)

根据您的操作对内容输入的处理方式,您的输出可能会产生以下类型

  • raw: 这将被按原样使用(文本)

  • code: 这将在代码编辑器中呈现

  • markdown: 这将显示解释的 Markdown

  • html: 这将显示解释的 HTML

  • image: 这将显示图像

高级:自定义卡片

如果您不想使用默认的内置卡片,可以自定义扩展页面上显示的卡片。

为此,您必须提供一个 Webcomponent,它将被加载到提供的卡片的位置,并在 Java Processor 中注册它。

cardPageBuildItem.setCustomCard("qwc-mycustom-card.js");

在 Javascript 端,您可以访问所有页面(如果您想创建链接)

import { pages } from 'build-time-data';

并且以下属性将被传入

  • extensionName(扩展名称)

  • description(描述)

  • 指南

  • namespace(命名空间)

  • logoUrl(Logo URL)

static properties = {
    extensionName: {type: String},
    description: {type: String},
    guide: {type: String},
    namespace: {type: String},
    logoUrl: {type: String}
}

Dev UI 日志

当使用 999-SNAPSHOT 版本运行本地应用程序时,Dev UI 将在页脚中显示 Dev UI 日志。这对于调试浏览器和 Quarkus 应用程序之间流动的所有 JSON RPC 消息非常有用。

在某些情况下,您可能在 Quarkus 核心之外开发扩展(例如 Quarkiverse),因此您的 Quarkus 版本不是 999-SNAPSHOT。在这种情况下,您仍然可以使用以下应用程序属性启用 Dev UI 日志:quarkus.dev-ui.show-json-rpc-log=true

Dev UI Json RPC Log

测试

您可以向扩展添加测试,以测试

  • 构建时数据

  • 通过 JsonRPC 的运行时数据

您必须将此添加到您的 pom 中

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx-http-dev-ui-tests</artifactId>
    <scope>test</scope>
</dependency>

这将使您可以访问两个用于创建这些测试的基类。

测试构建时数据

如果您添加了构建时数据,例如

cardPageBuildItem.addBuildTimeData("somekey", somevalue);

要测试您的构建时数据是否正确生成,您可以添加一个扩展 DevUIBuildTimeDataTest 的测试。

public class SomeTest extends DevUIBuildTimeDataTest {

    @RegisterExtension
    static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication();

    public SomeTest() {
        super("io.quarkus.my-extension");
    }

    @Test
    public void testSomekey() throws Exception {
        JsonNode somekeyResponse = super.getBuildTimeData("somekey");
        Assertions.assertNotNull(somekeyResponse);

        // Check more values on somekeyResponse
    }

}

测试运行时数据

如果您添加了具有运行时数据响应的 JsonRPC 服务,例如

public boolean updateProperties(String content, String type) {
    // ...
}

要测试 updateProperties 是否通过 JsonRPC 正确执行,您可以添加一个扩展 DevUIJsonRPCTest 的测试。

如果 pom 中尚未通过其他依赖项添加以下依赖项,则可能还需要将其添加到 pom 中,否则 Dev UI 将不会在测试期间启动

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx-http-deployment</artifactId>
    <scope>test</scope>
</dependency>
public class SomeTest extends DevUIJsonRPCTest {

    @RegisterExtension
    static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication();

    public SomeTest() {
        super("io.quarkus.my-extension");
    }

    @Test
    public void testUpdateProperties() throws Exception {

        JsonNode updatePropertyResponse = super.executeJsonRPCMethod("updateProperty",
                Map.of(
                        "name", "quarkus.application.name",
                        "value", "changedByTest"));
        Assertions.assertTrue(updatePropertyResponse.asBoolean());

        // Get the properties to make sure it is changed
        JsonNode allPropertiesResponse = super.executeJsonRPCMethod("getAllValues");
        String applicationName = allPropertiesResponse.get("quarkus.application.name").asText();
        Assertions.assertEquals("changedByTest", applicationName);
    }
}

相关内容