视图技术

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框架内置了将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“设置”和“共享变量”传递到FreeMarkerConfiguration对象(由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()))
	}
}

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

表单处理

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

绑定宏

一组标准宏在spring-webflux.jar文件中维护,以便它们始终可用于适当配置的应用程序。

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

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

表单宏

有关Spring的FreeMarker模板表单宏支持的详细信息,请参阅Spring MVC文档的以下部分。

脚本视图

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

脚本库 脚本引擎

Handlebars

Nashorn

Mustache

Nashorn

React

Nashorn

EJS

Nashorn

ERB

JRuby

字符串模板

Jython

Kotlin脚本模板

Kotlin

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

需求

您需要在类路径上拥有脚本引擎,具体细节因脚本引擎而异

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

  • 为了支持 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 renderingContextRenderingContext 提供对应用程序上下文、区域设置、模板加载器和 URL(自 5.0 版起)的访问。

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

如果您的模板技术需要一些自定义,您可以提供一个实现自定义渲染函数的脚本。例如,Handlebars 需要在使用模板之前编译它们,并且需要一个 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。在这种情况下,由于 此错误,需要 Java SE 8 更新 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 格式的服务器更新。这允许获得单页应用程序 (SPA) 的好处,而无需编写很多甚至任何 JavaScript。要获得良好的概述并了解更多信息,请访问它们的各自网站。

在 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> 或任何其他可通过 ReactiveAdapterRegistry 适应 Reactive Streams Publisher 的反应式生产者创建 FragmentsRendering。也可以直接返回 Flux<Fragment> 而无需 FragmentsRendering 包装器。

JSON 和 XML

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

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

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