Qute Templating Engine

Qute 是专门为满足 Quarkus 需求而设计的模板引擎。尽可能减少反射的使用,以减小原生镜像的大小。此 API 同时结合了命令式以及非阻塞反应式编码样式。在开发模式下,观察 `src/main/resources/templates`中所有文件的更改,修改内容会立即显示出来。此外,我们尝试在构建时检测到大部分模板问题。在本指南中,您将了解如何在应用程序中轻松呈现模板。

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 qute-quickstart directory

Serving Qute templates via HTTP

如果您想要通过 HTTP 提供模板:

  1. Qute Web 扩展允许您直接通过 HTTP 提供位于 `src/main/resources/templates/pub/`中的模板。在这种情况下,您不需要任何 Java 代码来“插入”模板,例如,模板 `src/main/resources/templates/pub/foo.html`将默认从路径 `/foo`和 `/foo.html`提供。

  2. 为了进行更精细的控制,您可以将它与 Quarkus REST 结合使用,以控制如何提供您的模板。位于 `src/main/resources/templates`目录及其子目录中的所有文件都注册为模板,并且可以将其注入到 REST 资源中。

pom.xml
<dependency>
    <groupId>io.quarkiverse.qute.web</groupId>
    <artifactId>quarkus-qute-web</artifactId>
</dependency>
build.gradle
implementation("io.quarkiverse.qute.web:quarkus-qute-web")

Qute Web 扩展虽然托管在 Quarkiverse 中,但它是 Quarkus 平台的一部分,其版本在 Quarkus Platform BOM 中定义。

Serving Hello World with Qute

让我们从 Hello World 模板开始:

src/main/resources/templates/pub/hello.html
<h1>Hello {http:param('name', 'Quarkus')}!</h1> 1
1 `{http:param('name', 'Quarkus')}`是一个在呈现模板时计算的表达式(Quarkus 是默认值)。

位于 `pub`目录中的模板是通过 HTTP 提供的。此行为是内置行为,不需要控制器。例如,模板 `src/main/resources/templates/pub/foo.html`将默认从路径 `/foo`和 `/foo.html`提供。

如果您的应用程序正在运行,您可以打开浏览器并按 [role="bare"][role="bare"]http://localhost:8080/hello?name=Martin

有关 Qute Web 选项的更多信息,请参见 Qute Web guide

Hello Qute and REST

为了进行更精细的控制,您可以将 Qute Web 与 Quarkus REST 或 Quarkus RESTEasy 结合使用,以控制如何提供您的模板

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-rest")

一个非常简单的文本模板:

hello.txt
Hello {name}! 1
1 `{name}`是一个在呈现模板时计算的值表达式。

现在让我们把“已编译”的模板注入到资源类中。

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("hello")
public class HelloResource {

    @Inject
    Template hello; 1

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return hello.data("name", name); 2 3
    }
}
1 如果没有提供 @Location 限定符,则会使用字段名来找到模板。在这种情况中,我们注入的模板路径为 templates/hello.txt
2 Template.data() 返回一个新模板实例,在触发实际渲染之前可以对其进行自定义。在这种情况下,我们将名称值放到键 name 之下。数据映射可在渲染期间访问。
3 请注意,我们不会触发渲染 - 这由特殊的 ContainerResponseFilter 实现自动完成。

如果你的应用程序正在运行,则可以请求端点:

$ curl -w "\n" http://localhost:8080/hello?name=Martin
Hello Martin!

Type-safe templates

在 Java 代码中声明模板的备用方法依赖于以下约定:

  • 按资源类别对 /src/main/resources/templates 目录中的模板文件进行组织,方法是将文件组合到每种资源类别一个目录中。因此,如果 ItemResource 类引用了两个模板 hellogoodbye,请把它们放置到 /src/main/resources/templates/ItemResource/hello.txt/src/main/resources/templates/ItemResource/goodbye.txt。按资源类别对模板进行分组,有助于更容易地导航到它们。

  • 在每个资源类中,在你的资源类内声明一个 @CheckedTemplate static class Template {} 类。

  • 为资源的每个模板文件声明一个 public static native TemplateInstance method();

  • 使用这些静态方法来构建你的模板实例。

