一、需求分析
当系统审核完成商品,需要将商品详情页进行展示,采用静态页面生成的方式。开发流程如下图所示:
执行步骤:
- 系统管理员(商家运维人员)修改或者审核商品的时候,会触发 canal 监控数据
- canal 微服务获取修改数据后,调用 静态页微服务的方法 生成静态页
- 静态页微服务只负责使用 thymeleaf 的模板技术生成静态页
二、商品静态化微服务创建
1、搭建项目
(1)在changgou-web下创建一个名称为 changgou-web-item 的模块 :
(2)changgou-web-item中导入依赖:
<?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>changgou-web</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-web-item</artifactId>
<dependencies>
<!--api 模块-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-goods-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(3)修改application.yml的配置
server:
port: 18088
Spring:
resources:
static-locations: classpath:/
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
spring:
thymeleaf:
cache: false
application:
name: item
main:
allow-bean-definition-overriding: true
# 生成静态页的位置
pagepath: D:\codes\ChangGou\changgou-parent\changgou-web\changgou-web-item\src\main\resources\static
(4)创建系统启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.goods.feign")
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class,args);
}
}
(5)生成静态页
页面发送请求,传递要生成的静态页的的商品的 SpuID,后台 controller 接收请求,调用 thyemleaf 的原生 API 生成商品静态页。
可以看到,需要查询 SPU 的 3个分类 作为面包屑显示,同时还需要查询 SKU 和 SPU 信息。
2、Feign 创建
因为需要查询 SPU 、SKU 以及 Category,所以我们需要先创建Feign,在 changgou-service-goods-api 模块中提供 CategoryFeign 类,提供根据 ID 查询分类数据的方法:
@FeignClient(value="goods")
@RequestMapping("/category")
public interface CategoryFeign {
@GetMapping("/{id}")
public Result<Category> findById(@PathVariable(name = "id") Integer id);
}
在 changgou-service-goods-api 中提供 SkuFeign,提供 根据 SpuID 查询 Sku 集合的方法:
@FeignClient(value="goods")
@RequestMapping("/sku")
public interface SkuFeign {
@PostMapping(value = "/search" )
public Result<List<Sku>> findList(@RequestBody(required = false) Sku sku);
}
在changgou-service-goods-api 中提供 SpuFeign,提供 根据 SpuID 查询 Spu 信息的方法:
@FeignClient(value="goods")
@RequestMapping("/spu")
public interface SpuFeign {
@GetMapping("/{id}")
public Result<Spu> findById(@PathVariable(name = "id") Long id);
}
3、 生成静态页实现
(1)创建 service
提供接口:
public interface PageService {
/**
* 根据商品 ID 生成静态页
* @param spuId
*/
public void createPageHtml(Long spuId);
}
实现:
public class PageServiceImpl implements PageService{
@Autowired
SpuFeign spuFeign;
@Autowired
CategoryFeign categoryFeign;
@Autowired
SkuFeign skuFeign;
@Autowired
TemplateEngine templateEngine;
@Value("${pagePath}")
private String pagepath;
private Map<String,Object> buildDataModel(Long spuId){
// 构建数据模型
Map<String,Object> dataMap=new HashMap<>();
// 根据 ID 查找 spu 信息
Result<Spu> result= spuFeign.findById(spuId);
Spu spu=result.getData();
dataMap.put("spu",spu);
// 获取分类信息,一、二、三级分类,用于显示面包屑数据
dataMap.put("category1",categoryFeign.findById(spu.getCategory1Id()).getData());
dataMap.put("category2",categoryFeign.findById(spu.getCategory2Id()).getData());
dataMap.put("category3",categoryFeign.findById(spu.getCategory3Id()).getData());
if(spu.getImage()!=null){
dataMap.put("imageList",spu.getImage().split(","));
}
dataMap.put("specificationList", JSON.parseObject(spu.getSpecItems(),Map.class));
// 根据 spuID 查 sku 信息
Sku skuCondition=new Sku();
skuCondition.setSpuId(spu.getId());
Result<List<Sku>> resultSku=skuFeign.findList(skuCondition);
dataMap.put("skuList",resultSku.getData());
return dataMap;
}
/**
* 生成静态页
* @param spuId
*/
@Override
public void createPageHtml(Long spuId) {
// 上下文
Context context=new Context();
Map<String, Object> dataModel=buildDataModel(spuId);
context.setVariables(dataModel);
// 准备文件
File dir=new File(pagepath);
if(!dir.exists()){
dir.mkdir();
}
File dest=new File(dir,spuId+".html");
// 生成页面
try(PrintWriter writer=new PrintWriter(dest,"UTF-8")) {
templateEngine.process("item",context,writer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
(2)创建 Controller
在 changgou-web-item 中创建 com.changgou.item.controller.PageController 用于接收请求,生成静态页:
@RestController
@RequestMapping("/page")
public class PageController {
@Autowired
private PageService pageService;
/**
* 生成静态页面
* @param id
* @return
*/
@RequestMapping("/createHtml/{id}")
public Result createHtml(@PathVariable(name="id") Long id){
pageService.createPageHtml(id);
return new Result(true, StatusCode.OK,"ok");
}
}
4、模板填充
(1)面包屑数据
修改 item.html,填充三个分类数据作为面包屑,代码如下:
<div class="crumb-wrap">
<ul class="sui-breadcrumb">
<li>
<a href="#" th:text="${category1.name}"></a>
</li>
<li>
<a href="#" th:text="${category2.name}"></a>
</li>
<li>
<a href="#" th:text="${category3.name}"></a>
</li>
</ul>
</div>
(2)商品图片
修改 item.html,将商品图片信息输出,在真实工作中需要做空判断,代码如下:
<div class="fl preview-wrap">
<!--放大镜效果-->
<div class="zoom">
<!--默认第一个预览-->
<div id="preview" class="spec-preview">
<span class="jqzoom"><img th:jqimg="${imageList[0]}" th:src="${imageList[0]}" /></span>
</div>
<!--下方的缩略图-->
<div class="spec-scroll">
<a class="prev"><</a>
<!--左右按钮-->
<div class="items">
<ul>
<li th:each="img:${imageList}"><img th:src="${img}" th:bimg="${img}" onmousemove="preview(this)" /></li>
</ul>
</div>
<a class="next">></a>
</div>
</div>
</div>
(3)规格输出
<div id="specification" class="summary-wrap clearfix">
<!--循环MAP-->
<dl th:each="spec,specStat:${specificationList}">
<dt>
<div class="fl title">
<i th:text="${spec.key}"></i>
</div>
</dt>
<dd th:each="arrValue:${specStat.current.value}">
<a href="javascript:;" >
<i th:text="${arrValue}"></i>
<span title="点击取消选择"> </span>
</a>
</dd>
</dl>
</div>
(4)默认 SKU 显示
静态页生成后,需要显示默认的 Sku,我们这里默认显示第 1 个 Sku 即可,这里可以结合着Vue 一起实现。可以先定义一个集合,再定义一个 spec 和 sku,用来存储当前选中的 Sku 信息和 Sku 的规格,代码如下:
<script th:inline="javascript">
var item = new Vue({
el: '#itemArray',
data: {
skuList : [[${
skuList}]],
sku:{
},
spec:{
}
},
created:function () {
this.sku=JSON.parse(JSON.stringify(this.skuList[0]));
this.spec=JSON.parse(this.skuList[0].spec);
}
})
</script>
new Vue() 是构建一个新的 Vue 实例,el 表示为实例提供挂载元素,挂载是指用 React 将组件进行渲染,并构造 DOM 元素,然后塞入页面。
页面显示 Sku 信息:
<div class="fr itemInfo-wrap" id="itemArray">
<div class="sku-name">
<h4 >{
{sku.name}}</h4>
</div>
<div class="news"><span th:text="${spu.caption}"></span></div>
<div class="summary">
<div class="summary-wrap" >
<div class="fl title">
<i>价 格</i>
</div>
<div class="fl price">
<i>¥</i>
<em>{
{sku.price}}</em>
<span>降价通知</span>
</div>
<div class="fr remark">
<i>累计评价</i><em>612188</em>
</div>
</div>
<div class="summary-wrap">
<div class="fl title">
<i>促 销</i>
</div>
<div class="fl fix-width">
<i class="red-bg">加价购</i>
<em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
</div>
</div>
</div>
<div class="support">
<div class="summary-wrap">
<div class="fl title">
<i>支 持</i>
</div>
<div class="fl fix-width">
<em class="t-gray">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</em>
</div>
</div>
<div class="summary-wrap">
<div class="fl title">
<i>配 送 至</i>
</div>
<div class="fl fix-width">
<em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
</div>
</div>
</div>
<div class="clearfix choose">
<div id="specification" class="summary-wrap clearfix">
<!--循环MAP-->
<dl th:each="spec,specStat:${specificationList}">
<dt>
<div class="fl title">
<i th:text="${spec.key}"></i>
</div>
</dt>
<dd th:each="arrValue:${specStat.current.value}">
<a href="javascript:;" th:v-bind:class="|{selected:sel('${spec.key}','${arrValue}')}|" th:@click="|selectSpecification('${spec.key}','${arrValue}')|" >
<i th:text="${arrValue}"></i>
<span title="点击取消选择"> </span>
</a>
</dd>
</dl>
</div>
<div class="summary-wrap">
<div class="fl title">
<div class="control-group">
<div class="controls">
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt" />
<a href="javascript:void(0)" class="increment plus">+</a>
<a href="javascript:void(0)" class="increment mins">-</a>
</div>
</div>
</div>
<div class="fl">
<ul class="btn-choose unstyled">
<li>
<a href="cart.html" target="_blank" class="sui-btn btn-danger addshopcar">加入购物车</a>
</li>
</ul>
</div>
</div>
</div>
</div>
(5)记录选中的 Sku
在当前 Spu 的所有 Sku 中,spec 值是唯一的,我们可以根据 spec 来判断用户选中的是哪个Sku,代码如下:
// 选择切换
selectSpecification:function(specName,specValue) {
// 选中的 spec 信息
this.$set(this.spec,specName,specValue);
// 遍历
for(var i=0;i<this.skuList.length;i++ ){
if(this.isQ(JSON.parse(this.skuList[i].spec),this.spec)){
//this.sku =this.skuList[i];
// 深克隆
this.sku=JSON.parse(JSON.stringify(this.skuList[i]));
// break ;
return;
}
}
// 如果没有找到 sku,提示下架操作
this.sku.id=0;
this.sku.name='提示:该商品已下架';
this.sku.price=0;
},
其中,JSON.parse(JSON.stringify(this.skuList[i]))
用来实现深克隆,它是先利用 JSON.stringify 将js 对象序列化成 JSON字符串,再使用 JSON.parse 来反序列化 即 还原 js 对象。对象本身存储的是一个地址映射,如果断电,对象将不存在,所以要将对象的内容转换成字符串的形式再保存在磁盘上。不过,这种实现深拷贝的方法有局限性,它只适用于一般数据 (对象、数组) 的拷贝。
判断两个 JSON 字符串是否相同:
isQ:function (map1,map2){
//判断两个对象属性个数是否相等
if( Object.keys(map1).length!=Object.keys(map2).length){
return false;
}
for(var k in map1){
if(map1[k]!=map2[k]){
return false;
}
}
return true;
}
(6)样式切换
点击不同规格后,实现样式选中,我们可以 根据每个规格 判断该规格 是否在当前选中的 Sku 规格中,如果在,则返回 true 添加 selected 样式,否则返回 false 不添加 selected 样式:
sel:function(name,value){
if(this.spec == undefined){
return false;
}
if(this.spec[name]==value){
return true;
}else{
return false;
}
},
页面添加样式绑定,代码如下:
5、静态资源过滤
生成的静态页我们可以先放到 changgou-web-item 工程中,后面项目实战的时候可以挪出来放到 Nginx 指定发布目录。一会儿我们将生成的静态页放到 resources/templates/items 目录下,
提供一个 EnableMvcConfig 类,开启静态资源过滤:
@ControllerAdvice
@Configuration
public class EnableMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/items/**").addResourceLocations("classpath:/templates/items/");
}
}
将静态资源导入 changgou-web-item 中:
然后,启动 eurekea 服务端、商品微服务、静态化微服务 changgou-web-item,访问生成静态页的路径 http://localhost:18088/page/createHtml/1148477873158365184。注意要输入一个 真实存在的 spu_id ,才能访问到 sku 。
还要注意,访问 css 的路径:
<link rel="stylesheet" type="text/css" href="../static/css/all.css" />
<link rel="stylesheet" type="text/css" href="../static/css/pages-item.css" />
<link rel="stylesheet" type="text/css" href="../static/css/pages-zoom.css" />
这样才能有 css 效果。
运行结果:
静态页生成后,访问地址:
6、Canal 监听生成静态页
当 商品微服务 审核商品之后,应当发送消息,这里采用了 Canal 监控数据变化,Canal 监听到数据的变化,直接调用 feign 生成静态页即可。数据变化后,调用 feign 实现生成静态页:
(1)Feign 创建
在 changgou-service-api 中创建 changgou-web-item-api ,该工程中主要创建 changgou-web-item 的对外依赖抽取信息。
创建 com.changgou.item.feign.PageFeign:
@FeignClient(name = "item")
@RequestMapping("/page")
public interface PageFeign {
@RequestMapping("/createHtml/{id}")
Result createHtml(@PathVariable(name = "id")long id);
}
(2)pom.xml依赖
修改 changgou-service-canal 工程的 pom.xml,引入如下依赖:
<!--静态页API 服务-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-web-item-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(3)修改 changgou-service-canal 工程中的启动类
监听类中,监听商品数据库的 tb_spu 的数据变化,当数据变化的时候,生成静态页 或者 删除静态页。
在原来的监听类中添加如下代码:
@Autowired
private PageFeign pageFeign;
@ListenPoint(destination = "example",
schema = "changgou_goods",
table = {
"tb_spu"},
eventType = {
CanalEntry.EventType.UPDATE,CanalEntry.EventType.INSERT,
CanalEntry.EventType.DELETE})
public void onEventCustomSpu(CanalEntry.EventType eventType,CanalEntry.RowData rowData){
// 判断操作类型
if(eventType== CanalEntry.EventType.DELETE){
String spuId="";
List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : beforeColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();//spuid
break;
}
}
//todo 删除静态页
}else{
//新增 或者 更新
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
String spuId = "";
for (CanalEntry.Column column : afterColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();
break;
}
}
//更新 生成静态页
pageFeign.createHtml(Long.valueOf(spuId));
}
}