Spring Reactive项目中如何触发Thymeleaf模板中非响应式值更新?
为什么isLoading标志不更新
你当前的代码中,modelMap.addAttribute("isLoading", isLoading.get())是将**AtomicBoolean的当前值(true)**存入模型,后续isLoading.set(false)的修改不会同步到模型中——因为模型里存储的是一个固定的布尔值,而非AtomicBoolean的引用。同时,Thymeleaf的服务器端渲染是一次性生成HTML内容(即使使用ReactiveDataDriverContextVariable流式输出,也是服务器端逐步推送HTML片段,而非实时更新已渲染的DOM),所以模板无法感知后端变量的后续变化。
是否可以混合响应式值与非响应式值
完全可以。Thymeleaf对Spring WebFlux的响应式类型(Mono/Flux)有原生支持,你可以在同一模板中同时使用响应式数据(如通过ReactiveDataDriverContextVariable处理的Flux)和普通的非响应式数据(如字符串、基本类型等),只要遵循响应式数据的渲染规则即可。
解决方案:实现加载器的显示与隐藏
推荐使用客户端JavaScript的方式处理,因为流式渲染的特性天然适合这种场景:浏览器会先接收到包含加载器的初始HTML,随后逐步接收Flux推送的表格行数据,此时通过JS监听DOM变化即可控制加载器的显示。
步骤1:修改模板,默认显示加载器
移除模板中th:if="${isLoading}"的判断,让加载器默认显示,然后通过JS在数据加载完成后隐藏:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <head> <title>Spring Reactive</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" th:href="@{/main.css}"/> <link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.2.3/css/bootstrap.css}"/> </head> <body> <main> <div class="card"> <h5 class="card-header">Average age of a person according to his/her name</h5> <div class="card-body card__center"> <table class="table table-striped"> <thead> <tr> <th>Name</th> <th>Count</th> <th>Age</th> </tr> </thead> <tbody> <tr data-th-each="age: ${ages}"> <td th:text="${age.name}"></td> <td th:text="${age.count}"></td> <td th:text="|${age.age} years|"></td> </tr> </tbody> </table> <!-- 默认显示加载器 --> <div class="spinner-border" role="status"></div> </div> </div> </main> <script> // 监听表格tbody的子元素变化,一旦有数据行加载就隐藏加载器 const tbody = document.querySelector('tbody'); const spinner = document.querySelector('.spinner-border'); const observer = new MutationObserver(mutations => { if (tbody.children.length > 0) { spinner.style.display = 'none'; observer.disconnect(); // 停止监听 } }); observer.observe(tbody, { childList: true }); // 兜底:页面完全加载后隐藏加载器 window.addEventListener('load', () => { spinner.style.display = 'none'; }); </script> </body> </html>
步骤2:简化后端代码
移除后端中关于AtomicBoolean的逻辑,因为加载状态不再由后端控制:
public Mono<ServerResponse> renderPersonsAge(ServerRequest ignoredServerRequest) { var ages = this.fetchPersonsAge(); var contextVariable = new ReactiveDataDriverContextVariable(ages, 1); return ServerResponse.ok() .contentType(MediaType.TEXT_HTML) .render("index", Map.of("ages", contextVariable)); }
备选服务器端方案(不推荐)
如果你希望通过后端响应式变量控制加载状态,可以将isLoading包装为Mono<Boolean>,利用delayUntil等待Flux完成后切换状态:
public Mono<ServerResponse> renderPersonsAge(ServerRequest ignoredServerRequest) { var ages = this.fetchPersonsAge(); // 初始为true,当ages完成后变为false Mono<Boolean> isLoading = Mono.just(true) .delayUntil(ignored -> ages.then()); var contextVariable = new ReactiveDataDriverContextVariable(ages, 1); return ServerResponse.ok() .contentType(MediaType.TEXT_HTML) .render("index", Map.of( "ages", contextVariable, "isLoading", isLoading )); }
模板中需要确保Thymeleaf能处理响应式布尔值:
<div th:if="${isLoading}" class="spinner-border" role="status"></div>
不过这种方式的体验不如客户端方案,因为服务器端需要等待Flux完成后才会推送isLoading=false的状态,加载器会一直显示到所有数据加载完成,无法实现逐行加载时提前隐藏。
内容的提问来源于stack exchange,提问作者Gilles

