13.商品詳情及靜態化

0.學習目標

  • 瞭解Thymeleaf的基本使用
  • 實現商品詳情頁的渲染
  • 知道頁面靜態化的作用
  • 實現頁面靜態化功能

1.商品詳情

當用戶搜索到商品,肯定會點擊查看,就會進入商品詳情頁,接下來我們完成商品詳情頁的展示,

1.1.Thymeleaf

在商品詳情頁中,我們會使用到Thymeleaf來渲染頁面,所以需要先了解Thymeleaf的語法。

詳見課前資料中《Thymeleaf語法入門.md》

1.2.商品詳情頁服務

商品詳情瀏覽量比較大,併發高,我們會獨立開啓一個微服務,用來展示商品詳情。

1.2.1.創建module

商品的詳情頁服務,命名爲:leyou-goods-web
在這裏插入圖片描述

目錄:
在這裏插入圖片描述

1.2.2.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.goods</groupId>
    <artifactId>leyou-goods-web</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.item</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

1.2.3.編寫啓動類

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouGoodsWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouGoodsWebApplication.class, args);
    }
}

1.2.4.application.yml文件

server:
  port: 8084
spring:
  application:
    name: goods-page
  thymeleaf:
    cache: false
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}

1.2.5.頁面模板

我們從leyou-portal中複製item.html模板到當前項目resource目錄下的templates中:
在這裏插入圖片描述

1.3.頁面跳轉

1.3.1.修改頁面跳轉路徑

首先我們需要修改搜索結果頁的商品地址,目前所有商品的地址都是:http://www.leyou.com/item.html
在這裏插入圖片描述

我們應該跳轉到對應的商品的詳情頁纔對。

那麼問題來了:商品詳情頁是一個SKU?還是多個SKU的集合?
在這裏插入圖片描述

通過詳情頁的預覽,我們知道它是多個SKU的集合,即SPU。

所以,頁面跳轉時,我們應該攜帶SPU的id信息。

例如:http://www.leyou.com/item/2314123.html

這裏就採用了路徑佔位符的方式來傳遞spu的id,我們打開search.html,修改其中的商品路徑:
在這裏插入圖片描述

刷新頁面後再看:
在這裏插入圖片描述

1.3.2.nginx反向代理

接下來,我們要把這個地址指向我們剛剛創建的服務:leyou-goods-web,其端口爲8084

我們在nginx.conf中添加一段邏輯:
在這裏插入圖片描述

把以/item開頭的請求,代理到我們的8084端口。

1.3.3.編寫跳轉controller

leyou-goods-web中編寫controller,接收請求,並跳轉到商品詳情頁:

@Controller
@RequestMapping("item")
public class GoodsController {

    /**
     * 跳轉到商品詳情頁
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){

        return "item";
    }
}

1.3.4.測試

啓動leyou-goods-page,點擊搜索頁面商品,看是能夠正常跳轉:
在這裏插入圖片描述

現在看到的依然是靜態的數據。我們接下來開始頁面的渲染

1.4.封裝模型數據

首先我們一起來分析一下,在這個頁面中需要哪些數據

我們已知的條件是傳遞來的spu的id,我們需要根據spu的id查詢到下面的數據:

  • spu信息
  • spu的詳情
  • spu下的所有sku
  • 品牌
  • 商品三級分類
  • 商品規格參數、規格參數組

1.4.1.商品微服務提供接口

1.4.1.1.查詢spu

以上所需數據中,查詢spu的接口目前還沒有,我們需要在商品微服務中提供這個接口:

GoodsApi

/**
 * 根據spu的id查詢spu
 * @param id
 * @return
 */
@GetMapping("spu/{id}")
public Spu querySpuById(@PathVariable("id") Long id);

GoodsController

@GetMapping("spu/{id}")
public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){
    Spu spu = this.goodsService.querySpuById(id);
    if(spu == null){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(spu);
}

GoodsService

public Spu querySpuById(Long id) {
    return this.spuMapper.selectByPrimaryKey(id);
}

1.4.1.2.查詢規格參數組

我們在頁面展示規格時,需要按組展示:
在這裏插入圖片描述

組內有多個參數,爲了方便展示。我們提供一個接口,查詢規格組,同時在規格組中持有組內的所有參數。

拓展SpecGroup類:

我們在SpecGroup中添加一個SpecParam的集合,保存改組下所有規格參數

@Table(name = "tb_spec_group")
public class SpecGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long cid;

    private String name;

    @Transient
    private List<SpecParam> params; // 該組下的所有規格參數集合
}

