文章目录
一、Thymeleaf
1、Thymeleaf 介绍
先了解模板的概念,模板 是 将一些重复内容写好,其中某些可能发生变化的内容,采用占位符方式动态加载,比如 JSP。
模板引擎 是 基于写好的模板,动态给写好的模板加载数据。
thymeleaf 是一个 XML/XHTML/HTML5 模板引擎,可用于 Web 与非Web 环境中的应用开发。它是一个开源的 Java 库,基于Apache License 2.0 许可,由 Daniel Fernández 创建,该作者还是J ava 加密库 Jasypt 的作者。
Thymeleaf 提供了一个 用于整合 Spring MVC 的可选模块,在应用开发中,可以使用 Thymeleaf 来完全代替 JSP 或 其他模板引擎,如 Velocity、FreeMarker 等。Thymeleaf的主要目标在于 提供一种可被浏览器正确显示的、格式良好 的 模板创建方式,因此也可以用作 静态建模。可以使用它 创建经过验证的 XML 与 HTML 模板。相对于编写逻辑或代码,开发者只需将标签属性添加到模板中即可。接下来,这些标签属性就会在 DOM(文档对象模型)上执行预先制定好的逻辑。
特点:开箱即用。
Thymeleaf 允许处理六种模板,每种模板称为模板模式:
- XML(可扩展标记语言)
- 有效的 XML(格式正确有效的 XML)
- XHTML(可扩展超文本标记语言)
- 有效的 XHTML(格式正确有效的 XHTML)
- HTML5
- 旧版 HTML5(HTML 过渡到 HTML5 的相关版本)
所有这些模式 都指的是 格式良好的 XML文件,但 Legacy HTML5 模式除外,它允许处理 HTML5 文件,其中包含独立(非关闭)标记,没有值的标记属性或不在引号之间写入的标记属性。为了在这种特定模式下处理文件,Thymeleaf 将首先执行转换,将文件转换为 格式良好 的 XML 文件,这些文件仍然是完全有效的 HTML5
然而,这些并不是 Thymeleaf 可以处理的唯一模板类型,并且用户始终能够通过指定在此模式下解析模板的方法和编写结果的方式来定义他/她自己的模式。这样,任何可以建模为 DOM 树(无论是否为 XML)的东西都可以被 Thymeleaf 有效地作为模板处理。
2、SpringBoot 整合 thymeleaf
使用 springboot 来集成使用 Thymeleaf 可以大大减少单纯使用 thymleaf 的代码量,所以我们接下来使用 springboot 集成使用 thymeleaf。
实现的步骤为:
(1)创建一个 sprinboot 项目
(2)添加 thymeleaf 的起步依赖
(3)添加 spring web 的起步依赖
(4)编写 html 使用 thymleaf 的语法,获取变量对应后台传递的值
(5)编写 controller 设置变量的值到 model 中
(对于视图解析器,之前在 SpringMVC 中是用 /WEB-INF/pages/xxx.jsp,现在是 classpath:/templates/ xxx.html)
接下来,在项目下创建一个独立的工程 springboot-thymeleaf,该工程为案例工程,不需要放到 changgou-parent 工程中。
在 pom.xml 中导入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<!--web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
在resources 中创建 templates 目录,在 templates 目录创建 demo1.html,代码如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Thymeleaf的入门</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<!--输出hello数据-->
<p th:text="${hello}"></p>
</body>
</html>
参数说明:
<html xmlns:th="http://www.thymeleaf.org">
声明使用 thymeleaf 标签<p th:text="${hello}"></p>
使用 th:text="${变量名}" 表示 使用 thymeleaf 获取文本数据,类似于 EL 表达式。
提供 控制层,把变量放置到 model 中:
@Controller
@RequestMapping(value = "/test")
public class TestController {
@GetMapping(value="/hello")
public String hello(Model model){
model.addAttribute("hello","hello thymeleaf");
return "demo";
}
}
提供 SpringBoot 启动类:
@SpringBootApplication
public class ThymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(ThymeleafApplication.class,args);
}
}
运行结果:
这时如果在代码中进行修改,运行程序,再去刷新页面,页面是没有变化的,需要创建 application.yml,并设置 thymeleaf 的缓存值为 false 。
spring:
thymeleaf:
cache: false
在 application.yml 中,其实还有一些默认配置,比如,视图前缀是 classpath:/templates/,视图后缀是 .html。
源码:
prefix 和 suffix 都有 set 方法,是可以在 application.yml 中进行修改的。
3、基本语法
(1)th:action
表单提交的地址控制,定义后台控制器路径,类似<form>
标签的 action 属性。
如:
<form id="login-form" th:action="@{/test/hello}">
<button>提交</button>
</form>
表示提交的请求地址为 /test/hello。
(2)th:each
对象遍历,功能类似 jstl 中 的 <c:forEach>
标签。
创建 com.jia.model.User,代码如下:
public class User {
private Integer id;
private String name;
private String address;
//..get..set
}
在 Controller 层 hello(Model model) 方法中添加数据:
// 创建 List<User>,并将它存入 model 中,在页面使用 Thymeleaf 显示
//集合数据
List<User> users = new ArrayList<User>();
users.add(new User(1,"张三","深圳"));
users.add(new User(2,"李四","北京"));
users.add(new User(3,"王五","武汉"));
model.addAttribute("users",users);
在 demo.html 中写页面输出:
<div>
<table>
<tr>
<td>下标</td>
<td>ID</td>
<td>name</td>
<td>address</td>
</tr>
<!--user 表示接受当前循环的对象 ,userState 表示当前循环对象的状态记录-->
<tr th:each="user,userState:${users}">
<td th:text="${userState.index}">下标</td>
<td th:text="${user.id}">ID</td>
<td th:text="${user.name}">name</td>
<td th:text="${user.address}">address</td>
</tr>
</table>
</div>
运行结果:
(3)Map 输出
在 controller 添加 Map
//Map定义
Map<String,Object> dataMap = new HashMap<String,Object>();
dataMap.put("No","123");
dataMap.put("address","深圳");
model.addAttribute("dataMap",dataMap);
在 demo.html 中 写 页面输出:
<div>
<div>
知道 key 时,根据 key 得到 value: <br/>
<span th:text="${dataMap.No}"></span>
<span th:text="${dataMap.address}"></span>
</div>
<div>
不知道 key 时,需要遍历 map:<br/>
<div th:each="datamap:${dataMap}">
<span th:text="${datamap.key}"></span>:<span th:text="${datamap.value}"></span>
</div>
</div>
</div>
运行结果:
(4)数组输出
在 controller 添加 数组:
String[] names = {
"张三","李四","王五"};
model.addAttribute("names",names);
在 demo.html 中 写 页面输出:
<div th:each="nm,nmStat:${names}">
<span th:text="${nmStat.count}"></span><span th:text="${nm}"></span>
</div>
运行结果:
(5)Date 输出
在 controller 添加 Date:
model.addAttribute("now",new Date());
在 demo.html 中 写 页面输出:
<div>
Date 数据获取
<div>
<span th:text="${#dates.format(now,'YYYY-MM-dd HH:mm:ss')}"></span>
</div>
</div>
运行结果:
(6)th:if 条件
在 controller 添加年龄 age:
model.addAttribute("age",22);
在 demo.html 中 写 页面输出:
<div>
if 条件判断
<div>
<span th:if="${age>=18}">成年</span>
<span th:unless="${age<18}">未成年</span>
</div>
</div>
th:unless 表示条件不成立时,输出指定数据。
(7)th:fragment 定义一个模块
可以定义一个独立的模块,创建 footer.html,模块名为 copy:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>fragment</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<div id="C" th:fragment="copy">
关于我们</br>
</div>
</body>
</html>
运行结果:
二、搜索微服务架构
搜索的业务流程如上图,用户每次搜索的时候,先经过搜索业务工程,搜索业务工程 调用 搜索微服务工程,这里搜索业务工程单独挪出来的原因是它涉及到了 模板渲染 以及 其他综合业务处理,以后可能会有移动端的搜索和 PC 端的搜索,后端渲染 如果直接在 搜索微服务 中进行,会对 微服务 造成一定的侵入,不推荐这么做,推荐微服务独立,只提供服务,如果有其他页面渲染操作,可以搭建一个 独立的消费工程调用微服务 达到目的。
1、搜索工程搭建
(1)工程创建
在 changgou-web 工程中创建 changgou-web-search 工程,在 它的父工程 changgou-web 的 pom.xml 中导入渲染页面都需要的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
(2)静态资源导入
将资源中的 页面/前端页面 /search.html 拷贝到工程的 resources/templates 目录下,js、css 等拷贝到 static 目录下,如下图:
(3)提供 application.yml 配置文件
server:
port: 18086
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
spring:
thymeleaf:
cache: false
application:
name: search-web
main:
allow-bean-definition-overriding: true
(4)Feign创建
修改 changgou-service-search-api,添加 com.changgou.search.feign.SkuFeign,实现调用搜索:
@FeignClient(name="search")
@RequestMapping("/search")
public interface SkuFeign {
@GetMapping
Map search(@RequestParam(required = false) Map searchMap);
}
(5)添加 api 依赖
<dependencies>
<!--search API依赖-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-search-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
(6)搜索调用
在 changgou-web-search 中创建 com.changgou.search.controller.SkuController,实现搜索调用,代码如下:
@Controller
@RequestMapping(value = "/search")
public class SkuController {
@Autowired
private SkuFeign skuFeign;
/**
* 搜索
* @param searchMap
* @return
*/
@GetMapping(value = "/list")
public String search(@RequestParam(required = false) Map searchMap, Model model){
//调用changgou-service-search微服务
Map resultMap = skuFeign.search(searchMap);
model.addAttribute("result",resultMap);
return "search";
}
}
(7)启动类创建
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.search.feign")
public class SearchWebApplication {
public static void main(String[] args) {
SpringApplication.run(SearchWebApplication.class,args);
}
}
注意,后端代码中应该使用绝对路径,而不是相对路径;比如,需要把 “./” 改为 “/”:
访问结果:
可以看到,通过 Spring MVC 的方式是可以访问到页面的,不过这些数据都是由 search.html 写好了的,文字是写定的,图片是刚刚导入的。接下来,我们需要使用 thymeleaf 来进行数据的填充,把后端数据给过去。
2、搜索数据填充
首先,要声明使用 thymeleaf 标签:
接下来,回顾一下商品搜索页面:
可以看到,搜索页面显示的内容分为 3 部分:
- 用户已经勾选的数据条件
- 筛选出的数据搜索条件
- 搜索的数据结果
第 3 部分对应着 search.html 中的<div class="goods-list">
,我们需要把它的数据改为 “result”。
商品名称:
<div class="attr">
<a th:utext="${#strings.abbreviate(item.name,45)}" target="_blank"></a>
</div>
这里使用 “utext” 是为了识别标签,因为高亮逻辑里是给关键字加了前后缀来实现高亮的,如果使用的是 “text”,就会把前后缀直接展示在页面上。并设置最多只输出 45 个字符。
价格:
<strong>
<em>¥</em>
<i th:text="${item.price}"></i>
</strong>
图片:
<div class="p-img">
<a href="item.html" target="_blank"><img th:src="${item.image}"/></a>
</div>
在上篇实现高亮搜索时我们知道,在页面中展示的是高亮数据,而高亮数据搜索的域是 name,所以必须输入 keywords,否则会得到空数据。这时还没到用户从前端传来 keywords 值,所以可以在 构建搜索条件的 builderBasicQuery 方法中 改一下逻辑,如果没传入 keywords,就不进行高亮显示了。
运行结果:
3、关键字、搜索条件回显
接下来,我们需要实现 用户输入关键字,把关键字从前端页面传到后台,根据关键字查询。
将 “搜索” 按钮改为 submit 提交,name 设置为 keywords:
<div class="input-append">
<input type="text" id="autocomplete" name="keywords" class="input-error input-xxlarge"/>
<button class="sui-btn btn-xlarge btn-danger" type="submit">搜索</button>
</div>
输入 “TCL”,发现路径确实变化了,也确实根据关键字查询的,但是搜索栏却被清空了:
解决这个问题,需要将搜索的条件进行存储,然后实现 回显。
model.addAttribute("searchMap",searchMap);
修改为:
<div class="input-append">
<input type="text" th:value="${searchMap.keywords}" id="autocomplete" name="keywords" class="input-error input-xxlarge"/>
<button class="sui-btn btn-xlarge btn-danger" type="submit">搜索</button>
</div>
但这时有一个问题,如果访问路径里没有传 keywords 参数,会报错:
在控制台的错误信息:org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field ‘keywords’ cannot be found on object of type ‘java.util.LinkedHashMap’ - maybe not public or not valid?
打断点发现, th:value="${searchMap.keywords}"
中,searchMap 的 size 是 0 ,也就获取不到 keywords 值。因为刚刚是在 构建条件的方法里给 searchMap 设置了 keywords 和对应的 value,即使没有输入关键词,查询时也会按照设置的初始值进行查询,但是出了这个方法,searchMap 还是 size =0 的,也就是说,在 controller 层的 search 方法中:
public String search(@RequestParam(required = false) Map<String,String> searchMap, Model model){
Map resultMap=skuFeign.search(searchMap);
model.addAttribute("result",resultMap);
// 将搜索条件进行存储
model.addAttribute("searchMap",searchMap);
return "search";
}
并没有对 searchMap 预设 keywords 值,所以会报错。
(而且打断点发现,searchMap 确实是 LinkedHashMap 类型,应该是因为参数需要按序传入。)
解决这个问题,可以从前端 和 后端 两个角度:
(1)前端:渲染数据前对 keywords 进行判断,如果 searchMap 里有 keywords,才取这个值回显到输入框中,这样逻辑比较严谨。
<!-- 判断 searchMap 中是否存在 keywords 存在的话,根据关键字进行查询-->
<input type="text" th:value="${#maps.containsKey(searchMap,'keywords')?searchMap.keywords:''}"
id="autocomplete" name="keywords" class="input-error input-xxlarge"/>
(2)后端:在 search 方法中判断 searchMap 中是否有 keywords,如果没有的话赋予初值,这样,model 里的 searchMap 无论如何都会有 keywords 的,而且初始时也会把这个初值显示在输入框里的。
public String search(@RequestParam(required = false) Map<String,String> searchMap, Model model){
if(searchMap==null||searchMap.size()==0){
searchMap=new LinkedHashMap<String,String>();
searchMap.put("keywords","华为");
}
Map resultMap=skuFeign.search(searchMap);
model.addAttribute("result",resultMap);
// 将搜索条件进行存储
model.addAttribute("searchMap",searchMap);
return "search";
}
实现了关键字回显后,接下来需要先实现把搜索得到的结果中的 categoryList、brandList、specMap 数据在页面显示。
分类条件:
<div class="type-wrap">
<div class="fl key">分类</div>
<div class="fl value">
<span th:each="category,categoryStat:${result.categoryList}">
<a th:text="${category}"></a>
<em th:unless="${categoryStat.last}">、</em>
</span>
</div>
品牌条件:
<div class="value logos">
<ul class="logo-list" th:each="brand:${result.brandList}">
<li>
<a th:text="${brand}"></a>
</li>
</ul>
</div>
规格条件:
<div class="type-wrap" th:each="spec:${result.specMap}">
<div class="fl key" th:text="${spec.key}"></div>
<div class="fl value">
<ul class="type-list">
<li th:each="opt:${spec.value}">
<a th:text="${opt}"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
逻辑上是没问题的,但是我们上篇在后端的实现里,比如,用户输入参数是 category=笔记本 时,搜索的结果里是没有 categoryList 的,所以前端页面渲染到要遍历 result.categoryList 时:
<span th:each="category,categoryStat:${result.categoryList}">
<a th:text="${category}"></a>
<em th:unless="${categoryStat.last}">、</em>
</span>
就渲染不下去了,页面显示将会是这样的:
修改前端逻辑为:
<!--用户没有选择分类时,才会显示分类栏-->
<div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'category')}">
<div class="fl key">分类</div>
<div class="fl value">
<span th:each="category,categoryStat:${result.categoryList}">
<a th:text="${category}"></a>
<em th:unless="${categoryStat.last}">、</em>
</span>
</div>
<div class="fl ext"></div>
</div>
对于规格,后端的逻辑是,无论搜索参数是什么,都会把整个 specMap 放到结果中,现在我们在前端修改逻辑,把用户已经输入的规格对应的数据不予显示:
<!--当用户没有输入规格时,显示规格列表-->
<div class="type-wrap" th:each="spec:${result.specMap}" th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
<div class="fl key" th:text="${spec.key}"></div>
<div class="fl value">
<ul class="type-list">
<li th:each="opt:${spec.value}">
<a th:text="${opt}"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
同样地,对于价格参数,只有当用户没有输入 price 时,才显示:
<ul class="type-list" th:unless="${#maps.containsKey(searchMap,'price')}">
OK,以上都是手动在路径中输入参数,接下来要做的是 让用户在前端页面选择参数,并且每次都在上一次搜索的基础上 追加 新的搜索条件。
4、条件搜索
在 SkuController 中添加方法,获取用户每次请求的地址:
public String url(Map<String,String> searchMap){
// 初始路径
String url="/search/list";
// 拼接
if(searchMap!=null||searchMap.size()>0){
url+="?";
for(Map.Entry<String,String> entry:searchMap.entrySet()){
String key=entry.getKey();
String value=entry.getValue();
url+=key+"="+value+"&";
}
url=url.substring(0,url.length()-1);
}
return url;
}
并在 search 方法中调用:
// 获取上次请求地址
String url=url(searchMap);
model.addAttribute("url",url);
这样前端的页面就可以获取到 url ,也就是当前的路径,然后在前端页面中点击某个参数,并把参数追加到路径上,再去访问路径。
实现分类的搜索:
<span th:each="category,categoryStat:${result.categoryList}">
<!--每次点击分类时,在上次请求的 url 上追加选择的分类作为参数-->
<a th:href="@{${url}(category=${category})}" th:text="${category}"></a>
<em th:unless="${categoryStat.last}">、</em>
</span>
运行起来是没问题的,照猫画虎实现其他的参数拼接:
<a th:href="@{${url}(brand=${brand})}" th:text="${brand}"></a>
<!--每次点击规格时,在上次请求的 url 上追加选择的规格作为参数-->
<a th:href="@{${url}('spec_'+${spec.key}=${opt})}" th:text="${opt}"></a>
<li>
<a th:href="@{${url}(price='0-500元')}">0-500元</a>
</li>
<li>
<a th:href="@{${url}(price='0-500元')}">500-1000元</a>
</li>
<li>
<a th:href="@{${url}(price='1000-1500元')}">1000-1500元</a>
</li>
<li>
<a th:href="@{${url}(price='1500-2000元')}">1500-2000元</a>
</li>
<li>
<a th:href="@{${url}(price='2000-3000元')}">2000-3000元 </a>
</li>
<li>
<a th:href="@{${url}(price='3000元以上')}">3000元以上</a>
</li>
5、条件回显
如上图,用户点击条件搜索后,要将选中的条件显示出来,并提供移除条件的 x 按钮。条件仍然是从 searchMap 中获取:
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</ul>
<ul class="fl sui-tag">
<li class="with-x" th:if="${#maps.containsKey(searchMap,'category')}">
<em th:text="${searchMap.category}"></em>
<i>x</i></li>
<li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">
<em th:text="${searchMap.brand}"></em>
<i>×</i></li>
<li class="with-x" th:if="${#maps.containsKey(searchMap,'price')}">
<em th:text="${searchMap.price}"></em>
<i>×</i></li>
<li class="with-x" th:each="spec:${searchMap}" th:if="${#strings.startsWith(spec.key,'spec_')}">
<em th:text="${#strings.replace(spec.key,'spec_','')}"></em>:<em th:text="${spec.value}"></em>
<i>×</i></li>
</ul>
6、排序
对于排序功能,如果是像上图,用户在前端页面 选择了参数,那么,其他的参数(比如 品牌、分类)还是要起作用的,但是其他的排序域就不起作用了,比如,如果之前选过“综合”,这时又切换到“价格”,因为只能根据一个域排序,应该由前端拼接 sortField=price & sortRule= ASC,而想要其他排序域不起作用,需要在 searchMap 中过滤掉排序参数;如果是手动输入访问路径,之后再选择其他参数(比如品牌、分类),是不需要过滤排序参数的。
因此,考虑将 url 分两种情况进行拼接:
public String[] url(Map<String,String> searchMap){
// 初始路径,不带排序参数的
String url="/search/list";
// 带排序参数的
String sorturl="/search/list";
// 拼接
if(searchMap!=null||searchMap.size()>0){
url+="?";
sorturl+="?";
for(Map.Entry<String,String> entry:searchMap.entrySet()){
String key=entry.getKey();
String value=entry.getValue();
// 遇到排序参数时,url 需要对它进行拼接,而 sorturl 不需要
if(key.equalsIgnoreCase("sortField")||key.equalsIgnoreCase("sortRule")){
url+=key+"="+value+"&";
}
// 非排序参数,url 和 sorturl 都需要拼接
else {
url+=key+"="+value+"&";
sorturl+=key+"="+value+"&";
}
// 去掉最后一个 "&"
url=url.substring(0,url.length()-1);
sorturl=sorturl.substring(0,sorturl.length()-1);
}
return new String[]{
url,sorturl};
}
前端:
<li>
<li>
<a th:href="@{${sorturl}(sortField=price,sortRule=ASC)}">价格↑</a>
</li>
<li>
<a th:href="@{${sorturl}(sortField=price,sortRule=DESC)}">价格↓</a>
</li>
</li>
以上是价格排序,还需要实现根据销量、新品(更新日期)、评价 进行排序。更新时间 updateTime 已经是索引库里的 field 了,如果在前端页面选择了它,只需要进行拼接参数并降序即可:
<li>
<a th:href="@{${sorturl}(sortField=updateTime,sortRule=DESC)}">新品</a>>
</li>
而 销量、评价 需要先在 skuInfo 中新建属性:
// 评论数
private Long commentNum;
// 销量
private Long saleNum;
/* getter、setter 方法省略 */
重新导入数据到索引库,然后在前端进行拼接:
<li>
<a th:href="@{${sorturl}(sortField=saleNum,sortRule=DESC)}">销量</a>
</li>
<li>
<a th:href="@{${sorturl}(sortField=commentNum,sortRule=DESC)}">评价</a>
</li>
访问起来都是没问题的,在前端对评价数 和 销量 进行展示:
<div class="commit">
<i class="command">已有<span th:text="${item.commentNum}"></span>人评价</i>
<i class="command">销量为<span th:text="${item.saleNum}"></span></i>
</div>
7、分页实现
在 comm 工程中添加 Page 分页对象,代码如下:
package entity;
import java.io.Serializable;
import java.util.List;
public class Page <T> implements Serializable{
// 页数(第几页)
private long currentpage;
// 查询数据库里面对应的数据有多少条
private long total;// 从数据库查处的总记录数
// 每页查5条
private int size;
// 下页
private int next;
private List<T> list;
// 最后一页
private int last;
private int lpage;//左边的开始的页码
private int rpage;//右边额开始的页码
//从哪条开始查
private long start;
//全局偏移量
public int offsize = 2;
/**
getter、setter 方法略 */
public Page() {
super();
}
/****
*
* @param currentpage
* @param total
* @param pagesize
*/
public void setCurrentpage(long currentpage,long total,long pagesize) {
//可以整除的情况下
long pagecount = total/pagesize;
//如果整除表示正好分N页,如果不能整除在N页的基础上+1页
int totalPages = (int) (total%pagesize==0? total/pagesize : (total/pagesize)+1);
//总页数
this.last = totalPages;
//判断当前页是否越界,如果越界,我们就查最后一页
if(currentpage>totalPages){
this.currentpage = totalPages;
}else{
this.currentpage=currentpage;
}
//计算start
this.start = (this.currentpage-1)*pagesize;
}
//上一页
public long getUpper() {
return currentpage>1? currentpage-1: currentpage;
}
//总共有多少页,即末页
public void setLast(int last) {
this.last = (int) (total%size==0? total/size : (total/size)+1);
}
/****
* 带有偏移量设置的分页
* @param total
* @param currentpage
* @param pagesize
* @param offsize
*/
public Page(long total,int currentpage,int pagesize,int offsize) {
this.offsize = offsize;
initPage(total, currentpage, pagesize);
}
/****
*
* @param total 总记录数
* @param currentpage 当前页
* @param pagesize 每页显示多少条
*/
public Page(long total,int currentpage,int pagesize) {
initPage(total,currentpage,pagesize);
}
/****
* 初始化分页
* @param total
* @param currentpage
* @param pagesize
*/
public void initPage(long total,int currentpage,int pagesize){
//总记录数
this.total = total;
//每页显示多少条
this.size=pagesize;
//计算当前页和数据库查询起始值以及总页数
setCurrentpage(currentpage, total, pagesize);
//分页计算
int leftcount =this.offsize, //需要向上一页执行多少次
rightcount =this.offsize;
//起点页
this.lpage =currentpage;
//结束页
this.rpage =currentpage;
//2点判断
this.lpage = currentpage-leftcount; //正常情况下的起点
this.rpage = currentpage+rightcount; //正常情况下的终点
//页差=总页数和结束页的差
int topdiv = this.last-rpage; //判断是否大于最大页数
/***
* 起点页
* 1、页差<0 起点页=起点页+页差值
* 2、页差>=0 起点和终点判断
*/
this.lpage=topdiv<0? this.lpage+topdiv:this.lpage;
/***
* 结束页
* 1、起点页<=0 结束页=|起点页|+1
* 2、起点页>0 结束页
*/
this.rpage=this.lpage<=0? this.rpage+(this.lpage*-1)+1: this.rpage;
/***
* 当起点页<=0 让起点页为第一页
* 否则不管
*/
this.lpage=this.lpage<=0? 1:this.lpage;
/***
* 如果结束页>总页数 结束页=总页数
* 否则不管
*/
this.rpage=this.rpage>last? this.last:this.rpage;
}
可以看到,设置的默认的全局偏移量 offsize 的值 是 2,也就是说,左右对称的应该是 2 个页码,即 包括当前页在内,每次显示 5 个页码,比如说:
public static void main(String[] args) {
//总记录数
//当前页
//每页显示多少条
int cpage =17;
Page page = new Page(1001,cpage,50,7);
System.out.println("开始页:"+page.getLpage()+"__当前页:"+page.getCurrentpage()+"__结束页"+page.getRpage()+"____总页数:"+page.getLast());
}
}
运行结果为:
开始页:15__当前页:17__结束页19____总页数:21。
总记录数为 1001 时,每页显示 50 条,得到的页码是 15、16、17、18、19, 5 个页,当前页 17 的左右是对称的。如果传入当前页是 21 ,得到的页码是 17、18、19、20、21 ,也就是说,以当前页为 主,在包含当前页的前提下,左右对称。
在 SkuServiceImpl 类的 searchlist 方法中,添加:
// 获取搜索封装信息中的分页信息
NativeSearchQuery query=builder.build();
Pageable pageable=query.getPageable();
int pageNumber = pageable.getPageNumber()+1;
int pageSize = pageable.getPageSize();
resultMap.put("pageNumber",pageNumber);
resultMap.put("pageSize", pageSize);
给 pageNumber +1 是因为如果没有传入当前页参数,默认是从 0 开始。
在 SkuController 中获取分页信息,并传到前端:
// 分页
Page<SkuInfo> pageInfo=new Page<SkuInfo>
(Long.parseLong(resultMap.get("totalNum").toString()),
// 当前页
Integer.parseInt(resultMap.get("pageNumber").toString()),
Integer.parseInt(resultMap.get("pageSize").toString())
);
model.addAttribute("pageInfo",pageInfo);
前端代码:
<ul>
<li class="prev disabled">
<a th:href="@{${url}(pageNum=${pageInfo.getUpper()})}">«上一页</a>
</li>
<li th:each="i:${#numbers.sequence(pageInfo.getLpage(),pageInfo.getRpage())}" th:class="${pageInfo.getCurrentpage()}==${i}?'active':''">
<a th:href="@{${url}(pageNum=${i})}" th:text="${i}"></a>
</li>
<li class="dotted"><span>...</span></li>
<li class="next">
<a th:href="@{${url}(pageNum=${pageInfo.next})}">下一页»</a>
</li>
</ul>
<div><span>共<em th:text="${pageInfo.last}">页</span><span>
不过这时有个问题,比如已经在 第 1 页,点击 “下一页”时,先获取 url “http://localhost:18086/search/list?pageNum=1”,这时会再追加 &pageNum。 所以需要对 url 进行处理,每次获取的 url 应该是不带当前页参数的,因为当前页参数是不需要保留的,比如选择了新的参数时,应该由默认的当前页 “1” 开始展示,而不是之前选择的当前页。也就是说,分页参数只在用户在路径中手动输入,或者在前端页码点击选中时有效。
在 url 方法里添加逻辑:
if(key.equalsIgnoreCase("pageNum")){
continue;
}
三、总结
(1)模板是将一些重复内容写好,其中某些可能发生变化的内容,采用占位符的方式动态加载(比如 JSP)。
模板引擎是基于写好的模板,动态给写好的模板加载数据。
Thymeleaf 是开源的 Java 库,提供了用于整合 Spring MVC 的可选模块。
对于视图解析器,之前在 SpringMVC 中是用 /WEB-INF/pages/xxx.jsp,现在是 classpath:/templates/ xxx.html。
用 SpringBoot 整合 thymeleaf ,实现步骤为 导入依赖、编写 controller 将后端变量的值设置到 model 中 (使用 model.addAttribute 方法)、使用 thymeleaf 语法编写 html,获取后台传来的变量。
(2)vue 中 ${}
是变量表达式,访问容器上下文环境中的变量。而像${#numbers.sequence(pageInfo.getLpage()
pageInfo 就不用再写个 ${} 了。