电商项目——页面详情及静态化

页面详情及静态化

商品详情

当用户搜索到商品,肯定会点击查看,就会进入商品详情页,商品详情页的展示

商品详情页服务

商品详情浏览量比较大,并发高,独立开启一个微服务,用来展示商品详情。

创建module

商品的详情页服务,命名为:tt-goods-page

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>taotao</artifactId>
        <groupId>com.taotao.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.taotao.service</groupId>
    <artifactId>tt-goods-page</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.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.taotao.service</groupId>
            <artifactId>tt-item-interface</artifactId>
            <version>${taotao.latest.version}</version>
        </dependency>
    </dependencies>
</project>

编写启动类:

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

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}

页面模板:

从taotao-portal中复制item.html模板到当前项目resource目录下的template中:

页面跳转

修改页面跳转路径

通过详情页的预览,它是多个SKU的集合,即SPU。所以,页面跳转时,应该携带SPU的id信息。

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

nginx反向代理

接下来,我们要把这个地址指向我们刚刚创建的服务:taotao-goods-page,其端口为8084

在nginx.conf中添加一段逻辑:

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

编写跳转controller

tt-goods-page中编写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";
    }
}

封装模型数据

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

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

商品微服务提供接口

查询spu接口

以上所需数据中,查询spu的接口目前还没有,需要在商品微服务中提供这个接口:

GoodsApi

/**
 * 根据spu的id查询spu
 * @param id
 * @return
 */
@GetMapping("spu/{id}")
ResponseEntity<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);
}

查询规格参数组

在页面展示规格时,需要按组展示

组内有多个参数,为了方便展示。提供一个接口,查询规格组,同时在规格组中持有组内的所有参数。

拓展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("{cid}")
    List<SpecGroup> querySpecsByCid(@PathVariable("cid") Long cid);

    @GetMapping("/params")
    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
    );
}

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中,调用之前编写过的方法,查询规格组,和规格参数,然后封装返回。

创建FeignClient

我们在tt-goods-page服务中,创建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{
}

封装数据模型

创建一个GoodsService,在里面来封装数据模型。