然後提供查詢接口:

SpecificationAPI:

@RequestMapping("spec")
public interface SpecificationApi {
    @GetMapping("groups/{cid}")
    public ResponseEntity<List<SpecGroup>> querySpecGroups(@PathVariable("cid") Long cid);

    @GetMapping("/params")
    public List<SpecParam> querySpecParam(
            @RequestParam(value = "gid", required = false) Long gid,
            @RequestParam(value = "cid", required = false) Long cid,
            @RequestParam(value = "searching", required = false) Boolean searching,
            @RequestParam(value = "generic", required = false) Boolean generic);

    // 查詢規格參數組,及組內參數
    @GetMapping("{cid}")
    List<SpecGroup> querySpecsByCid(@PathVariable("cid") Long cid);

}

SpecificationController

@GetMapping("{cid}")
public ResponseEntity<List<SpecGroup>> querySpecsByCid(@PathVariable("cid") Long cid){
    List<SpecGroup> list = this.specificationService.querySpecsByCid(cid);
    if(list == null || list.size() == 0){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}

SpecificationService

public List<SpecGroup> querySpecsByCid(Long cid) {
    // 查詢規格組
    List<SpecGroup> groups = this.querySpecGroups(cid);
    SpecParam param = new SpecParam();
    groups.forEach(g -> {
        // 查詢組內參數
        g.setParams(this.querySpecParams(g.getId(), null, null, null));
    });
    return groups;
}

在service中,我們調用之前編寫過的方法,查詢規格組,和規格參數,然後封裝返回。

1.4.2.創建FeignClient

我們在leyou-goods-web服務中,創建FeignClient:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N0r50wbz-1572786362886)(assets/1529916126099.png)]

BrandClient:

@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}

CategoryClient

@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}

GoodsClient:

@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}

SpecificationClient:

@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi{
}

1.4.3.封裝數據模型

我們創建一個GoodsService,在裏面來封裝數據模型。

這裏要查詢的數據:

  • SPU

  • SpuDetail

  • SKU集合

  • 商品分類

    • 這裏值需要分類的id和name就夠了,因此我們查詢到以後自己需要封裝數據
  • 品牌

  • 規格組

    • 查詢規格組的時候,把規格組下所有的參數也一併查出,上面提供的接口中已經實現該功能,我們直接調
  • sku的特有規格參數

    有了規格組,爲什麼這裏還要查詢?

    因爲在SpuDetail中的SpecialSpec中,是以id作爲規格參數id作爲key,如圖:
    在這裏插入圖片描述

    但是,在頁面渲染時,需要知道參數的名稱,如圖:

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KYgGYJ5P-1572786362888)(assets/1529922667759.png)]

    我們就需要把id和name一一對應起來,因此需要額外查詢sku的特有規格參數,然後變成一個id:name的鍵值對格式。也就是一個Map,方便將來根據id查找!

Service代碼

@Service
public class GoodsService {

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private SpecificationClient specificationClient;

    private static final Logger logger = LoggerFactory.getLogger(GoodsService.class);