以下是使用这种风格重写的先前示例:

我们将从一个非常简单的模板开始:

HelloResource/hello.txt
Hello {name}! 1
1 `{name}`是一个在呈现模板时计算的值表达式。

现在让我们在资源类中声明并使用这些模板。

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.CheckedTemplate;

@Path("hello")
public class HelloResource {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance hello(String name); 1
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return Templates.hello(name); 2
    }
}
1 这声明了一个路径为 templates/HelloResource/hello 的模板。
2 Templates.hello() 返回一个新模板实例,该实例由资源方法返回。请注意,我们不会触发渲染 - 这由特殊的 ContainerResponseFilter 实现自动完成。

声明了一个 @CheckedTemplate 类后,我们将检查其所有方法是否指向现有模板,因此,如果你尝试从你的 Java 代码中使用模板并且忘记添加它,我们将在构建时通知你 :)

请记住,此声明样式允许你引用其他资源中声明的模板:

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;

@Path("goodbye")
public class GoodbyeResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return HelloResource.Templates.hello(name);
    }
}

Top-level type-safe templates

自然地,如果你想在顶级直接声明模板,例如 /src/main/resources/templates/hello.txt,则可以将它们声明在顶级(非嵌套)Templates 类中:

HelloResource.java
package org.acme.quarkus.sample;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;
import io.quarkus.qute.CheckedTemplate;

@CheckedTemplate
public class Templates {
    public static native TemplateInstance hello(String name); 1
}
1 这声明了一个路径为 templates/hello 的模板。

Template Parameter Declarations

如果您在模板中声明 parameter declaration,那么 Qute 会尝试验证引用此参数的所有表达式,如果找到不正确的表达式,则构建失败。

我们假设我们有一个简单的类,如下所示:

Item.java
public class Item {
    public String name;
    public BigDecimal price;
}

并且我们希望呈现一个包含商品名称和价格的简单 HTML 页面。

让我们从模板开始:

ItemResource/item.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{item.name}</title> 1
</head>
<body>
    <h1>{item.name}</h1>
    <div>Price: {item.price}</div> 2
</body>
</html>
1 此表达式经过验证。尝试将表达式更改为 {item.nonSense},并且构建应该失败。
2 This is also validated.

最后,让我们使用类型安全的模板创建一个资源类:

ItemResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;
import io.quarkus.qute.CheckedTemplate;

@Path("item")
public class ItemResource {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance item(Item item); 1
    }

    @GET
    @Path("{id}")
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance get(@PathParam("id") Integer id) {
        return Templates.item(service.findItem(id)); 2
    }
}
1 声明一个方法,该方法为 templates/ItemResource/item.html 提供 TemplateInstance ,并声明其 Item item 参数,以便我们可以验证模板。
2 使模板中可以访问 Item 对象。

当启用 --parameters 编译器参数时,Quarkus REST 可以从方法参数名称推断参数名称,从而使 @PathParam("id") 注释在这种情况下可选。

Template parameter declaration inside the template itself

或者,你可以在模板文件本身中声明模板参数。

让我们从模板开始:

item.html
{@org.acme.Item item} 1
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{item.name}</title> 2
</head>
<body>
    <h1>{item.name}</h1>
    <div>Price: {item.price}</div>
</body>
</html>
1 可选参数声明。Qute 会尝试验证引用参数 item 的所有表达式。
2 此表达式经过验证。尝试将表达式更改为 {item.nonSense},并且构建应该失败。

最后,让我们创建一个资源类。

ItemResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("item")
public class ItemResource {

    @Inject
    ItemService service;

    @Inject
    Template item; 1

    @GET
    @Path("{id}")
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance get(Integer id) {
        return item.data("item", service.findItem(id)); 2
    }
}
1 使用路径 templates/item.html 注入模板。
2 使模板中可以访问 Item 对象。

Template Extension Methods

Template extension methods 用于扩展数据对象的可用属性集。

有时,你无法控制要在模板中使用的类,并且你无法向其中添加方法。模板扩展方法允许你为那些类声明新方法,这些方法与它们属于目标类一样可以从你的模板中获得。

让我们继续扩展我们的包含商品名称、价格的简单 HTML 页面,并添加一个折扣价格。折扣价有时称为“计算属性”。我们将实现一个模板扩展方法以便轻松地呈现此属性。让我们更新我们的模板:

HelloResource/item.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{item.name}</title>
</head>
<body>
    <h1>{item.name}</h1>
    <div>Price: {item.price}</div>
    {#if item.price > 100} 1
    <div>Discounted Price: {item.discountedPrice}</div> 2
    {/if}
</body>
</html>
1 if 是一个基本的控制流环节。
2 该表达式还会根据 Item 类进行验证,但显然没有声明这样的属性。但是,在 TemplateExtensions 类上声明了一个模板扩展方法 - 如下所示。

最后,让我们创建一个类,将我们所有扩展方法都放在其中:

TemplateExtensions.java
package org.acme.quarkus.sample;

import io.quarkus.qute.TemplateExtension;

@TemplateExtension
public class TemplateExtensions {

    public static BigDecimal discountedPrice(Item item) { 1
        return item.price.multiply(new BigDecimal("0.9"));
    }
}
1 可以使用静态模板扩展方法为数据类添加“计算属性”。第一个参数的类型用于匹配基对象,方法名称用于匹配属性名称。

如果你使用 `@TemplateExtension`注释,你可以在每个类中添加模板扩展方法,但我们建议按照目标类型进行分组或在单个 `TemplateExtensions`类中按照约定。

Rendering Periodic Reports

在呈现定期报告时,模板引擎也可以很有用。你需要首先添加 quarkus-schedulerquarkus-qute 扩展。在你的 pom.xml 文件中,添加:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-qute</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-scheduler</artifactId>
</dependency>

假设我们有一个 SampleService bean,它的 get() 方法返回一个示例列表。

Sample.java
public class Sample {
    public boolean valid;
    public String name;
    public String data;
}

模板很简单:

report.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Report {now}</title>
</head>
<body>
    <h1>Report {now}</h1>
    {#for sample in samples} 1
      <h2>{sample.name ?: 'Unknown'}</h2> 2
      <p>
      {#if sample.valid}
        {sample.data}
      {#else}
        <strong>Invalid sample found</strong>.
      {/if}
      </p>
    {/for}
</body>
</html>
1 此循环部分能够遍历可迭代对象、映射和流。
2 此值表达式使用 elvis operator - 如果名称为 null,则使用默认值。
ReportGenerator.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;

import io.quarkus.qute.Template;
import io.quarkus.qute.Location;
import io.quarkus.scheduler.Scheduled;

public class ReportGenerator {

    @Inject
    SampleService service;

    @Location("reports/v1/report_01") 1
    Template report;

    @Scheduled(cron="0 30 * * * ?") 2
    void generate() {
        String result = report
            .data("samples", service.get())
            .data("now", java.time.LocalDateTime.now())
            .render(); 3
        // Write the result somewhere...
    }
}
1 在这种情况下,我们使用 @Location 限定符来指定模板路径: templates/reports/v1/report_01.html.
2 使用 @Scheduled 注释,指导 Quarkus 每半小时执行此方法。有关更多信息,请参阅 Scheduler 指南。
3 TemplateInstance.render() 方法触发呈现。请注意此方法会阻塞当前线程。

Qute Reference Guide

要了解更多关于 Qute 的信息,请参阅 Qute reference guide.

Qute Configuration Reference

Unresolved include directive in modules/ROOT/pages/qute.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-qute.adoc[]