第十七章:基本搜索

此博客用于个人学习,来源于网上,对知识点进行一个整理。

1. 实现基本搜索:

1.1 页面分析:

1)页面跳转:

在首页的顶部,有一个输入框:当我们输入任何文本,点击搜索,就会跳转到搜索页 search.html 了,并且将搜索关键字以请求参数携带过来。

<script type="text/javascript">
    var vm = new Vue({
        el: "#searchApp",
        data: {
        },
        components:{
            // 加载页面顶部组件
            lyTop: () => import("./js/pages/top.js")
        }
    });
</script>

Vue 实例中,通过 import 导入的方式,加载了另外一个 js:top.js 并作为一个局部组件。

2)发起异步请求:

想要实现在页面加载后,就展示出搜索结果的效果。

分析逻辑:在页面加载时,获取地址栏请求参数,并发起异步请求,查询后台数据,然后在页面渲染。

在 data 中定义一个对象,记录请求的参数:

data: {
    search:{
        key:"", // 搜索页面的关键字
    }
}

通过钩子函数 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", ly.stringify(this.search)).then(resp=>{
        ly.http.post("/search/page", this.search).then(resp=>{
            console.log(resp);
        });
    }
}
  • 这里使用的 ly 是 common.js 中定义的工具对象。
  • 这里使用的是 post 请求,这样可以携带更多参数,并且以 json 格式发送。

最后要在 leyou-gateway 中的 CORS 配置类中,添加允许信任域名,并在 leyou-gateway 工程的 application.yml 中添加网关映射,接下来我们实现后台接口。

1.2 后台提供搜索接口:

1)controller:

  • 请求方式:Post

  • 请求路径:/search/page,不过前面的 /search 应该是网关的映射路径,因此真实映射路径 page,代表分页查询

  • 请求参数:json 格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有 page 属性,因此可以用一个对象来接收请求的 json 数据:

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 类。
@Controller
public class SearchController{

    @Autowired
    private SearchService searchService;

    @GetMapping("page")
    public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest searchRequest){
        PageResult<Goods> result = this.searchService.search(request);
        if (result == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(result);
    }
}

2)service:

@Service
public class SearchService {

    @Autowired
    private GoodsRepository goodsRepository;

    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、分页
        // 准备分页参数
        int page = request.getPage();
        int size = request.getSize();
        queryBuilder.withPageable(PageRequest.of(page - 1, size));

        // 4、查询,获取结果
        Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build());

        // 封装结果并返回
        return new PageResult<>(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent());
    }
}

要设置 SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。

3)优化:

当我们只查询部分字段时,结果 json 数据中有很多 null,于是在 leyou-search 的 application.yml 中添加一行配置,使得 json 处理时忽略空值:

spring:
  jackson:
    default-property-inclusion: non_null # 配置json处理时忽略空值

1.3 页面渲染:

1)保存搜索结果:

首先,在 data 中定义属性,保存搜索的结果:

在这里插入图片描述
在 loadData 的异步查询中,将结果赋值给 goodsList :

在这里插入图片描述
2)循环展示商品:

在 search.html 的中部,有一个 div,用来展示所有搜索到的商品:

在这里插入图片描述
可以看到,div 中有一个无序列表 ul,内部的每一个 li 就是一个商品 spu 了。于是删除多余的,只保留一个 li,然后利用 vue 的循环来展示搜索到的结果:

在这里插入图片描述

3)多 sku 展示:

一个商品位置,是多个sku的信息集合。当用户鼠标选择某个sku,对应的图片、价格、标题会随之改变,先实现 sku 的选择,才能去展示不同 sku 的数据。

可以看到,在列表中默认第一个是被选中的,那就需要做两件事情:

  • 在搜索到数据时,先默认把第一个 sku 作为被选中的,记录下来

  • 记录当前被选中的是哪一个 sku,记录在遍历到的 goods 对象自己内部,因为每一个 goods 都会有自己的 sku 信息