    public Map<String, Object> loadModel(Long spuId){

        try {
            // 查詢spu
            Spu spu = this.goodsClient.querySpuById(spuId);

            // 查詢spu詳情
            SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spuId);

            // 查詢sku
            List<Sku> skus = this.goodsClient.querySkuBySpuId(spuId);

            // 查詢品牌
            List<Brand> brands = this.brandClient.queryBrandByIds(Arrays.asList(spu.getBrandId()));

            // 查詢分類
            List<Category> categories = getCategories(spu);

            // 查詢組內參數
            List<SpecGroup> specGroups = this.specificationClient.querySpecsByCid(spu.getCid3());

            // 查詢所有特有規格參數
            List<SpecParam> specParams = this.specificationClient.querySpecParam(null, spu.getCid3(), null, false);
            // 處理規格參數
            Map<Long, String> paramMap = new HashMap<>();
            specParams.forEach(param->{
                paramMap.put(param.getId(), param.getName());
            });

            Map<String, Object> map = new HashMap<>();
            map.put("spu", spu);
            map.put("spuDetail", spuDetail);
            map.put("skus", skus);
            map.put("brand", brands.get(0));
            map.put("categories", categories);
            map.put("groups", specGroups);
            map.put("params", paramMap);
            return map;
        } catch (Exception e) {
            logger.error("加載商品數據出錯,spuId:{}", spuId, e);
        }
        return null;
    }

    private List<Category> getCategories(Spu spu) {
        try {
            List<String> names = this.categoryClient.queryNameByIds(
                    Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
            Category c1 = new Category();
            c1.setName(names.get(0));
            c1.setId(spu.getCid1());

            Category c2 = new Category();
            c2.setName(names.get(1));
            c2.setId(spu.getCid2());

            Category c3 = new Category();
            c3.setName(names.get(2));
            c3.setId(spu.getCid3());

            return Arrays.asList(c1, c2, c3);
        } catch (Exception e) {
            logger.error("查詢商品分類出錯,spuId:{}", spu.getId(), e);
        }
        return null;
    }
}

然後在controller中把數據放入model:

@Controller
@RequestMapping("item")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;
    /**
     * 跳轉到商品詳情頁
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){
        // 加載所需的數據
        Map<String, Object> modelMap = this.goodsService.loadModel(id);
        // 放入模型
        model.addAllAttributes(modelMap);
        return "item";
    }
}

1.4.4.頁面測試數據

我們在頁面中先寫一段JS,把模型中的數據取出觀察,看是否成功:

<script th:inline="javascript">
    const a = /*[[${groups}]]*/ [];
    const b = /*[[${params}]]*/ [];
    const c = /*[[${categories}]]*/ [];
    const d = /*[[${spu}]]*/ {};
    const e = /*[[${spuDetail}]]*/ {};
    const f = /*[[${skus}]]*/ [];
    const g = /*[[${brand}]]*/ {};
</script>

然後查看頁面源碼:
在這裏插入圖片描述

數據都成功查到了!

1.5.渲染麪包屑

在商品展示頁的頂部,有一個商品分類、品牌、標題的麪包屑

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QwkxL5Vm-1572786362888)(assets/1526978423084.png)]

其數據有3部分:

  • 商品分類
  • 商品品牌
  • spu標題

我們的模型中都有,所以直接渲染即可(頁面101行開始):

<div class="crumb-wrap">
    <ul class="sui-breadcrumb">
        <li th:each="category : ${categories}">
            <a href="#" th:text="${category.name}">手機</a>
        </li>
        <li>
            <a href="#" th:text="${brand.name}">Apple</a>
        </li>
        <li class="active" th:text="${spu.title}">Apple iPhone 6s</li>
    </ul>
</div>

1.6.渲染商品列表

先看下整體效果:
在這裏插入圖片描述

這個部分需要渲染的數據有5塊:

  • sku圖片
  • sku標題
  • 副標題
  • sku價格
  • 特有規格屬性列表

其中,sku 的圖片、標題、價格,都必須在用戶選中一個具體sku後,才能渲染。而特有規格屬性列表可以在spuDetail中查詢到。而副標題則是在spu中,直接可以在頁面渲染

因此,我們先對特有規格屬性列表進行渲染。等用戶選擇一個sku,再通過js對其它sku屬性渲染

1.6.1.副標題

副標題是在spu中,所以我們直接通過Thymeleaf渲染:

在第146行左右:

<div class="news"><span th:utext="${spu.subTitle}"></span></div>

副標題中可能會有超鏈接,因此這裏也用th:utext來展示,效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-x7aC48f2-1572786362889)(assets/1526980061592.png)]

1.6.2.渲染規格屬性列表

規格屬性列表將來會有事件和動態效果。我們需要有js代碼參與,不能使用Thymeleaf來渲染了。

因此,這裏我們用vue,不過需要先把數據放到js對象中,方便vue使用

初始化數據

我們在頁面的head中,定義一個js標籤,然後在裏面定義變量,保存與sku相關的一些數據:

<script th:inline="javascript">
    // sku集合
    const skus = /*[[${skus}]]*/ [];
    // 規格參數id與name對
	const paramMap = /*[[${params}]]*/ {};
    // 特有規格參數集合
    const specialSpec = JSON.parse(/*[[${spuDetail.specialSpec}]]*/ "");
</script>
  • specialSpec:這是SpuDetail中唯一與Sku相關的數據

    因此我們並沒有保存整個spuDetail,而是隻保留了這個屬性,而且需要手動轉爲js對象。

  • paramMap:規格參數的id和name對,方便頁面根據id獲取參數名

  • sku:特有規格參數集合

我們來看下頁面獲取的數據:
在這裏插入圖片描述

通過Vue渲染

我們把剛纔獲得的幾個變量保存在Vue實例中:
在這裏插入圖片描述

然後在頁面中渲染:

<div id="specification" class="summary-wrap clearfix">
    <dl v-for="(v,k) in specialSpec" :key="k">
        <dt>
            <div class="fl title">
                <i>{{paramMap[k]}}</i>
            </div>
        </dt>
        <dd v-for="(str,j) in v" :key="j">
            <a href="javascript:;" class="selected">
                {{str}}<span title="點擊取消選擇">&nbsp;</span>
            </a>
        </dd>
    </dl>
</div>

然後刷新頁面查看:
在這裏插入圖片描述

數據成功渲染了。不過我們發現所有的規格都被勾選了。這是因爲現在,每一個規格都有樣式:selected,我們應該只選中一個,讓它的class樣式爲selected纔對!

那麼問題來了,我們該如何確定用戶選擇了哪一個?

1.6.3.規格屬性的篩選

分析

規格參數的格式是這樣的:
在這裏插入圖片描述

每一個規格項是數組中的一個元素,因此我們只要保存被選擇的規格項的索引,就能判斷哪個是用戶選擇的了!

我們需要一個對象來保存用戶選擇的索引,格式如下:

{
    "4":0,
    "12":0,
    "13":0
}

但問題是,第一次進入頁面時,用戶並未選擇任何參數。因此索引應該有一個默認值,我們將默認值設置爲0。

我們在head的script標籤中,對索引對象進行初始化:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZBm2cWzK-1572786362891)(H:assets/1529923658242.png)]

然後在vue中保存:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3PWJCicj-1572786362892)(assets/1529923701283.png)]

頁面改造

我們在頁面中,通過判斷indexes的值來判斷當前規格是否被選中,並且給規格綁定點擊事件,點擊規格項後,修改indexes中的對應值:

<div id="specification" class="summary-wrap clearfix">
    <dl v-for="(v,k) in specialSpec" :key="k">
        <dt>
            <div class="fl title">
                <i>{{paramMap[k]}}</i>
            </div>
        </dt>
        <dd v-for="(str,j) in v" :key="j">
            <a href="javascript:;" :class="{selected: j===indexes[k]}" @click="indexes[k]=j">
                {{str}}<span v-if="j===indexes[k]" title="點擊取消選擇">&nbsp;</span>
            </a>
        </dd>
    </dl>
</div>

效果:
在這裏插入圖片描述

1.6.4.確定SKU

在我們設計sku數據的時候,就已經添加了一個字段:indexes:
在這裏插入圖片描述

這其實就是規格參數的索引組合。

而我們在頁面中,用戶點擊選擇規格後,就會把對應的索引保存起來:
在這裏插入圖片描述

因此,我們可以根據這個indexes來確定用戶要選擇的sku

我們在vue中定義一個計算屬性,來計算與索引匹配的sku:

computed:{
    sku(){
        const index = Object.values(this.indexes).join("_");
        return this.skus.find(s => s.indexes = index);
    }
}

在瀏覽器工具中查看:
在這裏插入圖片描述

1.6.5.渲染sku列表

既然已經拿到了用戶選中的sku,接下來,就可以在頁面渲染數據了

圖片列表

商品圖片是一個字符串,以,分割,頁面展示比較麻煩,所以我們編寫一個計算屬性:images(),將圖片字符串變成數組:

computed: {
    sku(){
        const index = Object.values(this.indexes).join("_");
        return this.skus.find(s=>s.indexes==index);
    },
    images(){
        return this.sku.images ? this.sku.images.split(",") : [''];
    }
},

頁面改造:

<div class="zoom">
   <!--默認第一個預覽-->
   <div id="preview" class="spec-preview">
      <span class="jqzoom">
         <img :jqimg="images[0]" :src="images[0]" width="400px" height="400px"/>
      </span>
   </div>
   <!--下方的縮略圖-->
   <div class="spec-scroll">
      <a class="prev">&lt;</a>
      <!--左右按鈕-->
      <div class="items">
         <ul>
            <li v-for="(image, i) in images" :key="i">
               <img :src="image" :bimg="image" onmousemove="preview(this)" />
            </li>
         </ul>
      </div>
      <a class="next">&gt;</a>
   </div>
</div>

效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-P18l7l4d-1572786362895)(assets/1526985783938.png)]

標題和價格

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Qou3AqpQ-1572786362895)(assets/1526985959427.png)]

完整效果

在這裏插入圖片描述

1.7.商品詳情

商品詳情頁面如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Lqq7LIl3-1572786362896)(assets/1526988361312.png)]

分成上下兩部分:

  • 上部:展示的是規格屬性列表
  • 下部:展示的是商品詳情

1.7.1.屬性列表(作業)

這部分內容與規格參數部分重複,我就不帶大家做了,大家可以自己完成

1.7.2.商品詳情

商品詳情是HTML代碼,我們不能使用 th:text,應該使用th:utext

在頁面的第444行左右:

<!--商品詳情-->
<div class="intro-detail" th:utext="${spuDetail.description}">
</div>

最終展示效果:
在這裏插入圖片描述

1.8.規格包裝:

規格包裝分成兩部分:

  • 規格參數
  • 包裝列表

而且規格參數需要按照組來顯示

1.8.1.規格參數

最終的效果:
在這裏插入圖片描述

我們模型中有一個groups,跟這個數據結果很像:
在這裏插入圖片描述

分成8個組,組內都有params,裏面是所有的參數。不過,這些參數都沒有值!

規格參數的值分爲兩部分:

  • 通用規格參數:保存在SpuDetail中的genericSpec中
  • 特有規格參數:保存在sku的ownSpec中

我們需要把這兩部分值取出來,放到groups中。

從spuDetail中取出genericSpec並取出groups:
在這裏插入圖片描述

把genericSpec引入到Vue實例:
在這裏插入圖片描述

因爲sku是動態的,所以我們編寫一個計算屬性,來進行值的組合:

groups(){
    groups.forEach(group => {
        group.params.forEach(param => {
            if(param.generic){
                // 通用屬性,去spu的genericSpec中獲取
                param.v = this.genericSpec[param.id] || '其它';
            }else{
                // 特有屬性值,去SKU中獲取
                param.v = JSON.parse(this.sku.ownSpec)[param.id]
            }
        })
    })
    return groups;
}

然後在頁面渲染:

<div class="Ptable">
    <div class="Ptable-item" v-for="group in groups" :key="group.name">
        <h3>{{group.name}}</h3>
        <dl>
            <div v-for="p in group.params">
                <dt>{{p.name}}</dt><dd>{{p.v + (p.unit || '')}}</dd>
            </div>
        </dl>
    </div>
</div>

1.8.2.包裝列表

包裝列表在商品詳情中,我們一開始並沒有賦值到Vue實例中,但是可以通過Thymeleaf來渲染

<div class="package-list">
    <h3>包裝清單</h3>
    <p th:text="${spuDetail.packingList}"></p>
</div>

最終效果:
在這裏插入圖片描述
在這裏插入圖片描述

1.9.售後服務

售後服務也可以通過Thymeleaf進行渲染:

<div id="three" class="tab-pane">
    <p>售後保障</p>
    <p th:text="${spuDetail.afterService}"></p>
</div>

效果:
在這裏插入圖片描述

2.頁面靜態化

2.1.簡介

2.1.1.問題分析

現在,我們的頁面是通過Thymeleaf模板引擎渲染後返回到客戶端。在後臺需要大量的數據查詢,而後渲染得到HTML頁面。會對數據庫造成壓力,並且請求的響應時間過長,併發能力不高。

大家能想到什麼辦法來解決這個問題?

首先我們能想到的就是緩存技術,比如之前學習過的Redis。不過Redis適合數據規模比較小的情況。假如數據量比較大,例如我們的商品詳情頁。每個頁面如果10kb,100萬商品,就是10GB空間,對內存佔用比較大。此時就給緩存系統帶來極大壓力,如果緩存崩潰,接下來倒黴的就是數據庫了。

所以緩存並不是萬能的,某些場景需要其它技術來解決,比如靜態化。

2.1.2.什麼是靜態化

靜態化是指把動態生成的HTML頁面變爲靜態內容保存,以後用戶的請求到來,直接訪問靜態頁面,不再經過服務的渲染。

而靜態的HTML頁面可以部署在nginx中,從而大大提高併發能力,減小tomcat壓力。

2.1.3.如何實現靜態化

目前,靜態化頁面都是通過模板引擎來生成,而後保存到nginx服務器來部署。常用的模板引擎比如:

  • Freemarker
  • Velocity
  • Thymeleaf

我們之前就使用的Thymeleaf,來渲染html返回給用戶。Thymeleaf除了可以把渲染結果寫入Response,也可以寫到本地文件,從而實現靜態化。

2.2.Thymeleaf實現靜態化

2.2.1.概念

先說下Thymeleaf中的幾個概念:

  • Context:運行上下文
  • TemplateResolver:模板解析器
  • TemplateEngine:模板引擎

Context

上下文: 用來保存模型數據,當模板引擎渲染時,可以從Context上下文中獲取數據用於渲染。

當與SpringBoot結合使用時,我們放入Model的數據就會被處理到Context,作爲模板渲染的數據使用。

TemplateResolver

模板解析器:用來讀取模板相關的配置,例如:模板存放的位置信息,模板文件名稱,模板文件的類型等等。

當與SpringBoot結合時,TemplateResolver已經由其創建完成,並且各種配置也都有默認值,比如模板存放位置,其默認值就是:templates。比如模板文件類型,其默認值就是html。

TemplateEngine

模板引擎:用來解析模板的引擎,需要使用到上下文、模板解析器。分別從兩者中獲取模板中需要的數據,模板文件。然後利用內置的語法規則解析,從而輸出解析後的文件。來看下模板引擎進行處理的函數:

templateEngine.process("模板名", context, writer);

