天天看點

JAVA商城項目(微服務架構)——第12天 elasticsearch2

0.學習目标

獨立編寫資料導入功能

獨立實作基本搜尋

獨立實作頁面分頁

獨立實作結果排序

1.索引庫資料導入

昨天我們學習了Elasticsearch的基本應用。今天就學以緻用,搭建搜尋微服務,實作搜尋功能。

1.1.建立搜尋服務

建立module:

JAVA商城項目(微服務架構)——第12天 elasticsearch2
JAVA商城項目(微服務架構)——第12天 elasticsearch2

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.以結果為導向

大家來看下搜尋結果頁:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

可以看到,每一個搜尋結果都有至少1個商品,當我們選擇大圖下方的小圖,商品會跟着變化。

是以,搜尋的結果是SPU,即多個SKU的集合。

既然搜尋的結果是SPU,那麼我們索引庫中存儲的應該也是SPU,但是卻需要包含SKU的資訊。

1.2.2.需要什麼資料

再來看看頁面中有什麼資料:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

直覺能看到的:圖檔、價格、标題、副标題

暗藏的資料:spu的id,sku的id

另外,頁面還有過濾條件:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

這些過濾條件也都需要存儲到索引庫中,包括:

商品分類、品牌、可用來搜尋的規格參數等

綜上所述,我們需要的資料格式有:

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);
}      

測試:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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 {
}      

是不是簡單多了?

項目結構:

JAVA商城項目(微服務架構)——第12天 elasticsearch2
JAVA商城項目(微服務架構)——第12天 elasticsearch2

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​

JAVA商城項目(微服務架構)——第12天 elasticsearch2
JAVA商城項目(微服務架構)——第12天 elasticsearch2

測試代碼:

@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);
    }
}      

結果:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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檢視:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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;
    }                                                   

}      

因為過濾參數中有一類比較特殊,就是數值區間:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

是以我們在存入時要進行處理:

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查詢, 可以看到資料成功導入:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

2.實作基本搜尋

2.1.頁面分析

2.1.1.頁面跳轉

在首頁的頂部,有一個輸入框:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

當我們輸入任何文本,點選搜尋,就會跳轉到搜尋頁search.html了:

并且将搜尋關鍵字以請求參數攜帶過來:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

我們打開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中,添加允許信任域名:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

并添加網關映射:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

重新整理頁面試試:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

因為背景沒有提供接口,是以無法通路。沒關系,接下來我們實作背景接口

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.測試

重新整理頁面測試:

JAVA商城項目(微服務架構)——第12天 elasticsearch2
JAVA商城項目(微服務架構)——第12天 elasticsearch2

資料是查到了,但是因為我們隻查詢部分字段,是以結果json 資料中有很多null,這很不優雅。

解決辦法很簡單,在leyou-search的application.yml中添加一行配置,json處理時忽略空值:

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

結果:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

2.3.頁面渲染

頁面已經拿到了結果,接下來就要渲染樣式了。

2.3.1.儲存搜尋結果

首先,在data中定義屬性,儲存搜尋的結果:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

在loadData的異步查詢中,将結果指派給goodsList:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

2.3.2.循環展示商品

在search.html的中部,有一個div,用來展示所有搜尋到的商品:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

可以看到,div中有一個無序清單ul,内部的每一個li就是一個商品spu了。

我們删除多餘的,隻保留一個li,然後利用vue的循環來展示搜尋到的結果:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

2.3.3.多sku展示

2.3.3.1.分析

接下來展示具體的商品資訊,來看圖:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

這裡我們可以發現,一個商品位置,是多個sku的資訊集合。當使用者滑鼠選擇某個sku,對應的圖檔、價格、标題會随之改變!

我們先來實作sku的選擇,才能去展示不同sku的資料。

JAVA商城項目(微服務架構)——第12天 elasticsearch2

可以看到,在清單中預設第一個是被選中的,那我們就需要做兩件事情:

在搜尋到資料時,先預設把第一個sku作為被選中的,記錄下來

記錄目前被選中的是哪一個sku,記錄在哪裡比較合适呢?顯然是周遊到的goods對象自己内部,因為每一個goods都會有自己的sku資訊。

2.3.3.2.初始化sku

查詢出的結果集skus是一個json類型的字元串,不是js對象

JAVA商城項目(微服務架構)——第12天 elasticsearch2

我們在查詢成功的回調函數中,對goods進行周遊,把skus轉化成對象,并添加一個selected屬性儲存被選中的sku:

JAVA商城項目(微服務架構)——第12天 elasticsearch2
JAVA商城項目(微服務架構)——第12天 elasticsearch2

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,那麼我們就可以在頁面展示了:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

重新整理頁面:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

看起來很完美是吧!

但其實有一些瑕疵

2.3.5.幾個問題

2.3.5.1.價格顯示的是分

首先價格顯示就不正确,我們資料庫中存放的是以分為機關,是以這裡要格式化。

好在我們之前common.js中定義了工具類,可以幫我們轉換。

改造:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

結果報錯:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

為啥?

因為在Vue範圍内使用任何變量,都會預設去Vue執行個體中尋找,我們使用ly,但是Vue執行個體中沒有這個變量。是以解決辦法就是把ly記錄到Vue執行個體:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

然後重新整理頁面:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

2.3.5.2.标題過長

标題内容太長了,已經無法完全顯示,怎麼辦?

截取一下:

1526656959487

最好在加個懸停展示所有内容的效果

2.3.5.3.sku點選不切換

還有一個錯誤比較隐蔽,不容易被發現。我們點選sku 的圖檔清單,發現沒有任何變化。

這不科學啊,為什麼?

通過控制台觀察,發現資料其實是變化了,但是Vue卻沒有重新渲染視圖。

這是因為Vue的自動渲染是基于對象的屬性變化的。比如頁面使用GoodsList進行渲染,如果GoodsList變化,或者其内部的任何子對象變化,都會Vue感覺,進而從新渲染頁面。

然而,這一切有一個前提,那就是當你第一次渲染時,對象中有哪些屬性,Vue就隻監視這些屬性,後來添加的屬性發生改變,是不會被監視到的。

而我們的goods對象中,本身是沒有selected屬性的,是我們後來才添加進去的:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

這段代碼稍微改造一下,即可:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

也就是說,我們先把selected屬性初始化完畢,然後才把整個對象指派給goodsList,這樣,goodsList已初始化時就有selected屬性,以後就會被正常監控了。

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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被覆寫,我們應該這麼做:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

不過,這個時候我們自己的search對象中的值就可有可無了

3.1.2.背景提供資料

背景傳回的結果中,要包含total和totalPage,我們改造下剛才的接口:

在我們傳回的PageResult對象中,其實是有totalPage字段的:

1526695144476

我們在傳回時,把這個值填上:

1526695592422

頁面測試一下:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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開始

是以,我們的頁面這樣來做:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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總頁數,稍作改進:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

分頁條的其它部分就比較簡單了:

<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:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

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.頁面頂部分頁條

在頁面商品清單的頂部,也有一個分頁條:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

我們把這一部分,也加上點選事件:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

4.排序(作業)

4.1.頁面搜尋排序條件

在搜尋商品清單的頂部,有這麼一部分内容:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

這是用來做排序的,預設按照綜合排序。點選新品,應該按照商品建立時間排序,點選價格應該按照價格排序。因為我們沒有統計銷量和評價,這裡咱們以新品和價格為例,進行講解,做法是想通的。

排序需要知道兩個内容:

排序的字段

排序的方式

是以,我們首先在search中記錄這兩個資訊,因為created鈎子函數會對search進行覆寫,是以我們在鈎子函數中對這兩個資訊進行初始化即可:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

然後,在頁面上給按鈕綁定點選事件,修改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>      

可以看到,頁面請求參數中已經有了排序字段了:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

4.2.背景添加排序邏輯

接下來,背景需要接收請求參數中的排序資訊,然後在搜尋中加入排序的邏輯。

現在,我們的請求參數對象SearchRequest中,隻有page、key兩個字段。需要進行擴充:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

然後在搜尋業務邏輯中,添加排序條件:

JAVA商城項目(微服務架構)——第12天 elasticsearch2

注意,因為我們存儲在索引庫中的的價格是一個數組,是以在按照價格排序時,會進行智能處理:

如果是價格降序,則會把數組中的最大值拿來排序

繼續閱讀