暢購商城(七):Thymeleaf實現靜態頁

Robod丶發表於2020-08-01

好好學習,天天向上

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航

Thymeleaf簡單入門

什麼是Thymeleaf

Thymeleaf是一個模板引擎,主要用於編寫動態頁面。

SpringBoot整合Thymeleaf

SpringBoot整合Thymeleaf的方式很簡單,共分為以下幾個步驟

  • 建立一個sprinboot專案
  • 新增thymeleaf和spring web的起步依賴
  • 在resources/templates/下編寫html(需要宣告使用thymeleaf標籤)
  • 在controller層編寫相應的程式碼

啟動類,配置檔案,依賴的程式碼下一節有,這裡就不貼了。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>SpringBoot整合Thymeleaf</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<!--輸出hello資料   ${變數名}  -->
<p th:text="${hello}"></p>
</body>
</html>
@Controller
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/hello")
    public String hello(Model model){
        model.addAttribute("hello","歡迎關注微信公眾號Robod");
        return "demo1";
    }
}

這樣將專案啟動起來,訪問http://localhost:8080/test/hello就可以成功跳轉到demo1.html頁面的內容了。

Thymeleaf常用標籤

  • th:action 定義後臺控制器路徑

現在訪問http://localhost:8080/test/hello2,如果控制檯輸出“demo2”,頁面還跳轉到demo2的話說明是OK的。

  • th:each 物件遍歷

訪問http://localhost:8080/test/hello3就可以看到結果了。

  • 遍歷Map

訪問http://localhost:8080/test/hello4就可以看到輸出結果。

  • 陣列輸出

訪問http://localhost:8080/test/hello5就可以看到輸出結果。

  • Date輸出

訪問http://localhost:8080/test/hello6就可以看到輸出結果。

  • th:if條件

訪問http://localhost:8080/test/hello7就可以看到輸出結果。

  • th:fragment th:include 定義和引入模組

比如我們在footer.html中定義了一個模組:

<div th:fragment="foot">
    歡迎關注微信公眾號Robod
</div>

然後在demo7中引用:

<div th:include="footer::foot"></div>

這樣訪問http://localhost:8080/test/hello7就可以看到效果了。

  • |....| 字串拼接
<span th:text="|${str1}${str2}|"></span>
--------------------------------------------
@RequestMapping("/hello8")
public String hello8(Model model){
    model.addAttribute("str1","字串1");
    model.addAttribute("str2","字串2");
    return "demo8";
}

訪問http://localhost:8080/test/hello8就可以看到輸出結果。

想要完整程式碼的小夥伴請點選下載

搜尋頁面

微服務搭建

我們建立一個搜尋頁面渲染微服務用來展示搜尋頁面,在這個微服務中,使用者進行搜尋後,呼叫搜尋微服務拿到資料,然後使用Thymeleaf將頁面渲染出來展示給使用者。在changgou-web下建立一個名為changgou-search-web的Module用作搜尋微服務的頁面渲染工程。因為有些依賴是所有頁面渲染微服務都要用到的,所以在changgou-web中新增依賴:

<dependencies>
    <!-- Thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <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>-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
    <!--amqp-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
</dependencies>

Feign的依賴這裡我發現了一個問題,因為我不是把SearchEntity根據下圖的流程通過Feign傳遞到changgou-service-search麼。如果新增我註釋的那個依賴就會出現HttpRequestMethodNotSupportedException: Request method 'POST' not supported異常。新增後面一個依賴就不會出現問題。我到網上查了一下,貌似是Feign的一個小Bug,就是如果在GET請求裡新增了請求體就會被轉換為POST請求。

因為我們需要使用到Feign在幾個微服務之間進行呼叫,所以在changgou-search-web新增對changgou-service-search-api的依賴。

<dependencies>
    <dependency>
        <groupId>com.robod</groupId>
        <artifactId>changgou-service-search-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

然後在changgou-service-search-api下編寫相應的Feign介面用來呼叫changgou-service-search:

@FeignClient(name="search")
@RequestMapping("/search")
public interface SkuEsFeign {

    /**
     * 搜尋
     * @param searchEntity
     * @return
     */
    @GetMapping
    Result<SearchEntity> searchByKeywords(@RequestBody(required = false) SearchEntity searchEntity);
}

然後在changgou-search-web下的resource目錄下將資料提供的靜態資源匯入進去。因為主要是做後端的功能,所以前端就不寫了,直接匯入:

最後將啟動類和配置檔案寫好:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.robod.feign")
public class SearchWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchWebApplication.class,args);
    }
}
server:
  port: 18086

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true

spring:
  thymeleaf:
    #構建URL時預先檢視名稱的字首,預設就是這個,寫在這裡是怕忘了怎麼配置
    prefix: classpath:/templates/
    suffix: .html   #字尾
    cache: false    #禁止快取

feign:
  hystrix:
    enabled: true
  application:
    name: search-web
  main:
    allow-bean-definition-overriding: true

# 不配置下面兩個的話可能會報timed-out and no fallback available異常
ribbon:
  ReadTimeout: 500000   # Feign請求讀取資料超時時間

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 50000   # feign連線超時時間

這樣我們的搜尋頁面微服務工程就搭建好了。然後在建立一個SkuSearchWebController類,然後建立一個searchByKeywords方法作為搜尋功能的入口

@GetMapping("/list")
public String searchByKeywords(SearchEntity searchEntity
        , Model model) {
    if (searchEntity == null || StringUtils.isEmpty(searchEntity.getKeywords())) {
        searchEntity = new SearchEntity("小米");
    }
    if (searchEntity.getSearchSpec() == null) {
        searchEntity.setSearchSpec(new HashMap<>(8));
    }
    SearchEntity result = skuFeign.searchByKeywords(searchEntity).getData();
    model.addAttribute("result", result);
    return "search";
}

這裡我指定了一個預設的關鍵詞,因為我發現如果searchEntity為null的話Feign就會報出timed-out and no fallback available,指定預設關鍵詞就可以解決這個問題,而且也符合邏輯,淘寶上如果不在搜尋欄填入任何內容就會搜尋預設的關鍵詞。

這個時候如果去訪問http://localhost:18086/search/list是沒有圖片和css樣式的,因為現在的seearch.html中指定的相對路徑,也就是去訪問search/img/下的圖片,其實是在img/下,所以我們還需要把相對路徑改為絕對路徑。把search中的href="./改為href="/,把src="./改為src="/,這樣訪問的就是img/下的圖片了。頁面就可以正常顯示了。

這樣的話搜尋頁面渲染微服務就搭建成功了。

資料填充

現在頁面所展示的資料並不是我們從ES中搜尋出來的真實資料,而是預先設定好的資料。所以現在我們需要把搜尋出來的資料填充到介面上。

頁面所展示的就是一堆的li標籤,我們所需要做的就是留一個li,然後使用Themeleaf標籤迴圈取出資料填入進去。

<div class="goods-list">
    <ul class="yui3-g">
        <li th:each="item:${result.rows}" class="yui3-u-1-5">
            <div class="list-wrap">
                <div class="p-img">
                    <a href="item.html" target="_blank"><img th:src="${item.getImage()}"/></a>
                </div>
                <div class="price">
                    <strong>
                        <em>¥</em>
                        <i th:text="${item.price}"></i>
                    </strong>
                </div>
                <div class="attr">
                    <!--th:utext可以識別標籤  strings.abbreviate控制長度-->
                    <a target="_blank" href="item.html" title="" 
                       th:utext="${#strings.abbreviate(item.name,150)}"></a>
                </div>
                <div class="commit">
                    <i class="command">已有<span>2000</span>人評價</i>
                </div>
                <div class="operate">
                    <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">
                        加入購物車</a>
                    <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
                </div>
            </div>
        </li>
    </ul>
</div>

頁面關鍵詞搜尋和回顯顯示

首先指定表單提交的路徑,然後指定name的值,將搜尋按鈕的type指定為“submit”就可以實現頁面關鍵詞搜尋;然後新增th:value="${result.keywords}"表示取出result.keywords的值,從而實現回顯顯示的功能。

搜尋條件回顯及條件過濾顯示

  • 分類和品牌

如果沒有指定分類和品牌資訊的話,後端會將分類和品牌進行統計然後傳到前端,當我們指定了分類和品牌之後就不用將分類和品牌進行分類統計了,這個在上一篇文章中說過,但是前端怎麼處理呢?使用th:each遍歷出資料顯示出來,當我們指定了分類或者品牌之後,頁面上就不去顯示分類或品牌選項。

th:unless 的意思是不滿足條件才輸出資料,所以判斷一下categotyList和brandList是不是空的,是空的就不輸出內容。不是空的就用th:each遍歷,然後用th:text輸出。

  • 規格

規格顯示和過濾和上面的類似。

searchSpec是傳到後端的規格Map<String,String>集合,sepcMap是後端傳到前端的規格Map<String,Set>集合。所以我們判斷sepcMap中是否包含searchSpec的key,包含則說明這個規格我們已經指定過了,就不去顯示,否則就遍歷顯示出來。

但是前端怎麼給後端的searchEntity.searchSpec賦值呢?我不知道,問了一下我哥,他說這樣寫:http://www.test.com/path?map[a]=1&map[b]=2,然後就報400錯誤了,控制檯顯示?

Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986

百度查了一下,這個是Tomcat的一個特性,按照RFC 3986規範進行解析,認為[ ] 是非法字元,所以攔截了。解決方法也很簡單,在啟動類中新增以下程式碼:

@Bean
public TomcatServletWebServerFactory webServerFactory() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
        @Override
        public void customize(Connector connector) {
            connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}");
            connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}");
        }
    });
    return factory;
}

這樣就可以完美地解決問題了,再來試一下?

OK!當我們指定了顏色這個規格的時候,就可以成功過濾顏色了,頁面也不會顯示顏色了。

搜尋條件點選

搜尋條件點選事件包括點選相應的搜尋條件的時候按照選擇的條件搜尋,並將條件顯示在介面上。當刪除已選擇的搜尋條件時,介面上把條件刪除掉,同時在後臺去掉該搜尋條件。

前端頁面的實現思路就是修改url,刪除條件就是刪掉url中對應的內容,然後重新傳送請求,增加搜尋條件就是在url中新增內容重新傳送請求。在SearchEntity中新增了一個url欄位用於存放url字串。

這是點選新增條件的程式碼,每次點選的時候就在原有的url基礎上把新增的條件再加上去,然後傳送請求。

點選 x 刪除條件就是在原有的url上刪除相應的條件,然後傳送請求。

但是這裡面有個問題,就是“6GB+128GB”中的加號傳到後端後會變成空格。我一開始想的是使用攔截器先攔截請求,把url中的空格換回加號後再傳到Controller中,但是貌似行不通。然後我就在Controller中遍歷searchEntity.SearchSpec,把裡面的空格換成加號,這樣確實可以實現。但是破壞了程式碼的美觀性。畢竟我是一個比較講究的人。然後我想到了使用AOP的方式,這樣在進入Controller之前預先對引數進行處理,程式碼就不會雜糅在一個方法裡。

@Aspect
@Component
public class SearchAspect {

    @Pointcut("execution(public * com.robod.controller.SkuSearchWebController.searchByKeywords(..)) " +
            "&& args(searchEntity,model,request))")
    public void searchAspect(SearchEntity searchEntity, Model model, HttpServletRequest request){
    }

    @Before(value = "searchAspect(searchEntity,model,request)",argNames = "searchEntity,model,request")
    public void doBeforeSearch(SearchEntity searchEntity,Model model, HttpServletRequest request)
        	throws Throwable {
        if (StringUtils.isEmpty(searchEntity.getKeywords())) {
            searchEntity.setKeywords("小米");
        }
        Map<String,String> specs = searchEntity.getSearchSpec();
        if (specs == null) {
            searchEntity.setSearchSpec(new HashMap<>(8));
        } else {
            for (String key:specs.keySet()){
                String value = specs.get(key).replace(" ","+");
                specs.put(key,value);
            }
        }
    }
}

這個AOP的程式碼,預先對SearchEntity進行一個處理。很符合邏輯,這樣進入到Controller中的引數就是格式正確的。

@GetMapping("/list")
public String searchByKeywords(SearchEntity searchEntity
        , Model model, HttpServletRequest request) {
    SearchEntity result = skuFeign.searchByKeywords(searchEntity).getData();
    result.setUrl(getUrl(request));
    model.addAttribute("result", result);
    return "search";
}

private String getUrl(HttpServletRequest request) {
    StringBuilder url = new StringBuilder("/search/list");
    Map<String, String[]> parameters = request.getParameterMap();
    if (parameters!=null&&parameters.size()>0){
        url.append("?");
        if (!parameters.containsKey("keywords")) {
            url.append("keywords=小米&");
        }
        for (String key:parameters.keySet()){
            url.append(key).append("=").append(parameters.get(key)[0]).append("&");
        }
        url.deleteCharAt(url.length()-1);
    }
    return url.toString().replace(" ","+");
}

Controller中的searchByKeywords方法果然變得很整潔。拼接URL就單獨抽取出來了,而且還考慮到了keywords沒有值的處理方式。很棒!

排序

