微服務架構(9):Thymeleaf頁面靜態化
學習目標
- 瞭解Thymeleaf的基本使用
- 實現商品詳情頁的渲染
- 知道頁面靜態化的作用
- 實現頁面靜態化功能
1.商品詳情
當用戶搜索到商品,肯定會點擊查看,就會進入商品詳情頁,接下來我們完成商品詳情頁的展示,
1.1.Thymeleaf
在商品詳情頁中,我們會使用到Thymeleaf來渲染頁面,所以需要先了解Thymeleaf的語法。
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,接收請求,並跳轉到商品詳情頁:
@RestController
@RequestMapping("item")
public class PageController {
/**
* 跳轉到商品詳情頁
* @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:
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,如圖:
但是,在頁面渲染時,需要知道參數的名稱,如圖:
我們就需要把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.渲染麪包屑
在商品展示頁的頂部,有一個商品分類、品牌、標題的麪包屑
其數據有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
來展示,效果:
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="點擊取消選擇"> </span>
</a>
</dd>
</dl>
</div>
然後刷新頁面查看:
數據成功渲染了。不過我們發現所有的規格都被勾選了。這是因爲現在,每一個規格都有樣式:selected
,我們應該只選中一個,讓它的class樣式爲selected纔對!
那麼問題來了,我們該如何確定用戶選擇了哪一個?
1.6.3.規格屬性的篩選
分析
規格參數的格式是這樣的:
每一個規格項是數組中的一個元素,因此我們只要保存被選擇的規格項的索引,就能判斷哪個是用戶選擇的了!
我們需要一個對象來保存用戶選擇的索引,格式如下:
{
"4":0,
"12":0,
"13":0
}
但問題是,第一次進入頁面時,用戶並未選擇任何參數。因此索引應該有一個默認值,我們將默認值設置爲0。
我們在head
的script標籤中,對索引對象進行初始化:
然後在vue中保存:
頁面改造
我們在頁面中,通過判斷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="點擊取消選擇"> </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"><</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">></a>
</div>
</div>
1.7.商品詳情
商品詳情頁面如下圖所示:
分成上下兩部分:
- 上部:展示的是規格屬性列表
- 下部:展示的是商品詳情
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,也可以不寫到response裏,而寫到本地文件,從而實現靜態化。
2.2.Thymeleaf實現靜態化
2.2.1.概念
先說下Thymeleaf中的幾個概念:
- Context:運行上下文
- TemplateResolver:模板解析器
- TemplateEngine:模板引擎
Context
上下文: 用來保存數據,當模板引擎渲染時,可以從Context上下文中獲取數據用於渲染。
當與SpringBoot結合使用時,我們放入Model的數據就會被處理到Context,作爲模板渲染的數據使用。
TemplateResolver
模板解析器:用來讀取模板相關的配置,例如:模板存放的位置信息,模板文件名稱,模板文件的類型等等。
當與SpringBoot結合時,TemplateResolver已經由其創建完成,並且各種配置也都有默認值,比如模板存放位置,其默認值就是:templates。比如模板文件類型,其默認值就是html。
TemplateEngine
模板引擎:用來解析模板的引擎,**需要使用到上下文、模板解析器。分別從兩者中獲取模板中需要的數據,模板文件。**然後利用內置的語法規則解析,從而輸出解析後的文件。來看下模板引擎進行處理的函數:
// 執行頁面靜態化方法
templateEngine.process("item", 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;
}
}
重啓測試:
發現請求速度得到了極大提升: