02、搜索页渲染:商品分页 - 计算分页条(了解)
首先,应该接收后端返回的总记录数 和 总页数:
然后看下我们要实现的效果:
这里最复杂的是中间的1~5的分页按钮,它需要动态变化。
按以下步骤一步步实现
1)一共显示多少个方块页码?
如果总页数小于5,就展示‘’总页数‘’个方块;如果总页数大于5,就展示5个方块。
<li class="active" v-for="i in Math.min(5,totalPage)">
<a href="#">{{i}}</a>
</li>
2)方块里面页码数字显示什么?
-
分析一下就知道方块里面的页码是变化的,所以这里可以定义index方法来计算方块显示的数字,index方法传入i,返回一个数字。该数字就是方块展示的数字。
-
紧接着,需要定义一个index方法来计算页码
-
index方法的实现逻辑
如果 当前页是前三页(当前页<=3 )或 总页数 <=5 ,方块展示的数字就应该是1 到 5,或1到总页数。
如果 当前页是后三页,且总页数大于5,方块应该展示的数字是 总页数-5+i
如果 当前页是前三页和后三页之间,且总页数大于5,此时,方块应该展示的的数字是当前页-3+i。
//计算方块里面数字
index(i){
if(this.searchParams.page<=3 || this.totalPage<=5){
//当前页为前三页的情况或总页数<=5的情况,返回 i
return i;
}else if(this.searchParams.page>=this.totalPage-2){
//当前页为后三页的情况,返回 总页数-5+i
return this.totalPage-5+i;
}else{
//当前页为前三页和后三页中间的情况,返回当前页-3+i
return this.searchParams.page-3+i;
}
}
3)显示其他数据
<div class="fr">
<div class="sui-pagination pagination-large">
<ul>
<li :class="{prev:true,disabled:searchParams.page==1}">
<a href="#">«上一页</a>
</li>
<li class="active" v-for="i in Math.min(5,totalPage)">
<a href="#">{{index(i)}}</a>
</li>
<li class="dotted"><span>...</span></li>
<li :class="{next:true,disabled:searchParams.page==totalPage}">
<a href="#">下一页»</a>
</li>
</ul>
<div><span>共{{totalPage}}页 </span><span>
03、搜索页渲染:商品分页 - 点击事件实现换页
1)添加点击事件
点击分页按钮后,自然是要修改page
的值
所以,我们在上一页
、下一页
、每页页码
按钮添加点击事件,对page进行修改,在数字按钮上绑定点击事件,点击直接修改page:
翻页事件的方法
//上一页
prevPage(){
if(this.searchParams.page>1){
this.searchParams.page--;
}
},
//下一页
nextPage(){
if(this.searchParams.page<this.totalPage){
this.searchParams.page++;
}
},
//跳转到指定页
toPage(curPage){
this.searchParams.page=curPage;
}
2)监听page页码的变化
监听当page
发生变化,我们应该去后台重新查询数据。
watch:{
//监听当前页的变化
"searchParams.page":{
handler(){
//只改变商品分页列表内容(不要改变规格参数)
this.pageChange();
}
}
},
提供pageChange方法
//商品分页变化
pageChange(){
ly.http.post('/search/page/change',this.searchParams).then(resp=>{
//接收数据
resp.data.forEach(item=>{
//1.把每个item的skus属性转换为Js对象
item.skus = JSON.parse(item.skus);
//2.在每个item里面添加selectedSku属性存入当前选中的Sku(默认第一个选中)
item.selectedSku = item.skus[0];
});
this.items = resp.data;
}).catch(e=>{
});
}
3)微服务提供换页方法
SearchController类提供换页方法
/**
* 商品换页
*/
@PostMapping("/page/change")
public ResponseEntity<List<GoodsDTO>> goodsSearchPageChange(@RequestBody SearchRequest searchRequest){
List<GoodsDTO> searchResult = searchService.goodsSearchPageChange(searchRequest);
return ResponseEntity.ok(searchResult);
}
SearchService类提供换页业务
public List<GoodsDTO> goodsSearchPageChange(SearchRequest searchRequest) {
PageResult<GoodsDTO> pageResult = itemQueryPage(searchRequest);
return pageResult.getItems();
}
04、搜索页渲染:商品分页 - URL变化问题
现象
我们会发现换页的时候,商品列表数据虽然更新了,但是浏览器的URL地址无变化,这样是有问题的,因为当我们把网址复制给别人的时候,不管怎样,别人打开的都是第一页
解决办法
在每次page属性变化的时候,把当前搜索参数全部设置到地址栏的URL中即可
watch:{
//监控page的变化,往后台发送请求
"searchParams.page":{
handler(){
//把searchParams对象中的所有参数追加到URL地址栏上面
//1)取出searchParams对象的所有参数,转换为 name=value&name=value
let paramStr = ly.stringify(this.searchParams);
//2)生成最终的新的URL地址
let newURL = location.origin + location.pathname + "?"+ paramStr;
//3)把新的URL地址改为到地址栏上面
//注意:不要使用location.href改变URL栏,因为发出重定向的请求,而是window.history.replaceState()方法
window.history.replaceState(null,null,newURL);
this.pageChange();
}
}
},
这时发现URL地址会改变了
但是URL复制给别人打开重新访问的时候,还是回到第一页!这时因为目前我们在created钩子函数中只添加了key作为条件,没有其他条件。解决办法很简单,把当前URL地址的所有参数设置到searchParams即可
//钩子函数
created(){
//从URL获取key关键词
let key = ly.getUrlParam('key');
//1)把URL的所有参数获取
let paramStr = location.search.substring(1);
//2)把参数格式转换js对象
let paramObj = ly.parse(paramStr);
//3)把js对象赋值给searchParams
//添加默认值
paramObj.page = paramObj.page || 1;
this.searchParams = paramObj;
//调用搜索方法
this.loadSearchPage();
},
05、搜索页渲染:商品分页 - 顶部分页条
在页面商品列表的顶部,也有一个分页条:
我们把这一部分,也加上点击事件:
<div class="top-pagination">
<span>共 <i style="color: #222;">{{total}}+</i> 商品</span>
<span><i style="color: red;">{{searchParams.page}}</i>/{{totalPage}}</span>
<a class="btn-arrow" href="javascript:void(0)" @click="prePage" style="display: inline-block"><</a>
<a class="btn-arrow" href="javascript:void(0)" @click="nextPage" style="display: inline-block">></a>
</div>
06、搜索过滤筛选:前端代码
1) 在data中定义变量接收过滤条件参数
2) 修改钩子函数
//钩子函数
created(){
//从URL获取key关键词
let key = ly.getUrlParam('key');
//1)把URL的所有参数获取
let paramStr = location.search.substring(1);
//2)把参数格式转换js对象
let paramObj = ly.parse(paramStr);
//3)把js对象赋值给searchParams
//添加默认值
paramObj.page = paramObj.page || 1;
paramObj.filterParams = paramObj.filterParams || {};
this.searchParams = paramObj;
//调用搜索方法
this.loadSearchPage();
},
3) 给所有过滤条件的值绑定点击事件
4) 定义点击触发事件
定义点击触发事件,修改查询过滤条件数据
//点击搜索过滤的参数
clickFilterParamHandler(key,value){
//单独把分类和品牌处理,只保留id即可
if(key=='分类' || key=='品牌'){
value = value.id;
}
//将key和value存入searchParams.filterParams参数中
//注意:如果往对象添加中文属性,必须使用obj[key]的语法,不能使用obj.key
this.searchParams.filterParams[key]= value;
//把点击的过滤参数追加到URL上面
this.replaceURL();
//发送请求到后台
this.loadSearchPage();
}
5) 抽取出一个通用改变URL地址函数
replaceURL(){
//当前页码变化的时候,把变化的参数追加到当前URL的后面
//1.取出searchParams对象,转换为普通参数格式:key=xxxx&page=1
let paramStr = ly.stringify(this.searchParams);
//2.把所有参数拼接在URL后面
let newURL = location.origin+location.pathname+"?"+paramStr;
//3.把拼接好最终的URL修改到浏览器上面
//注意:location.href不能使用,因为会重新刷新页面。使用window.history.replaceState只会修改URL地址,而不会刷新页面
window.history.replaceState(null,null,newURL);
}
6) 测试效果
07、搜索过滤筛选:后端代码(*)
1)原生查询语句(了解)
# 聚合查询
# 注意:在布尔的过滤条件中,只能添加term的不分词字段
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "手机",
"fields": ["all","spuName"]
}
}
],
"filter": [
{
"term": {
"specs.CPU核数.keyword": "八核"
}
},
{
"term": {
"specs.CPU品牌.keyword": "骁龙(Snapdragon)"
}
}
]
}
}
}
2) 修改接收参数的对象
package com.leyou.search.dto;
import java.util.Map;
/**
* 用于接收搜索页面传递的参数
*/
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private Map<String,Object> filterParams;//接收页面的规格参数
public Map<String, Object> getFilterParams() {
return filterParams;
}
public void setFilterParams(Map<String, Object> filterParams) {
this.filterParams = filterParams;
}
private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final Integer DEFAULT_PAGE = 1;// 默认页
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return DEFAULT_SIZE;
}
}
3) 添加查询过滤的业务方法
在之前抽取出来createNativeQueryBuilder公共方法中,再添加一个addFilterParams方法,来调用添加过滤条件的逻辑:
/**
* 创建查询条件对象
* @param searchRequest
* @return
*/
public NativeSearchQueryBuilder createNativeQueryBuilder(SearchRequest searchRequest) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//2.封装地查询构造器(*)
//1)创建布尔查询构造器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//2)在布尔查询构造器中追加must条件
//boolQueryBuilder.must( QueryBuilders.matchQuery("all",searchRequest.getKey()) );
boolQueryBuilder.must( QueryBuilders.multiMatchQuery(searchRequest.getKey(),"all","spuName") );
//添加过滤条件
Map<String, Object> filterParams = searchRequest.getFilterParams();
if(filterParams!=null){
//遍历过滤条件
filterParams.entrySet().forEach(entry->{
String key = entry.getKey();
Object value = entry.getValue();
//处理key
if(key.equals("分类")){
key = "categoryId";
}else if(key.equals("品牌")){
key = "brandId";
}else{
key = "specs."+key+".keyword";
}
//把过滤条件存入布尔查找中
boolQueryBuilder.filter( QueryBuilders.termQuery(key,value) );
});
}
//3)把布尔查询构造器存入本地查询构造器
queryBuilder.withQuery(boolQueryBuilder);
return queryBuilder;
}
08、搜索过滤筛选:页面过滤项超过一个才显示
现在页面基本完成,但是过滤项如果只剩下一个的时候,还可以点击,我们模仿京东,如果过滤项中只剩下一个,我们就不显示他了,这里用到vue的计算属性:
//计算属性
computed:{
//存放超过1个项目的过滤条件
remainFilterConditions(){
let remain = {};
for(let key in this.filterConditions){
//判断项目长度超过1
if(this.filterConditions[key].length>1){
remain[key] = this.filterConditions[key];
}
}
return remain;
}
},
加入了计算属性之后,遍历过滤条件的代码,也要修改:
然后测试,ok了!
09、商品详情:实现方案分析
当用户搜索到商品,肯定会点击查看,就会进入商品详情页,接下来我们完成商品详情页的展示,商品详情页在leyou-portal中对应的页面是:item.html
但是不同的商品,到达item.html需要展示的内容不同,该怎么做呢?
- 思路1:统一跳转到item.html页面,然后异步加载商品数据,渲染页面
- 思路2:将请求交给tomcat处理,在服务端完成数据渲染,给不同商品生成不同页面后,返回给用户
我们该选哪一种思路?
思路1:
- 优点:页面加载快,异步处理,用户体验好
- 缺点:会向服务端发起多次数据请求,增加服务端压力
思路2:
- 优点:服务端处理页面后返回,用户拿到是最终页面,不会再次向服务端发起数据请求。
- 缺点:在服务端处理页面,服务端压力过大,tomcat并发能力差
对于大型电商网站而言,必须要考虑的就是服务的高并发问题,因此要尽可能减少服务端压力,提高服务响应速度,所以这里我们两个方案都不会用,我们采用方案3:
方案3:页面静态化
页面静态化:顾名思义,就是把本来需要动态渲染的页面提前渲染完成,生成静态的HTML,当用户访问时直接读取静态HTML,提高响应速度,减轻服务端压力。
但是页面静态化要和纯静态页面 区分:
纯静态页面:和服务器完全没有任何交互的页面,所有数据都是写死在页面上。
页面静态化:是将纯静态页面,先渲染为动态页面,也就是说数据要从服务器获取。然后再使用静态化模板技术,将动态页面静态化,这时,之前动态页面从服务器获取的数据,也会被一并保存到静态化页面上。当再次访问页面的时候,无需经过服务器。但这时要考虑数据同步问题。
以前服务端渲染我们都使用的JSP,不过在SpringBoot中已经不推荐使用Jsp了,因此我们会使用另外的模板引擎技术:Thymeleaf。
与其类似的模板渲染技术有:Freemarker、Velocity等都可以。
商品详情页页面静态化的设计方案:
经过分析,我们接下来按照以下步骤逐一实现:
- 完成商品详情页面的跳转
- Nginx反向代理添加资源判断
- 搭建商品详情微服务
- 在商品详情微服务中使用Thymeleaf完成页面渲染(当前jsp使用)
- 编写测试类利用Thymeleaf生成静态页面,并存储到Nginx服务器
10、商品详情:了解Thymeleaf语法【了解】
在商品详情页中,我们会使用到Thymeleaf来渲染页面,如果需要先了解Thymeleaf的语法。详见课前资料中《Thymeleaf语法入门.md》
11、商品详情:完成商品详情页面的跳转
Thymeleaf我们已经入门完了,接下来,我就来开始做商品详情页了。
search.html页面修改路径修改如下:
测试:
12、商品详情:Nginx反向代理添加资源判断
server {
listen 80;
server_name www.leyou.com;
location /item {
# 先找本地
root html;
if (!-f $request_filename) { #请求的文件不存在,就反向代理
proxy_pass http://127.0.0.1:8084;
break;
}
}
location / {
proxy_pass http://leyou-portal;
proxy_connect_timeout 600;
proxy_read_timeout 5000;
}
}
把以上配置加入到nginx的对应配置中,意思是如果访问路径为item的资源,会先在nginx的html目录下的item目录下查询是否存在该为静态资源文件,如果找不到则交给Tomcat服务器作为动态资源处理
13、商品详情: 搭建静态页面微服务
1) 创建静态页面微服务
2) 导入jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ly-page</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--springmvc环境-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--feign包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--导入item的feign接口包-->
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-client-item</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--静态化模板包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--通用工具包-->
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3) 提供启动类
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyPageApplication {
public static void main(String[] args) {
SpringApplication.run(LyPageApplication.class, args);
}
}
4) 提供配置文件
server:
port: 8084
spring:
application:
name: page-service
thymeleaf:
cache: false
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
5) 创建templates目录并拷贝item.html
把leyou-portal项目的item.html拷贝过来
把html标签修改为Thymeleaf的格式
<html lang="en" xmlns:th="http://www.thymeleaf.org">
6) 提供跳转到静态页面的处理器
package com.leyou.page.controller;
import com.leyou.page.service.PageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller // 必须是Controller,不能@RestController
public class PageController {
@Autowired
private PageService pageService;
/**
* 接收商品详情
*/
@GetMapping("/item/{id}.html")
public String showGoodsDetail(@PathVariable("id") Long id){
//调用业务
pageService.getDetailData(id);
//返回th的模板
return "item";
}
}