需求背景
通常我们会碰到一些带表格的报表需求,一般是包含一些表格和一些循环列表的需求,每个表格的样式可以不一样。
有些公司会使用一些专业的报表软件,例如jasperReport,如意报表等,虽然这种软件功能很强大,但是需要一定的学习门槛,而且使用中调整样式并没有想象中方便,最重要的是此类模板设计、模板导出、模板上传都是跟业务系统分开的,不能嵌入系统中使用,不能做到在线编辑,在线预览。
为此,我专门研究了thymeleaf来完成此类需求的办法,希望做到报表上的所有表格都可以通过配置配出来,支持纯文本和动态变量,支持图片,所有表格的样式可以通过css来随意配置,支持循环列表,支持横向和纵向合并单元格
为什么选择thymeleaf而不是选择freemark等其他模板技术,主要基于两个原因:
1、spring官方推荐用thymeleaf,并且springboot有官方适配
2、thymeleaf编写的模板能直接打开预览,不会像freemark破坏了html原本的格式。
实现思路
核心思路是:设计一个根模板(root-template.html),它可以通过设计好的数据库配置生成新的具体模板(biz-template.html),并且biz-template.html里面包含具体的业务数据的变量,这样就可以结合业务数据展示出报表。这就是我称之为双层模板的原因。
技术细节包含以下几点:
1、根模板(root-template.html)如何设计
2、需要用thymeleaf解析html并生成html,就必须使用API的方式去实现,且html的路径可配置,否则线上环境无法生成html到jar包里
3、生成后的html也需要包含thymeleaf的标签,那就需要研究thymeleaf如何生成带标签的模板
4、样式和合并单元格如何控制
根模板设计
根页面定好10个table区域,每个区域都是一样的逻辑,判断table配置的数据集是否为空,为空则整个table不显示,不为空,则先循环table配置的tr信息,tr里面再循环配置的td信息。
根模板如下:
springboot整合thymeleaf,并支持api的方式生成html
1、pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
2、application.properties
server.port=8080
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.check-template-location=true
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.mode=HTML5
report.templates.path=
3、ThymeleafConfig配置类
@Configuration
@EnableWebMvc
public class ThymeleafConfig extends WebMvcAutoConfiguration {
@Autowired
ApplicationContext applicationContext;
/**
* eg: E:/data/resources/templates/
*/
@Value("${report.templates.path}")
private String reportTemplatesPath;
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
viewResolver.setCache(false);
return viewResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
// engine.setEnableSpringELCompiler(true);
engine.setTemplateResolver(templateResolver());
return engine;
}
@Bean
public ITemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
URL resource = this.getClass().getClassLoader().getResource("templates/"); //这里把系统获取到的Class的path替换为源码对应的Path,这样修改的时候就可以动态刷新
String devResource = resource.getFile().toString().replaceAll("target/classes", "src/main/resources");
if (reportTemplatesPath!=null&&!"".equals(reportTemplatesPath.trim())){
devResource=reportTemplatesPath.trim();
}
resolver.setPrefix("file://"+devResource);
resolver.setCacheable(false);//不允许缓存
resolver.setSuffix(".html");
return resolver;
}
}
这样就可以使用以下代码来生成html文件了
@Autowired
private TemplateEngine templateEngine;
Context context = new Context();
context.setVariable("table1", templateData.getTable1());
FileWriter write = new FileWriter(devResource+"/biz-template.html");
templateEngine.process("root-template", context, write);
利用thymeleaf生成带thymeleaf标签的html文件
主要利用两点,
-
一个是th:attr
例如,根模板里面:
<div th:attr="'th:text'=${td.value}"></div>
如果td.value=${userName},则通过根模板生成后的子模板html代码是:
<div th:text="${userName}"></div>
看到没,子模板就也可以再用thymeleaf进行解析
这里有两点特别说明:
1、<div th:attr="'th:attr'=${td.value}"></div> 这种方式行不通,通过th:attr无法增加th:attr标签
2、<div th:attr="'th:text'=${td.value}" th:text="${td.value}"></div> 这种方式也不行,只能解析后面那个th:text,解析结果如下:
<div>${userName}</div>
那如果又想加th:text标签又想设置text内容怎么办呢?答案是增加一层div:
<div th:attr="'th:text'=${td.value}" ><div th:text="${td.value}"></div></div>
-
一个是th:utext
例如,根模板里面:
<div th:utext="${td.value}"></div>
如果td.value是以下字符串<img th:src="${imgUrl}">,则通过根模板生成后的子模板html代码是:
<div><img th:src="${imgUrl}"></div>
不错吧,这样也可以再用thymeleaf解析
样式和合并单元格问题
使用th:style,th:class,th:rowspan,th:clospan来控制
根模板中要设置全局自定义样式变量可以使用如下配置:
<style th:inline="text">
[[${style}]]
</style>
效果演示
子模板template2
子模板2填充数据后:
子模板template3
子模板3填充数据后:
最终的html如果需要转成pdf,请参考最好的html转pdf工具(wkhtmltopdf)
常用语法
注释
1、静态文件打开可以看到,但是thymeleaf生成后看不到
<!--/*-->
<div>
you can see me only before Thymeleaf processes me!
</div>
<!--*/-->
2、静态文件打开看不到,但是thymeleaf生成后看得到
<span>hello!</span>
<!--/*/
<div th:text="${...}">
...
</div>
/*/-->
<span>goodbye!</span>
3、块代码段,th:block,可以配合thymeleaf的其他标签去定义代码块,例如th:each如果循环单行<tr>的时候,th:each可以放在<tr>标签里,但是如果要循环多行<tr>的话,则可以配合th:block来使用,如下:
<table>
<th:block th:each="user : ${users}">
<tr>
<td th:text="${user.login}">...</td>
<td th:text="${user.name}">...</td>
</tr>
<tr>
<td colspan="2" th:text="${user.address}">...</td>
</tr>
</th:block>
</table>
删除代码段
以下是Thymeleaf的一个例子。我们可以使用th:remove来删除指定的部分,这在原型设计和调试的时候很有用。
<tr th:remove="all">
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
th:remove可接受的值有5个:
- all: 移除标签和所有子元素
- body: 移除所有子元素,保留标签
- tag: 移除标签,保留子元素
- all-but-first: 保留第一个子元素,移除所有其他
- none : 什么也不做。这个值在动态求值的时候会有作用
特别注意点
Thymeleaf中无法利用map.key表达式获取到map的键值,特别是使用 th:if="map.key =null" 的时候,如果map里面不包含key,则立马报错,所以如果要用从map里面取某个key,则map里面必须要有这个key值(value为空也必须设置为null),否则会报错
参考
Springboot 之 引入Thymeleaf
thymeleaf 基本语法
spring boot(四):thymeleaf使用详解
Spring Web MVC框架(十二) 使用Thymeleaf
使用Thymeleaf API渲染模板生成静态页面
Spring Boot中Thymeleaf引擎动态刷新
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html