恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring Boot中Thymeleaf配置与实战:从语法到静态资源及常见坑
首页
资讯中心
/
Spring Boot中Thymeleaf配置与实战:从语法到静态资源及常见坑
Spring Boot中Thymeleaf配置与实战:从语法到静态资源及常见坑
发布时间:2026/10/15 16:46:26
1. 为什么还在用模板引擎Thymeleaf的定位与选型逻辑这几年前后端分离的风潮刮得很猛Vue、React成了不少团队的标配很多新人甚至没写过服务端渲染页面。但如果你做过管理后台、CMS系统、报表类页面或者对内业务系统就会发现模板引擎不仅没死反而在特定场景下依然是效率最高的方案。Spring Boot里标准的模板引擎是Thymeleaf这篇文章就围绕它的配置、语法、静态资源处理和常见坑展开把我实际踩过的、验证过的东西一次讲清楚。先回答一个绕不开的问题为什么不用JSPJSP在传统SSM项目中确实是主力但它有一个先天不足——Spring Boot以可执行JAR方式打包后JSP的渲染支持一直很别扭需要额外的依赖和配置而且在嵌入式容器下对JSP的支持历史上有不少限制。Thymeleaf是纯模板文件天然支持JAR包部署这一点在微服务化、容器化部署的趋势下非常关键。再说Thymeleaf和FreeMarker的区别。FreeMarker更偏程序员思维模板里可以写大量逻辑表达式自由度很高但这也意味着你很难限制页面里别写乱七八糟的业务逻辑。Thymeleaf的设计理念是原生HTML页面模板文件本身就是一个合法的HTML文件浏览器直接打开就能看到静态效果后端渲染时才把动态内容填进去。前端和后端各看各的不用依赖一个中间格式。这个特性在团队协作时非常有用——前端可以直接用浏览器预览页面后端专注填充数据。从我的实际体验来看选Thymeleaf还有一个务实理由它的标签语法和HTML属性很接近学习成本低。团队里来一个新人告诉他th:text替换文本、th:if条件判断、th:each循环基本半天就能上手写页面。这对维护成本来说是很实在的收益。Spring Boot对Thymeleaf的支持也足够傻瓜化——引入依赖后几乎零配置就能跑起来。默认的模板目录、默认的文件后缀、默认的编码都给你定好了。但零配置不等于不需要理解配置尤其是上了点规模的项目模板前缀、缓存策略、静态资源映射这些细节迟早会找上你。提前把原理弄清楚后面排查问题会省很多事。2. 依赖引入与工程结构先把骨架搭对2.1 引入依赖时的版本选择细节先说最基础的一步。创建一个Spring Boot项目用IDEA的Spring Initializr也好自己手写Maven工程也好核心依赖就一个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency这个starter会把Thymeleaf核心库和Spring Boot的自动配置包一起拉进来不需要再手动引入Thymeleaf本体和thymeleaf-spring5之类的适配层。我在早期版本见过有人手动配了四五个Thymeleaf相关依赖版本冲突调了半天其实一个starter就全搞定了。注意一点如果同时引入了spring-boot-starter-web和spring-boot-starter-thymeleafSpring Boot会优先使用Thymeleaf作为视图解析器。当然你还可以同时引入FreeMarker之类的依赖Spring Boot会按照自动配置的顺序各自注册ViewResolver但实际使用时建议只保留一种避免视图解析器优先级混乱。版本方面Spring Boot的父POM会统一管理Thymeleaf的版本一般情况下不需要手动指定版本号。只有当你需要用到Thymeleaf 3的新特性而Boot管理版本太老时才需要覆盖属性properties thymeleaf.version3.0.15.RELEASE/thymeleaf.version thymeleaf-extras-springsecurity5.version3.0.4.RELEASE/thymeleaf-extras-springsecurity5.version /properties能不动版本就别动跟着Boot的依赖管理走这是最省心的方案。2.2 模板文件到底该放在哪里Spring Boot的自动配置默认约定模板文件放在src/main/resources/templates目录下静态资源放在src/main/resources/static目录下。你的Controller返回逻辑视图名时Thymeleaf会去templates目录下拼上后缀找文件。比如Controller里这样写Controller public class IndexController { GetMapping(/) public String index(Model model) { model.addAttribute(appName, 订单管理后台); return index; } }它找的就是src/main/resources/templates/index.html。这里有一个容易忽略的坑如果把页面文件误放到了static目录Spring Boot会用静态资源处理器直接把它当普通文件返回而不会经过Thymeleaf渲染引擎。页面里的th:text这些属性全部原样出现在HTML源码里看起来就像模板没生效。排查这类问题时第一步先确认文件路径是不是在templates下。目录结构还可以用子目录组织比如templates/admin/user-list.htmlController返回admin/user-list即可。在Spring Boot中模板名里的斜杠会被正确映射到子目录不需要额外配置。2.3 一个可运行的最小示例为了让后面讲配置时你有一个可验证的基准我把最小示例的完整结构列出来。工程目录如下src/main/java/com/example/demo/ ├── DemoApplication.java ├── controller/ │ └── IndexController.java └── model/ └── User.java src/main/resources/ ├── static/ │ ├── css/ │ │ └── app.css │ └── js/ │ └── app.js └── templates/ └── index.htmlController代码如下Controller public class IndexController { GetMapping(/) public String index(Model model) { model.addAttribute(title, Thymeleaf 配置实践); model.addAttribute(users, Arrays.asList( new User(张三, zhangsanexample.com, 26), new User(李四, lisiexample.com, 31) )); return index; } }模板页面内容!DOCTYPE html html langzh-CN xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title th:text${title}首页/title link relstylesheet th:href{/css/app.css} href/css/app.css /head body h1 th:text${title}标题/h1 table thead tr th姓名/th th邮箱/th th年龄/th /tr /thead tbody tr th:eachuser : ${users} td th:text${user.name}姓名/td td th:text${user.email}邮箱/td td th:text${user.age}年龄/td /tr /tbody /table /body /html运行项目后访问http://localhost:8080/如果能看到表格里两行用户数据并且标题正确显示说明基础链路已经通了。有了这个可运行的最小工程后面改配置、做实验才有基准。3. 核心配置项详解每一个配置背后的原因3.1 application.properties里的主角配置Thymeleaf在Spring Boot中的配置项很多但日常开发真正用得上的也就七八个。我按重要性从高到低说。# 模板文件存放目录 spring.thymeleaf.prefixclasspath:/templates/ # 模板文件后缀 spring.thymeleaf.suffix.html # 模板编码 spring.thymeleaf.encodingUTF-8 # 模板模式关键配置 spring.thymeleaf.modeHTML # 是否开启模板缓存 spring.thymeleaf.cachefalse # 模板解析器顺序 spring.thymeleaf.check-template-locationtrue先说prefix和suffix。默认值就是classpath:/templates/和.html绝大多数场景不需要改。但我见过一种情况有的项目要把模板文件放在外部独立目录比如/opt/app/templates方便运维直接改页面不用重新打包。这时候prefix就要改成file:/opt/app/templates/。注意classpath:和file:前缀的区别——前者从依赖JAR或classes目录加载后者从文件系统加载。外部化模板是一次性配置、长期受益的功能如果你们的部署有这个诉求这个配置就非常值钱。3.2 缓存配置是开发期和上线期的分水岭spring.thymeleaf.cache是个很值得单独拎出来说的配置。开发阶段必须设为false这样修改模板后刷新页面立刻生效不用重启应用。生产环境必须设为true让Thymeleaf把解析过的模板缓存起来避免每次请求都重新解析模板文件显著降低IO和解析开销。这里有一个实战小技巧如果你想保留缓存带来的性能又想在某台机器上临时调试页面可以在启动命令里加参数覆盖java -jar app.jar --spring.thymeleaf.cachefalse开发时IDEA里还有一个容易混淆的问题——修改了html文件但浏览器里始终是旧页面。如果你确认spring.thymeleaf.cachefalse也设置了那多半是浏览器端缓存。按F12打开开发者工具勾选Disable cache同时注意看清楚请求是不是命中了304 Not Modified。还有一个我没有直接在配置文件里设过、但知道很多人问的属性spring.thymeleaf.render-hidden-markers-before-checkboxes。这是专门解决checkbox渲染时隐藏域处理问题的属于进阶参数碰到表单提交场景再说日常配置不需要动它。3.3 模板模式从HTML5到HTML的变迁spring.thymeleaf.mode这个配置在Thymeleaf 3.0之前叫HTML53.0之后改成HTML。如果你在旧文档里看到spring.thymeleaf.modeHTML5直接改成HTML就好写HTML5在Thymeleaf 3里反而不生效。为什么要用HTML模式因为HTML模式对标签的校验更宽松允许HTML5新增的标签和属性比如video、>spring.thymeleaf.modeHTML补充一个容易被忽略的spring.thymeleaf.check-template-location默认true它会检查prefix指向的目录是否存在。如果用classpath:/templates/并且确认目录存在就没问题。但有些工程把模板目录建成了src/main/resources/template/少了个s启动时会直接报错——因为check-template-location会寻找classpath:/templates/。这种低级错误我在帮别人排查时见过不止一次。3.4 加上一个内联配置让代码更干净还有一个我习惯性加的配置spring.thymeleaf.servlet.content-typetext/html默认值就是text/html不需要改。但如果你的页面需要返回application/xhtmlxml之类的特殊类型这里得过一眼。以上这些配置项我的建议是开发展阶段全部丢在application-dev.properties里生产用application-prod.properties利用Spring Boot的多Profile配置机制把它们分开。开发配置里cachefalse生产配置里cachetrue这样团队协作时不会有人忘记改配置。4. 静态资源与页面数据渲染配置只是起点4.1 静态资源映射规则与URL拼接Thymeleaf模板页里引用CSS、JS、图片等静态资源时要使用th:href和th:src并且用{}表达式。{}的主要作用是自动拼接context-path这样项目部署到带根路径的环境下页面资源也不会失效。!-- 错误的写法 -- link relstylesheet href/css/app.css !-- 正确的写法 -- link relstylesheet th:href{/css/app.css} href/css/app.css实际上Thymeleaf渲染时是拿th:href覆盖掉普通href属性的值。所以模板里同时保留th:href和href这样直接用浏览器打开模板文件时注意是templates里的文件不是渲染后的还能看到基本样式后端渲染后th:href生效。这就是Thymeleaf原生HTML页面设计理念的最直观体现。静态资源的访问路径在Spring Boot默认规则下是这样浏览器访问/css/app.css实际找的是classpath:/static/css/app.css浏览器访问/js/app.js实际找的是classpath:/static/js/app.js如果你想把静态资源放别的目录比如resources/static-assets需要自定义资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/resources/**) .addResourceLocations(classpath:/static-assets/); } }这里要注意addResourceHandler的第一个参数是浏览器URL模式第二个参数是实际文件系统或classpath路径。映射关系非常直接把URL前缀想象成门牌号把实际路径想象成实际地址。配置完访问/resources/css/app.css就能命中static-assets/css/app.css。提示addResourceLocations的末尾必须带/否则路径拼接会出问题。这个细节不仔细看极易踩。4.2 标准表达式语法精华速记Thymeleaf表达式是整个模板引擎的使用核心我挑几个最常用的讲。${...}是变量表达式用来读取Model中放入的属性也支持OGNL对象导航。比如${user.name}调用的是getName()方法不是直接访问字段。所以Model里放的对象必须有对应的getter方法。{...}是URL表达式前面已经提过。它可以拼接路径参数和查询参数!-- 路径参数 -- a th:href{/user/detail/{id}(id${user.id})}查看详情/a !-- 查询参数 -- a th:href{/user/list(page1,size10)}下一页/a这个语法比较灵活括号里放在路径中的变量会被拼成路径参数路径之外的变成查询字符串。*{...}是选择变量表达式通常配合th:object使用用于减少重复的对象前缀。举个例子没有th:object时你得这样写p th:text${user.name}姓名/p p th:text${user.email}邮箱/p p th:text${user.age}年龄/p用了th:object以后可以简化成div th:object${user} p th:text*{name}姓名/p p th:text*{email}邮箱/p p th:text*{age}年龄/p /div这里有一个容易踩的坑th:object只对所在标签内部的*{}表达式生效如果外层没有th:object就使用*{...}会抛出NoValueFoundException之类的错误。th:if和th:unless用于条件判断注意Thymeleaf对空值、空字符串、0、false、空集合这些都视为假。判断一个集合是否为空常用写法是div th:if${#lists.isEmpty(users)}暂无用户/divth:each循环时支持迭代状态。除了取每个元素还能拿到当前下标、当前元素总数、该元素是否是第一个/最后一个tr th:eachuser, stat : ${users} td th:text${stat.index}0/td td th:text${user.name}姓名/td td th:text${stat.count}1/td /tr其中stat是可选的不写时默认变量名是stat显式声明为user, stat更清晰。stat.index从0开始stat.count从1开始这个区别很容易混淆写表格序号时我建议直接用stat.count天然从1开始。4.3 日期和时间格式化到底怎么写日期格式化是模板开发里的高频需求也是新手最容易卡壳的地方。在Controller里传过来一个LocalDateTime对象模板里直接输出会是一长串带T的ISO格式不好看。正确做法是用#temporals工具类p th:text${#temporals.format(createTime, yyyy-MM-dd HH:mm:ss)}2024-01-01 00:00:00/p如果是普通的时间处理工具类还有一个更简洁的写法p th:text${#dates.format(localDate, yyyy-MM-dd)}2024-01-01/p但要特别注意#dates工具类只处理java.util.Date如果用Java 8的LocalDateTime#dates.format会直接抛异常。此时必须用#temporals。这两个工具类之间的区分是靠类型不对时异常信息提示的我第一次用#dates格式化LocalDateTime时愣是没看明白报错。现在规范的做法是——项目中时间类型统一用java.time包模板里日期格式化也就统一用#temporals。如果你觉得模板里到处写格式化表达式太啰嗦可以在Controller层先处理好直接把格式化后的字符串放进Model。页面展示的职责交给模板数据加工留在Controller这个边界把握好了模板文件会清爽很多。4.4 模板布局之碎片复用一个后台管理系统至少有几个公共区块顶部导航、侧边栏、页脚。把它们在每个页面里复制粘贴显然是Bad Practice。Thymeleaf的th:fragment和th:insert就是干这个用的。在templates/fragments/common.html中定义公共片段!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org body nav th:fragmentsidebar classsidebar ul lia th:href{/dashboard}仪表盘/a/li lia th:href{/user/list}用户管理/a/li lia th:href{/order/list}订单管理/a/li /ul /nav footer th:fragmentfooter classfooter p© 2024 管理系统/p /footer /body /html在页面中引用div th:insert~{fragments/common :: sidebar}/div或者如果不需要外层再包一层div可以直接用th:replacenav th:replace~{fragments/common :: sidebar}/navth:insert和th:replace的区别在于th:insert会把片段插入到当前标签内部th:replace会用片段替换整个当前标签。大多数布局场景我用th:replace结构更干净。~{fragments/common :: sidebar}这个表达式的意思是找到fragments/common.html文件里名为sidebar的片段。注意文件名不带后缀、路径省略了templates前缀。这个机制配合布局继承还能实现更精细的页面骨架统一。你用th:replace插入公共头尾中间内容区域每页自己写页面间就保持了高度一致的布局结构。实际项目中我把侧边栏、顶部导航、页脚、脚本区都做成了fragment新建一个页面只需要引用四个碎片代码量大幅下降后期改侧边栏菜单也只需要改一个文件。4.5 Controller返回逻辑视图名的几种姿势现在把Controller返回类型的情况完整梳理一遍。最常见的是方法返回String代表逻辑视图名比如返回index则拼接出classpath:/templates/index.html。注意在类上必须使用Controller而不是RestController。如果你用了RestController方法返回字符串时Spring MVC会把它当成HTTP响应体直接交给页面输出不会走视图解析器。有的项目里我见过类上标注RestController页面请求和接口请求写在一个类里结果页面请求返回了一段HTML代码而不是渲染后的页面。排查时先看Controller注解这个问题的发生频率远高于其他配置问题。如果不想让方法只绑定一个模板可以返回ModelAndViewGetMapping(/index) public ModelAndView index() { ModelAndView mv new ModelAndView(index); mv.addObject(appName, 订单管理后台); return mv; }还有一种更灵活的方式通过RedirectAttributes做重定向传参配合RedirectView实现POST后重定向到GET的经典PRG模式PostMapping(/user/save) public String saveUser(User user, RedirectAttributes ra) { userService.save(user); ra.addFlashAttribute(message, 保存成功); return redirect:/user/list; }Flash属性在重定向后的下一次请求里可以通过${message}直接拿到刷新页面后自动消失。这个机制用来做表单提交后的提示信息非常合适同时避免了刷新页面重复提交表单的问题。5. 常见问题与排查技巧实录全是真实踩过的坑5.1 页面修改后不生效是缓存还是路径问题这是问的最多的问题而且发生场景五花八门。按顺序检查以下三个原因第一spring.thymeleaf.cache是否确实为false。如果用了多个Profile确认当前激活的是开发Profile。IDEA里有时候配置文件加载顺序会出幺蛾子看下启动日志里Active profiles打印的是什么。第二浏览器缓存。Thymeleaf页面即使服务端已经更新浏览器端缓存了旧HTML刷新也是看的旧页面。无痕窗口打开一次能排除这个问题。第三是否真的改对了文件。src/main/resources/templates目录下的文件是热更新目标但如果你改了static目录下的CSS或JS文件由于静态资源没有经过Thymeleaf解析Spring Boot默认开启的静态资源缓存策略也会让刷新不生效。开发时建议把spring.web.resources.cache.cachecontrol.max-age设小一点或直接设0防止静态资源被浏览器缓存住。5.2 模板解析异常Whitelabel Error Page背后的日志页面显示Whitelabel错误页时第一件事去控制台看堆栈。Thymeleaf解析错误常见的有这么几类TemplatedException模板文件本身有问题比如标签未闭合、属性拼写错误、表达式语法错。报错信息里通常会精确到文件哪一行直接去改就好。SpelEvaluationException或OGNL表达式求值错误表达式的对象为空访问了不存在的属性或方法。比如${user.name}但user为null。排查思路是在Controller里给Model加一个当前用户或者表达式里加空判断p th:text${user ! null ? user.name : 未登录}未登录/pNoSuchMethodError之类的运行时异常Controller往Model里放的对象类型和模板里调用的方法不匹配。比如模板里写${user.createTime}但User类里压根没有getCreateTime()方法那只能是方法名拼错或者数据类型不对。5.3 中文乱码问题模板页面出现中文乱码绝大多数是文件编码和解析编码不一致导致。Spring Boot里配置了spring.thymeleaf.encodingUTF-8但HTML文件的真实编码可能不是UTF-8——IDE新建文件时默认编码如果不是UTF-8或者从别处复制过来的文件带着GBK编码都会渲染乱码。还有一个隐蔽的乱码来源HTML的meta charset声明和实际返回的Content-Type头里的charset不一致。Thymeleaf渲染时会以spring.thymeleaf.encoding为准设置响应编码浏览器优先读HTTP头里的编码。所以配置文件统一设UTF-8后HTML文件也保存为UTF-8编码就不会再出现中文乱码。我这里吃亏的经验是从同事那收到一个模板文件打开后IDE显示出中文正常但浏览器渲染出来全乱码。排查了很久最后发现那个HTML文件是GBK编码IDE自动按检测出的编码打开了所以显示正常但Thymeleaf按UTF-8读取就乱码。解决办法是用文本编辑器比如VSCode或Notepad另存为UTF-8无BOM格式。5.4 静态资源全部404页面能渲染但CSS、JS全部404。这类问题十有八九是静态资源映射被覆盖了。如果你在项目里实现了WebMvcConfigurer并重写了addResourceHandlers方法那么Spring Boot默认的静态资源映射会被你的自定义配置覆盖掉。要保留默认映射需要在方法里显式添加默认规则Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 显式保留Spring Boot默认的静态资源映射 registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/); // 再添加自定义映射 registry.addResourceHandler(/resources/**) .addResourceLocations(classpath:/custom-assets/); }另一个404原因是前置了DispatcherServlet的映射路径。比如个别项目把spring.mvc.servlet.path改成了/api那{/css/app.css}渲染出来的链接会多一段前缀。这种情况下th:href{/css/app.css}生成的URL可能不符合你路由规则需要检查链接的实际生成结果。5.5 请求JSON却返回了HTML页面这类问题常见于Controller同时提供页面和API接口的场景。比如一个类上标RestController其中一个方法返回一个Map对象Spring Boot会尝试把它序列化成JSON——这是正常行为。但如果另一个方法返回String类型且这个类同时被标注了RestController那这个String会被直接输出而不是渲染成模板页面。我自己的习惯是——凡是返回视图的Controller用Controller注解同时这个类里不写任何返回JSON的接口。返回JSON的接口单独用RestController写在别的类里或者在Controller类的方法上额外加ResponseBody做区分。这样职责明确不会出现我明明想返回页面却输出了一段HTML的迷惑现象。5.6 Thymeleaf 3的HTML模式适配Thymeleaf 3.0把默认模式从HTML5改成了HTML但是很多旧项目是从Thymeleaf 2.x直接升级上来的模板里可能还残留着旧版本才有的写法比如th:inlinetext的用法在两个版本中有差异。升级后渲染报错时注意看报错信息里提示的是不是模板兼容性问题不要急着去查配置。如果团队里有多个项目模板片段可能会在不同项目间复制迁移这时候模板文件里的xmlns:thhttp://www.thymeleaf.org声明也需要同步复制别漏了。有些编辑器比如老版本的IDEA在没有这个命名空间声明时对th:属性会标红报错虽然不是运行时错误但看着不舒服还是加上为好。5.7 表单提交表单回显的隐藏坑Thymeleaf处理表单回显时th:field可以自动回填表单控件的值。它要求绑定的对象必须存在。如果你使用了一个空表单页面比如新增用户Controller需要先往Model里放一个空对象GetMapping(/user/add) public String addUser(Model model) { model.addAttribute(user, new User()); return user-form; }不传这个空对象直接渲染模板th:field${user.name}在首次渲染时会报错。这是很多新手会遇到的情况——思路是错的以为是表单页面不展示数据就不用传对象。th:field的设计逻辑是表单必然绑定一个对象空对象也是对象只是字段都为空而已。还有一个checkbox绑定布尔值的例子。如果User类里有boolean vip属性模板里写th:field*{vip}首次渲染的时候对象里vip默认是falsecheckbox不会被选中提交时不勾选checkbox则后端收到null不是false。这种坑我记忆中花了不少时间才定位要是碰到可以检查一下是否存在checked状态没有正确回显。6. Thymeleaf配置复盘我个人的使用心得把配置Thymeleaf的整个链路走下来我的体会是Spring Boot把95%的体力活都干完了剩下的5%才是真正拉开开发效率的地方。第一开发环境一定把cache关掉一旦忘了关改完模板不见反应各种怀疑人生。关闭缓存后配合IDEA自带的Live Edit或者spring-boot-devtools模板文件改完保存就能看到效果体验接近前端的热更新。第二静态资源的路径映射尽早规划好。如果一个项目有前端参与建议在启动开发时就约定好静态资源放static目录模板引擎只做HTML渲染两者职责分离。等项目跑起来再去迁移资源路径牵一发动全身。第三模板文件里的表达式宁短勿长。单个模板文件里出现超过三层的嵌套表达式比如${user.orderList[0].productName}就要考虑在Controller层把数据预处理成扁平结构。模板是视图层越简单越好维护别把所有数据加工的活儿都堆在模板表达式里。第四安全意识。模板输出用户输入的富文本时存在XSS等风险场景。Thymeleaf默认会对th:text输出的内容进行HTML转义这是安全的。如果确实需要输出原始HTML比如富文本编辑器生成的内容使用th:utext时要明确知道你在禁用转义务必确保内容是可信的或经过清洗的。我实际项目中一般都禁用th:utext除非有100%的把握数据源干净。关于Thymeleaf和前端框架的关系我的看法是这不一定是二选一的关系。在实际项目里面对的是内部管理系统那类以数据展示和表单操作为主的场景时Thymeleaf服务端渲染比前端工程化方案更直接遇到交互复杂、实时性要求高的模块也可以用Thymeleaf渲染页面骨架局部再用Vue或原生JS增强交互。混合模式虽然需要约定好数据传递的接口但确实能两端兼顾。最后再分享一个配置上的小经验。不管项目多大多小建议把Thymeleaf相关配置集中放在一个配置类或配置文件的固定区域并加上清晰注释说明每一个配置项为什么这么设、哪个环境需要怎么改。后来接手的人打开配置文件一看就懂不用翻文档猜测某个配置是干什么的。工程上的这种小习惯维护成本降低是很明显的。