# 😎 知识点概览
> 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 `day09` 的内容
- [x] 构建基于 `Eureka` 的服务注册中心
- [x] `Ribbon` 的基本使用,以及使用 `Ribbon` 来进行服务间的负载均衡调用
- [x] 使用 `Feign` 实现服务之间的远程调用
- [x] 课程预览功能开发
# 目录
[TOC]
# 一、Eureka 注册中心
## 0x01 需求分析
在前后端分离架构中,服务层被拆分成了很多的微服务,微服务的信息如何管理?Spring Cloud中提供服务注册中心来管理微服务信息。
**为什么 要用注册中心?**
1、微服务数量众多,要进行远程调用就需要知道服务端的 `ip` 地址和端口,注册中心帮助我们管理这些服务的 `ip` 和端口。
2、微服务会实时上报自己的状态,注册中心统一管理这些微服务的状态,将存在问题的服务踢出服务列表,客户端获取到可用的服务进行调用。
## 0x02 Eureka介绍
`Spring Cloud Eureka` 是对 `Netflix` 公司的 `Eureka` 的二次封装,它实现了服务治理的功能,`Spring Cloud Eureka` 提供服务端与客户端,服务端即是 `Eureka` 服务注册中心,客户端完成微服务向 `Eureka` 服务的注册与发现。服务端和客户端均采用Java语言编写。下图显示了 `Eureka Server` 与 `Eureka Client` 的关系:

1、`Eureka Server` 是服务端,负责管理各各微服务结点的信息和状态。
2 、在微服务上部署 `Eureka Client` 程序,远程访问 `Eureka Server` 将自己注册在 `Eureka Server`。
3、微服务需要调用另一个微服务时从 `Eureka Server` 中获取服务调用地址,进行远程调用。
## 0x03 Eureka Server搭建
### 1、单机环境搭建
**1)创建 `xc-govern-center` 工程:**
包结构:com.xuecheng.govern.center

**2)添加依赖**
在父工程添加:(有了则不用重复添加)
```xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
```
**3)启动类**
```javascript
package com.xuecheng.govern.center;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer //标识这是一个EurekaServer
@SpringBootApplication
public class GorvernCenterApplication {
public static void main(String[] args) {
SpringApplication.run(GorvernCenterApplication.class,args);
}
}
```
> 需要在启动类上用 `@EnableEurekaServer` 标识此服务为Eureka服务
**4)配置文件**
从其它服务拷贝 `application.yml` 和 `logback-spring.xml`。
application.yml 的配置内容如下:
```yml
server:
port: 50101 #服务端口
spring:
application:
name: xc-govern-center #指定服务名
eureka:
client:
registerWithEureka: false #服务注册,是否将自己注册到Eureka服务中
fetchRegistry: false #服务发现,是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口)
defaultZone: http://localhost:50101/eureka/
server:
enable-self-preservation: false #是否开启自我保护模式
eviction-interval-timer-in-ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000)
```
- `registerWithEureka`:被其它服务调用时需向Eureka注册
- `fetchRegistry`:需要从Eureka中查找要调用的目标服务时需要设置为true
- `enable-self-preservation`:自保护设置,下边有介绍。
- `eviction-interval-timer-in-ms`:清理失效结点的间隔,在这个时间段内如果没有收到该结点的上报则将结点从服务列表中剔除。
**5)启动Eureka Server**
启动Eureka Server,浏览50101端口。

说明:
上图红色提示信息:
`THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OFNETWORK/OTHER PROBLEMS.`
自我保护模式被关闭。在网络或其他问题的情况下可能不会保护实例失效。
`Eureka Server` 有一种自我保护模式,当微服务不再向 `Eureka Server` 上报状态,`Eureka Server` 会从服务列表将此服务删除,如果出现网络异常情况(微服务正常),此时 `Eureka server` 进入自保护模式,不再将微服务从服务列表删除。
在开发阶段建议关闭自保护模式。
详细的参考资料:https://www.cnblogs.com/xishuai/p/spring-cloud-eureka-safe.html
### 2、高可用环境配置
`Eureka Server` 高可用环境需要部署两个 `Eureka server`,它们互相向对方注册。如果在本机启动两个 `Eureka` 需要注意两个 `Eureka Server` 的端口要设置不一样,这里我们部署一个 `Eureka Server` 工程,将端口可配置,制作两个 `Eureka Server` 启动脚本,启动不同的端口,如下图:

1、在实际使用时 `Eureka Server` 至少部署两台服务器,实现高可用。
2、两台 `Eureka Server` 互相注册。
3、微服务需要连接两台 `Eureka Server` 注册,当其中一台 `Eureka` 死掉也不会影响服务的注册与发现。
4、微服务会定时向 `Eureka server` 发送心跳,报告自己的状态。
5、微服务从注册中心获取服务地址以 `RESTful` 方式发起远程调用。
配置如下:
**1、端口配置加入变量**
```yml
server:
port: ${PORT:50101} #服务端口
```
**2、在 Eureka 服务端的地址加入变量**
```yml
eureka:
client:
registerWithEureka: true #服务注册,是否将自己注册到Eureka服务中
fetchRegistry: true #服务发现,是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口)
defaultZone: ${EUREKA_SERVER:http://eureka02:50102/eureka/}
```
**3、 配置 hostname**
`Eureka` 组成高可用,两个`Eureka` 互相向对方注册,这里需要通过域名或主机名访问,这里我们设置两个 `Eureka` 服务的主机名分别为 `eureka01`、`eureka02`。
完整配置如下
```yml
eureka:
client:
registerWithEureka: true #服务注册,是否将自己注册到Eureka服务中
fetchRegistry: true #服务发现,是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口)
defaultZone: ${EUREKA_SERVER:http://eureka02:50102/eureka/}
server:
enable-self-preservation: false #是否开启自我保护模式
eviction-interval-timer-in-ms: 60000 #服务注册表清理间隔(单位毫秒,默认是6)0*1000
instance:
hostname: ${EUREKA_DOMAIN:eureka01}
```
注意这里的 `eureka01`、`eureka02` 的域名,在开放环境中,我们需要在hosts 文件中增加相应的映射:
```
127.0.0.1 eureka01
127.0.0.1 eureka02
```
**4、在IDEA中制作启动脚本**

运行两个启动脚本,分别浏览:
http://localhost:50101/
http://localhost:50102/
Eureka 主画面如下:

## 0x04 服务注册
**1、将 cms 注册到 Eureka Server**
1)在 cms 服务中添加依赖
```xml
<!-- 导入Eureka客户端的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
```
2)在 `application.yml` 中进行配置
```yml
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://eureka01:50101/eureka/,http://eureka02:50102/eureka/},
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ip-address: ${IP_ADDRESS:127.0.0.1}
instance-id: ${spring.application.name}:${server.port} #指定实例id
```
3)在启动类上添加注解 `@EnableDiscoveryClient`
```java
@EnableDiscoveryClient
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.cms") //扫描公共的实体类
@ComponentScan(basePackages = {"com.xuecheng.api"}) //扫描接口
@ComponentScan(basePackages = {"com.xuecheng.manage_cms"}) // 扫描本项目下的所有类
@ComponentScan(basePackages = {"com.xuecheng.framework"}) // 扫描framework
public class ManageCmsApplication {
public static void main(String[] args) {
SpringApplication.run(ManageCmsApplication.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
}
```
4)刷新 Eureka Server 查看注册情况

**2、注册 course 服务**
方法同上。
1、在 `manage-course` 工程中添加 `spring-cloud-starter-eureka` 依赖:
2、在 `application.yml` 配置 `eureka`
3、在启动类上添加注解 `@EnableDiscoveryClient`
# 二、Feign 远程调用
在前后端分离架构中,服务层被拆分成了很多的微服务,服务与服务之间难免发生交互,比如:
课程发布需要调用 `CMS` 服务生成课程静态化页面,本节研究微服务远程调用所使用的技术。
下图是课程管理服务远程调用 `CMS` 服务的流程图:

工作流程如下:
1 、`cms` 服务将自己注册到注册中心。
2、课程管理服务从注册中心获取 `cms` 服务的地址。
3、课程管理服务远程调用 `cms` 服务。
Feign的实现是基于 `Ribbon` 的,所以在介绍 Feign 的使用之前,我们先来了解一下 Ribbon, 以便能更深刻的理解 `Feign`。
## 0x01 Ribbon 简介
`Ribbon` 是 `Netflix` 公司开源的一个负载均衡的项目,它是一个基于 HTTP、TCP的 **客户端负载均衡器**。
`Ribbon` 的开源地址:[https://github.com/Netflix/ribbon](https://github.com/Netflix/ribbon)
**1、什么是负载均衡?**
负载均衡是 微服务架构 中必须使用的技术,通过 负载均衡 来实现系统的 高可用、集群扩容 等功能。负载均衡 可通过 硬件设备 及 软件 来实现,硬件比如:`F5`、`Array` 等,软件比如:LVS、`Nginx` 等。
如下图是负载均衡的架构图:

用户请求先到达负载均衡器(也相当于一个服务),负载均衡器根据负载均衡算法将请求转发到微服务。负载均衡算法有:`轮训`、`随机`、`加权轮训`、`加权随机`、`地址哈希` 等方法,负载均衡器维护一份服务列表,根据负载均衡算法将请求转发到相应的微服务上。所以负载均衡可以为微服务集群分担请求,降低系统的压力。
**2、什么是客户端负载均衡?**
上图是 `服务端 负载均衡`,`客户端负载均衡` 与 `服务端负载均衡` 的区别在于 **客户端** 要维护一份服务列表,`Ribbon` 从 `Eureka Server` 获取服务列表,`Ribbon` 根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。如下图是 `Ribbon` 负载均衡的流程图:

1、在消费微服务中使用 `Ribbon` 实现负载均衡,`Ribbon` 先从 `EurekaServer` 中获取服务列表。
2、`Ribbon` 根据负载均衡的算法去调用微服务。
## 0x02 Ribbon 的基本使用
Spring Cloud 引入`Ribbon` 配合 `restTemplate` 实现客户端负载均衡。Java中远程调用的技术有很多,如:webservice、socket、rmi、Apache HttpClient、OkHttp 等,互联网项目使用基于 `http` 的客户端较多,本项目使用 `OkHttp`。
**1、在客户端添加Ribbon依赖:**
这里在 课程管理服务 配置 `ribbon` 依赖
```XML
<!--导入ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
```
由于依赖了 `spring-cloud-starter-eureka`,会自动添加 `spring-cloud-starter-ribbon` 依赖

**2、配置 Ribbbon 参数**
这里在课程管理服务的 `application.yml` 中配置 `ribbon` 参数
```yml
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
```
**3、负载均衡测试**
1)启动两个 `cms` 服务,注意端口要不一致
```yml
server:
port: ${PORT:31001}
```