在查询成功的回调函数中,对 goods 进行遍历,把 skus 转化成 json 对象集合,并添加一个 selected 属性保存被选中的 sku:

在这里插入图片描述
查看多个 sku 的图片列表位置:

在这里插入图片描述
看到又是一个无序列表,这里也一样删掉多余的,保留一个 li,需要注意选中的项有一个样式类:selected

<!--多sku图片列表-->
<ul class="skus">
    <li :class="{selected: sku.id == goods.selected.id}" v-for="sku in goods.skus" :key="sku.id"
        @mouseOver="goods.selected=sku">
        <img :src="sku.image">
    </li>
</ul>

注意:

  • class 样式通过 goods.selected 的 id 是否与当前 sku 的 id 一致来判断
  • 绑定了鼠标事件,鼠标进入后把当前 sku 赋值到 goods.selected

已经可以通过 goods.selected 获取用户选中的 sku,那么我们就可以在页面展示了:

在这里插入图片描述

1.4 页面存在的问题:

在这里插入图片描述
1)价格显示的是分:

价格显示就不正确,我们数据库中存放的是以分为单位,所以这里要格式化,之前 common.js 中定义了工具类,可以帮我们转换。

在这里插入图片描述
此时 ly 并没有在 vue 中定义,于是先把 ly 记录到 Vue 实例:

在这里插入图片描述
2)标题过长:

标题内容太长了,已经无法完全显示。

在这里插入图片描述
3)sku 点击不切换:

当点击 sku 的图片列表,发现没有任何变化。

这是因为 Vue 的自动渲染是基于对象的属性变化的。比如页面使用 GoodsList 进行渲染,如果 GoodsList 变化,或者其内部的任何子对象变化,都会 Vue 感知,从而从新渲染页面。然而,这一切有一个前提,那就是当你第一次渲染时,对象中有哪些属性,Vue 就只监视这些属性,后来添加的属性发生改变,是不会被监视到的。而我们的 goods 对象中,本身是没有 selected 属性的,是我们后来才添加进去的:

在这里插入图片描述
这段代码稍微改造一下,即可:

在这里插入图片描述
先把 selected 属性初始化完毕,然后才把整个对象赋值给 goodsList,这样,goodsList 已初始化时就有 selected 属性,以后就会被正常监控了。

2. 页面分页效果:

刚才的查询中,我们默认了查询的页码和每页大小,因此所有的分页功能都无法使用,接下来我们一起看看分页功能条该如何制作。

这里要分两步,

  • 第一步:如何生成分页条
  • 第二步:点击分页按钮,我们做什么

2.1 如何生成分页条:

先看下页面关于分页部分的代码:

在这里插入图片描述
可以看到所有的分页栏内容都是写死的。

1)需要的数据:

分页数据应该是根据总页数当前页总条数等信息来计算得出。

  • 当前页:肯定是由页面来决定的,点击按钮会切换到对应的页
  • 总页数:需要后台传递给
  • 总条数:需要后台传递给

首先在data中记录下这几个值:page-当前页,total-总条数,totalPage-总页数

data: {
    ly,
    search:{
        key: "",
        page: 1
    },
    goodsList:[], // 接收搜索得到的结果
    total: 0, // 总条数
    totalPage: 0 // 总页数
}

因为 page 是搜索条件之一,所以记录在 search 对象中。我们在 created 钩子函数中,会读取 url 路径的参数,然后赋值给 search。如果是第一次请求页面,page 是不存在的。因此为了避免 page 被覆盖,应该这么做:

在这里插入图片描述
2)后台提供数据:

后台返回的结果中,要包含 total 和 totalPage,于是在 PageResult 类中添加一个构造函数:

在这里插入图片描述
在返回时,把这个值填上:

在这里插入图片描述
3)页面计算分页条:

把后台提供的数据保存在 data 中:

在这里插入图片描述
其中最复杂的是中间的 1~5 的分页按钮,它需要动态变化。