这里要查询的数据:

  • SPU

  • 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 CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private SpecificationClient specificationClient;

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

    public Map<String, Object> loadModel(Long id) {
        try {
            // 模型数据
            Map<String, Object> modelMap = new HashMap<>();

            // 查询spu
            Spu spu = this.goodsClient.querySpuById(id);
            // 查询spuDetail
            SpuDetail detail = this.goodsClient.querySpuDetailById(id);
            // 查询sku
            List<Sku> skus = this.goodsClient.querySkuBySpuId(id);

            // 装填模型数据
            modelMap.put("spu", spu);
            modelMap.put("spuDetail", detail);
            modelMap.put("skus", skus);

            // 准备商品分类
            List<Category> categories = getCategories(spu);
            if (categories != null) {
                modelMap.put("categories", categories);
            }

            // 准备品牌数据
            List<Brand> brands = this.brandClient.queryBrandByIds(
                    Arrays.asList(spu.getBrandId()));
            modelMap.put("brand", brands.get(0));

            // 查询规格组及组内参数
            List<SpecGroup> groups = this.specificationClient.querySpecsByCid(spu.getCid3());
            modelMap.put("groups", groups);

            // 查询商品分类下的特有规格参数
            List<SpecParam> params =
                    this.specificationClient.querySpecParam(null, spu.getCid3(), null, false);
            // 处理成id:name格式的键值对
            Map<Long,String> paramMap = new HashMap<>();
            for (SpecParam param : params) {
                paramMap.put(param.getId(), param.getName());
            }
            modelMap.put("paramMap", paramMap);
            return modelMap;

        } catch (Exception e) {
            logger.error("加载商品数据出错,spuId:{}", id, 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";
    }
}

页面静态化

简介

问题分析

现在,我们的页面是通过Thymeleaf模板引擎渲染后返回到客户端。在后台需要大量的数据查询,而后渲染得到HTML页面。会对数据库造成压力,并且请求的响应时间过长,并发能力不高。

首先我们能想到的就是缓存技术,比如之前学习过的Redis。不过Redis适合数据规模比较小的情况。假如数据量比较大,例如我们的商品详情页。每个页面如果10kb,100万商品,就是10GB空间,对内存占用比较大。此时就给缓存系统带来极大压力,如果缓存崩溃,接下来倒霉的就是数据库了。

所以缓存并不是万能的,某些场景需要其它技术来解决,比如静态化。

什么是静态化

静态化是指把动态生成的HTML页面变为静态内容保存,以后用户的请求到来,直接访问静态页面,不再经过服务的渲染。

而静态的HTML页面可以部署在nginx中,从而大大提高并发能力,减小tomcat压力。

如何实现静态化

目前,静态化页面都是通过模板引擎来生成,而后保存到nginx服务器来部署。常用的模板引擎比如:

  • Freemarker
  • Velocity
  • Thymeleaf

使用的Thymeleaf,来渲染html返回给用户。Thymeleaf除了可以把渲染结果写入Response,也可以写到本地文件,从而实现静态化。

Thymeleaf实现静态化

概念

先说下Thymeleaf中的几个概念:

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

Context

上下文: 用来保存模型数据,当模板引擎渲染时,可以从Context上下文中获取数据用于渲染。

当与SpringBoot结合使用时,我们放入Model的数据就会被处理到Context,作为模板渲染的数据使用。

TemplateResolver

模板解析器:用来读取模板相关的配置,例如:模板存放的位置信息,模板文件名称,模板文件的类型等等。

当与SpringBoot结合时,TemplateResolver已经由其创建完成,并且各种配置也都有默认值,比如模板存放位置,其默认值就是:templates。比如模板文件类型,其默认值就是html。

TemplateEngine

模板引擎:用来解析模板的引擎,需要使用到上下文、模板解析器。分别从两者中获取模板中需要的数据,模板文件。然后利用内置的语法规则解析,从而输出解析后的文件。来看下模板引起进行处理的函数:

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

三个参数:

  • 模板名称
  • 上下文:里面包含模型数据
  • writer:输出目的地的流

在输出时,我们可以指定输出的目的地,如果目的地是Response的流,那就是网络响应。如果目的地是本地文件,那就实现静态化了。

而在SpringBoot中已经自动配置了模板引擎,因此我们不需要关心这个。现在我们做静态化,就是把输出的目的地改成本地文件即可!

具体实现

Service代码:

@Service
public class FileService {

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private TemplateEngine templateEngine;

    private String destPath="D:\\nginx-1.8.0\\html";

    /**
     * 创建html页面
     * @param id
     * @throws Exception
     */
    public void createHtml(Long id) throws Exception {
        // 创建上下文,
        Context context = new Context();
        // 把数据加入上下文
        context.setVariables(this.goodsService.loadModel(id));

        // 创建输出流,关联到一个临时文件
        File temp = new File(id + ".html");
        // 目标页面文件
        File dest = createPath(id);
        // 备份原页面文件
        File bak = new File(id + "_bak.html");
        try (PrintWriter writer = new PrintWriter(temp, "UTF-8")) {
            // 利用thymeleaf模板引擎生成 静态页面
            templateEngine.process("item", context, writer);

            if (dest.exists()) {
                // 如果目标文件已经存在,先备份
                dest.renameTo(bak);
            }
            // 将新页面覆盖旧页面
            FileCopyUtils.copy(temp,dest);
            // 成功后将备份页面删除
            bak.delete();
        } catch (IOException e) {
            // 失败后,将备份页面恢复
            bak.renameTo(dest);
            // 重新抛出异常,声明页面生成失败
            throw new Exception(e);
        } finally {
            // 删除临时页面
            if (temp.exists()) {
                temp.delete();
            }
        }

    }

    private File createPath(Long id) {
        if (id == null) {
            return null;
        }
        File dest = new File(this.destPath);
        if (!dest.exists()) {
            dest.mkdirs();
        }
        return new File(dest, id + ".html");
    }

    /**
     * 判断某个商品的页面是否存在
     * @param id
     * @return
     */
    public boolean exists(Long id){
        return this.createPath(id).exists();
    }

    /**
     * 异步创建html页面
     * @param id
     */
    public void syncCreateHtml(Long id){
        ThreadUtils.execute(() -> {
            try {
                createHtml(id);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

线程工具类:

public class ThreadUtils {

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

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

什么时候创建静态文件

假如大部分的商品都有了静态页面。那么用户的请求都会被nginx拦截下来,根本不会到达我们的tt-goods-page服务。只有那些还没有页面的请求,才可能会到达这里。

因此,如果请求到达了这里,我们除了返回页面视图外,还应该创建一个静态页面,那么下次就不会再来麻烦我们了。

所以,在GoodsController中添加逻辑,去生成静态html文件:

@GetMapping("{id}.html")
public String toItemPage(Model model, @PathVariable("id")Long id){
    // 加载所需的数据
    Map<String, Object> modelMap = this.goodsService.loadModel(id);
    // 放入模型
    model.addAllAttributes(modelMap);
    // 判断是否需要生成新的页面
    if(!this.fileService.exists(id)){
        this.fileService.syncCreateHtml(id);
    }
    return "item";
}

注意:生成html 的代码不能对用户请求产生影响,所以这里我们使用额外的线程进行异步创建。

重启测试:

访问一个商品详情,然后查看nginx目录

nginx代理静态页面

接下来,我们修改nginx,让它对商品请求进行监听,指向本地静态页面,如果本地没找到,才进行反向代理:

server {
    listen       80;
    server_name  www.taotao.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;
    }
}
上一篇:谷粒商城P94【商品系统】-> 【商品维护】->【商品管理】~ SKU检索


下一篇:第4章.商品管理