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


好好學習,天天向上

本文已收錄至我的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數據。

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

微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM