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 提供模板:
-
Qute Web 扩展允许您直接通过 HTTP 提供位于 `src/main/resources/templates/pub/`中的模板。在这种情况下,您不需要任何 Java 代码来“插入”模板,例如,模板 `src/main/resources/templates/pub/foo.html`将默认从路径 `/foo`和 `/foo.html`提供。
-
为了进行更精细的控制,您可以将它与 Quarkus REST 结合使用,以控制如何提供您的模板。位于 `src/main/resources/templates`目录及其子目录中的所有文件都注册为模板,并且可以将其注入到 REST 资源中。
<dependency>
<groupId>io.quarkiverse.qute.web</groupId>
<artifactId>quarkus-qute-web</artifactId>
</dependency>
implementation("io.quarkiverse.qute.web:quarkus-qute-web")
Qute Web 扩展虽然托管在 Quarkiverse 中,但它是 Quarkus 平台的一部分,其版本在 Quarkus Platform BOM 中定义。 |
Serving Hello World with Qute
让我们从 Hello World 模板开始:
<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 结合使用,以控制如何提供您的模板
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
一个非常简单的文本模板:
Hello {name}! 1
1 | `{name}`是一个在呈现模板时计算的值表达式。 |
现在让我们把“已编译”的模板注入到资源类中。
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
类引用了两个模板hello
和goodbye
,请把它们放置到/src/main/resources/templates/ItemResource/hello.txt
和/src/main/resources/templates/ItemResource/goodbye.txt
。按资源类别对模板进行分组,有助于更容易地导航到它们。 -
在每个资源类中,在你的资源类内声明一个
@CheckedTemplate static class Template {}
类。 -
为资源的每个模板文件声明一个
public static native TemplateInstance method();
。 -
使用这些静态方法来构建你的模板实例。
以下是使用这种风格重写的先前示例:
我们将从一个非常简单的模板开始:
Hello {name}! 1
1 | `{name}`是一个在呈现模板时计算的值表达式。 |
现在让我们在资源类中声明并使用这些模板。
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 实现自动完成。 |
声明了一个 |
请记住,此声明样式允许你引用其他资源中声明的模板:
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
类中:
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 会尝试验证引用此参数的所有表达式,如果找到不正确的表达式,则构建失败。
我们假设我们有一个简单的类,如下所示:
public class Item {
public String name;
public BigDecimal price;
}
并且我们希望呈现一个包含商品名称和价格的简单 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. |
最后,让我们使用类型安全的模板创建一个资源类:
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 对象。 |
当启用 |
Template parameter declaration inside the template itself
或者,你可以在模板文件本身中声明模板参数。
让我们从模板开始:
{@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} ,并且构建应该失败。 |
最后,让我们创建一个资源类。
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 页面,并添加一个折扣价格。折扣价有时称为“计算属性”。我们将实现一个模板扩展方法以便轻松地呈现此属性。让我们更新我们的模板:
<!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 类上声明了一个模板扩展方法 - 如下所示。 |
最后,让我们创建一个类,将我们所有扩展方法都放在其中:
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-scheduler
和 quarkus-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()
方法返回一个示例列表。
public class Sample {
public boolean valid;
public String name;
public String data;
}
模板很简单:
<!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,则使用默认值。 |
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.