启动完成观察 `Eureka Server` 的服务列表

2)定义 `RestTemplate`,使用 `@LoadBalanced` 注解
在课程管理服务的启动类中定义 `RestTemplate`
```java
@EnableDiscoveryClient
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.course")//扫描实体类
@ComponentScan(basePackages={"com.xuecheng.api"})//扫描接口
@ComponentScan(basePackages={"com.xuecheng.manage_course"})
@ComponentScan(basePackages={"com.xuecheng.framework"})//扫描common下的所有类
public class ManageCourseApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ManageCourseApplication.class, args);
}
@Bean
@LoadBalanced //开启ribbon负载均衡拦截器
public RestTemplate restTemplate(){
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
}
```
3 )测试代码
在课程管理服务工程创建单元测试代码,远程调用cms的查询页面接口:
```java
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestRibbon {
@Autowired
RestTemplate restTemplate;
@Test
public void testRibbon(){
String serviceId = "xc-service-manage-cms";
for (int i = 0; i < 10; i++) {
ResponseEntity<Map> forEntity = restTemplate.getForEntity("http://" + serviceId + "/cms/page/get/5a795ac7dd573c04508f3a56", Map.class);
Map body = forEntity.getBody();
System.out.println(body);
}
}
}
```
4)负载均衡测试
添加 `@LoadBalanced` 注解后,`restTemplate` 会走 `LoadBalancerInterceptor` 拦截器,此拦截器中会通过 `RibbonLoadBalancerClient` 查询服务地址,可以在此类打断点观察每次调用的服务地址和端口,两个 `cms` 服务会轮流被调用。

## 0x03 Feign
### 1、Fegin 介绍
`Feign`是 `Netflix` 公司开源的轻量级 `rest` **客户端**,使用 `Feign` 可以非常方便的实现`Http` 客户端。Spring Cloud 引入 `Feign` 并且集成了 `Ribbon` 实现客户端负载均衡调用。
### 2、Feign 测试
**1 、在客户端添加依赖**
在 课程管理服务 添加下边的依赖:
```xml
<!--feign相关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
```
**2、定义 FeignClient 接口**
参考 `Swagger` 文档定义`FeignClient`,注意接口的 `Url`、请求参数类型、返回值类型与`Swagger` 接口一致。在课程管理服务中创建 `client`包,定义查询 `cms` 页面的客户端的接口
```java
package com.xuecheng.manage_course.client;
import com.xuecheng.framework.domain.cms.response.CmsPageResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "XC-SERVICE-MANAGE-CMS")
//这里可以将所有服务做成一个枚举列表
//@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)
public interface CmsPageClient {
@GetMapping("/cms/page/get/{id}")
//这里我在CmsPage接口定义的返回类型为 CmsPageResult 类型,所以远程调用接口这里也要接收 CmsPageResult 类型
CmsPageResult findById(@PathVariable("id") String id);
}
```
**3、启动类添加@EnableFeignClients注解**
```java
@EnableFeignClients //开启Feign远程调用功能
@EnableDiscoveryClient
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.course")//扫描实体类
@ComponentScan(basePackages={"com.xuecheng.api"})//扫描接口
@ComponentScan(basePackages={"com.xuecheng.manage_course"})
@ComponentScan(basePackages={"com.xuecheng.framework"})//扫描common下的所有类
public class ManageCourseApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ManageCourseApplication.class, args);
}
@Bean
@LoadBalanced //开启ribbon负载均衡拦截器
public RestTemplate restTemplate(){
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
}
```
4、**测试**
创建一个单元测试
```java
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestFeign {
@Autowired
CmsPageClient cmsPageClient;
@Test
public void testFeign(){
CmsPageResult byId = cmsPageClient.findById("5a795ac7dd573c04508f3a56");
System.out.println(byId.getCmsPage());
}
}
```
运行测试

**Feign 工作原理如下:**
1、 启动类添加 `@EnableFeignClients` 注解,`Spring`会扫描标记了`@FeignClient` 注解的接口,并生成此接口的代理对象
2、 `@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)` 即指定了 `cms` 的服务名称,`Feign` 会从注册中心获取 `cms` 服务列表,并通过负载均衡算法进行服务调用。
3、在接口方法 中使用注解 `@GetMapping("/cms/page/get/{id}")`,指定调用的`url`,`Feign` 将根据 `url` 进行远程调用。
### 3、使用 Feign 有哪些需要注意的地方?
SpringCloud 对 `Feign` 进行了增强兼容了 `SpringMVC` 的注解 ,我们在使用 `SpringMVC` 的注解时需要注意:
1、FeignClient 接口 有参数在参数必须加 `@PathVariable("XXX")` 和 `@RequestParam("XXX")`
2、FeignClient 返回值为复杂对象时其类型必须有无参构造函数。
# 三、课程预览技术方案
## 0x01 需求分析
**课程预览** 是为了保证课程发布后的 **正确性**,通过课程预览可以直观的通过 **课程详情页面** 看到课程的信息是否正确,通过 课程预览 看到的页面内容 和 课程发布后的页面 内容是一致的。
下图是课程详情页面的预览图:

## 0x02 课程详细页面 技术方案
### 1、技术需求
课程详情页面是向用户展示课程信息的窗口,课程相当于网站的 **商品**,本页面的访问量会非常大。此页面的内容设计不仅要展示出课程 **核心重要的内容** 而且用户访问页面的 **速度要有保证**,有统计显示打开一个页面超过4秒用户就走掉了,所以本页面的性能要求是本页面的重要需求。
本页面另一个需求就是 `SEO`,要非常有利于爬虫抓取页面上信息,并且生成页面快照,利于用户通过搜索引擎搜索课程信息。
### 2、解决方案
那么如何在保证 `SEO` 的前提下提高页面的访问速度?
**方案1:**
对于信息获取类的需求,要想提高页面速度就要使用 **缓存** 来减少或避免对数据库的访问,从而提高页面的访问速度。下图是使用缓存与不使用缓存的区别

此页面为动态页面,会根据课程的不同而不同,方案一采用传统的 `JavaEE Servlet/jsp` 的方式在 `Tomcat` 完成页面渲染,相比不加缓存速度会有提升。
优点:使用 `redis` 作为缓存,速度有提升。
缺点:采用 `Servlet/jsp` 动态页面渲染技术,服务器使用 `Tomcat`,面对高并发量的访问存在性能瓶颈。
**方案2:**
对于不会频繁改变的信息可以采用 **页面静态化** 的技术,提前让页面生成 `html` 静态页面存储在`nginx` 服务器,用户直接访问 `nginx` 即可,对于一些动态信息可以访问服务端获取 `json`数据在页面渲染。

优点:使用 `Nginx` 作为 `web` 服务器,并且直接访问 `html` 页面,性能出色。
缺点:需要维护大量的静态页面,增加了维护的难度。
这里我们选择 **方案2** 作为课程详情页面的技术解决方案,将课程详情页面生成 `Html` 静态化页面,并发布到 `Nginx` 上。
## 0x03 课程预览技术 解决方案
根据要求:课程详情页面采用静态化技术生成 `Html` 页面,课程预览的效果要与最终静态化的`Html` 页面内容一致。所以,课程预览功能也采用静态化技术生成 `Html` 页面,课程预览使用的模板与课程详情页面模板一致,这样就可以保证课程预览的效果与最终课程详情页面的效果一致。
**操作流程:**
1、制作课程详情页面模板
2、开发课程详情页面数据模型的查询接口(为静态化提供数据)
3、调用cms课程预览接口通过浏览器浏览静态文件

# 四、课程详细页面静态化
## 0x01 静态页面测试
### 1、页面内容组成
我们在编写一个页面时需要知道哪些信息是静态信息,哪些信息为动态信息,下图是页面的设计图:

