0.學習目标
獨立編寫資料導入功能
獨立實作基本搜尋
獨立實作頁面分頁
獨立實作結果排序
1.索引庫資料導入
昨天我們學習了Elasticsearch的基本應用。今天就學以緻用,搭建搜尋微服務,實作搜尋功能。
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>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.search</groupId>
<artifactId>leyou-search</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- eureka -->
<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>
</dependencies>
</project>
application.yml:
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.56.101:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒發送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不發送就過期
prefer-ip-address: true
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port}
啟動類:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchService {
public static void main(String[] args) {
SpringApplication.run(LySearchService.class, args);
}
}
1.2.索引庫資料格式分析
接下來,我們需要商品資料導入索引庫,便于使用者搜尋。
那麼問題來了,我們有SPU和SKU,到底如何儲存到索引庫?
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", type = "docs", 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;// sku資訊的json結構
private Map<String, Object> specs;// 可搜尋的規格參數,key是參數名,值是參數值
}
一些特殊字段解釋:
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.商品分類名稱查詢
controller:
/**
* 根據商品分類id查詢名稱
* @param ids 要查詢的分類id集合
* @return 多個名稱的集合
*/
@GetMapping("names")
public ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids){
List<String > list = this.categoryService.queryNameByIds(ids);
if (list == null || list.size() < 1) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
測試:
1.3.2.編寫FeignClient
1.3.2.1.問題展現
操作leyou-search工程
現在,我們要在搜尋微服務調用商品微服務的接口。
第一步要引入商品微服務依賴:leyou-item-interface。
<!--商品微服務-->
<dependency>
<groupId>com.leyou.service</groupId>
<artifactId>ly-item-interface</artifactId>
<version>${leyou.latest.version}</version>
</dependency>
第二步,編寫FeignClient
@FeignClient(value = "item-service")
@RequestMapping("/goods")
public interface GoodsClient {
/**
* 分頁查詢商品
* @param page
* @param rows
* @param saleable
* @param key
* @return
*/
@GetMapping("/spu/page")
ResponseEntity<PageResult<SpuBo>> querySpuByPage(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "rows", defaultValue = "5") Integer rows,
@RequestParam(value = "saleable", defaultValue = "true") Boolean saleable,
@RequestParam(value = "key", required = false) String key);
/**
* 根據spu商品id查詢詳情
* @param id
* @return
*/
@GetMapping("/spu/detail/{id}")
ResponseEntity<SpuDetail> querySpuDetailById(@PathVariable("id") Long id);
/**
* 根據spu的id查詢sku
* @param id
* @return
*/
@GetMapping("sku/list")
ResponseEntity<List<Sku>> querySkuBySpuId(@RequestParam("id") Long id);
}
以上的這些代碼直接從商品微服務中拷貝而來,完全一緻。差别就是沒有方法的具體實作。大家覺得這樣有沒有問題?
而FeignClient代碼遵循SpringMVC的風格,是以與商品微服務的Controller完全一緻。這樣就存在一定的問題:
代碼備援。盡管不用寫實作,隻是寫接口,但服務調用方要寫與服務controller一緻的代碼,有幾個消費者就要寫幾次。
增加開發成本。調用方還得清楚知道接口的路徑,才能編寫正确的FeignClient。
1.3.2.2.解決方案
是以,一種比較友好的實踐是這樣的:
我們的服務提供方不僅提供實體類,還要提供api接口聲明
調用方不用字自己編寫接口方法聲明,直接繼承提供方給的Api接口即可,
第一步:服務的提供方在leyou-item-interface中提供API接口,并編寫接口聲明:
1526613268722
商品分類服務接口:
@RequestMapping("category")
public interface CategoryApi {
@GetMapping("names")
ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids);
}
商品服務接口,傳回值不再使用ResponseEntity:
@RequestMapping("/goods")
public interface GoodsApi {
/**
* 分頁查詢商品
* @param page
* @param rows
* @param saleable
* @param key
* @return
*/
@GetMapping("/spu/page")
PageResult<SpuBo> querySpuByPage(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "rows", defaultValue = "5") Integer rows,
@RequestParam(value = "saleable", defaultValue = "true") Boolean saleable,
@RequestParam(value = "key", required = false) String key);
/**
* 根據spu商品id查詢詳情
* @param id
* @return
*/
@GetMapping("/spu/detail/{id}")
SpuDetail querySpuDetailById(@PathVariable("id") Long id);
/**
* 根據spu的id查詢sku
* @param id
* @return
*/
@GetMapping("sku/list")
List<Sku> querySkuBySpuId(@RequestParam("id") Long id);
}
需要引入springMVC及leyou-common的依賴:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>leyou-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
第二步:在調用方leyou-search中編寫FeignClient,但不要寫方法聲明了,直接繼承leyou-item-interface提供的api接口:
商品的FeignClient:
@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}
商品分類的FeignClient:
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
是不是簡單多了?
項目結構:
1.3.2.3.測試
在leyou-search中引入springtest依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
建立測試類:
在接口上按快捷鍵:
Ctrl + Shift + T
測試代碼:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class CategoryClientTest {
@Autowired
private CategoryClient categoryClient;
@Test
public void testQueryCategories() {
List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(1L, 2L, 3L));
names.forEach(System.out::println);
}
}
結果:
1.4.導入資料
導入資料隻做一次,以後的更新删除等操作通過消息隊列來操作索引庫
1.4.1.建立GoodsRepository
java代碼:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
1.4.2.建立索引
我們建立一個測試類,在裡面進行資料的操作:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class ElasticsearchTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void createIndex(){
// 建立索引
this.elasticsearchTemplate.createIndex(Goods.class);
// 配置映射
this.elasticsearchTemplate.putMapping(Goods.class);
}
}
通過kibana檢視:
1.4.3.導入資料
導入資料其實就是查詢資料,然後把查詢到的Spu轉變為Goods來儲存,是以我們先編寫一個SearchService,然後在裡面定義一個方法, 把Spu轉為Goods
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
private ObjectMapper mapper = new ObjectMapper();
public Goods buildGoods(Spu spu) throws IOException {
Goods goods = new Goods();
// 查詢商品分類名稱
List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
// 查詢sku
List<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId());
// 查詢詳情
SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId());
// 查詢規格參數
List<SpecParam> params = this.specificationClient.querySpecParam(null, spu.getCid3(), true, null);
// 處理sku,僅封裝id、價格、标題、圖檔,并獲得價格集合
List<Long> prices = new ArrayList<>();
List<Map<String, Object>> skuList = new ArrayList<>();
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.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
skuList.add(skuMap);
});
// 處理規格參數
Map<String, Object> genericSpecs = mapper.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {
});
Map<String, Object> specialSpecs = mapper.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<String, Object>>() {
});
// 擷取可搜尋的規格參數
Map<String, Object> searchSpec = new HashMap<>();
// 過濾規格模闆,把所有可搜尋的資訊儲存到Map中
Map<String, Object> specMap = new HashMap<>();
params.forEach(p -> {
if (p.getSearching()) {
if (p.getGeneric()) {
String value = genericSpecs.get(p.getId().toString()).toString();
if(p.getNumeric()){
value = chooseSegment(value, p);
}
specMap.put(p.getName(), StringUtils.isBlank(value) ? "其它" : value);
} else {
specMap.put(p.getName(), specialSpecs.get(p.getId().toString()));
}
}
});
goods.setId(spu.getId());
goods.setSubTitle(spu.getSubTitle());
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setAll(spu.getTitle() + " " + StringUtils.join(names, " "));
goods.setPrice(prices);
goods.setSkus(mapper.writeValueAsString(skuList));
goods.setSpecs(specMap);
return goods;
}
}
因為過濾參數中有一類比較特殊,就是數值區間:
是以我們在存入時要進行處理:
private String chooseSegment(String value, SpecParam p) {
double val = NumberUtils.toDouble(value);
String result = "其它";
// 儲存數值段
for (String segment : p.getSegments().split(",")) {
String[] segs = segment.split("-");
// 擷取數值範圍
double begin = NumberUtils.toDouble(segs[0]);
double end = Double.MAX_VALUE;
if(segs.length == 2){
end = NumberUtils.toDouble(segs[1]);
}
// 判斷是否在範圍内
if(val >= begin && val < end){
if(segs.length == 1){
result = segs[0] + p.getUnit() + "以上";
}else if(begin == 0){
result = segs[1] + p.getUnit() + "以下";
}else{
result = segment + p.getUnit();
}
break;
}
}
return result;
}
然後編寫一個測試類,循環查詢Spu,然後調用IndexService中的方法,把SPU變為Goods,然後寫入索引庫:
@Test
public void loadData(){
// 建立索引
this.elasticsearchTemplate.createIndex(Goods.class);
// 配置映射
this.elasticsearchTemplate.putMapping(Goods.class);
int page = 1;
int rows = 100;
int size = 0;
do {
// 查詢分頁資料
PageResult<SpuBo> result = this.goodsClient.querySpuByPage(page, rows, true, null);
List<SpuBo> spus = result.getItems();
size = spus.size();
// 建立Goods集合
List<Goods> goodsList = new ArrayList<>();
// 周遊spu
for (SpuBo spu : spus) {
try {
Goods goods = this.searchService.buildGoods(spu);
goodsList.add(goods);
} catch (Exception e) {
break;
}
}
this.goodsRepository.saveAll(goodsList);
page++;
} while (size == 100);
}
通過kibana查詢, 可以看到資料成功導入:
2.實作基本搜尋
2.1.頁面分析
2.1.1.頁面跳轉
在首頁的頂部,有一個輸入框:
當我們輸入任何文本,點選搜尋,就會跳轉到搜尋頁search.html了:
并且将搜尋關鍵字以請求參數攜帶過來:
我們打開search.html,在最下面會有提前定義好的Vue執行個體:
<script type="text/javascript">
var vm = new Vue({
el: "#searchApp",
data: {
},
components:{
// 加載頁面頂部元件
lyTop: () => import("./js/pages/top.js")
}
});
</script>
這個Vue執行個體中,通過import導入的方式,加載了另外一個js:top.js并作為一個局部元件。top其實是頁面頂部導航元件,我們暫時不管
2.1.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中,添加允許信任域名:
并添加網關映射:
重新整理頁面試試:
因為背景沒有提供接口,是以無法通路。沒關系,接下來我們實作背景接口
2.2.背景提供搜尋接口
2.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類
代碼:
@RestController
@RequestMapping
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 (result == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(result);
}
}
2.2.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,來選擇要傳回的結果,否則傳回一堆沒用的資料,影響查詢效率。
2.2.3.測試
重新整理頁面測試:
資料是查到了,但是因為我們隻查詢部分字段,是以結果json 資料中有很多null,這很不優雅。
解決辦法很簡單,在leyou-search的application.yml中添加一行配置,json處理時忽略空值:
spring:
jackson:
default-property-inclusion: non_null # 配置json處理時忽略空值
結果:
2.3.頁面渲染
頁面已經拿到了結果,接下來就要渲染樣式了。
2.3.1.儲存搜尋結果
首先,在data中定義屬性,儲存搜尋的結果:
在loadData的異步查詢中,将結果指派給goodsList:
2.3.2.循環展示商品
在search.html的中部,有一個div,用來展示所有搜尋到的商品:
可以看到,div中有一個無序清單ul,内部的每一個li就是一個商品spu了。
我們删除多餘的,隻保留一個li,然後利用vue的循環來展示搜尋到的結果:
2.3.3.多sku展示
2.3.3.1.分析
接下來展示具體的商品資訊,來看圖:
這裡我們可以發現,一個商品位置,是多個sku的資訊集合。當使用者滑鼠選擇某個sku,對應的圖檔、價格、标題會随之改變!
我們先來實作sku的選擇,才能去展示不同sku的資料。
可以看到,在清單中預設第一個是被選中的,那我們就需要做兩件事情:
在搜尋到資料時,先預設把第一個sku作為被選中的,記錄下來
記錄目前被選中的是哪一個sku,記錄在哪裡比較合适呢?顯然是周遊到的goods對象自己内部,因為每一個goods都會有自己的sku資訊。
2.3.3.2.初始化sku
查詢出的結果集skus是一個json類型的字元串,不是js對象
我們在查詢成功的回調函數中,對goods進行周遊,把skus轉化成對象,并添加一個selected屬性儲存被選中的sku:
2.3.3.3.多sku圖檔清單
接下來,我們看看多個sku的圖檔清單位置:
1532240706261
看到又是一個無序清單,這裡我們也一樣删掉多餘的,保留一個li,需要注意選中的項有一個樣式類:selected
我們的代碼:
<!--多sku圖檔清單-->
<ul class="skus">
<li :class="{selected: sku.id == goods.selected.id}" v-for="sku in goods.skus" :key="sku.id"
@mouseEnter="goods.selected=sku">
<img :src="sku.image">
</li>
</ul>
注意:
class樣式通過 goods.selected的id是否與目前sku的id一緻來判斷
綁定了滑鼠事件,滑鼠進入後把目前sku指派到goods.selected
2.3.4.展示sku其它屬性
現在,我們已經可以通過goods.selected擷取使用者選中的sku,那麼我們就可以在頁面展示了:
重新整理頁面:
看起來很完美是吧!
但其實有一些瑕疵
2.3.5.幾個問題
2.3.5.1.價格顯示的是分
首先價格顯示就不正确,我們資料庫中存放的是以分為機關,是以這裡要格式化。
好在我們之前common.js中定義了工具類,可以幫我們轉換。
改造:
結果報錯:
為啥?
因為在Vue範圍内使用任何變量,都會預設去Vue執行個體中尋找,我們使用ly,但是Vue執行個體中沒有這個變量。是以解決辦法就是把ly記錄到Vue執行個體:
然後重新整理頁面:
2.3.5.2.标題過長
标題内容太長了,已經無法完全顯示,怎麼辦?
截取一下:
1526656959487
最好在加個懸停展示所有内容的效果
2.3.5.3.sku點選不切換
還有一個錯誤比較隐蔽,不容易被發現。我們點選sku 的圖檔清單,發現沒有任何變化。
這不科學啊,為什麼?
通過控制台觀察,發現資料其實是變化了,但是Vue卻沒有重新渲染視圖。
這是因為Vue的自動渲染是基于對象的屬性變化的。比如頁面使用GoodsList進行渲染,如果GoodsList變化,或者其内部的任何子對象變化,都會Vue感覺,進而從新渲染頁面。
然而,這一切有一個前提,那就是當你第一次渲染時,對象中有哪些屬性,Vue就隻監視這些屬性,後來添加的屬性發生改變,是不會被監視到的。
而我們的goods對象中,本身是沒有selected屬性的,是我們後來才添加進去的:
這段代碼稍微改造一下,即可:
也就是說,我們先把selected屬性初始化完畢,然後才把整個對象指派給goodsList,這樣,goodsList已初始化時就有selected屬性,以後就會被正常監控了。
3.頁面分頁效果
剛才的查詢中,我們預設了查詢的頁碼和每頁大小,是以所有的分頁功能都無法使用,接下來我們一起看看分頁功能條該如何制作。
這裡要分兩步,
第一步:如何生成分頁條
第二步:點選分頁按鈕,我們做什麼
3.1.如何生成分頁條
先看下頁面關于分頁部分的代碼:
1526692249371
可以看到所有的分頁欄内容都是寫死的。
3.1.1.需要的資料
分頁資料應該是根據總頁數、目前頁、總條數等資訊來計算得出。
目前頁:肯定是由頁面來決定的,點選按鈕會切換到對應的頁
總頁數:需要背景傳遞給我們
總條數:需要背景傳遞給我們
我們首先在data中記錄下這幾個值:page-目前頁,total-總條數,totalPage-總頁數
data: {
ly,
search:{
key: "",
page: 1
},
goodsList:[], // 接收搜尋得到的結果
total: 0, // 總條數
totalPage: 0 // 總頁數
}
因為page是搜尋條件之一,是以記錄在search對象中。
要注意:我們在created鈎子函數中,會讀取url路徑的參數,然後指派給search。如果是第一次請求頁面,page是不存在的。是以為了避免page被覆寫,我們應該這麼做:
不過,這個時候我們自己的search對象中的值就可有可無了
3.1.2.背景提供資料
背景傳回的結果中,要包含total和totalPage,我們改造下剛才的接口:
在我們傳回的PageResult對象中,其實是有totalPage字段的:
1526695144476
我們在傳回時,把這個值填上:
1526695592422
頁面測試一下:
OK
3.1.3.頁面計算分頁條
首先,把背景提供的資料儲存在data中:
1526695967230
然後看下我們要實作的效果:
1526695821870
這裡最複雜的是中間的1~5的分頁按鈕,它需要動态變化。
思路分析:
最多有5個按鈕,是以我們可以用v-for循環從1到5即可
但是分頁條不一定是從1開始:
如果目前頁值小于等于3的時候,分頁條位置從1開始到5結束
如果總頁數小于等于5的時候,分頁條位置從1開始到5結束
如果目前頁碼大于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頁,我們就不應該周遊15,而是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" "alert(1)">确定</button>
</span>
</div>
</div>
3.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);
}
}
}
再次重新整理,OK了!
3.3.頁面頂部分頁條
在頁面商品清單的頂部,也有一個分頁條:
我們把這一部分,也加上點選事件:
4.排序(作業)
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 @click="search.sortBy='createTime'" :class="{active: search.sortBy==='createTime'}">
<a href="#">新品</a>
</li>
<li>
<a href="#">評價</a>
</li>
<li @click="search.sortBy='price'; search.descending = !search.descending"
:class="{active: search.sortBy==='price'}">
<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兩個字段。需要進行擴充:
然後在搜尋業務邏輯中,添加排序條件:
注意,因為我們存儲在索引庫中的的價格是一個數組,是以在按照價格排序時,會進行智能處理:
如果是價格降序,則會把數組中的最大值拿來排序