Qute - 另一个模板引擎(为何如此)

Qute 是一项实验性功能。

在解决方案成熟之前,不保证平台的稳定性或长期存在。

这里有 介绍指南 和更全面的 参考指南

让我们从一个很好的问题开始:“为什么又一个模板引擎?”。Java 中有很多模板库。Quarkus 以构建在“一流库和标准”之上而闻名。这是真的。另一方面,Quarkus 社区也是一个强大的创新催化剂。因此,我们决定启动 Qute(QUarkus TEmplates)——一个专门为满足 Quarkus 需求而设计的模板引擎。我们相信,即使在模板这样一个已经被广泛探索的领域,我们也能带来新的想法。

基本理念

我们的主要目标是提供一个有主见的创新模板引擎。但我们不想重新发明轮子。相反,我们从现有技术中汲取灵感。举几个例子:

但这还不是全部。我们基于 Quarkus 的原则引入了新功能……

异步数据解析 - 通往响应式之路

在我们开始设计 Qute 时,我们有一个重要的方面——数据解析 API 应该是异步的。这允许更好的资源利用,并符合 Quarkus 的响应式模型。此设计决策的另一个后果是,可以直接从模板利用非阻塞客户端,即从各种源异步获取数据。

非阻塞客户端数据获取示例
{@org.acme.Client client} (1)
<html>
<body>
    <h1>Quarkus Open Pull Requests</h1>
    {#for pull in client.pullRequests} (2)
        <p>{pull.title} - {pull.user.login}</p>
    {/for}
</body>
</html>
1 参数声明 - 将 client 映射到 org.acme.Client。有关更多信息,请参阅 下一节
2 org.acme.Client#getPullRequests() 使用非阻塞 Vert.x 客户端直接从 GitHub API 获取数据。由于数据解析是异步的,线程不会被阻塞,可以继续执行其他任务。
CompletionStage<JsonArray> getPullRequests() {
   return webClient
            .get(80, "api.github.com", "/repos/quarkusio/quarkus/pulls?state=open&per_page=10")
            .as(BodyCodec.jsonArray())
            .send()
            .thenCompose(r -> {
               if (r.statusCode() == 200) {
                  return CompletableFuture.completedFuture(r.body());
               } else {
                  // Log errors etc.
               }
            });
}

类型安全模板

大多数模板引擎都不是类型安全的,即不防止类型错误。这很自然,因为模板中的动态性通常非常实用。另一方面,用户不会免受由拼写错误和各种重构后果引起的繁琐错误的影响。Qute 模板可以选择性地进行类型安全。这到底意味着什么?一个模板可以包含一个或多个参数声明。参数声明将具体的类型信息绑定到当前上下文中的给定标识符。拥有类型安全模板有什么好处?

  • Quarkus 会验证引用参数声明的所有表达式。如果找到无效/不正确的表达式,构建将失败。

在开发模式下,src/main/resources/templates 目录中的所有文件都会被监视更改,修改会立即可见。这也意味着,只要出现类型错误,您的应用程序就会快速失败。
  • 为参数声明中使用的所有类型生成值解析器,以便无需反射即可访问其属性。这在定位 GraalVM 原生镜像时非常有用。

  • 我们的待办事项清单还有一些想法,例如类型安全表达式的性能优化等。

类型安全模板示例
{@org.acme.Foo foo} (1)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qute Hello</title>
</head>
<body>
  <h1>{title}</h1>  (2)
  <p>{foo.message}</p>  (3)
  {#for foo in baz.foos}
    <p>Hello {foo.message}!</p>  (4)
  {/for}
</body>
</html>
1 参数声明 - 将 foo 映射到 org.acme.Foo
2 {title} 未经验证 - 与参数声明不匹配。
3 {foo.message} 已经验证。org.acme.Foo 必须具有 message 属性或存在匹配的模板扩展方法。
4 {foo.message} 未经验证,因为 foo 在循环部分被覆盖,并且没有可用的类型信息。
目前仅在表达式中验证属性;“虚拟方法”(如 foo.getBar(baz.name))当前被忽略。

一流的 Quarkus 公民

尽管 Qute 对 Quarkus 进行了高度优化,但核心引擎被开发为一个独立的库,可以集成到任何环境中。

在 Quarkus 中,src/main/resources/templates 目录中的所有模板都经过验证,并且可以轻松注入。

模板注入示例
package org.acme.qute;

import io.quarkus.qute.Template;

class MyBean {

    @Inject
    Template items; (1)

    @Inject
    Service service;

    String renderItems() {
       return items.data("items", service.getItems()).render(); (2)
    }
}
1 字段名用于定位模板。在此特定情况下,容器将尝试定位路径为 src/main/resources/templates/items.html 的模板。如果不存在这样的模板,构建将失败。
2 请参阅 Hello World 示例 来探索基本工作流程。

此外,还提供了一个预配置的 Engine 实例,可供注入。Engine 是模板管理的核心点,并提供了一些底层 API。

RESTEasy 集成

如果与 RESTEasy 一起使用,资源方法可以返回 TemplateInstance,集成代码将负责所有必要的步骤并将输出呈现给响应。有关更多信息,请参阅 RESTEasy 集成

JAX-RS 资源示例
package org.acme.qute;
...
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) {
        // the template looks like: Hello {name}!
        return hello.data("name", name); (2) (3)
    }
}
1 字段名用于定位模板。在此特定情况下,我们注入的是路径为 templates/hello.txt 的模板。
2 Template.data() 返回一个新的模板实例,可以在触发实际渲染之前对其进行自定义。在本例中,我们将 name 值放在键 name 下。在渲染期间可以访问数据映射。
3 请注意,我们不会触发渲染 - 这是由特殊的 ContainerResponseFilter 实现自动完成的。

Mailer 集成

创建电子邮件消息时,模板可能很有用。Mailer 扩展与 Qute 集成,提供了发送电子邮件的便捷方式。特别是,消息正文是使用 src/main/resources/templates 目录中的 *.html*.txt 模板自动创建的。有关更多详细信息,请参阅 发送电子邮件 指南。

结论

Qute 最初在 Quarkus 1.1.0.Final 中发布。自那时以来,我们修复了许多错误并实现了一些功能请求。欢迎加入我们的社区,稳定 API,加强实现并探索新的可能性!