- 打开静态页面,观察每部分的内容。
- 红色表示动态信息,红色以外表示静态信息。
- 红色动态信息:表示一个按钮,根据用户的登录状态、课程的购买状态显示按钮的名称及按钮的事件。
包括以下信息内容:
**1、课程信息**
课程标题、价格、课程等级、授课模式、课程图片、课程介绍、课程目录。
**2、课程统计信息**
课程时长、评分、收藏人数
**3、教育机构信息**
公司名称、公司简介
**4、教育机构统计信息**
好评数、课程数、学生人数
**5、教师信息**
老师名称、老师介绍
### 2、页面拆分
1. 本页头文件和门户使用的页头为同一个文件。
参考:`代码\页面与模板\include\header.html`
2. 页面尾
本页尾文件和门户使用的页尾为同一个文件。
参考:`代码\页面与模板\include\footer.html`
3. 课程详情主页面
每个课程对应一个文件,命名规则为:`课程id.html`(课程id动态变化)
模板页面参考:`\代码\页面与模板\course\detail\course_main_template.html`
4. 教育机构页面
每个教育机构对应一个文件,文件的命名规则为:`company_info_公司id.html`(公司id动态变化)
参考:`代码\页面与模板\company\company_info_template.html`
5. 老师信息页面
每个教师信息对应一个文件,文件的命名规则为:`teacher_info_教师id.html`(教师id动态变化)
参考:`代码 \页面与模板\teacher\teacher_info_template01.html`
6. 课程统计页面
每个课程对应一个文件,文件的命名规则为:`course_stat_课程id.json`(课程id动态变化)
参考:`\代码\页面与模板\stat\course\course_stat_template.json`
7. 教育机构统计页面
每个教育机构对应一个文件,文件的命名规则为:`company_stat_公司id.json`(公司id动态变化)
参考:`\代码\页面与模板\stat\company\company_stat_template.json`
### 3、页面测试
#### 1)页面加载思路
打开课程资料中的 “静态页面目录” 中的课程详情模板页面,研究页面加载的思路。
模板页面路径如下:
```html
静态页面目录\static\course\detail\course_main_template.html
```
- 主页面
我们需要在主页面中通过SSI加载:页头、页尾、教育机构、教师信息
- 异步加载课程统计与教育机构统计信息
课程统计信息(json)、教育机构统计信息(json)
- 马上学习按钮事件
用户点击“马上学习”会根据课程收费情况、课程购买情况执行下一步操作。
#### 2)静态资源虚拟主机
静态资源虚拟主机负责处理课程详情、公司信息、老师信息、统计信息等页面的请求:
将课程资料中的 “**静态页面目录**” 中的目录拷贝到 `F:/develop/xuecheng/static` 下
在nginx中配置静态虚拟主机如下:
```
#学成网静态资源
server {
listen 91;
server_name localhost;
#公司信息
location /static/company/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xuecheng/static/company/;
}
#老师信息
location /static/teacher/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xuecheng/static/teacher/;
}
#统计信息
location /static/stat/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xuecheng/static/stat/;
}
location /course/detail/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xuecheng/static/course/detail/;
}
}
```
**通过 `www.xuecheng.com` 虚拟主机转发到静态资源**
由于课程页面需要通过SSI加载页头和页尾所以需要通过 `www.xuecheng.com` 虚拟主机转发到静态资源
在 `www.xuecheng.com` 虚拟主机加入如下配置:
```
#静态页面资源
location /static/company/ {
proxy_pass http://static_server_pool;
}
location /static/teacher/ {
proxy_pass http://static_server_pool;
}
location /static/stat/ {
proxy_pass http://static_server_pool;
}
location /course/detail/ {
proxy_pass http://static_server_pool;
}
```
配置 `upstream` 实现请求转发到资源服务虚拟主机:
```
#静态资源服务
upstream static_server_pool{
server 127.0.0.1:91 weight=10;
}
```
#### 3)门户静态资源路径
门户中的一些图片、样式等静态资源统一通过 `/static` 路径对外提供服务,在 `www.xuecheng.com` 虚拟主机中配置如下:
```
#静态资源,包括系统所需要的图片,js、css等静态资源
location /static/img/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xc-ui-pc-static-portal/img/;
}
location /static/css/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xc-ui-pc-static-portal/css/;
}
location /static/js/ {
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xc-ui-pc-static-portal/js/;
}
location /static/plugins/ {
# 跨域参数
alias G:/job/code/Project/XueChengOnline/xcEduUI01/xc-ui-pc-static-portal/plugins/;
add_header Access-Control-Allow-Origin http://ucenter.xuecheng.com;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET;
}
```
cors 跨域参数:
- Access-Control-Allow-Origin:允许跨域访问的外域地址如果允许任何站点跨域访问则设置为*,通常这是不建议的。
- Access-Control-Allow-Credentials: 允许客户端携带证书访问
- Access-Control-Allow-Methods:允许客户端跨域访问的方法
#### 4)页面测试
请求:http://www.xuecheng.com/course/detail/course_main_template.html 测试课程详情页面模板是否可以正常浏览。

#### 5)页面动态脚本
为了方便日后的维护,我们将 `javascript` 实现的动态部分单独编写一个 `html` 文件,在门户的 `include`目录下定义 `course_detail_dynamic.html` 文件,此文件通过 `ssi` 包含在课程详情页面中.
文件地址:`资料 \静态页面目录\include\course_detail_dynamic.html`
所有的课程公用一个 页面动态脚本。
在课程详情主页面下端添加如下代码,通过 `SSI` 技术包含课程详情页面动态脚本文件:
```html
<script>var courseId = "template"</script>
<!--#include virtual="/include/course_detail_dynamic.html"-->
</body>
</html>
```
本页面使用 `vue.js` 动态获取信息,使用 vue 的 `created` 钩子函数在页面初始化前获取动态的数据,详细请阅读该文件。
## 0x02 课程数据模型接口
### 1、API接口定义
数据模型
```java
package com.xuecheng.framework.domain.course.ext;
import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CourseMarket;
import com.xuecheng.framework.domain.course.CoursePic;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
//Serializable ?
@Data
public class CourseView implements Serializable {
CourseBase courseBase; //课程基本信息
CourseMarket courseMarket; //课程营销信息
CoursePic coursePic; //课程图片
TeachplanNode teachplanNode; //课程营销计划
}
```
API 接口,再 `CourseControllerApi` 下定义
```java
@ApiOperation("课程视图查询")
public CourseView courseView(String courseId);
```
### 2、Dao
根据数据模型,dao 层我们可以沿用之前开发中定义的
- `CourseBaseRepository`
- `CourseMarketRepository`
- `CoursePicRepository`
- `TeachplanMapper`
### 3、Service
```java
/**
* 获取课程视图数据模型
* @param courseId
* @return
*/
public CourseView getCourseView(String courseId) {
CourseView courseView = new CourseView();
//获取课程基本信息
Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(courseId);
if(courseBaseOptional.isPresent()){
CourseBase courseBase = courseBaseOptional.get();
courseView.setCourseBase(courseBase);
}
//获取课程营销信息
CourseMarket courseMarketById = courseMarketService.findCourseMarketById(courseId);
courseView.setCourseMarket(courseMarketById);
//获取课程图片
Optional<CoursePic> coursePicOptional = coursePicRepository.findById(courseId);
if(coursePicOptional.isPresent()){
CoursePic coursePic = coursePicOptional.get();
courseView.setCoursePic(coursePic);
}
//获取课程计划
TeachplanNode teachplanNode = teachplanMapper.selectList(courseId);
courseView.setTeachplanNode(teachplanNode);
return courseView;
}
```
### 4、Controller
```java
/**
* 课程预览数据模型查询
* @param courseId
* @return
*/
@Override
@GetMapping("/preview/model/{id}")
public CourseView courseView(@PathVariable("id") String courseId) {
return courseService.getCourseView(courseId);
}
```
### 5、测试
在 Swagger 中测试

## 0x03 课程信息模板设计
在确定了静态化所需要的数据模型之后,就可以编写页面模板了,课程详情页面由多个静态化页面组成,所以我们
需要创建多个页面模板,本章节创建课程详情页面的主模板,即课程信息模板。
### 1、模板内容
完整的模板请参考 `资料\课程详情页面模板\course.ftl` 文件,下边列出模板中核心的内容:
- 课程基本信息
```html
<div class="banner-left">
<p class="tit">${courseBase.name}</p>
<p class="pic"><span class="new-pic">特惠价格¥${courseMarket.price}</span> <span
class="old-pic">原价¥${courseMarket.price_old!}</span></p>
<p class="info">
<a href="http://ucenter.xuecheng.com/#/learning/${courseBase.id}/0"
target="_blank" v-if="learnstatus == 1" v-cloak>马上学习</a>
<a href="#" @click="addopencourse" v-if="learnstatus == 2" v-cloak>立即报名</a>
<a href="#" @click="buy" v-if="learnstatus == 3" v-cloak>立即购买</a>
<span><em>难度等级</em>
<#if courseBase.grade=='200001'>
低级
<#elseif courseBase.grade=='200002'>
中级
<#elseif courseBase.grade=='200003'>
高级
</#if>
</span>
<span><em>课程时长</em><stat v-text="course_stat.s601001"></stat>
</span>
<span><em>评分</em><stat v-text="course_stat.s601002"></stat></span>
<span><em>授课模式</em>
<#if courseBase.studymodel=='201001'>
自由学习
<#elseif courseBase.studymodel=='201002'>
任务式学习
</#if>
</span>
</p>
</div>
<div class="banner-rit">
<#if coursePic.pic??>
<p><img src="http://img.xuecheng.com/${coursePic.pic}" alt="" width="270" height="156">
</p>
<#else>
<p><img src="/static/img/widget-video.png" alt="" width="270" height="156"> </p>
</#if>
<p class="vid-act"><span> <i class="i-heart"></i>收藏 <stat v-
text="course_stat.s601003"></stat> </span> <span>分享 <i class="i-weixin"></i><i class="i-qq">
</i></span></p>
</div>
```
- 课程计划
```html
<div class="content">
<#if teachplanNode.children??>
<#list teachplanNode.children as firstNode>
<div class="item">
<div class="title act"><i class="i-chevron-top">
</i>${firstNode.pname}</div>
<div class="about">${firstNode.description!}</div>
<div class="drop-down" style="height: ${firstNode.children?
size * 50}px;">
<ul class="list-box">
<#list firstNode.children as secondNode>
<li>${secondNode.pname}</li>
</#list>
</ul>
</div>
</div>
</#list>
</#if>
</div>
```
- 页头引入
```html
<body data-spy="scroll" data-target="#articleNavbar" data-offset="150">
<!-- 页面头部 -->
<!--#include virtual="/include/header.html"-->
```
- 页尾引入
```freemarker
<!-- 页面底部 -->
<!--底部版权-->
<!--#include virtual="/include/footer.html"-->
```
- 动态脚本文件引入
```freemarker
<script>
//课程id
var courseId = "template"
</script>
<!--#include virtual="/include/course_detail_dynamic.html"-->
```
- 教师信息
从课程数据中获取课程所属的教师Id,这里由于教师信息管理功能没有开发我们使用固定的教师信息文件
```html
<div class="content-com course">
<div class="title"><span>课程制作</span></div>
<!--#include virtual="/teacher/teacher_info_template01.html"-->
</div>
```
- 教育机构文件
同教师信息一样,由于教育机构功能模块没有开发,这里我们使用固定的教育机构文件
```html
<div class="about-teach">
<!--机构信息-->
<!--#include virtual="/company/company_info_template.html"-->
</div>
```
### 2、模板测试
使用test-freemarker工程测试模板
编写模板过程采用test-freemarker工程测试模板。
将 `course.ftl` 拷贝到 `test-freemarker` 工程的 `resources/templates` 下,并在 `test-freemarker` 工程的`controller` 中加测试方法。
```java
//课程详情页面测试
@RequestMapping("/course")
public String course(Map<String,Object> map){
ResponseEntity<Map> forEntity =
restTemplate.getForEntity("http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc40000", Map.class);
Map body = forEntity.getBody();
map.put("model",body);
return "course";
}
```
注意:上边的测试页面不显示样式,原因是页面通过 `SSI` 包含了页面头,而使用 `test-freemarker` 工程无法加载页头,测试模板主要查看 `html` 页面内容是否正确,待课程预览时解决样式不显示问题。
### 3、模板保存
模板编写并测试通过后要在数据库保存:
1、模板信息保存在 `xc_cms` 数据库(mongodb)的 `cms_template`表
2、模板文件保存在 `mongodb` 的 `GridFS` 中。
这里我们在 `cms` 服务的单元测试中进行保存。
```java
//存文件
@Test
public void testGridFs() throws FileNotFoundException {
//要储存的文件
File file = new File("d:/resources/course.ftl");
//定义输入流
FileInputStream fileInputStream = new FileInputStream(file);
//向GridFS存储文件
ObjectId objectId = gridFsTemplate.store(fileInputStream, "index-banner");
//得到文件ID
String fileId = objectId.toString();
System.out.println(fileId);
}
```
保存成功需要记录模板文件的id,即上边代码中的fileId。
第二步:向 `cms_template` 表添加模板记录(请不要重复添加)
使用 `Studio 3T` 连接`mongodb`,向 `cms_template` 添加记录:
```json
{
"_class" : "com.xuecheng.framework.domain.cms.CmsTemplate",
"siteId" : "5a751fab6abb5044e0d19ea1",
"templateName" : "课程详情页面正式模板",
"templateFileId" : "5e93d284cc53e43424beea42"
}
```
### 4、其他模板
除了课程详情主页面需要设计模板所有静态化的页面都要设计模板,如下:
`教育机构页面模板`、`教师信息页面模板`、`课程统计信息json模板`、`教育机构统计信息json模板`。
本项目我们实现课程详情主页面模板的制作和测试,其它页面模板的开发参考课程详情页面去实现。
# 五、课程预览功能开发
## 0x01 需求分析
课程预览功能将使用 `cms` 系统提供的页面预览功能,业务流程如下:
1、用户进入课程管理页面,点击课程预览,请求到课程管理服务
2、课程管理服务远程调用 `cms` 添加页面接口向 `cms` 添加课程详情页面
3、课程管理服务得到 `cms` 返回课程详情页面 `id`,并拼接生成课程预览 `Url`
4、课程管理服务将课程预览 `Url` 给前端返回
5、用户在前端页面请求课程预览 `Url` ,打开新窗口显示课程详情内容
## 0x02 CMS页面预览测试
`CMS` 服务已经提供了页面预览功能,课程预览功能要使用 `CMS` 页面预览接口实现,下边通过 `cms` 页面预览接口测试课程预览的效果。
**1、向 `cms_page` 表插入一条页面记录或者从 `cms_page` 找一个页面进行测试。**
注意:页面配置一定要正确,需设置正确的模板 `id` 和 `dataUrl`。
如下,是一条页面的记录。
```json
{
"_id" : ObjectId("5b3469f794db44269cb2bff1"),
"_class" : "com.xuecheng.framework.domain.cms.CmsPage",
"siteId" : "5a751fab6abb5044e0d19ea1",
"pageName" : "4028e581617f945f01617f9dabc40000.html",
"pageAliase" : "bootstrip课程详情页面",
"pageWebPath" : "/course/detail/",
"pagePhysicalPath" : "/course/detail/",
"pageType" : "1",
"pageCreateTime" : ISODate("2018-02-03T05:37:53.256+0000"),
"templateId" : "5e93d2e3d79e7d6ed1009b95",
"dataUrl" : "http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc40000"
}
```
**2、课程详细页面 使用 `ssi` 注意**
由于 `Nginx` 先请求 `cms` 的课程预览功能得到 `html` 页面,再解析页面中的 `ssi` 标签,这里必须保证 `cms` 页面预览返回的页面的 `Content-Type` 为 `text/html;charset=utf-8`
在 `cms` 页面预览的 `controller` 方法中添加:
```
response.setHeader("Content-type","text/html;charset=utf-8");
```
**3、测试**
请求:http://www.xuecheng.com/cms/preview/5b3469f794db44269cb2bff1传入页面 Id,测试效果如下:
## 0x03 CMS添加页面接口
在之前进行 `cms` 服务的开发中,已经开发有页面添加的接口,为什么还要重新写一个?
因为每次进行课程预览都需要进行页面添加的操作,如果页面已经存在则不需要重复添加,只需要执行更新操作即可。
所以这里我们在 cms服务中 实现一个 `save` 接口:如果不存在页面则添加,否则就更新页面信息。
### 1、API 接口
```java
/**
* 保存页面数据
*/
@ApiOperation("保存页面数据")
@ApiImplicitParams({
@ApiImplicitParam(name="cmsPage",value = "请提交json形式的页面数据",required=true,paramType="CmsPage",dataType="CmsPage"),
})
public CmsPageResult saveCmsPage(CmsPage cmsPage);
```
### 2、Service
```java
/**
* 保存页面:如果不存在则添加,存在则更新。
* @param cmsPage
* @return
*/
public CmsPageResult saveCmsPage(CmsPage cmsPage) {
//效验cmsPage是否为空
if(cmsPage == null){
//抛出异常,非法参数
ExceptionCast.cast(CommonCode.INVALID_PARAM);
}
//验证数据唯一性:sizeId、pageName、pageWebPath
CmsPage one = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath());
//如果页面已存在则进行更新操作
if (cmsPage1 != null) {
return this.updateCmsPage(one.getPageId(),one);
}
//不存在则直接添加
return this.addCmsPage(cmsPage);
}
```
### 3、Controller
```java
/**
* 保存页面
* @param cmsPage
* @return
*/
@Override
@PostMapping("/save")
public CmsPageResult saveCmsPage(CmsPage cmsPage) {
return pageService.saveCmsPage(cmsPage);
}
```
## 0x04 课程预览服务端
### 1、API 定义
此 `Api` 是课程管理前端请求服务端进行课程预览的Api
请求:课程 `Id`
响应:状态码,课程预览 `Url`
1、定义响应类型
```java
@Data
@NoArgsConstructor
public class CoursePublishResult extends ResponseResult {
String previewUrl;
public CoursePublishResult(ResultCode resultCode, String previewUrl) {
super(resultCode);
this.previewUrl = previewUrl;
}
}
```
2、接口定义如下
```java
@ApiOperation("课程发布预览")
public CoursePublishResult CoursePublishPreview(String courseId);
```
### 2、创建 Feign Client
```java
@PostMapping("/cms/page/save")
CmsPageResult saveCmsPage(@RequestBody CmsPage cmsPage);
```
### 3、Service
```java
//从配置文件获取课程发布的基本配置
@Value("${course-publish.dataUrlPre}")
private String publish_dataUrlPre;
@Value("${course-publish.pagePhysicalPath}")
private String publish_page_physicalpath;
@Value("${course-publish.pageWebPath}")
private String publish_page_webpath;
@Value("${course-publish.siteId}")
private String publish_siteId;
@Value("${course-publish.templateId}")
private String publish_templateId;
@Value("${course-publish.previewUrl}")
private String previewUrl;
@Autowired
CmsPageClient cmsPageClient;
//根据id查询课程基本信息
public CourseBase findCourseBaseById(String courseId){
//获取课程信息
Optional<CourseBase> optionalCourseBase = courseBaseRepository.findById(courseId);
if(!optionalCourseBase.isPresent()){
//课程不存在抛出异常
ExceptionCast.cast(CourseCode.COURSE_NOTEXIST);
return null;
}
return optionalCourseBase.get();
}
/**
* 课程详细页面发布前预览
* @param courseId
* @return
*/
public CoursePublishResult coursePublishPreview(String courseId) {
//获取课程信息
CourseBase courseBaseById = this.findCourseBaseById(courseId);
//拼装页面基本信息
CmsPage cmsPage = new CmsPage();
cmsPage.setDataUrl(publish_dataUrlPre + courseId);
cmsPage.setPagePhysicalPath(publish_page_physicalpath);
cmsPage.setPageWebPath(publish_page_webpath);
cmsPage.setSiteId(publish_siteId);
cmsPage.setTemplateId(publish_templateId);
//页面别名
cmsPage.setPageAliase(courseBaseById.getName());
//远程调用,保存页面信息
CmsPageResult cmsPageResult = cmsPageClient.saveCmsPage(cmsPage);
if(!cmsPageResult.isSuccess()){
return new CoursePublishResult(CommonCode.FAIL,null);
}
//页面id
String cmsPageId = cmsPageResult.getCmsPage().getPageId();
//返回预览url
String url = previewUrl + cmsPageId;
return new CoursePublishResult(CommonCode.SUCCESS,url);
}
```
### 4、Controller
```java
/**
* 发布页面
* @param pageId
* @return
*/
@Override
@PostMapping("/postPage/{pageId}")
public ResponseResult post(@PathVariable("pageId") String pageId) {
return pageService.postPage(pageId);
}
```
### 5、测试
先在 course 服务中的 swagger 进行测试

得到预览url,访问测试

这里实际上只动态渲染了课程部分的数据,例如课程名称,课程介绍等,教师信息和公司介绍等都是静态数据,需要后期进行完善。
## 0x05 前端开发
1、API 方法
```js
/*预览课程*/
export const preview = id => {
return http.requestPost(apiUrl+'/course/preview/'+id);
}
```
2、页面
创建 `course_pub.vue`
```html
<template>
<div>
<el‐card class="box‐card">
<div slot="header" class="clearfix">
<span>课程预览</span>
</div>
<div class="text item">
<el‐button type="primary" @click.native="preview" >课程预览</el‐button>
<br/><br/>
<span v‐if="previewurl && previewurl!=''"><a :href="previewurl" target="_blank">点我查看课
程预览页面 </a> </span>
</div>
</el‐card>
</div>
</template>
```
数据对象:
```js
data() {
return {
dotype:'',
courseid:'',
course: {"id":"","name":"","status":""},
previewurl:''
}
```
方法 :
```js
// 预览
preview(){
courseApi.preview(this.courseid).then((res) => {
if(res.success){
this.$message.error('预览页面生成成功,请点击下方预览链接');
if(res.url){
//预览url
this.previewurl = res.url
}
}else{
this.$message.error(res.message);
}
});
}
```
3、测试
点击预览后成功链接

访问预览链接

# 😁 认识作者
作者:👦 LCyee ,一个向往体面生活的代码🐕
自建博客:[https://www.codeyee.com](https://www.codeyee.com)
> 记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。
CSDN 博客:[https://blog.csdn.net/codeyee](https://blog.csdn.net/codeyee)
> 记录和分享一些开发过程中遇到的问题以及解决的思路。
欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

![微服务[学成在线] day09:Eureka、Feign、课程预览实现](https://qnoss.codeyee.com/TIM%E5%9B%BE%E7%89%8720200721182350_1595327150222.jpg)
微服务[学成在线] day09:Eureka、Feign、课程预览实现