赞
踩
1.什么是springboot? SpringBoot是Spring项目中的一个子工程,与Spring-framework 同属于spring的产品,可以将其看做是搭建spring应用程序的脚手架,由于其提供了许多默认的配置,而且具有自己的内嵌的tomcat,使开发人员在开发时不需要花大量时间在写xml等配置文件上,而是关注于业务逻辑的实现,大大提高了开发效率。
2.springboot的好处? 简单来说有两点明显的好处:一个是解决了原先ssm框架配置复杂的问题,使得程序员更多地关注业务逻辑的实现,而不用花太多时间写配置,因为springboot提供了许多默认的配置,当然你也可以编写配置文件来修改这些默认的配置。另外一个是解决了混乱的依赖管理问题:如果一个项目需要使用很多库,有些库又引用了这之中别的库,就会导致依赖关系很复杂,需要弄清楚某些库的版本是否和其他库的版本有冲突,这些不兼容问题一旦出现很难解决,而springboot帮我们解决了这个问题。
3.springboot的特点?
springboot的项目不像传统的ssm框架编写的项目,没有webapp目录,不需要对tomcat进行配置,因为其内置了tomcat,只需要编写一个引导类即可(这里命名为TestApplication),而且整个应用只有一个配置文件,默认命名为application.properties或者application.yml.
SpringBoot提供了一个名为spring-boot-starter-parent的工程,里面已经对各种常用依赖(并非全部)的版本进行了管理,我们的项目需要以这个项目为父工程,这样我们就不用操心依赖的版本问题了,需要什么依赖,直接引入坐标即可(由于springboot整合了各种框架,所以需要使用什么框架就导入该框架的starter即可,注意这里导入的web启动器并没有指定版本,这是因为在引入的父工程中所有的版本都已经管理好了,不会出现冲突) 注意:springboot会根据我们导入的依赖自动地进行猜测我们想要的配置,并自动地进行默认配置,例如我们引入了web启动器,该启动器会导入tomcat和springmvc的依赖,那么springboot就会帮你完成springmvc和tomcat的默认配置!
<?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"> <modelVersion>4.0.0</modelVersion> <groupId>com.itheima</groupId> <artifactId>springboot</artifactId> <version>1.0-SNAPSHOT</version> <!-- 所有的工程都应该以该工程为父工程--> <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.0.6.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.24</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 启动器,每个启动器背后都是一推依赖,这里的启动器是web启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.drtrang</groupId> <artifactId>druid-spring-boot2-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> </project>
/**
* 引导类:springboot应用的入口
*/
//@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) //声明自动配置,它会根据你导入的依赖来猜测你会需要什么样的配置
//@ComponentScan //包扫描注解,默认扫描被该注解修饰的类所在的包以及该包的子子孙孙包
@SpringBootApplication(exclude={DruidAutoConfiguration.class}) //该注解相当于以上两个注解之和,再加上@SpringBootConfiguration, 该注解相当于@Configuration,但是只能有一个这样的注解,而@Configuration可以有多个
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
上图中exclude属性用于从自动配置中剔除指定的配置类,当你想对程序的某个部分进行自己的配置时,需要将该部分的默认配置先剔除,然后再编写自己的配置类.
springboot的默认配置方式没有任何的xml。那么如果自己要新增配置该怎么办?比如我们要配置一个数据库连接池,spring会在配置文件中用如下方式配置:
<!-- 配置连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
现在在springboot中,我们需要使用java配置类的方式.
首先在pom.xml中,引入Druid连接池依赖:
<dependency>
<groupId>com.github.drtrang</groupId>
<artifactId>druid-spring-boot2-starter</artifactId>
<version>1.1.10</version>
</dependency>
然后在配置文件application.properties中添加配置(这里使用mysql8.0):
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.username=root
jdbc.password=a123456
然后接下来一共有四种配置方式:
package com.itheima.springboot.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; @Component //@ConfigurationProperties(prefix = "jdbc") public class JdbcProperties { //使用set方法自动注入 private String driverClassName; private String url; private String username; private String password; public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
package com.itheima.springboot.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; @Configuration //表明该类是一个配置类,相当于一个xml配置文件 //@PropertySource("classpath:application.properties") //读取资源文件 //@EnableConfigurationProperties(JdbcProperties.class) public class JdbcConfig { // @Value("${jdbc.driverClassName}") //方法一 // private String driverClassName; // @Value("${jdbc.url}") // private String url; // @Value("${jdbc.username}") // private String username; // @Value("${jdbc.password}") // private String password; // @Autowired // private JdbcProperties jdbcProperties; 注意这里可以不写@Autowired, 可以写一个该属性的构造函数来进行赋值 // @Bean //将方法的返回值注入到spring容器 //方法二 // public DataSource getDataSource(){ // DruidDataSource druidDataSource = new DruidDataSource(); // druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName()); // druidDataSource.setUrl(jdbcProperties.getUrl()); // druidDataSource.setUsername(jdbcProperties.getUsername()); // druidDataSource.setPassword(jdbcProperties.getPassword()); // return druidDataSource; // } // @Bean //将方法的返回值注入到spring容器 // public DataSource getDataSource(JdbcProperties jdbcProperties){ //方法三 // DruidDataSource druidDataSource = new DruidDataSource(); // druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName()); // druidDataSource.setUrl(jdbcProperties.getUrl()); // druidDataSource.setUsername(jdbcProperties.getUsername()); // druidDataSource.setPassword(jdbcProperties.getPassword()); // return druidDataSource; // } @Bean //将方法的返回值注入到spring容器 @ConfigurationProperties(prefix = "jdbc") // 方法四,这里会自动调用dataSource的set方法进行参数注入 public DataSource getDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } }
注意@EnableAutoConfiguration会开启SpringBoot的自动配置,并且根据你引入的依赖来生效对应的默认配置。这些默认配置在引入的依赖:spring-boot-autoconfigure中,其中定义了大量自动配置类,那么伴随着就有三个问题:
SpringBoot为我们提供了默认配置,而默认配置生效的条件一般有两个:
1)启动器
之所以,我们如果不想配置,只需要引入依赖即可,而依赖版本我们也不用操心,因为只要引入了SpringBoot提供的stater(启动器),就会自动管理依赖及版本了。
因此,玩SpringBoot的第一件事情,就是找启动器,SpringBoot提供了大量的默认启动器,参考课前资料中提供的《SpringBoot启动器.txt》
2)全局配置
另外,SpringBoot的默认配置,都会读取默认属性,而这些属性可以通过自定义application.properties文件来进行覆盖。这样虽然使用的还是默认配置,但是配置中的值改成了我们自定义的。
因此,玩SpringBoot的第二件事情,就是通过application.properties来覆盖默认属性值,形成自定义配置。我们需要知道SpringBoot的许多默认属性key,这个需要积累,熟能生巧。
首先引入web启动器:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
然后编写controller即可:
package com.itheima.springboot.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.sql.DataSource; @RestController //相当于@Controller和@RequestBody两个注解,表明该类的所有方法返回值均为json类型 @RequestMapping("hello") public class HelloController { @Autowired private DataSource dataSource; @GetMapping("show") public String test(){ return "hello springboot 1"; } public static void main(String[] args) { SpringApplication.run(HelloController.class,args); } }
如果需要修改服务运行的端口,就在配置文件中配置server.port即可;另外,由于没有webapp目录,那么静态资源应该存放在哪里呢?配置文件中默认的静态资源路径为:
只要静态资源放在这些目录中任何一个,SpringMVC都会帮我们处理。我们习惯会把静态资源放在classpath:/static/目录下。另外关于编写拦截器,整合连接池等问题,在项目中再复习吧。
SpringBoot官方并没有提供Mybatis的启动器,不过Mybatis官方自己实现了:
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
配置,基本没有需要配置的:
# mybatis 别名扫描
mybatis.type-aliases-package=cn.itcast.pojo
# mapper.xml文件位置,如果没有映射文件,请注释掉
mybatis.mapper-locations=classpath:mappers/*.xml
需要注意,这里没有配置mapper接口扫描包,因此我们需要给每一个Mapper接口添加@Mapper注解,才能被识别。
通用Mapper的作者也为自己的插件编写了启动器,我们直接引入即可:
<!-- 通用mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}
其实,我们引入jdbc或者web的启动器,就已经引入事务相关的依赖及默认配置了,只需要在使用的地方加上注解@Transactional即可
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id){
return this.userMapper.selectByPrimaryKey(id);
}
@Transactional
public void deleteById(Long id){
this.userMapper.deleteByPrimaryKey(id);
}
}
Thymeleaf之前没听说过,刚开始学的时候用的jsp(一种典型的异步方式,浏览器向服务器发送请求到服务器返回结果给浏览器的这段时间浏览器就干等着什么也不做,不过现在都2022年了,jsp是不是早已经被淘汰了?),后来学了ajax请求(同步方式),基于Jquery框架的ajax用起来比原生的ajax方便多了,springboot默认不支持jsp,但天生支持Thymeleaf,其实用起来之后发现语法和jsp也差不多。
下面是关于Thymeleaf的一些资料:
简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较于其他的模板引擎,它有如下四个极吸引人的特点:
系统架构的变迁:集中式架构,垂直拆分,分布式服务,SOA,微服务架构
网站流量很小时,可以采用上图的集中式架构,所有服务都耦合在一起,能够减少节点部署的成本。
存在的问题:
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。
以前出现了什么问题?
服务治理要做什么?
缺点:
前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实却有一些差别:
微服务的特点:
为什么会有系统架构的演变?个人理解就是之前的服务访问流量小,代码量也小,代码只要能写出来完成需要的功能即可,不需要什么别的要求,但是随着服务访问人数的增加,功能的增多,就需要整体设计一个结构,以方便代码的编写(简单来说就是系统太大了,需要很多人合作,那么就需要提前想好这个系统应该怎么设计,每个人负责什么写哪个模块,代码如果出了问题应该找谁负责,如果还是像以前一样一堆代码堆在一起,结构混乱,如果出了问题就根本不知道bug在哪里,也不知道这个bug是谁写的,这样解决问题就更加耗费时间,还不如一开始废点时间设计个结构出来,到时候出了问题可以迅速定位到负责的人),另外就是当访问人数过大时,用一台服务器压力太大,那么就需要使用服务器集群,使得有很多台服务器提供同一个功能,这样即时一台服务器挂了,还可以访问别的服务器来实现这个功能,提高用户的体验度,这样随之而来的问题就是负载均衡啊这些问题,另外还需要一个统一的中心来管理这些服务,慢慢地最终演变为微服务架构。
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下2种:
如果公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。相反,如果公司的技术栈多样化,而且更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在这个项目中用的是SpringCloud套件,因此我们会使用Http方式来实现服务间调用。
微服务是一种架构方式,最终肯定需要技术架构去实施。
微服务的实现方式很多,但是最火的莫过于Spring Cloud了。原因如下:
SpringCloud将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
架构图:
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
简单来说,eureka组件就是一个中间商,用来管理所有的微服务
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
一般来说,玩微服务分三步走:
1.在pom.xml导入相应微服务需要的依赖
2.在application.yml文件中添加我们需要的配置
3.添加引导类
接下来我们详细讲解Eureka的原理及配置。
Eureka架构中的三个核心角色:
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能。
服务提供者
提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。
Eureka Server即服务的注册中心,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
服务提供者
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 10秒即过期
lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
服务消费者
获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
失效剔除和自我保护
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不要修改。
这个会对我们开发带来极大的不变,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(itcast-eureka)
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
在实际环境中,对于同一个微服务,我们往往会开启很多个集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
因为Eureka中已经集成了Ribbon,所以使用ribbon我们无需引入新的依赖。
修改consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced注解,然后修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用(为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。显然有人帮我们根据service名称,获取到了服务实例的ip和端口。经过源码解读,发现它就是LoadBalancerInterceptor):
//@SpringBootApplication //@EnableDiscoveryClient //@EnableCircuitBreaker //开启熔断组件 @SpringCloudApplication //以上三个注解的组合注解 @EnableFeignClients //开启feign组件 开启feign组件之后就不需要restTemplate了 public class TestConsumerApplication { public static void main(String[] args) { SpringApplication.run(TestConsumerApplication.class,args); } /*@Bean @LoadBalanced //开启ribbon组件,即开启负载均衡组件,该组件不需要导入依赖,因为导入eureka的时候会将该组件导入 public RestTemplate getRestTemplate(){ return new RestTemplate(); }*/ }
import cn.itcast.service.client.UserClient; import cn.itcast.service.pojo.User; import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; /** * 4.定义熔断方法:局部(要和被熔断的方法返回值和参数列表一致) 全局(返回值类型要被熔断的方法一致,参数列表必须为空) * 5.@HystrixCommand(fallbackMethod="局部熔断方法名"):声明被熔断的方法 * 6.@DefaultProperties(defaultFallback="全局熔断方法名") */ @RestController @RequestMapping("consumer/user") //@DefaultProperties(defaultFallback = "globalHystrix") public class UserController { // @Autowired // private RestTemplate restTemplate; @Autowired private UserClient userClient; // @Autowired // private DiscoveryClient discoveryClient; //从服务端拉取到的所有可用服务,包含了拉取的所有服务信息(// eureka客户端,可以获取到eureka中服务的信息) // @GetMapping("{id}") // public User queryById(@PathVariable("id") Long id){ //return restTemplate.getForObject("http://localhost:8081/user/"+id,User.class); // List<ServiceInstance> instances = discoveryClient.getInstances("service-provider"); // ServiceInstance instance = instances.get(0); // return restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id,User.class); // return restTemplate.getForObject("http://service-provider/user/"+id,User.class); // } @GetMapping("{id}") //@HystrixCommand(fallbackMethod = "localHystrix") //@HystrixCommand public String queryById(@PathVariable("id") Long id){ //return restTemplate.getForObject("http://localhost:8081/user/"+id,User.class); //List<ServiceInstance> instances = discoveryClient.getInstances("service-provider"); //ServiceInstance instance = instances.get(0); //return restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id,User.class); //return restTemplate.getForObject("http://service-provider/user/"+id,String.class); return this.userClient.queryById(id).toString(); } public String localHystrix(Long id){ return "局部熔断:请求失败,请稍后再试!"; } public String globalHystrix(){ return "全局熔断:请求失败,请稍后再试!"; } }
注意Ribbon默认的负载均衡策略是简单的轮询,当然SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在consumer的application.yml中添加如下配置即可:
server:
port: 80
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的实现类。
1.引入组件的启动器
2.覆盖默认配置
3.在引导类上添加注解,开发相关组件
Map<serviceId, Map<服务实例名,实例对象(instance)>>
1.架构的演变
传统架构–>水平拆分–>垂直拆分(最早的分布式)–>soa(dubbo)–>微服务(springCloud)
2.远程调用技术:rpc http
rpc协议:自定义数据格式,限定技术,传输速度快,效率高 tcp,dubbo
http协议:统一的数据格式,不限定技术 rest接口 tcp协议 springCloud
3.什么是springCloud
微服务架构的解决方案,是很多组件的集合
eureka:注册中心,服务的注册与发现
zull:网关协议,路由请求,过滤器(ribbon hystrix)
ribbon:负载均衡组件
hystrix:熔断组件
feign:远程调用组件(ribbon hystrix)
*4.eureka
注册中心:itcast-eureka(1.引入启动器, 2.配置spring.application.name=itcast-eureka 3.在引导类上@EnableEurekaServer)
客户端:itcast-service-provider itcast-service-consumer
(1.引入启动器 2.配置spring.application.name eureka.client.service-url.defaultZone=http://localhost:10086/eureka 3.@EnableDiscoveryClient(启用eure客户端))
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:
例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystix解决雪崩问题的手段有两个:
解读:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
触发Hystix服务降级的情况:
那么熔断器怎么使用呢?
1.引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.开启熔断
//@SpringBootApplication //@EnableDiscoveryClient //@EnableCircuitBreaker //开启熔断组件 @SpringCloudApplication //以上三个注解的组合注解 @EnableFeignClients //开启feign组件 开启feign组件之后就不需要restTemplate了 public class TestConsumerApplication { public static void main(String[] args) { SpringApplication.run(TestConsumerApplication.class,args); } /*@Bean @LoadBalanced //开启ribbon组件,即开启负载均衡组件,该组件不需要导入依赖,因为导入eureka的时候会将该组件导入 public RestTemplate getRestTemplate(){ return new RestTemplate(); }*/ }
3.编写降级逻辑
我们改造consumer,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成:
@Controller @RequestMapping("consumer/user") public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand(fallbackMethod = "queryUserByIdFallBack") public String queryUserById(@RequestParam("id") Long id) { String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); return user; } public String queryUserByIdFallBack(Long id){ return "请求繁忙,请稍后再试!"; } }
要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。说明:
@Controller @RequestMapping("consumer/user") @DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法 public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand // 标记该方法需要熔断 public String queryUserById(@RequestParam("id") Long id) { String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class); return user; } /** * 熔断方法 * 返回值要和被熔断的方法的返回值一致 * 熔断方法不需要参数 * @return */ public String fallBackMethod(){ return "请求繁忙,请稍后再试!"; } }
Hystrix的默认超时时长为1,我们可以通过配置修改这个值:
我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystrix超时时间。该配置没有提示。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
熔断状态机3个状态:
默认的熔断触发要求较高,休眠时间较短,为了测试方便,我们可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
解读:
feign的中文意思是伪装,为什么叫伪装?
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
如何使用feign组件?
1.导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.在启动类上,添加注解,开启Feign功能
@SpringCloudApplication
@EnableFeignClients // 开启feign客户端
public class ItcastServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
注意删除RestTemplate:因为feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
3.在服务的调用方编写feign
package cn.itcast.service.client;
import cn.itcast.service.client.impl.UserClientImpl;
import cn.itcast.service.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "service-provider", fallback = UserClientImpl.class) //声明该结构是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
public User queryById(@PathVariable("id") Long id);
}
Feign中本身已经集成了Ribbon依赖和自动配置:因此我们不需要额外引入依赖,也不需要再注册RestTemplate对象。
Feign默认也有对Hystrix的集成:只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:(在consumer工程添加配置内容)
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
但是,Feign中的Fallback配置不像hystrix中那样简单了。首先,我们要定义一个类实现刚才编写的UserClient,作为fallback的处理类,然后在UserClient中,指定刚才编写的实现类。
package cn.itcast.service.client.impl; import cn.itcast.service.client.UserClient; import cn.itcast.service.pojo.User; import org.springframework.stereotype.Component; /** * 实现相应接口,实现的方法就是对应的熔断方法 */ @Component public class UserClientImpl implements UserClient { @Override public User queryById(Long id) { User user = new User(); user.setUserName("请求超时,请稍后访问!"); return user; } }
接下来的部分用的不多,了解即可
请求压缩
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
日志级别
Feign支持4种级别:
日志级别这部分我没测试过,以后再说吧!
通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
先来说说这样架构需要做的一些事儿以及存在的不足:
破坏了服务无状态特点。
为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。
无法直接复用既有接口。
当我们需要对一个既有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
面对类似上面的问题,我们要如何解决呢?答案是:服务网关!
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
加入Zuul之后的架构:
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
玩zuul的过程:
1.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
2.编写配置文件
server: port: 10010 spring: application: name: itcast-zuul eureka: client: service-url: defaultZone: http://localhost:10086/eureka zuul: #不配置路由也可以,因为有默认配置 routes: service-provider: /user/** #路由名称,可以随便写 service-consumer: /consumer/** # path: /service-provider/** # 也可以不写path和serviceId,直接把这段写在service-provider后面(前面表示服务ID,后面表示服务前缀等等) # url: http://localhost:8081 # serviceId: service-provider prefix: /api #网关前缀,可加可不加,用于标识网关,但是加了之后一定要在请求路径上也加上
3.编写引导类
package cn.itcast.zuul; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableZuulProxy //开启Zuul组件 @EnableDiscoveryClient //开启Eureka客户端 public class ItcastZuulApplication { public static void main(String[] args) { SpringApplication.run(ItcastZuulApplication.class,args); } }
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
Zuul指定了默认的路由规则:默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:service-provider,则默认的映射路径就 是:/service-provider/**
过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
正常流程:
异常流程:
使用场景
自定义过滤器
package cn.itcast.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.apache.http.HttpStatus; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; @Component public class TestFilter extends ZuulFilter { /** * 返回过滤器的类型,一共有四种类型: pre,post,route,error * @return */ @Override public String filterType() { return "pre"; } /** * 返回过滤器的优先级,值越小优先级越高 * @return */ @Override public int filterOrder() { return 10; } /** * 判断run方法应不应该执行,返回true则执行,否则不执行 * @return */ @Override public boolean shouldFilter() { return true; } /** * 用该方法判断应该拦截还是放行 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { //得到目前的环境 RequestContext context = RequestContext.getCurrentContext(); //拿到HttpServletRequest对象 HttpServletRequest request = context.getRequest(); //拿到相应的参数 String token = request.getParameter("token"); if(token == null){ context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED); //设置响应码为401,表示未授权 context.setResponseBody("request error!!"); } return null; //表示过滤器什么都不做 } }
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
1.引入启动器
2.覆盖默认配置
3.在引导类上启用组件
1.高可用 itcast-eureka
10086 10087
2.心跳过期 itcast-service-provider
eureka:
instance:
lease-renewal-interval-in-seconds: 5 # 心跳时间
lease-expiration-duration-in-seconds: 15 # 过期时间
3.拉取服务的间隔时间 itcast-service-consumer
eureka:
client:
registry-fetch-interval-seconds: 5
4.关闭自我保护,定期清除无效连接
eureka:
server:
eviction-interval-timer-in-ms: 5000
enable-self-preservation: false
ribbon: 负载均衡组件
1.eureka集成了
2.@LoadBalanced:开启负载均衡
3.this.restTemplate.getForObject(“http://service-provider/user/” + id, User.class);
hystrix:容错组件
降级:检查每次请求,是否请求超时,或者连接池已满
1.引入hystrix启动器
2.熔断时间,默认1s, 6s
3.在引导类上添加了一个注解:@EnableCircuitBreaker @SpringCloudApplication
4.定义熔断方法:局部(要和被熔断的方法返回值和参数列表一致) 全局(返回值类型要被熔断的方法一致,参数列表必须为空)
5.@HystrixCommand(fallbackMethod=“局部熔断方法名”):声明被熔断的方法
6.@DefaultProperties(defaultFallback=“全局熔断方法名”)
熔断:不再发送请求
1.close:闭合状态,所有请求正常方法
2.open:打开状态,所有请求都无法访问。如果在一定时间内容,失败的比例不小于50%或者次数不少于20次
3.half open:半开状态,打开状态默认5s休眠期,在休眠期所有请求无法正常访问。过了休眠期会进入半开状态,放部分请求通过
feign
1.引入openFeign启动器
2.feign.hystrix.enable=true,开启feign的熔断功能
3.在引导类上 @EnableFeignClients
4.创建一个接口,在接口添加@FeignClient(value=“服务id”, fallback=实现类.class)
5.在接口中定义一些方法,这些方法的书写方式跟之前controller类似
6.创建了一个熔断类,实现feign接口,实现对应的方法,这些实现方法就是熔断方法
zuul
1.引入zuul的启动器
2.配置:
zuul.routes.<路由名称>.path=/service-provider/**
zuul.routes.<路由名称>.url=http://localhost:8082
zuul.routes.<路由名称>.path=/service-provider/**
zuul.routes.<路由名称>.serviceId=service-provider
zuul.routes.服务名=/service-provider/**
不用配置,默认就是服务id开头路径
3.@EnableZuulProxy
过滤器:
创建一个类继承ZuulFilter基类
重写四个方法
filterType:pre route post error
filterOrder:返回值越小优先级越高
shouldFilter:是否执行run方法。true执行
run:具体的拦截逻辑
传统项目
各种企业里面用的管理系统(ERP、HR、OA、CRM、物流管理系统等)
互联网项目
门户网站、电商网站:baidu.com、qq.com、taobao.com、jd.com等
电商项目技术特点
常见电商模式
一些专业术语
系统架构图
系统架构解读
整个乐优商城可以分为两部分:后台管理系统、前台门户系统。
无论是前台还是后台系统,都共享相同的微服务集群,包括:
前端技术:
后端技术:
开发环境我选用的是jdk11,然而它要求使用jdk8,所以在整个过程之中我遇到不少问题,但是最终都解决了,除了一个至今还没解决的bug,但是已经不重要了。
整个项目的结构如下:
首先创建project(leyou)作为父工程,并在父工程的pom.xml文件之中提前配置规定好各个依赖的版本:
<?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"> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.parent</groupId> <artifactId>leyou</artifactId> <packaging>pom</packaging> <version>1.0.0-SNAPSHOT</version> <modules> <module>leyou-registry</module> <module>leyou-gateway</module> <module>leyou-item</module> <module>leyou-item/leyou-item-interface</module> <module>leyou-item/leyou-item-service</module> <module>leyou-common</module> <module>leyou-upload</module> <module>leyou-search</module> <module>leyou-goods-web</module> <module>leyou-user</module> <module>leyou-sms</module> <module>leyou-auth</module> <module>leyou-auth/leyou-auth-common</module> <module>leyou-auth/leyou-auth-service</module> <module>leyou-cart</module> <module>leyou-order</module> </modules> <name>leyou</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>11</java.version> <spring-cloud.version>Finchley.SR2</spring-cloud.version> <mybatis.starter.version>1.3.2</mybatis.starter.version> <mapper.starter.version>2.0.2</mapper.starter.version> <druid.starter.version>1.1.9</druid.starter.version> <mysql.version>8.0.24</mysql.version> <pageHelper.starter.version>1.2.3</pageHelper.starter.version> <leyou.latest.version>1.0.0-SNAPSHOT</leyou.latest.version> <fastDFS.client.version>1.26.1-RELEASE</fastDFS.client.version> </properties> <dependencyManagement> <dependencies> <!-- springCloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- mybatis启动器 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.starter.version}</version> </dependency> <!-- 通用Mapper启动器 --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>${mapper.starter.version}</version> </dependency> <!-- 分页助手启动器 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pageHelper.starter.version}</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--FastDFS客户端--> <dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> <version>${fastDFS.client.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
然后加入微服务leyou-registry作为eureka注册中心,配置文件如下:
server:
port: 10086
spring:
application:
name: leyou-registry
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
register-with-eureka: false
fetch-registry: false
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 10000
再创建微服务leyou-gateway作为zuul网关,配置文件如下:
server: port: 10010 spring: application: name: leyou-gateway eureka: client: service-url: defaultZone: http://localhost:10086/eureka registry-fetch-interval-seconds: 5 zuul: prefix: /api routes: item-service: /item/** #路由到商品的微服务 search-service: /search/** #路由到搜索的微服务 goods-web: /goods/** #路由到商品详情的微服务 user-service: /user/** #路由到用户的微服务 auth-service: /auth/** #路由到授权的微服务 cart-service: /cart/** #路由到购物车的微服务 order-service: /order/** #路由到订单的微服务 add-host-header: true #携带请求本身的head头信息 sensitive-headers: # 配置禁止使用的头信息,这里设置为null,否则set-cookie无效 leyou: jwt: pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址 cookieName: LY_TOKEN filter: allowPaths: - /api/auth - /api/search - /api/user/register - /api/user/check - /api/user/code - /api/item - /api/goods/item
另外,由于我使用的是jdk11而不是Jdk8导致我需要增加一些额外的依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
到这里,我们已经把基础架构搭建完毕。
接下来创建第一个真正意义上的微服务:商品微服务
既然是一个全品类的电商购物平台,那么核心自然就是商品。因此我们要搭建的第一个服务,就是商品微服务。其中会包含对于商品相关的一系列内容的管理,包括:
因为与商品的品类相关,我们的工程命名为leyou-item.
需要注意的是,我们的leyou-item是一个微服务,那么将来肯定会有其它系统需要来调用服务中提供的接口,获取的接口数据,也需要对应的实体类来封装,因此肯定也会使用到接口中关联的实体类。
因此这里我们需要使用聚合工程,将要提供的接口及相关实体类放到独立子工程中,以后别人引用的时候,只需要知道坐标即可。
我们会在leyou-item中创建两个子工程:
调用关系如图所示:
然后根据需要在leyou-item-service导入相应的依赖:
<?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-item</artifactId> <groupId>com.leyou.item</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>leyou-item-service</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.leyou.item</groupId> <artifactId>leyou-item-interface</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.leyou.common</groupId> <artifactId>leyou-common</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> </dependencies> </project>
最后,有些工具或通用的约定内容,我们希望各个服务共享,因此需要创建一个工具模块:leyou-common
ECMAScript是浏览器脚本语言的规范,而各种我们熟知的js语言,如JavaScript则是该规范的具体实现。ECMAScript几乎每年都要更新一次。这里使用的是第6版的标准。
新版的js特性这部分就跳过吧,有点多,没必要列出来。
这里只简单复习一下vue的知识,这个我没有重点关注,因为这是前端的内容,当前阶段懂得基础的操作能看懂能用就行。
关于前端的内容,我从刚开始学习的jsp(同步)再到基于Jquery的ajax请求(异步)最后到vue。不过jsp现在是不是已经被淘汰了。
2009年,Ryan Dahl在谷歌的Chrome V8引擎基础上,打造了基于事件循环的异步IO框架:Node.js。
node.js的伟大之处不在于让JS迈向了后端开发,而是构建了一个庞大的生态系统。
2010年,NPM作为node.js的包管理系统首次发布,开发人员可以遵循Common.js规范来编写Node.js模块,然后发布到NPM上供其他开发人员使用。目前已经是世界最大的包模块管理系统。
随后,在node的基础上,涌现出了一大批的前端框架:
MVVM模式
在MVVM之前,开发人员从后端获取需要的数据模型,然后要通过DOM操作Model渲染到View中。而后当用户操作视图,我们还需要通过DOM获取View中的数据,然后同步到Model中。
而MVVM中的VM要做的事情就是把DOM操作完全封装起来,开发人员不用再关心Model和View之间是如何互相影响的:
把开发人员从繁琐的DOM操作中解放出来,把关注点放在如何操作Model上。
前端框架三巨头:Vue.js、React.js、AngularJS,vue.js以其轻量易用著称,vue.js和React.js发展速度最快,AngularJS目前用得最多?
1.NPM是Node提供的模块管理工具,可以非常方便的下载安装很多前端框架,包括Jquery、AngularJS、VueJs都有
2.Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合
3.可以使用npm来安装vue,node_modules是通过npm安装的所有模块的默认位置
vue的一个重点就是其生命周期和钩子函数
每个 Vue 实例在被创建时都要经过一系列的初始化过程 :创建实例,装载模板,渲染模板等等。Vue为生命周期中的每个状态都设置了钩子函数(监听函数)。每当Vue实例处于不同的生命周期时,对应的函数就会被触发调用。
钩子函数
beforeCreated:我们在用Vue时都要进行实例化,因此,该函数就是在Vue实例化时调用,也可以将他理解为初始化函数比较方便一点,在Vue1.0时,这个函数的名字就是init。
created:在创建实例之后进行调用。
beforeMount:页面加载完成,没有渲染。如:此时页面还是{{name}}
mounted:我们可以将他理解为原生js中的window.οnlοad=function({.,.}),相当于jquery中的$(document).ready(function(){….}),他的功能就是:在dom文档渲染完毕之后将要执行的函数,该函数在Vue1.0版本中名字为compiled。 此时页面中的{{name}}已被渲染成name属性的值
beforeDestroy:该函数将在销毁实例前进行调用 。
destroyed:该函数将在销毁实例时进行调用。
beforeUpdate:组件更新之前调用。
updated:组件更新之后调用。
在这个项目中我几乎只用到了created,别的没怎么测试过。
指令 (Directives) 是带有 v- 前缀的特殊特性。指令特性的预期值是:单个 JavaScript 表达式。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
1.{{表达式}}
2.使用{{}}在网速较慢的情况下可能会出现插值闪烁的问题,可以使用使用v-text和v-html指令来替代
3.v-text和v-html可以看做是单向绑定,数据影响了视图渲染,但是反过来就不行。而v-model是双向绑定,视图(View)和模型(Model)之间会互相影响。既然是双向绑定,一定是在视图中可以修改数据,这样就限定了视图的元素类型。目前v-model的可使用元素有:
基本上除了最后一项,其它都是表单的输入项。
4.v-on指令用于给页面元素绑定事件。事件绑定可以简写,例如v-on:click='add’可以简写为@click=‘add’
5.事件修饰符:
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。修饰符是由点开头的指令后缀来表示的。
6.按键修饰符:
在监听键盘事件时,我们经常需要检查常见的键值。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:
<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">
记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:
<!-- 同上 -->
<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">
全部的按键别名:
7.组合按钮
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
例如:
<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
8.v-for
v-for="item in items"
v-for="(item,index) in items"
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
9.增加key属性可以提高v-for的渲染效率
<li v-for="(item,index) in items" :key=index></li>
10.v-if和v-show
v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。而带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display。
11.当v-if和v-for出现在一起时,v-for优先级更高。也就是说,会先遍历,再判断条件。
<li v-for="(user, index) in users" v-if="user.gender == '女'">
{{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}
</li>
12.v-else
v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。
v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:
<div id="app"> <button v-on:click="random=Math.random()">点我呀</button><span>{{random}}</span> <h1 v-if="random >= 0.75"> 看到我啦?!if </h1> <h1 v-else-if="random > 0.5"> 看到我啦?!if 0.5 </h1> <h1 v-else-if="random > 0.25"> 看到我啦?!if 0.25 </h1> <h1 v-else> 看到我啦?!else </h1> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script type="text/javascript"> var app = new Vue({ el: "#app", data: { random: 1 } }) </script>
13.v-bind
html属性不能使用双大括号形式绑定,只能使用v-bind指令。
在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
<div id="app">
<!--可以是数据模型,可以是具有返回值的js代码块或者函数-->
<div v-bind:title="title" style="border: 1px solid red; width: 50px; height: 50px;"></div>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
title: "title",
}
})
</script>
<div id="app">
<div v-bind:class="activeClass"></div>
<div v-bind:class="errorClass"></div>
<div v-bind:class="[activeClass, errorClass]"></div>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
activeClass: 'active',
errorClass: ['text-danger', 'text-error']
}
})
</script>
我们可以传给 v-bind:class 一个对象,以动态地切换 class:
<div v-bind:class="{ active: isActive }"></div>
上面的语法表示 active 这个 class 存在与否将取决于数据属性 isActive 的 truthiness(所有的值都是真实的,除了false,0,“”,null,undefined和NaN)。
你可以在对象中传入更多属性来动态切换多个 class。此外,v-bind:class 指令也可以与普通的 class 属性共存。如下模板:
<div class="static"
v-bind:class="{ active: isActive, text-danger: hasError }">
</div>
和如下data:
data: {
isActive: true,
hasError: false
}
结果渲染为:
<div class="static active"></div>
active样式和text-danger样式的存在与否,取决于isActive和hasError的值。本例中isActive为true,hasError为false,所以active样式存在,text-danger不存在。
数组语法可以将多个样式对象应用到同一个元素上:
<div v-bind:style="[baseStyles, overridingStyles]"></div>
数据:
data: {
baseStyles: {'background-color': 'red'},
overridingStyles: {border: '1px solid black'}
}
渲染后的结果:
<div style="background-color: red; border: 1px solid black;"></div>
v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用单引号括起来) 来命名:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
数据:
data: {
activeColor: 'red',
fontSize: 30
}
效果:
<div style="color: red; font-size: 30px;"></div>
v-bind:class可以简写为:class
14.计算属性
在插值表达式中使用js表达式是非常方便的,而且也经常被用到。但是如果表达式的内容很长,就会显得不够优雅,而且后期维护起来也不方便,于是Vue中提供了计算属性,来替代复杂的表达式:
var vm = new Vue({
el:"#app",
data:{
birthday:1429032123201 // 毫秒值
},
computed:{
birth(){// 计算属性本质是一个方法,但是必须返回结果
const d = new Date(this.birthday);
return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDay();
}
}
})
计算属性本质就是方法,但是一定要返回数据。然后页面渲染时,可以把这个方法当成一个变量来使用。我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。
15.watch监听机制
watch可以让我们监控一个值的变化。从而做出相应的反应。
<div id="app"> <input type="text" v-model="message"> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el:"#app", data:{ message:"" }, watch:{ message(newVal, oldVal){ console.log(newVal, oldVal); } } }) </script>
接下来是之前练习写的html文件,贴在这里方便复习:
<!DOCTYPE html> <html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml" xmlns:v-bind="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>vue实例</title> </head> <style> .active{ color: red; background-color: gold; } </style> <body> <!-- vue对象的模板--> <div id="app"> <input type="text" v-model="search"> <br><br> {{birth}} <br><br> <input type="text" v-bind:class="{active: store > 0}" v-model="store"><br> <br><br> <input type="button" value="点我生成随机数" @click="random=Math.random()"> <span v-if="random>0.75">{{random}}我大于0.75</span> <span v-else-if="random>0.5">{{random}}我大于0.5</span> <span v-else-if="random>0.25">{{random}}我大于0.25</span> <span v-else>{{random}}我大于0.1</span> <br><br> <ul> <li v-if="user.gender=='女'" v-for="(user,index) in users">{{index}} name:{{user.name}} gender:{{user.gender}} age:{{user.age}}</li> </ul> <br><br> <input type="button" value="点我呀" @click="show=!show"><br> <span v-if="show">如果你能看到我,那么说明我是{{show}},if</span><br> <span v-show="show">如果你能看到我,那么说明我是{{show}},show</span><br> <br> <ul> <li v-for="(user,index) in users">{{index}} name:{{user.name}} gender:{{user.gender}} age:{{user.age}}</li> </ul> <br> <ul> <li v-for="(val,key,index) in user" :key="index">{{index}} {{key}}:{{val}}</li> </ul> <!-- 双向绑定,v-model:数据模型--> <input type="text" v-model="num" @keyup.enter="submit()"> <input type="text" v-model="num" @keyup.alt.67="submit()"> <!-- v-on:事件名=js表达式--> <input type="button" v-on:click="f" value="点我呀"> <input type="button" @click="f" value="点我呀"> <input type="button" @contextmenu.prevent="f" value="点我呀"> <!-- 花括号:js表达式--> <h1>大家好,我是{{name}},有{{num}}个妹子迷恋我</h1> <h1>大家好,我是<span v-text="name">张学友</span>,有{{num}}个妹子迷恋我</h1> <h1>大家好,我是<span v-html="name">张学友</span>,有{{num}}个妹子迷恋我</h1> <br> <input type="checkbox" value="C++" v-model="language">C++ <input type="checkbox" value="C" v-model="language">C <input type="checkbox" value="java" v-model="language">java {{language.join(",")}} </div> </body> <script src="node_modules/vue/dist/vue.js"></script> <script> //初始化一个vue实例 const app = new Vue({ el: "#app", //element,选择器 data:{ // 定义数据模型 name: "刘德华", num: 20, language: [], users:[ {name:'柳岩', gender:'女', age: 21}, {name:'峰哥', gender:'男', age: 18}, {name:'范冰冰', gender:'女', age: 24}, {name:'刘亦菲', gender:'女', age: 18}, {name:'古力娜扎', gender:'女', age: 25} ], user: {name:'柳岩', gender:'女', age: 21}, show: true, random: 0.1, store: 0, birthday: 34798549857334, //毫秒值 search: "" }, methods:{ //定义方法 f(){ this.num++; }, submit(){ console.log("您已经提交了!!"); } }, created(){ //发送ajax请求 this.num = -1; }, computed: { //计算属性,里面也可以定义方法,但是这些方法必须有返回值,计算属性可以像数据模型一样使用,计算属性一个好处是如果其依赖(这里是birthday)没有发生改变,那么下次执行时会立即返回结果,而不会重新计算,但是函数每次都会重新计算 birth(){ const date = new Date(this.birthday); return date.getFullYear()+"年"+date.getMonth()+"月"+date.getDay()+"日"; } }, watch:{ //监听 search(newVal,oldVal){ //发送ajax请求 console.log(newVal,oldVal); } } }); </script> </html>
在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。但是如果每个页面都独自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同页面就可以共享这些组件,避免重复开发。在vue里,所有的vue实例都是组件
全局组件
我们通过Vue的component方法来定义一个全局组件。
<div id="app"> <!--使用定义好的全局组件--> <counter></counter> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script type="text/javascript"> // 定义全局组件,两个参数:1,组件名称。2,组件参数 Vue.component("counter",{ template:'<button v-on:click="count++">你点了我 {{ count }} 次,我记住了.</button>', data(){ return { count:0 } } }) var app = new Vue({ el:"#app" }) </script>
定义好的组件,可以任意复用多次:
<div id="app">
<!--使用定义好的全局组件-->
<counter></counter>
<counter></counter>
<counter></counter>
</div>
每个组件内部的值能够互不干扰是因为data 选项是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝
局部组件
一旦全局注册,就意味着即便以后你不再使用这个组件,它依然会随着Vue的加载而加载。因此,对于一些并不频繁使用的组件,我们会采用局部注册。我们先在外部定义一个对象,结构与创建组件时传递的第二个参数一致:
const counter = {
template:'<button v-on:click="count++">你点了我 {{ count }} 次,我记住了.</button>',
data(){
return {
count:0
}
}
};
然后在Vue中使用它:
var app = new Vue({
el:"#app",
components:{
counter:counter // 将定义的对象注册为组件
}
})
之前练习写的关于组件的html文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>全局组件</title> </head> <body> <div id="app"> haha <counter></counter> <counter></counter> <counter></counter> <counter></counter><br> <hello1></hello1> </div> </body> <script src="node_modules/vue/dist/vue.js"></script> <script> Vue.component("counter",{ template: "<button @click='num++'>点我加1呀,{{num}}</button>", data(){ return { num: 0 } } }) const hello = { template: "<button @click='num=num+2'>点我加2呀,{{num}}</button>", data(){ return { num: 0 } } } const app = new Vue({ el: "#app", components:{ hello1: hello } }); </script> </html>
组件通信
各个组件之间以嵌套的关系组合在一起,那么这个时候不可避免的会有组件间通信的需求。
props(父向子传递)
父组件使用子组件,并自定义了title属性:
<div id="app"> <h1>打个招呼:</h1> <!--使用子组件,同时传递title属性--> <introduce title="大家好,我是锋哥"/> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script type="text/javascript"> Vue.component("introduce",{ // 直接使用props接收到的属性来渲染页面 template:'<h1>{{title}}</h1>', props:['title'] // 通过props来接收一个父组件传递的属性 }) var app = new Vue({ el:"#app" }) </script>
我们定义一个子组件,并接收复杂数据:
const myList = {
template: '\
<ul>\
<li v-for="item in items" :key="item.id">{{item.id}} : {{item.name}}</li>\
</ul>\
',
props: {
items: {
type: Array,
default: [],
required: true
}
}
};
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告
我们在父组件中使用它:
<div id="app">
<h2>传智播客已开设如下课程:</h2>
<!-- 使用子组件的同时,传递属性,这里使用了v-bind,指向了父组件自己的属性lessons -->
<my-list :items="lessons"/>
</div>
var app = new Vue({
el:"#app",
components:{
myList // 当key和value一样时,可以只写一个
},
data:{
lessons:[
{id:1, name: 'java'},
{id:2, name: 'php'},
{id:3, name: 'ios'},
]
}
})
动态静态传递
给 prop 传入一个静态的值:
<introduce title="大家好,我是锋哥"/>
给 prop 传入一个动态的值: (通过v-bind从数据模型中,获取title的值)
<introduce :title="title"/>
静态传递时,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 props。
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>
<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
子向父的通信:$emit
来看这样的一个案例:
<div id="app"> <h2>num: {{num}}</h2> <!--使用子组件的时候,传递num到子组件中--> <counter :num="num"></counter> </div> <script src="./node_modules/vue/dist/vue.js"></script> <script type="text/javascript"> Vue.component("counter", {// 子组件,定义了两个按钮,点击数字num会加或减 template:'\ <div>\ <button @click="num++">加</button> \ <button @click="num--">减</button> \ </div>', props:['num']// count是从父组件获取的。 }) var app = new Vue({ el:"#app", data:{ num:0 } }) </script>
子组件接收到父组件属性后,默认是不允许修改的。怎么办?
既然只有父组件能修改,那么加和减的操作一定是放在父组件:
var app = new Vue({
el:"#app",
data:{
num:0
},
methods:{ // 父组件中定义操作num的方法
increment(){
this.num++;
},
decrement(){
this.num--;
}
}
})
但是,点击按钮是在子组件中,那就是说需要子组件来调用父组件的函数,怎么做?
我们可以通过v-on指令将父组件的函数绑定到子组件上:
<div id="app">
<h2>num: {{num}}</h2>
<counter :count="num" @inc="increment" @dec="decrement"></counter>
</div>
在子组件中定义函数,函数的具体实现调用父组件的实现,并在子组件中调用这些函数。当子组件中按钮被点击时,调用绑定的函数:
Vue.component("counter", { template:'\ <div>\ <button @click="plus">加</button> \ <button @click="reduce">减</button> \ </div>', props:['count'], methods:{ plus(){ this.$emit("inc"); }, reduce(){ this.$emit("dec"); } } })
vue提供了一个内置的this.$emit()函数,用来调用父组件绑定的函数
最后是我自己练习时写的html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>父子组件的消息传递</title> </head> <body> <div id="app"> <counter :num1="num" @incr="incr()"></counter> </div> </body> <script src="node_modules/vue/dist/vue.js"></script> <script> Vue.component("counter",{ template: "<button @click='incr1()'>点我加1呀,{{num1}}</button>", data(){ return { num: 0 } }, props: { num1: { type: Number, default: 100 } }, methods:{ incr1(){ this.num1++; //this.$emit("incr"); } } }) const app = new Vue({ el: "#app", data: { num: 1 }, methods:{ incr(){ this.num++; } } }); </script> </html>
这部分不想再解释了,直接贴上之前练习写的文件吧
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录注册</title> </head> <body> <div id="app"> <span><router-link to="/login">登录</router-link></span> <span><router-link to="/register">注册</router-link></span> <hr> <router-view></router-view> </div> </body> <script src="../node_modules/vue/dist/vue.js"></script> <script src="../node_modules/vue-router/dist/vue-router.js"></script> <script src="js/login.js"></script> <script src="js/register.js"></script> <script> const router = new VueRouter({ //这个名字必须是router routes: [ { path: "/login", component: loginForm }, { path: "/register", component: registerForm } ] }); const app = new Vue({ el: "#app", components:{ loginForm, registerForm }, router }); </script> </html>
login.js
const loginForm = {
template : `
<div>
登录页<br>
用户名:<input type="text" ><br>
密 码:<input type="password" ><br>
<input type="button" value="登录">
</div>
`
}
register.js
const registerForm = { //组件内的template只能有一个根标签
template : `
<div>
注册页<br>
用 户 名:<input type="text" ><br>
密  码:<input type="password" ><br>
确认密码:<input type="password" ><br>
<input type="button" value="注册">
</div>
`
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。