此博客用于个人学习,来源于网上,对知识点进行一个整理。
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}}页 </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 页面顶部分页条:
在页面商品列表的顶部,也有一个分页条,我们把这一部分,也加上点击事件: