乐优商城(07)–搜索服务
一、索引库数据导入
1.1、创建搜索微服务
创建module
pom文件
<?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>myLeyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.search</groupId>
<artifactId>leyou-search</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--Nacos服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- elasticsearch -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
application.yaml配置文件
server:
port: 8083
spring:
application:
name: search-service
elasticsearch:
rest:
uris: ip地址:9200
cloud:
nacos:
discovery:
server-addr: ip地址6:8848
username: nacos
password: nacos
启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouSearchApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouSearchApplication.class,args);
}
}
1.2、索引库数据格式分析
目前已有的数据是SKU和SPU,那么如何保存在索引库中?
1.2.1.以结果为导向
先看下搜索页面:
可以看到,每一个搜索结果都有至少1个商品,当选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
1.2.2、需要的数据
直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:
这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、可用来搜索的规格参数等
综上所述,需要的数据格式有:
spuId、skuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
1.2.3、最终的数据结构
@Document(indexName = "goods", shards = 1, replicas = 0)
public class Goods {
@Id
private Long id; // spuId
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌
@Field(type = FieldType.Keyword, index = false)
private String subTitle;// 卖点
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private List<Long> price;// 价格
@Field(type = FieldType.Keyword, index = false)
private String skus;// List<sku>信息的json结构
private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
//get和set
}
一些特殊字段解释:
-
all:用来进行全文检索的字段,里面包含标题、商品分类信息
-
price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤
-
skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段
-
specs:所有规格参数的集合。key是参数名,值是参数值。
例如:在specs中存储 内存:4G,6G,颜色为红色,转为json就是:
{ "specs":{ "内存":[4G,6G], "颜色":"红色" } }
当存储到索引库时,elasticsearch会处理为两个字段:
- specs.内存:[4G,6G]
- specs.颜色:红色
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
- specs.颜色.keyword:红色
1.3、商品微服务提供接口
索引库中的数据来自于数据库,不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以只能调用商品微服务提供的接口服务。
先确定需要的数据:
- SPU信息
- SKU信息
- SPU的详情
- 商品分类名称(拼接all字段)
再确定需要哪些服务:
- 第一:分批查询spu的服务,已有。
- 第二:根据spuId查询sku的服务,已有
- 第三:根据spuId查询SpuDetail的服务,已有
- 第四:根据商品分类id,查询商品分类名称,没有
- 第五:根据商品品牌id,查询商品的品牌,没有
因此需要额外提供一个查询商品分类名称的接口。
1.3.1.商品分类名称查询
在CategoryController中添加接口:
/**
* 根据分类id查询分类名称
* @param ids
* @return
*/
@GetMapping("/names")
public ResponseEntity<List<String>> queryNamesByIds(@RequestParam("ids") List<Long> ids){
List<String> names = this.categoryService.queryNamesByIds(ids);
if (CollectionUtils.isEmpty(names)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(names);
}
1.3.2、提供接口
第一步:服务的提供方在leyou-item-interface
中提供API接口,即创建api包在里面编写接口声明:
品牌的接口:
@RequestMapping("/brand")
public interface BrandApi {
/**
* 根据品牌id查询品牌
* @param id
* @return
*/
@GetMapping("{id}")
Brand queryBrandById(@PathVariable("id") Long id);
}
分类的接口:
@RequestMapping("/category")
public interface CategoryApi {
/**
* 根据分类id查询分类名称
* @param ids
* @return
*/
@GetMapping("/names")
List<String> queryNamesByIds(@RequestParam("ids") List<Long> ids);
}
商品接口:
public interface GoodsApi {
/**
* 根据查询条件分页并排序查询商品信息
* @param key
* @param saleable
* @param page
* @param rows
* @return
*/
@GetMapping("/spu/page")
PageResult<SpuBo> querySpuBoByPage(
@RequestParam(value = "key",required = false) String key,
@RequestParam(value = "saleable",required = false) Boolean saleable,
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "sortBy",required = false) String sortBy,
@RequestParam(value = "desc",required = false) Boolean desc);
/**
* 通过 spuId 查询 spuDetail
* @param spuId
* @return
*/
@GetMapping("/spu/detail/{spuId}")
SpuDetail querySpuDetailBySpuId(@PathVariable("spuId") Long spuId);
/**
* 通过 spuId 查询 Sku 集合
* @param spuId
* @return
*/
@GetMapping("/sku/list")
List<Sku> querySkusBySpuId(@RequestParam("id") Long spuId);
}
商品参数接口:
@RequestMapping("/spec")
public interface SpecificationApi {
/**
* 根据 多参数查询具体 SpecParam
* @param gid
* @param cid
* @param generic
* @param searching
* @return
*/
@GetMapping("/params")
List<SpecParam> queryParams(
@RequestParam(value = "gid",required = false)Long gid,
@RequestParam(value = "cid",required = false)Long cid,
@RequestParam(value = "generic",required = false)Boolean generic,
@RequestParam(value = "searching",required = false)Boolean searching);
}
需要引入springMVC及leyou-common的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>leyou-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
第二步:在调用方leyou-search
中编写FeignClient,但不要写方法声明了,直接继承leyou-item-interface
提供的api接口:
品牌的FeignClient:
@FeignClient(value = "item-service")
public interface BrandClient extends BrandApi {
}
商品分类的FeignClient:
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
商品的FeignClient:
@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}
规格参数的FeignClient:
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}
测试
在leyou-search中引入springtest依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
创建测试类:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class CategoryClientTest {
@Autowired
private CategoryClient categoryClient;
@Test
public void testQueryCategories(){
List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(1L, 2L, 3L));
names.forEach(System.out::println);
}
}
启动时会报一个错误,说说明bean不等被注册,已经存在
解决方法:
在leyou-search
的application.yaml
中添加以下内容:
spring:
main:
allow-bean-definition-overriding: true
原因:
springCloud 的2.1.0以上版本的,将不再默认支持 FeignClient 的name属性 的相同名字。
即 :多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled(覆盖 是 禁止的/关闭的)。
1.3.3、导入数据
创建GoodsRepository
在leyou-search
的repository
包下创建GoodsRepository
接口
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
创建索引
新建一个测试类,在里面进行数据的操作:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class ElasticsearchTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Test
public void createIndex(){
//设置索引信息(绑定实体类) 返回IndexOperations
IndexOperations indexOperations = this.elasticsearchRestTemplate.indexOps(Goods.class);
//创建索引库,这里必须得注释掉,原因在于Goods实体类中有注解指明了要创建的索引,若这里再创建会报错
//indexOperations.create();
Document mapping = indexOperations.createMapping();
indexOperations.putMapping(mapping);
}
}
将数据转换
导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此得编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods
public interface SearchService {
/**
* 将 Spu对象转换为 Good对象
*
* @param spu
* @return
*/
Goods buildGoods(Spu spu) throws IOException;
}
实现类:
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private BrandClient brandClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
@Autowired
private GoodsRepository goodsRepository;
//序列化工具
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将 Spu对象转换为 Good对象
*
* @param spu
* @return
*/
@Override
public Goods buildGoods(Spu spu) throws IOException {
// 创建goods对象
Goods goods = new Goods();
// 查询品牌
Brand brand = this.brandClient.queryBrandById(spu.getBrandId());
// 查询分类名称
List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
// 查询spu下的所有sku
List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());
List<Long> prices = new ArrayList<>();
List<Map<String, Object>> skuMapList = new ArrayList<>();
// 遍历skus,获取价格集合
skus.forEach(sku ->{
prices.add(sku.getPrice());
Map<String, Object> skuMap = new HashMap<>();
skuMap.put("id", sku.getId());
skuMap.put("title", sku.getTitle());
skuMap.put("price", sku.getPrice());
skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : "");
skuMapList.add(skuMap);
});
// 查询出所有的搜索规格参数
List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
// 查询spuDetail。获取规格参数值
SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
// 获取通用的规格参数
Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() {
});
// 获取特殊的规格参数
Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() {
});
// 定义map接收{规格参数名,规格参数值}
Map<String, Object> paramMap = new HashMap<>();
params.forEach(param -> {
// 判断是否通用规格参数
if (param.getGeneric()) {
// 获取通用规格参数值
String value = genericSpecMap.get(param.getId()).toString();
// 判断是否是数值类型
if (param.getNumeric()){
// 如果是数值的话,判断该数值落在那个区间
value = chooseSegment(value, param);
}
// 把参数名和值放入结果集中
paramMap.put(param.getName(), value);
} else {
paramMap.put(param.getName(), specialSpecMap.get(param.getId()).toString());
}
});
// 设置参数
goods.setId(spu.getId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setBrandId(spu.getBrandId());
goods.setCreateTime(spu.getCreateTime());
goods.setSubTitle(spu.getSubTitle());
goods.setAll(spu.getTitle() + brand.getName() + StringUtils.join(names, " "));
goods.setPrice(prices);
goods.setSkus(MAPPER.writeValueAsString(skuMapList));
goods.setSpecs(paramMap);
return goods;
}
/**
* 判断数值落在那个区间
* @param value
* @param param
* @return
*/
private String chooseSegment(String value, SpecParam param) {
double val = NumberUtils.toDouble(value);
String result = "其他";
for (String segment : param.getSegments().split(",")) {
String[] segs = segment.split("-");
//获取取值范围
double start = NumberUtils.toDouble(segs[0]);
double end = Double.MAX_VALUE;
if (segs.length == 2){
end = NumberUtils.toDouble(segs[1]);
}
//判断数值在哪个范围
if (val >= start && val < end){
if (segs.length == 1){
result = segs[0] + param.getUnit() + "以上";
}else if (start == 0){
result = segs[1] + param.getUnit() + "以下";
}else {
result = segment + param.getUnit();
}
break;
}
}
return result;
}
}
然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:
@Test
public void loadData(){
int page = 1; //当前页
int rows = 100;//当前页数据大小
do {
// 分批查询spuBo
PageResult<SpuBo> pageResult = this.goodsClient.querySpuBoByPage("", true, page, rows);
// 遍历spubo集合转化为List<Goods>
List<Goods> goodsList = pageResult.getItems().stream().map(spuBo -> {
try {
return this.searchService.buildGoods((Spu) spuBo);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}).collect(Collectors.toList());
this.goodsRepository.saveAll(goodsList);
// 获取当前页的数据条数,如果是最后一页,没有100条,循环会退出
rows = pageResult.getItems().size();
//每次循环页码+1
page++;
}while (rows == 100);
}
去kibana查询:可以看到数据成功导入
二、实现基本搜索
2.1、页面分析
在首页的顶部,有一个输入框:
当在搜索栏输入任何文本,点击搜索,就会跳转到搜索页search.html
了,并且会将搜索关键字以请求参数携带过来:
2.2、发起异步请求
要想在页面加载后,就展示出搜索结果。应该在页面加载时,获取地址栏请求参数,并发起异步请求,查询后台数据,然后在页面渲染。
在data中定义一个对象,记录请求的参数:
然后通过钩子函数created,在页面加载时获取请求参数,并记录下来。
created(){
// 判断是否有请求参数
if(!location.search){
return;
}
// 将请求参数转为对象
const search = ly.parse(location.search.substring(1));
// 记录在data的search对象中
this.search = search;
// 发起请求,根据条件搜索
this.loadData();
}
发起请求,搜索数据:
methods: {
loadData(){
ly.http.post("/search/page", this.search).then(resp=>{
console.log(resp);
});
}
}
- 这里使用
ly
是common.js中定义的工具对象。 - 这里使用的是post请求,这样可以携带更多参数,并且以json格式发送
在leyou-gateway
中的CORS配置类中,添加允许信任域名,解决跨域问题:
并在leyou-gateway
工程的application.yml
中添加网关映射:
- id: search-router
uri: http://127.0.0.1:8083
predicates:
- Path=/api/search/**
filters:
- StripPrefix=1
刷新页面试试:
2.3、后端实现
SearchController
-
请求方式:Post
-
请求路径:/search/page,代表分页查询
-
请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此可以用一个对象来接收请求的json数据:
package com.leyou.search.bo; public class SearchRequest { private String key;// 搜索条件 private Integer page;// 当前页 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; } }
-
返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,可以使用之前定义的PageResult类
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private SearchService searchService;
/**
* 搜索商品
*
* @param request
* @return
*/
@PostMapping("/page")
public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request){
PageResult<Goods> result = this.searchService.search(request);
if (null == result){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(result);
}
}
SearchService
/**
* 搜索商品
*
* @param request
* @return
*/
PageResult<Goods> search(SearchRequest request);
实现类:
/**
* 搜索商品
*
* @param request
* @return
*/
@Override
public PageResult<Goods> search(SearchRequest request) {
String key = request.getKey();
// 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
if (StringUtils.isBlank(key)) return null;
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1、对key进行全文检索查询
queryBuilder.withQuery(QueryBuilders.matchQuery("all",key).operator(Operator.AND));
// 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));
// 3、分页
// 准备分页参数
Integer page = request.getPage();
Integer size = request.getSize();
queryBuilder.withPageable(PageRequest.of(page - 1,size));
//Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
// 4、查询,获取结果
SearchHits<Goods> searchHits = this.elasticsearchRestTemplate.search(queryBuilder.build(), Goods.class);
Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
// 封装结果并返回
return new PageResult<>(goodsPage.getTotalElements(),goodsPage.getTotalPages(),goodsPage.getContent());
}
注意点:要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。
测试
注意:查询返回的数据是带有空值的,将这些空值去除。
在leyou-search
的application.yml中添加一行配置,json处理时忽略空值:
spring:
jackson:
default-property-inclusion: non_null # 配置json处理时忽略空值
2.4、页面渲染
保存搜索结果
首先,在data中定义属性,保存搜索的结果:
在loadData
的异步查询中,将结果赋值给goodsList
:
查询出的结果集skus是一个json类型的字符串,不是js对象,在查询成功的回调函数中,对goods进行遍历,把skus转化成对象,并添加一个selected属性保存被选中的sku:
循环展示商品
在search.html的中部,有一个div
,用来展示所有搜索到的商品:
保留其中一个<li>
元素,然后利用vue的循环来展示搜索到的结果:
多sku展示
这里可以发现,一个商品位置,是多个sku的信息集合。**当用户鼠标选择某个sku,对应的图片、价格、标题会随之改变!**先来实现sku的选择,才能去展示不同sku的数据。
可以看到,在列表中默认第一个是被选中的,那就需要做两件事情:
- 在搜索到数据时,先默认把第一个sku作为被选中的,记录下来
- 记录当前被选中的是哪一个sku,记录在哪里比较合适呢?显然是遍历到的goods对象自己内部,因为每一个goods都会有自己的sku信息。
多sku图片列表
注意:
- class样式通过 goods.selected的id是否与当前sku的id一致来判断
- 绑定了鼠标事件,鼠标进入后把当前sku赋值到goods.selected
展示sku其它属性
已经可以通过goods.selected获取
用户选中的sku,那么就可以在页面展示了:
这里在common.js
页面的ly对象中添加一个函数:
/**
* 字符串截取
* @param val
* @returns {string}
*/
formatStr(val){
return val.substr(0,18)+"....";
},
三、页面分页效果
3.1.如何生成分页条
分页数据应该是根据总页数、当前页、总条数等信息来计算得出。
- 当前页:肯定是由页面来决定的,点击按钮会切换到对应的页
- 总页数:需要后台传递给我们
- 总条数:需要后台传递给我们
首先在data中记录下这几个值:page-当前页,total-总条数,totalPage-总页数
data: {
ly,
search:{
key: "",
page: 1
},
goodsList:[], // 接收搜索得到的结果
total: 0, // 总条数
totalPage: 0 // 总页数
}
因为page是搜索条件之一,所以记录在search中。
要注意:在created钩子函数中,会读取url路径的参数,然后赋值给search。如果是第一次请求页面,page是不存在的。因此为了避免page被覆盖,需要做以下修改:
3.2、页面计算分页条
思路分析:
-
最多有5个按钮,因此可以用v-for循环从1到5即可
-
但是分页条不一定是从1开始:
- 如果当前页值小于等于3的时候,分页条位置从1开始到5结束
- 如果总页数小于等于5的时候,分页条位置从1开始到5结束
- 如果当前页码大于3,应该从page-3开始
- 但是如果当前页码大于totalPage-3,应该从totalPage-5开始
所以,页面这样来做:
需要注意的是,如果总页数不足5页,就不应该遍历15,而是1总页数 :Math.min(5,totalPage)
a标签中的分页数字通过index
函数来计算,需要把i
传递过去:
分页总代码:
3.3、翻页事件
接下来编写翻页事件:
prev() {
if (this.search.page > 1) {
this.search.page--;
}
},
next() {
if (this.search.page < this.totalPage) {
this.search.page++;
}
},
当page发生变化,就应该去后台重新查询数据。
不过,如果直接发起ajax请求,那么浏览器的地址栏中是不会有变化的,没有记录下分页信息。如果用户刷新页面,那么就会回到第一页。
这样不太友好,应该把搜索条件记录在地址栏的查询参数中。
因此,需要监听search的变化,然后把search的过滤字段拼接在url路径后:
watch: {
search: {
deep: true,
handler(newVal, oldVal) {
// 如果旧的search值为空,或者search中的key为空,证明是第一次
if (!oldVal || !oldVal.key) {
return;
}
// 把search对象变成请求参数,拼接在url路径
location = "http://www.leyou.com/search.html?" + ly.stringify(this.search);
}
}
},
若没有if (!oldVal || !oldVal.key) {return;}
这行代码会发生什么?
页面无限刷新!
因为Vue实例初始化的钩子函数中,读取请求参数,赋值给search的时候,会触发了watch监视!也就是说,每次页面创建完成,都会触发watch,然后就会去修改window.location路径,然后页面被刷新,再次触发created钩子,又触发watch,周而复始,无限循环。
页面顶部分页条
直接在代码中加上点击事件即可:
四、排序
4.1、页面搜索排序条件
在搜索商品列表的顶部,有以下内容:
这是用来做排序的,默认按照综合排序。点击新品,应该按照商品创建时间排序,点击价格应该按照价格排序。
排序需要知道两个内容:
- 排序的字段
- 排序的方式
因此,首先在search中记录这两个信息,因为created钩子函数会对search进行覆盖,因此在钩子函数中对这两个信息进行初始化即可:
然后,在页面上给按钮绑定点击事件,修改sortBy和descending的值:
<ul class="sui-nav">
<li :class="{active:!search.sortBy}" @click="search.sortBy=''">
<a href="#">综合</a>
</li>
<li>
<a href="#">销量</a>
</li>
<li :class="{active: search.sortBy==='createTime'}" @click="search.sortBy='createTime'">
<a href="#">新品</a>
</li>
<li>
<a href="#">评价</a>
</li>
<li :class="{active: search.sortBy==='price'}"
@click="search.sortBy='price';search.descending = !search.descending ">
<a href="#">价格
<v-icon v-show="search.descending">arrow_drop_down</v-icon>
<v-icon v-show="!search.descending">arrow_drop_up</v-icon>
</a>
</li>
</ul>
可以看到,页面请求参数中已经有了排序字段了:
4.2、后端实现
后台需要接收请求参数中的排序信息,然后在搜索中加入排序的逻辑。
现在的请求参数对象SearchRequest
中,只有page、key两个字段。需要进行扩展:(记得添加get和set方法)
然后在搜索业务逻辑中,添加排序条件:
注意,因为存储在索引库中的的价格是一个数组:
因此在按照价格排序时,会进行智能处理:
- 如果是价格降序,则会把数组中的最大值拿来排序
- 如果是价格升序,则会把数组中的最小值拿来排序
存在一个问题
比方选中价格降序,显示的商品中有sku的价格是符合降序的,但是默认显示的sku图片却不是符合要求的商品
解决方法:
修改loadData函数,在给goods.selected赋值时不在是把skus中第一个商品赋给它,而是查询符合要求的商品。
loadData() {
ly.http.post("/search/page", this.search).then(({data}) => {
data.items.forEach(goods => {
let max = 0;
let min = 0;
//转换skus:把字符串转变为对象
goods.skus = JSON.parse(goods.skus);
//添加默认选中项,如果按价格排序则选出skus中价格最低的或者最高的,否则选skus中的第一个
if (this.search.sortBy === "price"){
if (this.search.descending === true){
//降序,则skus中价格选最高的
goods.skus.forEach(sku => {
if (sku.price > max){
max = sku.price;
}
});
goods.skus.forEach(sku => {
if (sku.price === max){
goods.selected = sku;
}
});
} else {
//升序,则skus中价格选最低的
min = goods.skus[0].price;
goods.skus.forEach(sku => {
if (sku.price < min){
min = sku.price;
}
});
goods.skus.forEach(sku => {
if (sku.price === min){
goods.selected = sku;
}
});
}
} else {
goods.selected = goods.skus[0];
}
});
this.goodsList = data.items;
this.totalPage = data.totalPage;
this.total = data.total;
}