视图技术

Spring WebFlux 中的视图渲染是可插拔的。无论你选择使用 Thymeleaf、FreeMarker 还是其他视图技术,主要都是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。

有关视图渲染的更多上下文信息,请参阅 视图解析

Spring WebFlux 应用程序的视图位于应用程序的内部信任边界内。视图可以访问应用程序上下文中的 bean,因此,我们不建议在外部源可以编辑模板的应用程序中使用 Spring WebFlux 模板支持,因为这可能存在安全隐患。

Thymeleaf

Thymeleaf 是一个现代的服务器端 Java 模板引擎,强调自然 HTML 模板,可以在浏览器中通过双击预览,这对于独立进行 UI 模板工作(例如,由设计师)而无需运行服务器非常有用。Thymeleaf 提供了丰富的功能集,并且正在积极开发和维护中。有关更完整的介绍,请参阅 Thymeleaf 项目主页。

Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目管理。配置涉及一些 bean 声明,例如 SpringResourceTemplateResolverSpringWebFluxTemplateEngineThymeleafReactiveViewResolver。有关更多详细信息,请参阅 Thymeleaf+Spring 以及 WebFlux 集成公告

FreeMarker

Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件等各种文本输出。Spring Framework 内置了使用 Spring WebFlux 与 FreeMarker 模板集成的支持。

视图配置

以下示例展示了如何将 FreeMarker 配置为视图技术

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.freeMarker();
	}

	// Configure FreeMarker...

	@Bean
	public FreeMarkerConfigurer freeMarkerConfigurer() {
		FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
		configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
		return configurer;
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	override fun configureViewResolvers(registry: ViewResolverRegistry) {
		registry.freeMarker()
	}

	// Configure FreeMarker...

	@Bean
	fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
		setTemplateLoaderPath("classpath:/templates/freemarker")
	}
}

你的模板需要存储在 FreeMarkerConfigurer 指定的目录中,如前面的示例所示。根据前面的配置,如果你的控制器返回视图名称 welcome,解析器将查找 classpath:/templates/freemarker/welcome.ftl 模板。

FreeMarker 配置

你可以通过在 FreeMarkerConfigurer bean 上设置相应的 bean 属性,将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 的 Configuration 对象(由 Spring 管理)。freemarkerSettings 属性需要一个 java.util.Properties 对象,而 freemarkerVariables 属性需要一个 java.util.Map。以下示例展示了如何使用 FreeMarkerConfigurer

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	// ...

	@Bean
	public FreeMarkerConfigurer freeMarkerConfigurer() {
		Map<String, Object> variables = new HashMap<>();
		variables.put("xml_escape", new XmlEscape());

		FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
		configurer.setTemplateLoaderPath("classpath:/templates");
		configurer.setFreemarkerVariables(variables);
		return configurer;
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	// ...

	@Bean
	fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
		setTemplateLoaderPath("classpath:/templates")
		setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
	}
}

有关应用于 Configuration 对象的设置和变量的详细信息,请参阅 FreeMarker 文档。

表单处理

Spring 提供了一个用于 JSP 的标签库,其中包含(除其他外)一个 <spring:bind/> 元素。此元素主要用于表单显示来自表单支持对象的 值,并显示来自 Web 或业务层的 Validator 的验证失败结果。Spring 在 FreeMarker 中也支持相同的功能,并提供了用于生成表单输入元素本身的额外便利宏。

绑定宏

spring-webflux.jar 文件中维护了一套标准的 FreeMarker 宏,因此它们总是可用于适当配置的应用程序。

Spring 模板库中定义的一些宏被认为是内部(私有)的,但在宏定义中不存在这样的作用域,使得所有宏对于调用代码和用户模板都可见。以下章节仅关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,该文件名为 spring.ftl,位于 org.springframework.web.reactive.result.view.freemarker 包中。

有关绑定支持的更多详细信息,请参阅 Spring MVC 的 简单绑定

表单宏

有关 Spring 对 FreeMarker 模板的表单宏支持的详细信息,请查阅 Spring MVC 文档的以下章节。

脚本视图

Spring Framework 内置了对使用 Spring WebFlux 与任何可以在 JSR-223 Java 脚本引擎之上运行的模板库进行集成的支持。下表显示了我们在不同脚本引擎上测试过的模板库

脚本库 脚本引擎

Handlebars

Nashorn

Mustache

Nashorn

React

Nashorn

EJS

Nashorn

ERB

JRuby

字符串模板

Jython

Kotlin 脚本模板

Kotlin

集成任何其他脚本引擎的基本规则是它必须实现 ScriptEngineInvocable 接口。

要求

你需要将脚本引擎放在你的类路径中,具体细节因脚本引擎而异

  • Nashorn JavaScript 引擎随 Java 8+ 提供。强烈建议使用最新的可用更新版本。

  • 对于 Ruby 支持,应将 JRuby 添加为依赖项。

  • 对于 Python 支持,应将 Jython 添加为依赖项。

  • 对于 Kotlin 脚本支持,应添加 org.jetbrains.kotlin:kotlin-script-util 依赖项以及一个包含 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 行的 META-INF/services/javax.script.ScriptEngineFactory 文件。有关更多详细信息,请参阅此示例

你需要拥有脚本模板库。对于 JavaScript,一种方法是通过 WebJars

脚本模板

你可以声明一个 ScriptTemplateConfigurer bean 来指定要使用的脚本引擎、要加载的脚本文件、调用哪个函数来渲染模板等等。以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.scriptTemplate();
	}

	@Bean
	public ScriptTemplateConfigurer configurer() {
		ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
		configurer.setEngineName("nashorn");
		configurer.setScripts("mustache.js");
		configurer.setRenderObject("Mustache");
		configurer.setRenderFunction("render");
		return configurer;
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	override fun configureViewResolvers(registry: ViewResolverRegistry) {
		registry.scriptTemplate()
	}

	@Bean
	fun configurer() = ScriptTemplateConfigurer().apply {
		engineName = "nashorn"
		setScripts("mustache.js")
		renderObject = "Mustache"
		renderFunction = "render"
	}
}

render 函数使用以下参数调用

  • String template: 模板内容

  • Map model: 视图模型

  • RenderingContext renderingContext: 提供访问应用程序上下文、区域设置、模板加载器和 URL 的 RenderingContext(从 5.0 开始)

Mustache.render() 本身就与此签名兼容,因此你可以直接调用它。

如果你的模板技术需要一些自定义,你可以提供一个实现自定义渲染函数的脚本。例如,Handlerbars 在使用模板之前需要先编译它们,并且需要一个polyfill 来模拟服务器端脚本引擎中不可用的一些浏览器功能。以下示例展示了如何设置自定义渲染函数

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.scriptTemplate();
	}

	@Bean
	public ScriptTemplateConfigurer configurer() {
		ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
		configurer.setEngineName("nashorn");
		configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
		configurer.setRenderFunction("render");
		configurer.setSharedEngine(false);
		return configurer;
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	override fun configureViewResolvers(registry: ViewResolverRegistry) {
		registry.scriptTemplate()
	}

	@Bean
	fun configurer() = ScriptTemplateConfigurer().apply {
		engineName = "nashorn"
		setScripts("polyfill.js", "handlebars.js", "render.js")
		renderFunction = "render"
		isSharedEngine = false
	}
}
当使用非线程安全的脚本引擎和未设计用于并发的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要将 sharedEngine 属性设置为 false。在这种情况下,由于此 bug,需要 Java SE 8 update 60,但通常建议无论如何都使用最新的 Java SE 补丁版本。

polyfill.js 仅定义了 Handlebars 正常运行所需的 window 对象,如下面的代码片段所示

var window = {};

这个基本的 render.js 实现在使用模板之前先编译它。一个生产就绪的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端完成,以及你需要的任何自定义(例如管理模板引擎配置)。以下示例展示了如何编译模板

function render(template, model) {
	var compiledTemplate = Handlebars.compile(template);
	return compiledTemplate(model);
}

查看 Spring Framework 单元测试的 Java资源,获取更多配置示例。

HTML 片段

HTMXHotwire Turbo 强调一种“HTML over the wire”方法,客户端接收服务器的更新是 HTML 格式而不是 JSON 格式。这使得无需编写大量甚至任何 JavaScript 即可获得 SPA(单页应用)的好处。要获得良好的概览并了解更多信息,请访问它们各自的网站。

在 Spring WebFlux 中,视图渲染通常涉及指定一个视图和一个模型。然而,在 HTML over the wire 中,一个常见的功能是发送多个 HTML 片段,浏览器可以使用这些片段更新页面的不同部分。为此,控制器方法可以返回 Collection<Fragment>。例如

  • Java

  • Kotlin

@GetMapping
List<Fragment> handle() {
	return List.of(Fragment.create("posts"), Fragment.create("comments"));
}
@GetMapping
fun handle(): List<Fragment> {
	return listOf(Fragment.create("posts"), Fragment.create("comments"))
}

通过返回专用类型 FragmentsRendering 也可以做到同样的事情

  • Java

  • Kotlin

@GetMapping
FragmentsRendering handle() {
	return FragmentsRendering.with("posts").fragment("comments").build();
}
@GetMapping
fun handle(): FragmentsRendering {
	return FragmentsRendering.with("posts").fragment("comments").build()
}

每个片段都可以有一个独立的模型,并且该模型继承了请求的共享模型的属性。

HTMX 和 Hotwire Turbo 支持通过 SSE(服务器发送事件)进行流式更新。控制器可以使用 Flux<Fragment> 创建 FragmentsRendering,或者通过 ReactiveAdapterRegistry 将任何其他响应式生产者适配到 Reactive Streams Publisher。也可以直接返回 Flux<Fragment> 而无需 FragmentsRendering 包装。

JSON 和 XML

出于 内容协商 的目的,根据客户端请求的内容类型,能够在 HTML 模板或其他格式(如 JSON 或 XML)之间交替渲染模型非常有用。为了支持这一点,Spring WebFlux 提供了 HttpMessageWriterView,你可以使用它来插入 spring-web 中任何可用的 编解码器,例如 Jackson2JsonEncoderJackson2SmileEncoderJaxb2XmlEncoder

与其他视图技术不同,HttpMessageWriterView 不需要 ViewResolver,而是被 配置 为默认视图。你可以配置一个或多个此类默认视图,包装不同的 HttpMessageWriter 实例或 Encoder 实例。在运行时,会使用与请求内容类型匹配的那个。

在大多数情况下,模型包含多个属性。为了确定要序列化哪个属性,你可以通过配置 HttpMessageWriterView 指定用于渲染的模型属性名称。如果模型只包含一个属性,则使用该属性。