乐优商城(八)商品详情

1. 搭建商品详情微服务

当用户搜索到商品后,如果想要了解商品的更多信息,就需要进入商品详情页。

由于商品详情浏览量比较大,所以我们会创建一个微服务,用来展示商品详情。我们的商品详情页会采用 Thymeleaf 模板引擎渲染后,再返回到客户端。

1.1 创建工程

  1. 右键 leyou 项目 --> New Module --> Maven --> Next

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 添加依赖

    <?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-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.parent</groupId>
        <artifactId>leyou-goods-web</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</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>com.leyou.common</groupId>
                <artifactId>leyou-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.leyou.item</groupId>
                <artifactId>leyou-item-interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
        </dependencies>
    </project>
    
  5. 编写配置文件 application.yaml

    server:
      port: 8084
    spring:
      application:
        name: goods-web-service
      thymeleaf:
        cache: false
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:10086/eureka
      instance:
        lease-renewal-interval-in-seconds: 5
        lease-expiration-duration-in-seconds: 10
    
  6. 编写启动类

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

1.2 创建页面模板

  1. 从 leyou-portal 项目中复制 item.html 模板到当前项目 resource 目录下的 templates 中。

    在这里插入图片描述

  2. 把 HTML 的名称空间改成 xmlns:th="http://www.thymeleaf.org",这样页面就由 Thymeleaf 的引擎解析了。

    在这里插入图片描述

1.3 页面跳转

1.3.1 修改页面跳转路径

当我们点击某个商品图片时,应该携带该商品的 SpuId 跳转到商品详情页。

例如:

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

我们打开 search.html,修改其中的商品路径:

在这里插入图片描述

1.3.2 Nginx 反向代理

接下来,我们要把这个地址指向我们的 leyou-goods-web 服务,其端口为 8084。

我们在 nginx.conf 中添加配置,并重启 Nginx

在这里插入图片描述

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 测试

  1. 启动 leyou-goods-web 工程

  2. 点击一个搜索到的商品,成功跳转到商品详情页

    在这里插入图片描述

1.4 后台提供接口

1.4.1 分析模型数据

首先我们一起来分析一下,在这个页面中需要哪些数据。

我们已知的条件是传递来的 Spu 的 id,我们需要根据 Spu 的 id 查询到下面的数据:

  • Spu 信息
  • Spu 详情
  • Spu 下的所有 Sku
  • 品牌
  • 商品三级分类
  • 规格参数组
  • 规格参数

1.4.2 商品微服务提供接口

为了查询到上面的数据,我们需要在商品微服务中提供一些接口。

通过 Spu 的 id 查询 Spu

  1. 在 SpuApi 接口中添加方法 querySpuById

    /**
     * 通过 spuId 查询 Spu
     * @param spuId
     * @return
     */
    @GetMapping("{spuId}")
    public Spu querySpuById(@PathVariable("spuId") Long spuId);
    
  2. 在 SpuController 中添加方法 querySpuById

    /**
     * 通过 spuId 查询 Spu
     * @param spuId
     * @return
     */
    @GetMapping("{spuId}")
    public ResponseEntity<Spu> querySpuById(@PathVariable("spuId") Long spuId){
        Spu spu = spuService.querySpuById(spuId);
        if (spu == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(spu);
    }
    
  3. 在 SpuService 中添加方法 querySpuById

    /**
     * 通过 spuId 查询 Spu
     * @param spuId
     * @return
     */
    public Spu querySpuById(Long spuId) {
        Spu spu = spuMapper.selectByPrimaryKey(spuId);
        return spu;
    }
    

通过分类 id 查询规格参数组

商品详情页需要展示商品的规格参数组,以及其下的规格参数。所以我们需要提供一个接口,通过 Spu 的 id 查询规格参数组,并将规格参数封装其中。

在这里插入图片描述

  1. 在 SpecificationApi 接口中添加方法 queryGroupWithCid

    /**
     * 通过分类 id 查询规格参数组
     * @param cid
     * @return
     */
    @GetMapping("/group/param/{cid}")
    public List<SpecGroup> queryGroupWithCid(@PathVariable("cid") Long cid);
    
  2. 在 SpecificationController 中添加方法 queryGroupsWithParam

    /**
     * 通过分类 id 查询规格参数组
     *
     * @param cid
     * @return
     */
    @GetMapping("/group/param/{cid}")
    public ResponseEntity<List<SpecGroup>> queryGroupsWithParam(@PathVariable("cid") Long cid) {
        List<SpecGroup> specGroups = specificationService.queryGroupsWithParam(cid);
        if (CollectionUtils.isEmpty(specGroups)) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(specGroups);
    }
    
  3. 在 SpecificationService 中添加方法 queryGroupsWithParam

    /**
     * 通过分类 id 查询规格参数组
     *
     * @param cid
     * @return
     */
    public List<SpecGroup> queryGroupsWithParam(Long cid) {
        List<SpecGroup> specGroups = querySpecGroupsByCid(cid);
        for (SpecGroup specGroup : specGroups) {
            List<SpecParam> params = querySpecParams(specGroup.getId(), null, null, null);
            specGroup.setParams(params);
        }
        return specGroups;
    }
    

1.4.3 创建 FeignClient

我们在 leyou-goods-web 服务中,创建 FeignClient

在这里插入图片描述

@FeignClient(value = "item-service")
public interface BrandClient extends BrandApi {
}
@FeignClient(value = "item-service")
public interface SpuClient extends SpuApi {
}
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi {
}

1.4.4 商品详情微服务提供接口

再来回顾一下商品详情页需要的数据,如下:

  • Spu 信息
  • Spu 详情
  • Spu 下的所有 Sku
  • 品牌
  • 商品三级分类
  • 规格参数组
  • 规格参数

我们可以使用 Map<String, Object> 的数据结构封装这些数据,第一个参数为数据名称,第二个参数为数据。

  1. 在 GoodsController 中查询到所需数据,并放入 model

    @Controller
    @RequestMapping("/item")
    public class GoodsController {
        @Autowired
        private GoodsService goodsService;
    
        /**
         * 通过 spuId 查询所需数据
         *
         * @param model
         * @param id
         * @return
         */
        @GetMapping("/{id}.html")
        public String toItemPage(Model model, @PathVariable("id") Long id) {
            // 通过 spuId 查询所需数据
            Map<String, Object> modelMap = this.goodsService.loadData(id);
            // 放入模型
            model.addAllAttributes(modelMap);
            return "item";
        }
    }
    
  2. 在 GoodsService 中添加方法 loadData

    @Service
    public class GoodsService {
        @Autowired
        private BrandClient brandClient;
    
        @Autowired
        private CategoryClient categoryClient;
    
        @Autowired
        private SpuClient spuClient;
    
        @Autowired
        private SpecificationClient specificationClient;
    
        /**
         * 通过 spuId 查询所需数据
         * @param spuId
         * @return
         */
        public Map<String, Object> loadData(Long spuId) {
            Map<String, Object> map = new HashMap<>();
    
            // 查询 Spu
            Spu spu = this.spuClient.querySpuById(spuId);
    
            // 查询 SpuDetail
            SpuDetail spuDetail = this.spuClient.querySpuDetailBySpuId(spuId);
    
            // 查询 Sku 集合
            List<Sku> skus = this.spuClient.querySkusBySpuId(spuId);
    
            // 查询分类
            List<Long> cids = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3());
            List<String> names = this.categoryClient.queryNamesByIds(cids);
            List<Map<String, Object>> categories = new ArrayList<>();
            for (int i = 0; i < cids.size(); i++) {
                Map<String, Object> categoryMap = new HashMap<>();
                categoryMap.put("id", cids.get(i));
                categoryMap.put("name", names.get(i));
                categories.add(categoryMap);
            }
    
            // 查询品牌
            Brand brand = this.brandClient.queryBrandById(spu.getBrandId());
    
            // 查询规格参数组
            List<SpecGroup> groups = this.specificationClient.queryGroupsWithParam(spu.getCid3());
    
            // 查询特殊的规格参数
            List<SpecParam> params = this.specificationClient.querySpecParams(null, spu.getCid3(), false, null);
            Map<Long, String> paramMap = new HashMap<>();
            params.forEach(param -> {
                paramMap.put(param.getId(), param.getName());
            });
    
            // 封装 Spu
            map.put("spu", spu);
            // 封装 SpuDetail
            map.put("spuDetail", spuDetail);
            // 封装 Sku 集合
            map.put("skus", skus);
            // 封装分类
            map.put("categories", categories);
            // 封装品牌
            map.put("brand", brand);
            // 封装规格参数组
            map.put("groups", groups);
            // 封装特殊规格参数
            map.put("paramMap", paramMap);
    
            return map;
        }
    }
    
    

