乐优商城(09)--商品详情
樂優商城(09)–商品詳情
一、商品詳情
1.1、商品詳情頁服務
商品詳情瀏覽量比較大,并發高,所以獨立開發一個微服務,用來展示商品詳情
創建module
商品的詳情頁服務,命名為:leyou-goods-web
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>myLeyou</artifactId><groupId>com.leyou.parent</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.leyou.goods</groupId><artifactId>leyou-goods-web</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>0.0.1-SNAPSHOT</version></dependency></dependencies></project>啟動類
@SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class LeyouGoodsWebApplication {public static void main(String[] args) {SpringApplication.run(LeyouGoodsWebApplication.class,args);} }application.yaml
server:port: 8084 spring:application:name: goods-webthymeleaf:cache: false #關閉緩存,利于開發cloud:nacos:discovery:server-addr: ip地址:8848username: nacospassword: nacos頁面模板
從leyou-portal中復制item.html模板到當前項目resource目錄下的templates中:
1.2、頁面跳轉
1.2.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.2.2、nginx反向代理
接下來,要把這個地址指向剛剛創建的服務:leyou-goods-web,其端口為8084
需要在nginx.conf中添加一段配置:
把以/item開頭的請求,代理到機器的8084端口。
1.2.3、編寫跳轉controller
在leyou-goods-web中編寫controller,接收請求,并跳轉到商品詳情頁:
@RestController @RequestMapping("/item") public class GoodsController {/*** 跳轉到商品詳情頁* @param id* @return*/@GetMapping("/{id}.html")public String toItemPage(@PathVariable("id")Long id){return "item";} }測試
啟動leyou-goods-page,點擊搜索頁面商品,看是能夠正常跳轉.
現在看到的依然是靜態的數據。接下來開始頁面的渲染
1.3、封裝模型數據
首先分析一下,在這個頁面中需要哪些數據
已知的條件是傳遞來的spu的id,然后需要根據spu的id查詢到下面的數據:
- spu信息
- spu的詳情
- spu下的所有sku
- 品牌
- 商品三級分類
- 商品規格參數、規格參數組
1.3.1、商品微服務提供接口
以上所需數據中,根據id查詢spuspu的接口目前還沒有,需要在商品微服務中提供這個接口:
GoodsController
/*** 根據spu的id查詢spu對象* @param id* @return*/ @GetMapping("/spu/{id}") public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){Spu spu = this.goodsService.querySpuById(id);if(null == spu){return ResponseEntity.notFound().build();}return ResponseEntity.ok(spu); }GoodsService
/*** 根據spu的id查詢spu對象* @param id* @return*/ Spu querySpuById(Long id);實現類:
/*** 根據spu的id查詢spu對象** @param id* @return*/ @Override public Spu querySpuById(Long id) {return this.spuMapper.selectByPrimaryKey(id); }GoodsApi
/*** 根據spu的id查詢spu* @param id* @return*/ @GetMapping("spu/{id}") Spu querySpuById(@PathVariable("id") Long id);1.3.2、查詢規格參數組
在頁面展示規格時,需要按組展示:
組內有多個參數,為了方便展示。在leyou-item-service中提供一個接口,查詢規格組,同時在規格組內的所有參數。
SpecificationController
/*** 查詢規格參數組,及組內參數* @param cid* @return*/ @GetMapping("/groups/param/{cid}") public ResponseEntity<List<SpecGroup>> queryGroupsParamsByCid(@PathVariable("cid") Long cid){List<SpecGroup> groups = this.specificationService.queryGroupsParamsByCid(cid);if (CollectionUtils.isEmpty(groups)){return ResponseEntity.notFound().build();}return ResponseEntity.ok(groups); }SpecificationService
/*** 查詢規格參數組,及組內參數* @param cid* @return*/ List<SpecGroup> queryGroupsParamsByCid(Long cid);實現類:
/*** 查詢規格參數組,及組內參數** @param cid* @return*/ @Override public List<SpecGroup> queryGroupsParamsByCid(Long cid) {//查詢分組List<SpecGroup> groups = this.queryGroupsByCid(cid);groups.forEach(group ->{//查詢具體規格參數group.setParams(this.queryParams(group.getId(),null,null,null));});return groups; }SpecificationApi
/*** 查詢規格參數組,及組內參數* @param cid* @return*/ @GetMapping("/groups/param/{cid}") List<SpecGroup> queryGroupsParamsByCid(@PathVariable("cid")Long cid);1.4、創建FeignClient
在leyou-goods-web服務中,創建FeignClient,直接將leyou-search的client的類復制過來
記得添加允許創建相同bean的配置:
spring:main:allow-bean-definition-overriding: true1.5、封裝數據模型
創建一個GoodsService,在里面來封裝數據模型。
這里要查詢的數據:
-
SPU
-
SpuDetail
-
SKU集合
-
商品分類
- 這里值需要分類的id和name就夠了,因此查詢到以后需要自己封裝數據
-
品牌對象
-
規格組
- 查詢規格組的時候,把規格組下所有的參數也一并查出,上面提供的接口中已經實現該功能,直接調用
-
sku的特有規格參數
有了規格組,為什么這里還要查詢?
因為在SpuDetail中的SpecialSpec中,是以id作為規格參數id作為key,
但是,在頁面渲染時,需要知道參數的名稱,如圖:
需要把id和name一一對應起來,因此需要額外查詢sku的特有規格參數,然后變成一個id:name的鍵值對格式。也就是一個Map,方便將來根據id查找!
GoodsService
public interface GoodsService {/*** 封裝商品詳情時所需的數據* @param spuId* @return*/Map<String, Object> loadData(Long spuId); }實現類:
@Service public class GoodsServiceImpl implements GoodsService {@Autowiredprivate BrandClient brandClient;@Autowiredprivate CategoryClient categoryClient;@Autowiredprivate GoodsClient goodsClient;@Autowiredprivate SpecificationClient specificationClient;/*** 封裝商品詳情時所需的數據** @param spuId* @return*/@Overridepublic Map<String, Object> loadData(Long spuId) {//查詢spuSpu spu = this.goodsClient.querySpuById(spuId);//查詢spuDetailSpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spuId);//查詢skusList<Sku> skus = this.goodsClient.querySkusBySpuId(spuId);//查詢分類信息//先獲取到spu的三級分類idList<Long> ids = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3());//根據分類id查詢分類名稱List<String> names = this.categoryClient.queryNamesByIds(ids);//查詢具體的分類信息List<Map<String,Object>> categories = new ArrayList<>();for (int i = 0; i < ids.size(); i++) {Map<String,Object> categoryMap = new HashMap<>();categoryMap.put("id",ids.get(i));categoryMap.put("name",names.get(i));categories.add(categoryMap);}//查詢品牌Brand brand = this.brandClient.queryBrandByIds(Arrays.asList(spu.getBrandId())).get(0);//查詢參數組List<SpecGroup> groups = this.specificationClient.queryGroupsParamsByCid(spu.getCid3());//查詢特殊規格參數List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), false, null);//將每個參數封裝成id:val鍵值對形式Map<Long,String> paramMap = new HashMap<>();params.forEach(param -> paramMap.put(param.getId(),param.getName()));//初始化一個map 用于結果返回Map<String,Object> result = new HashMap<>();// 封裝spuresult.put("spu",spu);// 封裝spuDetailresult.put("spuDetail",spuDetail);// 封裝sku集合result.put("skus",skus);// 分類result.put("categories",categories);//品牌result.put("brand",brand);//規格參數result.put("groups",groups);//特殊規格參數result.put("paramMap",paramMap);return result;} }然后在controller中把數據放入model:
@Controller @RequestMapping("/item") public class GoodsController {@Autowiredprivate 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.loadData(id);// 放入模型model.addAllAttributes(modelMap);return "item";} }測試
在頁面中先寫一段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.6、渲染面包屑
在商品展示頁的頂部,有一個商品分類、品牌、標題的面包屑
其數據有3部分:
- 商品分類
- 商品品牌
- spu標題
封裝的數據模型中都有,所以直接渲染即可(頁面101行開始):
1.7、渲染商品列表
預期效果如下:
這個部分需要渲染的數據有5塊:
- sku圖片
- sku標題
- 副標題
- sku價格
- 特有規格屬性列表
其中,sku 的圖片、標題、價格,都必須在用戶選中一個具體sku后,才能渲染。而特有規格屬性列表可以在spuDetail中查詢到。而副標題則是在spu中,直接可以在頁面渲染
因此,先對特有規格屬性列表進行渲染。等用戶選擇一個sku,再通過js對其它sku屬性渲染
1.7.1、副標題
1.7.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}]]*/ "");const indexes = {};Object.keys(paramMap).forEach(param => indexes[param] = 0) </script>-
specialSpec:這是SpuDetail中唯一與Sku相關的數據
因此并沒有保存整個spuDetail,而是只保留了這個屬性,并且需要手動轉為js對象。
-
paramMap:規格參數的id和name鍵值對,方便頁面根據id獲取參數名
-
skus:sku集合
-
indexes:保存被選擇的規格項的索引,默認值設置為0
具體數據:
通過Vue渲染
把剛才獲得的幾個變量保存在Vue實例中:
然后在頁面中渲染:
刷新頁面:
1.7.3、確定SKU
在設計sku數據的時候,就已經添加了一個字段:indexes:
這其實就是規格參數的索引組合。
在頁面中,當用戶點擊選擇規格后,就會把對應的索引保存起來。
因此,可以根據這個indexes來確定用戶要選擇的sku,在vue中定義一個計算屬性,來計算與索引匹配的sku:
在瀏覽器工具中查看:
1.7.4、渲染sku列表
既然已經拿到了用戶選中的sku,接下來,就可以在頁面渲染數據了
圖片列表
商品圖片是一個字符串,所以需要將其分割成數組,然后頁面遍歷即可
標題和價格
完整效果
1.8、商品詳情
商品詳情頁面如下圖所示:
分成上下兩部分:
- 上部:展示的是規格屬性列表
- 下部:展示的是商品詳情
1.8.1、屬性列表
1.8.2、商品詳情
商品詳情是HTML代碼,不能使用 th:text,應該使用th:utext
1.9、規格包裝
規格包裝分成兩部分:
- 規格參數
- 包裝列表
而且規格參數需要按照組來顯示
1.9.1、規格參數
最終效果:
數據模型中有一個groups,跟這個數據結果很像:
分成8個組,組內都有params,里面是所有的參數。不過,這些參數都沒有值!
規格參數的值分為兩部分:
- 通用規格參數:保存在SpuDetail中的genericSpec中
- 特有規格參數:保存在sku的ownSpec中
需要把這兩部分值取出來,放到groups中。
從spuDetail中取出genericSpec并取出groups
把genericSpec引入到Vue實例:
因為sku是動態的,所以需要編寫一個計算屬性,來進行值的組合:
然后在頁面渲染:
1.9.2、包裝列表
包裝列表在商品詳情中,一開始并沒有賦值到Vue實例中,但是可以通過Thymeleaf來渲染
1.9.3、售后服務
售后服務也是通過Thymeleaf進行渲染:
二、頁面靜態化
2.1、問題分析
現在的頁面是通過Thymeleaf模板引擎渲染后返回到客戶端。在后臺需要大量的數據查詢,而后渲染得到HTML頁面。會對數據庫造成壓力,并且請求的響應時間過長,并發能力不高。
首先能想到的就是緩存技術,比如之前學習過的Redis。不過Redis適合數據規模比較小的情況。假如數據量比較大,例如商品詳情頁。每個頁面如果10kb,100萬商品,就是10GB空間,對內存占用比較大。此時就給緩存系統帶來極大壓力,如果緩存崩潰,接下來倒霉的就是數據庫了。
所以緩存并不是萬能的,某些場景需要其它技術來解決,比如靜態化。
2.2、什么是靜態化
靜態化是指把動態生成的HTML頁面變為靜態內容保存,以后用戶的請求到來,直接訪問靜態頁面,不再經過服務的渲染。
而靜態的HTML頁面可以部署在nginx中,從而大大提高并發能力,減小tomcat壓力。
2.3、如何實現靜態化
目前,靜態化頁面都是通過模板引擎來生成,而后保存到nginx服務器來部署。常用的模板引擎比如:
- Freemarker
- Velocity
- Thymeleaf
因為之前就使用的Thymeleaf,來渲染html返回給用戶。Thymeleaf除了可以把渲染結果寫入Response,也可以寫到本地文件,從而實現靜態化。
2.4、Thymeleaf實現靜態化
2.4.1、概念
先說下Thymeleaf中的幾個概念:
- Context:運行上下文
- TemplateResolver:模板解析器
- TemplateEngine:模板引擎
Context
上下文: 用來保存模型數據,當模板引擎渲染時,可以從Context上下文中獲取數據用于渲染。
當與SpringBoot結合使用時,我們放入Model的數據就會被處理到Context,作為模板渲染的數據使用。
TemplateResolver
模板解析器:用來讀取模板相關的配置,例如:模板存放的位置信息,模板文件名稱,模板文件的類型等等。
當與SpringBoot結合時,TemplateResolver已經由其創建完成,并且各種配置也都有默認值,比如模板存放位置,其默認值就是:templates。比如模板文件類型,其默認值就是html。
TemplateEngine
模板引擎:用來解析模板的引擎,需要使用到上下文、模板解析器。分別從兩者中獲取模板中需要的數據,模板文件。然后利用內置的語法規則解析,從而輸出解析后的文件。來看下模板引擎進行處理的函數:
templateEngine.process("模板名", context, writer);三個參數:
- 模板名稱
- 上下文:里面包含模型數據
- writer:輸出目的地的流
在輸出時,可以指定輸出的目的地,如果目的地是Response的流,那就是網絡響應。如果目的地是本地文件,那就實現靜態化了。
而在SpringBoot中已經自動配置了模板引擎,因此不需要關心這個。現在做靜態化,就是把輸出的目的地改成本地文件即可!
2.5、具體實現
新建一個接口為GoodsHtmlService以及相應實現類
/*** 實現頁面靜態化接口*/ public interface GoodsHtmlService {/*** 實現商品詳情頁靜態化* @param spuId*/void createHtml(Long spuId);/*** 新建線程處理頁面靜態化* @param spuId*/void asyncExecute(Long spuId);}實現類:
@Service public class GoodsHtmlServiceImpl implements GoodsHtmlService {@Autowiredprivate GoodsService goodsService;@Autowiredprivate TemplateEngine templateEngine;private final static Logger LOGGER = LoggerFactory.getLogger(GoodsHtmlServiceImpl.class);/*** 實現商品詳情頁靜態化** @param spuId*/@Overridepublic void createHtml(Long spuId) {PrintWriter printWriter = null;try {//需要頁面靜態化的數據Map<String, Object> map = this.goodsService.loadData(spuId);//創建上下文Context context = new Context();context.setVariables(map);//文件輸出File file = new File("D:\\JavaEnvironment\\Nginx\\nginx-1.20.1\\html\\item\\" + spuId + ".html");printWriter = new PrintWriter(file);//模板引擎執行靜態化this.templateEngine.process("item",context,printWriter);} catch (FileNotFoundException e) {e.printStackTrace();}finally {if (printWriter != null){printWriter.close();}}}/*** 新建線程處理頁面靜態化* @param spuId*/@Overridepublic void asyncExecute(Long spuId){ThreadUtils.execute(() -> createHtml(spuId));} }線程類:
public class ThreadUtils {/*** 自定義一個線程池*/private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5, 2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());/*** 提交任務* @param runnable*/public static void execute(Runnable runnable){poolExecutor.submit(runnable);}}2.6、什么時候創建靜態文件
編寫好了創建靜態文件的service,那么問題來了:什么時候去調用它呢
場景描述:
假如大部分的商品都有了靜態頁面。那么用戶的請求都會被nginx攔截下來,根本不會到達leyou-goods-web服務。只有那些還沒有頁面的請求,才可能會到達這里。
因此,如果請求到達了這里,除了返回頁面視圖外,還應該創建一個靜態頁面,那么下次就不會再來訪問leyou-goods-web服務了。
所以,在GoodsController中添加邏輯,去生成靜態html文件:
注意:生成html 的代碼不能對用戶請求產生影響,所以這里使用額外的線程進行異步創建。
重啟測試
訪問一個商品詳情,然后查看nginx目錄:
2.7、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;}proxy_connect_timeout 600;proxy_read_timeout 600;}location / {proxy_pass http://127.0.0.1:9002;proxy_connect_timeout 600;proxy_read_timeout 600;} }重啟測試,可以打開瀏覽器發現讀取速度得到很大的提升
總結
以上是生活随笔為你收集整理的乐优商城(09)--商品详情的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html5干货,干货:详解HTML5中常
- 下一篇: 账号注册的form