三個參數:

  • 模板名稱
  • 上下文:裏面包含模型數據
  • writer:輸出目的地的流

在輸出時,我們可以指定輸出的目的地,如果目的地是Response的流,那就是網絡響應。如果目的地是本地文件,那就實現靜態化了。

而在SpringBoot中已經自動配置了模板引擎,因此我們不需要關心這個。現在我們做靜態化,就是把輸出的目的地改成本地文件即可!

2.2.2.具體實現

在這裏插入圖片描述

Service代碼:

@Service
public class GoodsHtmlService {

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private TemplateEngine templateEngine;

    private static final Logger LOGGER = LoggerFactory.getLogger(GoodsHtmlService.class);

    /**
     * 創建html頁面
     *
     * @param spuId
     * @throws Exception
     */
    public void createHtml(Long spuId) {

        PrintWriter writer = null;
        try {
            // 獲取頁面數據
            Map<String, Object> spuMap = this.goodsService.loadModel(spuId);

            // 創建thymeleaf上下文對象
            Context context = new Context();
            // 把數據放入上下文對象
            context.setVariables(spuMap);

            // 創建輸出流
            File file = new File("C:\\project\\nginx-1.14.0\\html\\item\\" + spuId + ".html");
            writer = new PrintWriter(file);

            // 執行頁面靜態化方法
            templateEngine.process("item", context, writer);
        } catch (Exception e) {
            LOGGER.error("頁面靜態化出錯:{},"+ e, spuId);
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    /**
     * 新建線程處理頁面靜態化
     * @param spuId
     */
    public void asyncExcute(Long spuId) {
        ThreadUtils.execute(()->createHtml(spuId));
        /*ThreadUtils.execute(new Runnable() {
            @Override
            public void run() {
                createHtml(spuId);
            }
        });*/
    }
}

線程工具類:

public class ThreadUtils {

    private static final ExecutorService es = Executors.newFixedThreadPool(10);

    public static void execute(Runnable runnable) {
        es.submit(runnable);
    }
}

2.2.3.什麼時候創建靜態文件

我們編寫好了創建靜態文件的service,那麼問題來了:什麼時候去調用它呢

想想這樣的場景:

假如大部分的商品都有了靜態頁面。那麼用戶的請求都會被nginx攔截下來,根本不會到達我們的leyou-goods-web服務。只有那些還沒有頁面的請求,纔可能會到達這裏。

因此,如果請求到達了這裏,我們除了返回頁面視圖外,還應該創建一個靜態頁面,那麼下次就不會再來麻煩我們了。

所以,我們在GoodsController中添加邏輯,去生成靜態html文件:

@GetMapping("{id}.html")
public String toItemPage(@PathVariable("id")Long id, Model model){

    // 加載所需的數據
    Map<String, Object> map = this.goodsService.loadModel(id);
    // 把數據放入數據模型
    model.addAllAttributes(map);

    // 頁面靜態化
    this.goodsHtmlService.asyncExcute(id);

    return "item";
}

注意:生成html 的代碼不能對用戶請求產生影響,所以這裏我們使用額外的線程進行異步創建。

2.2.4.重啓測試:

訪問一個商品詳情,然後查看nginx目錄:
在這裏插入圖片描述

2.3.nginx代理靜態頁面

接下來,我們修改nginx,讓它對商品請求進行監聽,指向本地靜態頁面,如果本地沒找到,才進行反向代理:

server {
    listen       80;
    server_name  www.leyou.com;

    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location /item {
        # 先找本地
        root html;
        if (!-f $request_filename) { #請求的文件不存在,就反向代理
            proxy_pass http://127.0.0.1:8084;
            break;
        }
    }

    location / {
        proxy_pass http://127.0.0.1:9002;
        proxy_connect_timeout 600;
        proxy_read_timeout 600;
    }
}

重啓測試:

發現請求速度得到了極大提升:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章