當我們點選不同的排序規則的時候,就修改相應的排序規則,但是當我們點選分類條件的時候,之前的排序規格應該帶上。所以我們再準備一個sortUrl,我們把排序的規則新增到sortUrl中傳到後端,後端再把sortFIleld和sortRule新增到url中再返回到前端,返回到前端的sortUrl中是不帶sortFIleld和sortRule的。

<li>
    <a th:href="@{${result.sortUrl}(sortField=price,sortRule=ASC)}">價格升序</a>
</li>
<li>
    <a th:href="@{${result.sortUrl}(sortField=price,sortRule=DESC)}">價格降序</a>
</li>
private String[] getUrl(HttpServletRequest request) {
    StringBuilder sortUrl = new StringBuilder("/search/list");
	…………
        for (String key:parameters.keySet()){
            url.append(key).append("=").append(parameters.get(key)[0]).append("&");
            if (!("sortField".equalsIgnoreCase(key)||"sortRule".equalsIgnoreCase(key))){
                sortUrl.append(key).append("=").append(parameters.get(key)[0]).append("&");
            }
        }
        url.deleteCharAt(url.length()-1);
        sortUrl.deleteCharAt(sortUrl.length()-1);
    }
    return new String[]{url.toString().replace(" ","+"),
            sortUrl.toString().replace(" ","+")};
}

這樣就可以實現排序了。

分頁

分頁的功能後端我們已經實現過了,現在要做的就是在前端去實現分頁的顯示。所以我們需要一些基本的分頁資訊,總頁數當前頁等。這些資訊封裝在了Page類中,所以我們首先要將Page新增到SearchEntity中。在SkuEsServiceImpl的searchByKeywords方法中新增Page物件。

Page<SkuInfo> pageInfo = new Page<>(skuInfos.getTotalElements(),
        skuInfos.getPageable().getPageNumber()+1,
        skuInfos.getPageable().getPageSize());
searchEntity.setPageInfo(pageInfo);

第一個引數是總頁數,第二個引數是當前頁,getPageNumber()是從0開始的,所以需要+1,第三個引數是每頁顯示的條數。然後就是在前端頁面顯示了:

<ul>
    <li th:class="${result.pageInfo.currentpage}==1?'prev disabled':'prev'">
        <a th:href="@{${result.url}(searchPage=${result.pageInfo.upper})}">«上一頁</a>
    </li>
    <li th:each="i:${#numbers.sequence(result.pageInfo.lpage,result.pageInfo.rpage)}"
        th:class="${result.pageInfo.currentpage==i ? 'active' : ''}">
        <a th:href="@{${result.url}(searchPage=${i})}" th:text="${i}"></a>
    </li>
    <!--<li class="dotted"><span>...</span></li>-->
    <li th:class="${result.pageInfo.currentpage==result.pageInfo.last}?'next disabled':'next'">
        <a th:href="@{${result.url}(searchPage=${result.pageInfo.next})}">下一頁»</a>
    </li>
</ul>

顯示頁碼資訊從pageInfo中取。點選事件就是拼接url,將所需的searchPage拼接到url中。但是為了避免以下情況:

後端返回到前端的url資訊中不應該包含searPage,所以我們在getUrl()方法中拼接字串的時候把searchPage過濾掉。這樣分頁功能就大功告成啦!

商品詳情頁面

這個功能視訊上沒有,讓我們照著講義自己做,但是講義給的Vue程式碼是有問題的,

就是這一部分,sku和spec是沒有值的,但是我不會Vue,不知道怎麼從skuList中取值。然後我就把sku從後端拿到然後存到map中。然後這裡寫成

data: {
    return {
        skuList: [[${skuList}]],
        sku: [[${sku}]],
        spec: {}
    }
},

這樣確實可以取出sku的值。但是{{sku.name}}和{{sku.price}}。我們也不懂Vue,不知道咋回事,就直接用th:text取值了,沒用Vue的方式。

這裡面有個要注意的點,就是把src="./ href="./裡面的點刪掉,不然樣式載入不了。

Canal監聽生成靜態頁


這個是我畫的流程圖,程式碼就不貼了,想要的小夥伴去Github下載即可。

小結

這篇文章的內容有點多,先是介紹了Thymeleaf的基本使用。然後實現了搜尋頁面以及商品詳情頁面。最後使用Canal來監聽資料庫變化,從而修改生成新的靜態頁以及修改Es資料。

如果我的文章對你有些幫助,不要忘了點贊收藏轉發關注。要是有什麼好的意見歡迎在下方留言。讓我們下期再見!

微信公眾號

相關文章