思路分析:

  • 最多有5个按钮,因此可以用 v-for 循环从1到5即可
  • 但是分页条不一定是从1开始:
    • 如果当前页值小于等于3的时候,分页条位置从1开始到5结束
    • 如果总页数小于等于5的时候,分页条位置从1开始到总页数结束
    • 如果当前页码大于3,应该从 page-3开始
    • 但是如果当前页码大于 totalPage-3,应该从 totalPage-5开始

在这里插入图片描述
a 标签中的分页数字通过 index 函数来计算,需要把 i 传递过去:

index(i){
    if(this.search.page <= 3 || this.totalPage <= 5){
        // 如果当前页小于等于3或者总页数小于等于5
        return i;
    } else if(this.search.page > 3) {
        // 如果当前页大于3
        return this.search.page - 3 + i;
    } else {
        return this.totalPage - 5 + i;
    }
}

如果总页数不足5页,我们就不应该遍历1~5,而是1~总页数,稍作改进:

在这里插入图片描述
分页条的其他内容:

<div class="sui-pagination pagination-large">
    <ul style="width: 550px">
        <li :class="{prev:true,disabled:search.page === 1}">
            <a href="#">«上一页</a>
        </li>
        <li :class="{active: index(i) === search.page}" v-for="i in Math.min(5,totalPage)" :key="i">
            <a href="#">{{index(i)}}</a>
        </li>
        <li class="dotted" v-show="totalPage > 5"><span>...</span></li>
        <li :class="{next:true,disabled:search.page === totalPage}">
            <a href="#">下一页»</a>
        </li>
    </ul>
    <div>
        <span>共{{totalPage}}页&nbsp;</span>
        <span>
            到第
            <input type="text" class="page-num" :value="search.page"><button class="page-confirm" onclick="alert(1)">确定</button>
        </span>
    </div>
</div>

2.2 点击分页后的操作:

点击分页按钮后,自然是要修改 page 的值

所以,我们在上一页、下一页按钮添加点击事件,对 page 进行修改,在数字按钮上绑定点击事件,点击直接修改 page:

在这里插入图片描述
翻页事件的方法:

prevPage(){
    if(this.search.page > 1){
        this.search.page--
    }
},
nextPage(){
    if(this.search.page < this.totalPage){
        this.search.page++
    }
}

当 page 发生变化,我们应该去后台重新查询数据。

不过,如果直接发起 ajax 请求,那么浏览器的地址栏中是不会有变化的,没有记录下分页信息。如果用户刷新页面,那么就会回到第一页,应该把搜索条件记录在地址栏的查询参数中

因此,我们监听 search 的变化,然后把 search 的过滤字段拼接在 url 路径后:

watch:{
    search:{
        deep:true,
        handler(val){
            // 把search对象变成请求参数,拼接在url路径
            window.location.href = "http://www.leyou.com/search.html?" + ly.stringify(val);
        }
    }
},

刷新页面测试,然后就出现重大 bug:页面无限刷新!因为 Vue 实例初始化的钩子函数中,我们读取请求参数,赋值给 search 的时候,也触发了 watch 监视!也就是说,每次页面创建完成,都会触发 watch,然后就会去修改 window.location 路径,然后页面被刷新,再次触发 created 钩子,又触发 watch,周而复始,无限循环。

所以,需要在 watch 中进行监控,如果发现是第一次初始化,则不继续向下执行。那么问题是,如何判断是不是第一次?

第一次初始化时,search 中的 key 值肯定是空的,所以,我们这么做:

watch:{
    search:{
        deep:true,
        handler(val,old){
            if(!old || !old.key){
                // 如果旧的search值为空,或者search中的key为空,证明是第一次
                return;
            }
            // 把search对象变成请求参数,拼接在url路径
            window.location.href = "http://www.leyou.com/search.html?" + ly.stringify(val);
        }
    }
}

2.3 页面顶部分页条:

在页面商品列表的顶部,也有一个分页条,我们把这一部分,也加上点击事件:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值