1.4.5 测试

  1. 在 item.html 页面写一段 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>
    
  2. 重启 leyou-item 和 leyou-goods-web 工程

  3. 点击一个商品详情页,查看网页源码,成功查到数据

    在这里插入图片描述

1.5 渲染页面

略,交给前端吧。最终效果如下:

在这里插入图片描述

在这里插入图片描述

2. 页面静态化

2.1 问题分析

现在我们的商品详情页会采用 Thymeleaf 模板引擎渲染后,再返回到客户端。

但这样在后台需要做大量的数据查询,而后渲染得到 HTML 页面。会对数据库造成压力,并且请求的响应时间过长,并发能力不高。

有没有办法解决这些问题呢?

  • 首先,我们能想到的就是缓存技术。比如使用 Redis 缓存,不过 Redis 适合数据规模比较小的情况。假如数据量比较大,比如我们的商品详情页,每个页面如果 10 kb,100 万商品,就是 10 GB 空间,对内存占用比较大,此时就给缓存系统带来极大压力,如果缓存崩溃,接下来倒霉的就是数据库了。
  • 其次,可以使用静态化技术。静态化是指把动态生成的 HTML 页面变为静态内容保存,以后用户的请求到来,直接访问静态页面,不再经过服务器的渲染。而静态的 HTML 页面可以部署在 Nginx 中,从而大大提高并发能力。

2.2 如何实现静态化

原来,我们商品详情页通过 Thymeleaf 模板引擎生成后,直接就返回给客户端了。

现在,我们生成商品详情页后,将它先部署一份在 Nginx 中,再返回给客户端。下一次在访问这个页面时,就直接访问 Niginx 中的静态页面。

2.3 实现页面静态化

  1. 在 leyou-goods-web 工程中的 service 包下创建 GoodsHtmlService

    @Service
    public class GoodsHtmlService {
    
        @Autowired
        private GoodsService goodsService;
    
        @Autowired
        private TemplateEngine templateEngine;
    
    
        /**
         * 创建 HTML 静态页面
         *
         * @param spuId
         * @throws Exception
         */
        public void createHtml(Long spuId) {
    
            PrintWriter writer = null;
            try {
                // 获取页面数据
                Map<String, Object> spuMap = this.goodsService.loadData(spuId);
    
                // 创建 Thymeleaf 上下文对象
                Context context = new Context();
                // 把数据放入上下文对象
                context.setVariables(spuMap);
    
                // 创建输出流
                File file = new File("D:\\nginx-1.14.0\\html\\item\\" + spuId + ".html");
                writer = new PrintWriter(file);
    
                // 执行页面静态化方法
                templateEngine.process("item", context, writer);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (writer != null) {
                    writer.close();
                }
            }
        }
    }
    
  2. 在 GoodsController 中调用页面静态化方法

    /**
     * 通过 spuId 查询所需数据
     *
     * @param model
     * @param id
     * @return
     */
    @GetMapping("/{id}.html")
    public String toItemPage(Model model, @PathVariable("id") Long id) {
        // 通过 spuId 查询所需数据
        Map<String, Object> modelMap = this.goodsService.loadData(id);
        // 放入模型
        model.addAllAttributes(modelMap);
        // 页面静态化
        goodsHtmlService.createHtml(id);
        return "item";
    }
    
  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;
        }
    }
    

2.4 测试

  1. 重启商品详情微服务

  2. 重启 Nginx

  3. 访问一个商品详情页后,成功生成静态页面

    在这里插入图片描述

  4. 再次访问,速度得到极大提升

    在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值