乐优商城个人笔记上-主要框架、基础知识、管理系统代码
SpringBoot
- 内置tomcat 提供了自动配置,搭建spring应用的脚手架
- 复杂的配置,混乱的依赖关系
入门项目
1.引入依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.6.RELEASE</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>
2. 编写Controller
3. 优化controller
- 旧的都要 main和@EnableAutoConfiguration:
- 创建引导类
- 除了EAC再加@ComponentScan 扫描包下所有Controller (必须在类的主或子包才可扫到)
4.新增配置
- 引入依赖
<dependency><groupId>com.github.drtrang</groupId><artifactId>druid-spring-boot2-starter</artifactId><version>1.1.10</version>
</dependency>
- resources创建properties文件
- java中创建JdbcConfiguration类,用于注入数据源
@Configuration
@PropertySource("classpath:jdbc.properties")
public class JdbcConfiguration {@Value("${jdbc.url}")String url;@Value("${jdbc.driverClassName}")String driverClassName;@Value("${jdbc.username}")String username;@Value("${jdbc.password}")String password;@Beanpublic DataSource dataSource() {DruidDataSource dataSource = new DruidDataSource();dataSource.setUrl(url);dataSource.setDriverClassName(driverClassName);dataSource.setUsername(username);dataSource.setPassword(password);return dataSource;}
}
属性注入
- 新建JdbcProperties,用来进行属性注入
@ConfigurationProperties(prefix = "jdbc")
public class JdbcProperties {private String url;private String driverClassName;private String username;private String password;// ... 略// getters 和 setters
}
注意:没有指定属性文件的地址,SpringBoot默认会读取文件名为application.properties的资源文件,所以我们把jdbc.properties名称改为application.properties
SpringBoot的四种属性注入方式:用于在JdbcConfiguration中注入 JdbcProperties.class
- @Autowired注入
@Autowiredprivate JdbcProperties jdbcProperties;
- 构造方法注入
private JdbcProperties jdbcProperties;public JdbcConfiguration(JdbcProperties jdbcProperties){this.jdbcProperties = jdbcProperties;}
- @Bean方法形参注入
- 直接在@Bean 方法上使用
@Configuration
@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcConfiguration {@Beanpublic DataSource dataSource(JdbcProperties jdbcProperties) {// ...}
}
- 重要注释
- @RestController
- @EnableAutoConfiguration
- @ComponentScan
- @SpringBootConfiruration
- @SpringBootApplication √
- @PropertySource
- @Bean √
- @Value
- @ConfigurationProperties √
- @EnableConfigurationProperties√
SpringBoot 实战
创建工程
- maven
- 无父工程
- 路径要在demo工程目录下
- 导入基本依赖
- 创建控制类
- 创建启动类
整合springMVC
- 配置端口 application.properties server.port
- 访问静态页面:ResourceProperties类中定义静态资源默认查找路径
- classpath:/META-INF/resources/
- classpath:/resources/
- classpath:/static/ (常用此)
- classpath:/public/
- 添加拦截器:创建interceptors文件夹创建类实现HandlerInterceptor接口
@Component
public class MyInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle method is running!");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle method is running!");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion method is running!");}
}
- 通过实现WebMvcConfigurer并添加@Configuration注解来实现自定义部分SpringMvc配置。
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {@Autowiredprivate HandlerInterceptor myInterceptor;/*** 重写接口中的addInterceptors方法,添加自定义拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(myInterceptor).addPathPatterns("/**");}
}
- 运行查看日志只有打印信息,因为springMVC记录的log级别是debug,springboot默认是显示info以上,我们需要进行配置。
- application.properties中写logging.level.org.springframework=debug
整合连接池
- 导入启动器和连接池
<!--jdbc的启动器,默认使用HikariCP连接池-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--不要忘记数据库驱动,因为springboot不知道我们使用的什么数据库,这里选择mysql-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
- 配置连接池参数
# 连接四大参数
spring.datasource.url=jdbc:mysql://localhost:3306/heima
spring.datasource.username=root
spring.datasource.password=root
# 连接池参数,可省略,SpringBoot自行判断
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=10
整合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
- 创建pojo包-User类
- 没有配置mapper接口扫描包,因此每个Mapper接口都需要添加@Mapper注解
- 创建Mapper包-UserMapper类
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{
}
- 配置 通用mapper启动器
<!-- 通用mapper -->
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>2.0.2</version>
</dependency>
整合事务
- 无需引入依赖
- 创建service-UserService
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public User queryById(Long id){return this.userMapper.selectByPrimaryKey(id);}@Transactionalpublic void deleteById(Long id){this.userMapper.deleteByPrimaryKey(id);}
}
启动测试
- Controller中注入service 以及修改方法注入参数
@RestController
@RequestMapping("user")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("{id}")@ResponseBodypublic User queryUserById(@PathVariable("id")Long id){return this.userService.queryById(id);}@GetMapping("hello")public String test(){return "hello ssm";}
}
Thymeleaf 快速入门
- Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP
- 特点:
- 动静结合:有无网络都可用
- 开箱即用
- 多方言支持
- 与SpringBoot完美整合
引入启动器
- 与解析JSP的InternalViewResolver类似,Thymeleaf也会根据前缀和后缀来确定模板文件的位置:
- 默认前缀:classpath:/templates/
- 默认后缀:.html
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
静态页面
- 注意,把html 的名称空间,改成:xmlns:th=“http://www.thymeleaf.org”
- ${} :类似el表达式,但其实是ognl的语法,更加强大
th-
指令:th-
是利用了Html5中的自定义属性来实现的。如果不支持H5,可以用data-th-
来代替th:each
:类似于c:foreach
遍历集合,但是语法更加简洁th:text
:声明标签中的文本
模板缓存
# 开发阶段关闭thymeleaf的模板缓存
spring.thymeleaf.cache=false
- 在Idea中,我们需要在修改页面后按快捷键:
Ctrl + Shift + F9
对项目进行rebuild才可以。
SpringCloud
架构的演变
- 集中式结构存在的问题:
- 代码耦合,开发维护困难
- 无法针对不同模块进行针对性优化
- 无法水平扩展
- 单点容错率低,并发能力差
- 垂直拆分:功能拆分
- 分布式服务:对基础服务进行抽取,相互调用,维护难
- 流动计算架构:SOA
- 微服务架构:单一职责,粒度小,面向服务、自治
服务调用方式
服务的远程调用方式-RPC与HTTP
- RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
- HTTP:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
HTTP客户端工具
- HttpClient
- OKHttp
- URLConnection
- 不过这些不同的客户端API各不相同
Spring的RestTemplate
- 对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化
- 支持常用三种客户端类型
- HttpClient
- OkHttp
- JDK原生的URLConnection(默认的)
- 首先注册RestTemplate对象,启动类位置注册
@SpringBootApplication
public class HttpDemoApplication {public static void main(String[] args) {SpringApplication.run(HttpDemoApplication.class, args);}@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
- 测试类中注入
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {@Autowiredprivate RestTemplate restTemplate;@Testpublic void httpGet() {// 调用springboot案例中的rest接口User user = this.restTemplate.getForObject("http://localhost/user/1", User.class);System.out.println(user);}
}
- 通过RestTemplate的getForObject()方法,传递url地址及实体类的字节码,RestTemplate会自动发起请求,接收响应,并且帮我们对响应结果进行反序列化。
初识SpringCloud
- SpringCloud整合了大量框架,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
- Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
- Zuul:网关组件,提供智能路由,访问过滤功能
- Ribbon:客户端负载均衡的服务调用组件(客户端负载)
- Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
- Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)
- 版本:
- 版本名:是伦敦的地铁名
- 版本号:SR(Service Releases)是固定的 ,大概意思是稳定版本。后面会有一个递增的数字。
微服务场景模拟
- 搭建两个工程:
- 服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。
- 服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。
服务提供者
- 搭建一个项目:itcast-service-provider,对外提供根据id查询用户的服务。
Spring脚手架创建工程
- new Module-SpringInitializr-√Default
- next填写项目信息:Group:cn.itcast.service Artifact:itcast-service-provider Package:cn.itcast.service
- next-添加web依赖 Web-√Web
- 添加mybatis依赖:SQL-√MySQL√JDBC√MyBatis
- next-填写项目位置-生成的项目结构已经包含了引导类itcastServiceProviderApplication
- 依赖也已经自动导入
- 要使用mapper 手动添加一条依赖
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>2.0.4</version>
</dependency>
RunDashboard-show,可以快速启动。如果取消则在leyou资料里idea快速上手指南打开
编写代码
- controller-UserController
- mapper-UserMapper
- pojo-User
- servcieUserService
- application.yml
server:port: 8081
spring:datasource:url: jdbc:mysql://localhost:3306/mybatis #你学习mybatis时,使用的数据库地址username: rootpassword: root
mybatis:type-aliases-package: cn.itcast.service.pojo
服务调用者
创建工程
- next填写项目信息:Group:cn.itcast.service Artifact:itcast-service-consumer Package:cn.itcast.service
- next-添加web依赖 Web-√Web
编写代码
- 引导类中注册RestTemplate:
@SpringBootApplication
public class ItcastServiceConsumerApplication {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}public static void main(String[] args) {SpringApplication.run(ItcastServiceConsumerApplication.class, args);}
}
编写配置(application.yml)
controller-UserController
mapper-UserMapper
pojo-User
servcieUserService
启动测试
存在的问题
- 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
- consumer需要记忆provider的地址,如果出现变更,可能得不到通知,地址将失效
- consumer不清楚provider的状态,服务宕机也不知道
- provider只有1台服务,不具备高可用性
- 即便provider形成集群,consumer还需自己实现负载均衡
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
- 服务管理
- 如何自动注册和发现
- 如何实现状态监管
- 如何实现动态路由
- 服务如何实现负载均衡
- 服务如何解决容灾问题
- 服务如何实现统一配置
以上的问题,我们都将在SpringCloud中得到答案。
Eureka注册中心
原理图
- Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
- 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
- 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
入门案例
搭建EurekaServer
引入启动器
- 使用spring快速搭建工具
- 填写项目信息:Group:cn.itcast.eureka Artifact:itcast-eureka Package:cn.itcast.eureka
- New Module-Cloud Discovery-√Eureka Server
- 选择依赖:EurekaServer-服务注册中心依赖,Eureka Discovery-服务提供方和服务消费方。因为,对于eureka来说:服务提供方和服务消费方都属于客户端
覆盖默认配置
- 编写application.yml配置:
server:port: 10086 # 端口
spring:application:name: eureka-server # 应用名称,会在Eureka中显示
eureka:client:service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。defaultZone: http://127.0.0.1:${server.port}/eureka
修改引导类
@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class ItcastEurekaApplication {public static void main(String[] args) {SpringApplication.run(ItcastEurekaApplication.class, args);}
}
服务端注册到Eureka
- 注册服务就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到Eureka Server 中
- 修改itcast-service-provider工程
1. 在pom.xml中,添加springcloud的相关依赖。
- 先添加依赖
<!-- SpringCloud的依赖 -->
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Finchley.SR2</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
- 然后是Eureka的客户端
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 版本号-properties
2. 在application.yml中,添加springcloud的相关依赖。
server:port: 8081
spring:datasource:url: jdbc:mysql://localhost:3306/heimausername: rootpassword: rootdriverClassName: com.mysql.jdbc.Driverapplication:name: service-provider # 应用名称,注册到eureka后的服务名称
mybatis:type-aliases-package: cn.itcast.service.pojo
eureka:client:service-url: # EurekaServer地址defaultZone: http://127.0.0.1:10086/eureka
- 注意:这里我们添加了spring.application.name属性来指定应用名称,将来会作为应用的id使用。
3. 在引导类上添加注解,把服务注入到eureka注册中心。
@SpringBootApplication
@MapperScan("cn.itcast.service.mapper")//mapper 接口包扫描
@EnableDiscoveryClient //启动Eureka的客户端 @EnableEurekaClient 也可以,不常用
public class ItcastServiceProviderApplication {public static void main(String[] args) {SpringApplication.run(ItcastServiceApplication.class, args);}
}
从Eureka获取服务
- 接下来我们修改itcast-service-consumer,尝试从EurekaServer获取服务。
1.prom.xml
- 先添加依赖
<!-- SpringCloud的依赖 -->
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Finchley.SR2</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
- 然后是Eureka的客户端
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 版本号-properties
2.application.yml
server:port: 80
spring:application:name: service-consumer
eureka:client:service-url: defaultZone: http://localhost:10086/eureka
3.启动类开启Eureka客户端
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class ItcastServiceConsumerApplication {@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}public static void main(String[] args) {SpringApplication.run(ItcastServiceConsumerApplication.class, args);}
}
4.修改UserController代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:
@Controller
@RequestMapping("consumer/user")
public class UserController {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate DiscoveryClient discoveryClient; // eureka客户端,可以获取到eureka中服务的信息@GetMapping@ResponseBodypublic User queryUserById(@RequestParam("id") Long id){// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");// 因为只有一个Service-provider。所以获取第一个实例ServiceInstance instance = instances.get(0);// 获取ip和端口信息,拼接成服务地址String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id;User user = this.restTemplate.getForObject(baseUrl, User.class);return user;}}
Eureka详解
高可用Eureka
- 可注册多个可用的Eureka,形成集群。某个节点被访问时,会将服务信息同步给集群中每个节点实现数据同步。
- EurekaApplication右键 copy configuration 重新配置启动器
- 启动第一个eurekaServer,我们修改原来的EurekaServer配置:
server:port: 10086 # 端口
spring:application:name: eureka-server # 应用名称,会在Eureka中显示
eureka:client:service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087defaultZone: http://127.0.0.1:10087/eureka
- 启动另一个eurekaServer,再次修改itcast-eureka的配置:
server:port: 10087 # 端口
spring:application:name: eureka-server # 应用名称,会在Eureka中显示
eureka:client:service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087defaultZone: http://127.0.0.1:10086/eureka
- 访问时会相互自动跳转
服务提供者
服务注册
- 服务提供者在启动时,会检测配置属性中的:
eureka.client.register-with-eureka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。 - Map<serviceId,Map<服务实例名,实例对象(instance)>>
- 服务Id,一般是配置中的
spring.application.name
属性 - 服务的实例id。一般host+ serviceId + port,例如:
locahost:service-provider:8081
- 实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。
- 服务Id,一般是配置中的
服务续约
- 在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
eureka:instance:lease-expiration-duration-in-seconds: 90lease-renewal-interval-in-seconds: 30
- lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
- lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
服务消费者
- 获取服务列表
- 当服务消费者启动时,会检测
eureka.client.fetch-registry=true
参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:client:registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
失效提出和自我保护
- 服务下线。正常关闭触发RESR请求给Eureka Server,收到后设为下线状态
- 失效剔除:服务无法正常工作,会剔出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
- server中的yml的
eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
- server中的yml的
- 自我保护:心跳失败实例比例超标,但是不予剔除。开发阶段都会关闭
eureka:server:enable-self-preservation: false # 关闭自我保护模式(缺省为打开)eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
负载均衡Ribbon
- 有助于控制HTTP和TCP客户端的行为。面对provider的集群,可基于某种负载均衡算法自动帮助服务消费者请求。
- 启动两个provider:复制configuration,启动一次之后改端口号,再启动一次
开启负载均衡
- 修改itcast-service-consumer的引导类,在RestTemplate的配置方法上添加
@LoadBalanced
注解: - 修改调用方式,不再手动获取ip和端口,而是通过服务器名称调用。
@Controller
@RequestMapping("consumer/user")
public class UserController {@Autowiredprivate RestTemplate restTemplate;//@Autowired//private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表@GetMapping@ResponseBodypublic User queryUserById(@RequestParam("id") Long id){// 通过client获取服务提供方的服务列表,这里我们只有一个// ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0);String baseUrl = "http://service-provider/user/" + id;User user = this.restTemplate.getForObject(baseUrl, User.class);return user;}}
负载均衡策略
- 修改负载均衡规则配置
- 在在itcast-service-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的实现类。
Hystrix
- 英文豪猪
- Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
- 微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路
- Hystix解决雪崩问题的手段有两个:
- 线程隔离
- 服务熔断
线程隔离,服务降级
- Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
- 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,返回提示信息
- 服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
- 触发Hystix服务降级的情况:
- 线程池已满
- 请求超时
引入依赖
- 首先在itcast-service-consumer的pom.xml中引入Hystrix依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
开启熔断
@SpringCloudApplication
public class ItcastServiceConsumerApplication {@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}public static void main(String[] args) {SpringApplication.run(ItcastServiceConsumerApplication.class, args);}
}
- @SpringCloudApplication=@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker(开启熔断)
编写降级逻辑
- 我们改造itcast-service-consumer,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成:
@Controller
@RequestMapping("consumer/user")
//@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法
public class UserController {@Autowiredprivate 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 "请求繁忙,请稍后再试!";}
}
- 我们可以把Fallback配置加在类上,实现默认fallback,不用任何参数,可以匹配多方法,但是返回值一定一致
- 此时@HystrixCommand:在方法上直接使用该注解,使用默认的剪辑方法。不用写括号与其中的内容
- 定义熔断方法:
- 局部(要和被熔断的方法返回值和参数列表一致)
- 全局(返回值类型要被熔断的方法一致,参数列表必须为空)
设置超时
- consumer中的yml
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds # 单位ms
服务熔断
- 熔断器,也叫断路器,其英文单词为:Circuit Breaker
- 熔断状态机3个状态:
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时
实践内容
- 我们在consumer的调用业务中加入一段逻辑:
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){if(id == 1){throw new RuntimeException("太忙了");}String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);return user;
}
- 这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空service-provider中的休眠逻辑)
不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
解读:
- requestVolumeThreshold:触发熔断的最小请求次数,默认20
- errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
- sleepWindowInMilliseconds:休眠时长,默认是5000毫秒
Feign
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
快速入门
导入依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
开启Feign功能
启动类(consumer的)上添加注解开启Feign功能
@SpringCloudApplication
@EnableFeignClients // 开启feign客户端
public class ItcastServiceConsumerApplication {public static void main(String[] args) {SpringApplication.run(ItcastServiceConsumerApplication.class, args);}
}
删除RestTemplate:feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
Feign的客户端
在itcast-service-consumer工程中,添加client/UserClient接口:
@FeignClient(value = "service-provider") // 标注该类是一个feign接口
public interface UserClient {@GetMapping("user/{id}")User queryById(@PathVariable("id") Long id);
}
- 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称- 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
@Controller
@RequestMapping("consumer/user")
public class UserController {@Autowiredprivate UserClient userClient;@GetMapping@ResponseBodypublic User queryUserById(@RequestParam("id") Long id){User user = this.userClient.queryUserById(id);return user;}}
负载均衡
- 默认置入
- 因此我们不需要额外引入依赖,也不需要再注册
RestTemplate
对象。
Hystrix支持
- 默认集成,但是默认关闭
- 通过下面的参数来开启:(在itcast-service-consumer工程添加配置内容)
feign:hystrix:enabled: true # 开启Feign的熔断功能
- 但是,Feign中的Fallback配置不像hystrix中那样简单了。
1)首先,我们要定义一个类UserClientFallback(在client文件夹中),实现刚才编写的UserClient,作为fallback的处理类
@Component
public class UserClientFallback implements UserClient {@Overridepublic User queryById(Long id) {User user = new User();user.setUserName("服务器繁忙,请稍后再试!");return user;}
}
2)然后在UserFeignClient中,指定刚才编写的实现类
@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口
public interface UserClient {@GetMapping("user/{id}")User queryUserById(@PathVariable("id") Long id);
}
请求压缩
日志级别
ZUUl网关
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
- 架构的不足
- 破坏了服务无状态特点
- 无法直接复用既有接口
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由
、均衡负载
功能之外,它还具备了权限控制
等功能。
快速入门
新建工程
- New Moudle——Group:cn.itcast.zuul Artifact:itcast-zuul Package:cn.itcast.zuul
- 添加Zuul依赖-Cloud Routing-√Zuul
编写配置
server:port: 10010 #服务端口
spring:application:name: api-gateway #指定服务名
编写引导类
通过@EnableZuulProxy
注解开启Zuul的功能:
@SpringBootApplication
@EnableZuulProxy // 开启网关功能
public class ItcastZuulApplication {public static void main(String[] args) {SpringApplication.run(ItcastZuulApplication.class, args);}
}
编写路由规则
映射规则
server:port: 10010 #服务端口
spring:application:name: api-gateway #指定服务名
zuul:routes:service-provider: # 这里是路由id,随意写path: /service-provider/** # 这里是映射路径url: http://127.0.0.1:8081 # 映射路径对应的实际url地址
面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了
添加Eureka客户端依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加Eureka配置,获取服务信息
eureka:client:registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5sservice-url:defaultZone: http://127.0.0.1:10086/eureka
开启Eureka客户端发现功能
@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulDemoApplication {public static void main(String[] args) {SpringApplication.run(ZuulDemoApplication.class, args);}
}
修改映射配置,通过服务名称获取
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
zuul:routes:service-provider: # 这里是路由id,随意写path: /service-provider/** # 这里是映射路径serviceId: service-provider # 指定服务名称
简化的路由配置(常用)
在刚才的配置中,我们的规则是这样的:
zuul.routes.<route>.path=/xxx/**
: 来指定映射路径。<route>
是自定义的路由名zuul.routes.<route>.serviceId=service-provider
:来指定服务名。
而大多数情况下,我们的<route>
路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>
比方说上面我们关于service-provider的配置可以简化为一条:
zuul:routes:service-provider: /service-provider/** # 这里是映射路径
省去了对服务名称的配置。
默认的路由规则
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
- 默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:
service-provider
,则默认的映射路径就 是:/service-provider/**
也就是说,刚才的映射规则我们完全不配置也是OK的,不信就试试看。
路由前缀
配置示例:
zuul:routes:service-provider: /service-provider/**service-consumer: /service-consumer/**prefix: /api # 添加路由前缀
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。
过滤器
ZuulFilter
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{abstract public String filterType();abstract public int filterOrder();boolean shouldFilter();// 来自IZuulFilterObject run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:pre
:请求在被路由之前执行route
:在路由请求时调用post
:在route和errror过滤器之后调用error
:处理请求时发生错误调用
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
过滤器执行生命周期
- 正常:pre-route-post
- 异常
- pre或route出现异常,直接进入error处理完毕后给post
- error异常,最终也会进入post
- post异常,转到error。不会再到post
使用场景
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
自定义过滤器
- 定义过滤器类 创建在zuul/filter下
@Component
public class LoginFilter extends ZuulFilter {/*** 过滤器类型,前置过滤器* @return*/@Overridepublic String filterType() {return "pre";}/*** 过滤器的执行顺序* @return*/@Overridepublic int filterOrder() {return 1;}/*** 该过滤器是否生效* @return*/@Overridepublic boolean shouldFilter() {return true;}/*** 登陆校验逻辑* @return* @throws ZuulException*/@Overridepublic Object run() throws ZuulException {// 获取zuul提供的上下文对象RequestContext context = RequestContext.getCurrentContext();// 从上下文对象中获取请求对象HttpServletRequest request = context.getRequest();// 获取token信息String token = request.getParameter("access-token");// 判断if (StringUtils.isBlank(token)) {// 过滤该请求,不对其进行路由context.setSendZuulResponse(false);// 设置响应状态码,401context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);// 设置响应信息context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");}// 校验通过,把登陆信息放入上下文信息,继续向后执行context.set("token", token);return null;}
}
负载均衡和熔断
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms
项目搭建
项目分类
传统项目
各种企业里面用的管理系统(ERP、HR、OA、CRM、物流管理系统。。。。。。。)
- 需求方:公司、企业内部
- 盈利模式:项目本身卖钱
- 技术侧重点:业务功能
互联网项目
门户网站、电商网站:baidu.com、qq.com、taobao.com、jd.com …
- 需求方:广大用户群体
- 盈利模式:虚拟币、增值服务、广告收益…
- 技术侧重点:网站性能、业务功能
而我们今天要聊的就是互联网项目中的重要角色:电商
- 技术特点:
- 技术范围广
- 技术新
- 高并发(分布式、静态化技术、缓存技术、异步并发、池化、队列)
- 高可用(集群、负载均衡、限流、降级、熔断)
- 数据量大、业务复杂、数据安全
- 常见电商模式
- B2C:商家对个人,如:亚马逊、当当等
- C2C平台:个人对个人,如:闲鱼、拍拍网、ebay
- B2B平台:商家对商家,如:阿里巴巴、八方资源网等
- O2O:线上和线下结合,如:饿了么、电影票、团购等
- P2P:在线金融,贷款,如:网贷之家、人人聚财等。
- B2C平台:天猫、京东、一号店等
- 一些专业术语
SaaS:软件即服务
SOA:面向服务
RPC:远程过程调用
RMI:远程方法调用
PV:(page view),即页面浏览量;用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计
UV:(unique visitor),独立访客。指访问某个站点或点击某条新闻的不同IP地址的人数。在同一天内,uv只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。
PV与带宽:
- 计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。
- 计算公式是:网站带宽= ( PV * 平均页面大小(单位MB)* 8 )/统计时间(换算到秒)
- 为什么要乘以8?
- 网站大小为单位是字节(Byte),而计算带宽的单位是bit,1Byte=8bit
- 这个计算的是平均带宽,高峰期还需要扩大一定倍数PV、QPS、并发
- QPS:每秒处理的请求数量。
- 比如你的程序处理一个请求平均需要0.1S,那么1秒就可以处理10个请求。QPS自然就是10,多线程情况下,这个数字可能就会有所增加。
- 由PV和QPS如何需要部署的服务器数量?
- 根据二八原则,80%的请求集中在20%的时间来计算峰值压力:
- (每日PV * 80%) / (3600s * 24 * 20%) * 每个页面的请求数 = 每个页面每秒的请求数量
- 然后除以服务器的QPS值,即可计算得出需要部署的服务器数量
- QPS:每秒处理的请求数量。
- 项目开发流程
- 项目经理:管人
- 技术经理:
- 产品经理:设计需求原型
- 测试:
- 前端:大前端:UI 前端页面。mongodb\nodejs\reactjs\vuejs
- 后端:
- 移动端:
乐优商城介绍
项目介绍
- 乐优商城是一个全品类的电商购物网站(B2C)。
- 用户可以在线购买商品、加入购物车、下单
- 可以评论已购买商品
- 管理员可以在后台管理商品的上下架、促销活动
- 管理员可以监控商品销售状况
- 客服可以在后台处理退款操作
- 希望未来3到5年可以支持千万用户的使用
系统架构
后台管理:
- 后台系统主要包含以下功能:
- 商品管理,包括商品分类、品牌、商品规格等信息的管理
- 销售管理,包括订单统计、订单退款处理、促销活动生成等
- 用户管理,包括用户控制、冻结、解锁等
- 权限管理,整个网站的权限控制,采用JWT鉴权方案,对用户及API进行权限控制
- 统计,各种数据的统计分析展示
- 后台系统会采用前后端分离开发,而且整个后台管理系统会使用Vue.js框架搭建出单页应用(SPA)。
- 后台系统主要包含以下功能:
前台门户
- 前台门户面向的是客户,包含与客户交互的一切功能。例如:
- 搜索商品
- 加入购物车
- 下单
- 评价商品等等
- 前台系统我们会使用Thymeleaf模板引擎技术来完成页面开发。出于SEO优化的考虑,我们将不采用单页应用。
- 前台门户面向的是客户,包含与客户交互的一切功能。例如:
无论是前台还是后台系统,都共享相同的微服务集群,包括:
- 商品微服务:商品及商品分类、品牌、库存等的服务
- 搜索微服务:实现搜索功能
- 订单微服务:实现订单相关
- 购物车微服务:实现购物车相关功能
- 用户中心:用户的登录注册等功能
- Eureka注册中心
- Zuul网关服务
- …
项目搭建
技术选型
前端技术:
- 基础的HTML、CSS、JavaScript(基于ES6标准)
- JQuery
- Vue.js 2.0以及基于Vue的框架:Vuetify(UI框架)
- 前端构建工具:WebPack
- 前端安装包工具:NPM
- Vue脚手架:Vue-cli
- Vue路由:vue-router
- ajax框架:axios
- 基于Vue的富文本框架:quill-editor
后端技术:
- 基础的SpringMVC、Spring 5.x和MyBatis3
- Spring Boot 2.0.7版本
- Spring Cloud 最新版 Finchley.SR2
- Redis-4.0
- RabbitMQ-3.4
- Elasticsearch-6.3
- nginx-1.14.2
- FastDFS - 5.0.8
- MyCat
- Thymeleaf
- mysql 5.6
开发环境
- IDE:我们使用Idea 2017.3 版本
- JDK:统一使用JDK1.8
- 项目构建:maven3.3.9以上版本即可(3.5.2)
- 版本控制工具:git
域名
我们在开发的过程中,为了保证以后的生产、测试环境统一。尽量都采用域名来访问项目。
一级域名:www.leyou.com,leyou.com leyou.cn
二级域名:manage.leyou.com/item , api.leyou.com
我们可以通过switchhost工具来修改自己的host对应的地址,只要把这些域名指向127.0.0.1,那么跟你用localhost的效果是完全一样的。
创建父工程
- new project-maven Group:com.leyou.parent Artifact:leyou Version:1.0.0-SNAPSHOT
- projectname leyou
- 引入依赖
<?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><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><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.7.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>1.8</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>5.1.32</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><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
创建EurekaServer
创建工程
- new Module-maven
- Group:com.leyou.registery Artifact:leyou-registery Version:1.0.0-SNAPSHOT
- next Module name:leyou-registery content-root: leyou\leyou-registery
添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>leyou</artifactId><groupId>com.leyou.parent</groupId><version>1.0.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.leyou.common</groupId><artifactId>leyou-registry</artifactId><version>1.0.0-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies>
</project>
注意:今天在运行程序的时候,一直报“java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present”的错误,
百度原因,发现是因为用了jdk12的缘故。因为JAXB-API是java ee的一部分,在jdk12中没有在默认的类路径中。从jdk9开始java引入了模块的概念, 可以使用模块命令–add-modles java.xml.bind引入jaxb-api。也可以选择另一种解决方法,在maven里面加入下面依赖,可以解决这个问题:
<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>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.0</version></dependency><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency>
编写启动类
@SpringBootApplication
@EnableEurekaServer
public class LeyouRegistryApplication {public static void main(String[] args) {SpringApplication.run(LeyouRegistryApplication.class, args);}
}
配置文件
server:port: 10086
spring:application:name: leyou-registry
eureka:client:service-url:defaultZone: http://127.0.0.1:${server.port}/eurekaregister-with-eureka: false # 把自己注册到eureka服务列表fetch-registry: false # 拉取eureka服务信息server:enable-self-preservation: false # 关闭自我保护eviction-interval-timer-in-ms: 5000 # 每隔5秒钟,进行一次服务列表的清理
创建Zuul网关
创建工程
- new Module-maven
Group:com.leyou.gateway Artifact:leyou-gateway Version:1.0.0-SNAPSHOT
Module name:leyou-gateway
添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>leyou</artifactId><groupId>com.leyou.parent</groupId><version>1.0.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.leyou.common</groupId><artifactId>leyou-gateway</artifactId><version>1.0.0-SNAPSHOT</version><dependencies><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><!-- springboot提供微服务检测接口,默认对外提供几个接口 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies>
</project>
编写启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class LeyouGatewayApplication {public static void main(String[] args) {SpringApplication.run(LeyouGatewayApplication.class, args);}
}
配置文件
server:port: 10010
spring:application:name: leyou-gateway
eureka:client:registry-fetch-interval-seconds: 5service-url:defaultZone: http://127.0.0.1:10086/eureka
zuul:prefix: /api # 路由路径前缀
创建商品微服务
包含对商品相关的一系列内容管理
- 商品分类管理
- 品牌管理
- 商品规格参数管理
- 商品管理
- 库存管理
微服务的结构
为了方便微服务之间相互调用,使用聚合工程将要提供的接口及相关实体类放到独立子工程中
在leyou-item中创建两个子工程:
- leyou-item-interface:主要是对外暴露的接口及相关实体类
- leyou-item-service:所有业务逻辑及内部使用接口
构建
- new Module-maven
Group:com.leyou.item Artifact:leyou-item Version:1.0.0-SNAPSHOT
Module name:leyou-item
依赖
因为是聚合工程,所以把项目打包方式设置为pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>leyou</artifactId><groupId>com.leyou.parent</groupId><version>1.0.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.leyou.item</groupId><artifactId>leyou-item</artifactId><version>1.0.0-SNAPSHOT</version><!-- 打包方式为pom --><packaging>pom</packaging></project>
leyou-item-interface
在leyou-item工程上点击右键,选择new --> module:
依然是使用maven构建,注意父工程是leyou-item:
Group:com.leyou.item Artifact:leyou-item-interface Version:1.0.0-SNAPSHOT
Module name:leyou-item-interface
注意:目录结构,保存到leyou-item
下的leyou-item-interface
目录中:
leyou-item-service
在leyou-item工程上点击右键,选择new --> module:
依然是使用maven构建,注意父工程是leyou-item:
Group:com.leyou.item Artifact:leyou-item-service Version:1.0.0-SNAPSHOT
Module name:leyou-item-service
注意:目录结构,保存到leyou-item
下的leyou-item-service
目录中:
添加依赖
- 考虑需要什么
- Eureka客户端
- web启动器
- mybatis启动器
- 通用mapper启动器
- 分页助手启动器
- 连接池,我们用默认的Hykira
- mysql驱动
- 千万不能忘了,我们自己也需要
ly-item-interface
中的实体类
这些依赖,我们在顶级父工程:leyou中已经添加好了。所以直接引入即可:
<?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><groupId>com.leyou.item</groupId><artifactId>leyou-item-service</artifactId><version>1.0.0-SNAPSHOT</version><dependencies><!-- web启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis的启动器 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><!-- 通用mapper启动器 --><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId></dependency><!-- 分页助手启动器 --><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId></dependency><!-- jdbc启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.leyou.item</groupId><artifactId>leyou-item-interface</artifactId><version>1.0.0-SNAPSHOT</version></dependency><!-- springboot检测服务启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies></project>
leyou-item-interface中需要什么我们暂时不清楚,所以先不管。以后需要什么依赖,再引入。
编写启动和配置
在整个leyou-item工程
中,只有leyou-item-service
是需要启动的。因此在其中编写启动类即可:
@SpringBootApplication
@EnableDiscoveryClient
public class LeyouItemServiceApplication {public static void main(String[] args) {SpringApplication.run(LeyouItemServiceApplication.class, args);}
}
然后是全局属性文件:
server:port: 8081
spring:application:name: item-servicedatasource:url: jdbc:mysql://localhost:3306/leyouusername: rootpassword: roothikari:max-lifetime: 28830000 # 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';)maximum-pool-size: 9 # 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eurekainstance:lease-renewal-interval-in-seconds: 5 # 5秒钟发送一次心跳lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
添加商品微服务的路由规则
既然商品微服务已经创建,接下来肯定要添加路由规则到Zuul中,我们不使用默认的路由规则。
修改leyou-gateway工程的application.yml配置文件:
zuul:prefix: /api # 路由路径前缀routes:item-service: /item/** # 商品微服务的映射路径
测试路由规则为了测试路由规则是否畅通,我们是不是需要在item-service中编写一个controller接口呢?
其实不需要,SpringBoot提供了一个依赖:actuator
只要我们添加了actuator的依赖,它就会为我们生成一系列的访问接口:
- /info
- /health
- /refresh
- …
添加依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
重启后访问Eureka控制台:
添加通用工具模块
右键leyou工程,使用maven来构建module:
Group:com.leyou.common Artifact:leyou-common Version:1.0.0-SNAPSHOT
Module name:leyou-common
结构
ES6语法指南
就是ECMAScript第6版标准。
新建空工程 demo-es6
新建模块、Static Web -StaticWeb
let 和 const 命令
var 定义的变量可能变为循环变量
let定义的变量只在代码块内有效
const声明的变量是常量,不能被修改
字符串扩展
新API
includes()
:返回布尔值,表示是否找到了参数字符串。startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。
字符串模板:在两个`之间的部分都会被作为字符串的值,不管你任意换行,甚至加入js脚本
解构表达式
数组解构
比如对一个数组 原本智能通过角标访问
现在可以直接赋值
let arr = [1,2,3]
const [x,y,z] = arr;// x,y,z将与arr中的每个位置对应来取值
// 然后打印
console.log(x,y,z); //得1 2 3
对象解构
const person = {
name:“jack”,
age:21,
language: [‘java’,‘js’,‘css’]
}
const {name,age,language} = person; 各自完成赋值
如果要用其他变量接收,需要额外指定别名
const {name:n} =person
{name:n}
:name是person中的属性名,冒号后面的n是解构后要赋值给的变量。
函数优化
参数默认值
在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
function add(a , b) {// 判断b是否为空,为空就给默认值1b = b || 1;return a + b;}// 传一个参数console.log(add(10));//nowfunction add(a , b = 1) {return a + b;
}
// 传一个参数
console.log(add(10));
箭头参数
一个参数时:
var print = function (obj) {console.log(obj);
}
// 简写为:
var print2 = obj => console.log(obj);
多个参数:
// 两个参数的情况:
var sum = function (a , b) {return a + b;
}
// 简写为:
var sum2 = (a,b) => a+b;
代码不止一行,可以用{}
括起来
var sum3 = (a,b) => {return a + b;
}
对象的函数属性简写
比如一个Person对象,里面有eat方法:
let person = {name: "jack",// 以前:eat: function (food) {console.log(this.name + "在吃" + food);},// 箭头函数版:eat2: food => console.log(person.name + "在吃" + food),// 这里拿不到this// 简写版:eat3(food){console.log(this.name + "在吃" + food);}
}
箭头函数结合解构表达式
比如有一个函数:
const person = {name:"jack",age:21,language: ['java','js','css']
}function hello(person) {console.log("hello," + person.name)
}
如果用箭头函数和解构表达式
var hi = ({name}) => console.log("hello," + name);
map和reduce
map
map()
:接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
举例:有一个字符串数组,我们希望转为int数组
let arr = ['1','20','-5','3'];
console.log(arr)arr = arr.map(s => parseInt(s));
console.log(arr)
reduce
reduce()
:接收一个函数(必须)和一个初始值(可选)。
第一个参数(函数)接收两个参数:
- 第一个参数是上一次reduce处理的结果
- 第二个参数是数组中要处理的下一个元素
reduce()
会从左到右依次把数组中的元素用reduce处理,并把处理的结果作为下次reduce的第一个参数。如果是第一次,会把前两个元素作为计算参数,或者把用户指定的初始值作为起始参数
举例:
const arr = [1,20,-5,3]
没有初始值:
arr.reduce((a,b)=> a+b) //19
指定初始值:
arr.reduce((a,b)=> a*b,1) //-300
对象扩展
ES6给Object拓展了许多新的方法,如:
- keys(obj):获取对象的所有key形成的数组
- values(obj):获取对象的所有value形成的数组
- entries(obj):获取对象的所有key和value形成的二维数组。格式:
[[k1,v1],[k2,v2],...]
- assign(dest, …src) :将多个src对象的值 拷贝到 dest中(浅拷贝)。
数组扩展
- find(callback):数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
- findIndex(callback):数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
- includes(数组元素):与find类似,如果匹配到元素,则返回true,代表找到了。
Vue入门
MVVM
基于事件循环的异步IO框架:Node.js
在node的基础上
都是MVC的分支
- M:Model,模型,包括数据和一些基础操作
- V:即View,视图、页面渲染结果
- VM:View-Model,模型与视图间的双向操作(无需开发人员干涉)
在MVVM之前,开发人员从后端获取需要的数据模型,然后要通过DOM操作Model渲染到View中。而后当用户操作视图,我们还需要通过DOM获取View中的数据,然后同步到Model中。
而MVVM中的VM要做的事情就是把DOM操作完全封装起来,开发人员不用再关心Model和View之间是如何互相影响的:
- 只要我们Model发生了改变,View上自然就会表现出来。
- 当用户修改了View,Model中的数据也会跟着改变。
认识Vue
Vue (读音 /vjuː/,类似于 view) 用于构建用户界面的渐进式框架
Node和NPM
NPM是Node提供的模块管理工具,可以非常方便的下载安装很多前端框架,包括Jquery、AngularJS、VueJs都有。为了后面学习方便,我们先安装node及NPM工具。
下载Node.js
https://nodejs.org/en/
查看node版本信息:控制台输入node-v
node自带NPM //dos 用命令 npm-v 可查看
npm默认的仓库地址是在国外网站,速度较慢,建议大家设置到淘宝镜像。但是切换镜像是比较麻烦的。推荐一款切换镜像的工具:nrm
- dos中输入 npm install nrm -g //-g 代表全局安装
- 通过
nrm ls
命令查看npm的仓库列表,带*的就是当前选中的镜像仓库: - 通过
nrm use taobao
来指定要使用的镜像源: - 然后通过
nrm test npm
来测试速度:
入门案例
new module static web
执行命令方式
- 可进入目录下 用窗口执行命令 hm49\code\demo-es6\hello-vue 打开cmd窗口
- 工具执行命令:Terminal cd hello-vue
- package.json 其中内容相当于依赖
- npm install vue -save //本地安装
Vue声明式渲染
<body>
<!--vue对象的html模板--><div id="app"><!--花括号内是js表达式--><h2>{{name}},非常帅!!!</h2></div>
</body>
<script src="node_modules/vue/dist/vue.js" ></script> <!--引入一定要全写不能简写-->
<script>// 创建vue实例var app = new Vue({el:"#app", // el即element,该vue实例要渲染的页面元素data:{ // 渲染页面需要的数据name: "峰哥"}});</script>
- 首先通过 new Vue()来创建Vue实例
- 然后构造函数接收一个对象,对象中有一些属性:
- el:是element的缩写,通过id选中要渲染的页面元素,本例中是一个div
- data:数据,数据是一个对象,里面有很多属性,都可以渲染到视图中
- name:这里我们指定了一个name属性
- 页面中的
h2
元素中,我们通过{{name}}的方式,来渲染刚刚定义的name属性。
双向绑定
我们对刚才的案例进行简单修改:
<body><div id="app"><input type="text" v-model="num"><h2>{{name}},非常帅!!!有{{num}}位女神为他着迷。</h2></div>
</body>
<script src="node_modules/vue/dist/vue.js" ></script>
<script>// 创建vue实例var app = new Vue({el: "#app", // el即element,该vue实例要渲染的页面元素data: { // 渲染页面需要的数据name: "峰哥",num: 5},methods:{incr(){this.num++;}}});</script>
- 我们在data添加了新的属性:
num
- 在页面中有一个
input
元素,通过v-model
与num
进行绑定。 - 同时通过
{{num}}
在页面输出
我们可以观察到,输入框的变化引起了data中的num的变化,同时页面输出也跟着变化。
- input与num绑定,input的value值变化,影响到了data中的num值
- 页面
{{num}}
与数据num绑定,因此num值变化,引起了页面效果变化。
没有任何dom操作,这就是双向绑定的魅力。
事件处理
我们在页面添加一个按钮:
<button v-on:click="num++">点我</button>
- 这里用
v-on
指令绑定点击事件,而不是普通的onclick
,然后直接操作num - 普通click是无法直接操作num的。
- num也可以是一个方法名
Vue实例
创建Vue实例
每个 Vue 应用都是通过用 Vue
函数创建一个新的 Vue 实例开始的:
var vm = new Vue({// 选项
})
在构造函数中传入一个对象,并且在对象中声明各种Vue需要的数据和方法,包括:
- el
- data
- methods
等等
模板元素
每个Vue实例都需要关联一段Html模板,Vue会基于此模板进行视图渲染。
我们可以通过el属性来指定。
数据
当Vue实例被创建时,它会尝试获取在data中定义的所有属性,用于视图的渲染,并且监视data中的属性变化,当data发生改变,所有相关的视图都将重新渲染,这就是“响应式“系统。
- name的变化会影响到
input
的值 - input中输入的值,也会导致vm中的name发生改变
方法
Vue实例中除了可以定义data属性,也可以定义方法,并且在Vue实例的作用范围内使用。
生命周期钩子
生命周期
每个 Vue 实例在被创建时都要经过一系列的初始化过程 :创建实例,装载模板,渲染模板等等。Vue为生命周期中的每个状态都设置了钩子函数(监听函数)。每当Vue实例处于不同的生命周期时,对应的函数就会被触发调用。
钩子函数
beforeCreated:我们在用Vue时都要进行实例化,因此,该函数就是在Vue实例化时调用,也可以将他理解为初始化函数比较方便一点,在Vue1.0时,这个函数的名字就是init。
created(常用):在创建实例之后进行调用。
beforeMount:页面加载完成,没有渲染。如:此时页面还是{{name}}
mounted:我们可以将他理解为原生js中的window.οnlοad=function({.,.}),或许大家也在用jquery,所以也可以理解为jquery中的$(document).ready(function(){….}),他的功能就是:在dom文档渲染完毕之后将要执行的函数,该函数在Vue1.0版本中名字为compiled。 此时页面中的{{name}}已被渲染成峰哥
beforeDestroy:该函数将在销毁实例前进行调用 。
destroyed:改函数将在销毁实例时进行调用。
beforeUpdate:组件更新之前。
updated:组件更新之后。
指令
指令 (Directives) 是带有 v-
前缀的特殊特性。指令特性的预期值是:单个 JavaScript 表达式。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
插值表达式:声明式渲染
花括号
格式:
{{表达式}}
说明:
- 该表达式支持JS语法,可以调用js内置函数(必须有返回值)
- 表达式必须有返回结果。例如 1 + 1,没有结果的表达式不允许使用,如:var a = 1 + 1;
- 可以直接获取Vue实例中定义的数据或函数
插值闪烁
花括号缺陷:使用{{}}方式在网速较慢时会出现问题。在数据未加载完成时,页面会显示出原始的{{}}
,加载完毕后才显示正确数据,我们称为插值闪烁。
v-text和v-html
使用v-text和v-html指令来替代{{}}
说明:
- v-text:将数据输出到元素内部,如果输出的数据有HTML代码,会作为普通文本输出
- v-html:将数据输出到元素内部,如果输出的数据有HTML代码,会被渲染
并且不会出现插值闪烁,当没有数据时,会显示空白。
<div id="app">v-text:<span v-text="hello"></span> <br/>v-html:<span v-html="hello"></span>
</div>
JS:
var vm = new Vue({el:"#app",data:{hello: "<h1>大家好,我是峰哥</h1>"}
})
v-model:双向绑定
刚才的v-text和v-html可以看做是单向绑定,数据影响了视图渲染,但是反过来就不行。接下来学习的v-model是双向绑定,视图(View)和模型(Model)之间会互相影响。
既然是双向绑定,一定是在视图中可以修改数据,这样就限定了视图的元素类型。目前v-model的可使用元素有:
- input
- select
- textarea
- checkbox
- radio
- components(Vue中的自定义组件)
基本上除了最后一项,其它都是表单的输入项。
举例:
<div id="app"><input type="checkbox" v-model="language" value="Java" />Java<br/><input type="checkbox" v-model="language" value="PHP" />PHP<br/><input type="checkbox" v-model="language" value="Swift" />Swift<br/><h1>你选择了:{{language.join(',')}}</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var vm = new Vue({el:"#app",data:{language: []}})
</script>
- 多个
CheckBox
对应一个model时,model的类型是一个数组,单个checkbox值默认是boolean类型 - radio对应的值是input的value值
text
和textarea
默认对应的model是字符串select
单选对应字符串,多选对应也是数组
v-on:事件
基本用法
v-on指令用于给页面元素绑定事件。
语法:
v-on:事件名="js片段或函数名"
示例:
<div id="app"><!--事件中直接写js片段--><button v-on:click="num++">增加一个</button><br/><!--事件指定一个回调函数,必须是Vue实例中定义的函数--><button v-on:click="decrement">减少一个</button><br/><h1>有{{num}}个女神迷恋峰哥</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var app = new Vue({el:"#app",data:{num:100},methods:{decrement(){this.num--;}}})
</script>
另外,事件绑定可以简写,例如v-on:click='add'
可以简写为@click='add'
事件修饰符
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。修饰符是由点开头的指令后缀来表示的。
.stop
:阻止事件冒泡到父元素.prevent
:阻止默认事件发生*.capture
:使用事件捕获模式.self
:只有元素自身触发事件才执行。(冒泡或捕获的都不执行).once
:只执行一次
阻止默认事件
<div id="app"><!--右击事件,并阻止默认事件发生--><button v-on:contextmenu.prevent="num++">增加一个</button><br/><!--右击事件,不阻止默认事件发生--><button v-on:contextmenu="decrement($event)">减少一个</button><br/><h1>有{{num}}个女神迷恋峰哥</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var app = new Vue({el: "#app",data: {num: 100},methods: {decrement(ev) {// ev.preventDefault();this.num--;}}})
</script>
效果:(右键“增加一个”,不会触发默认的浏览器右击事件;右键“减少一个”,会触发默认的浏览器右击事件)
按键修饰符
在监听键盘事件时,我们经常需要检查常见的键值。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">
全部的按键别名:
.enter
*.tab
.delete
(捕获“删除”和“退格”键).esc
.space
.up
.down
.left
.right
组合按钮
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
例如:
<!-- Alt + C -->
<input @keyup.alt.67="clear"><!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
v-for:遍历
遍历数组
语法:
v-for="item in items"
- items:要遍历的数组,需要在vue的data中定义好。
- item:迭代得到的数组元素的别名
示例
<div id="app"><ul><li v-for="user in users">{{user.name}} - {{user.gender}} - {{user.age}}</li></ul>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var app = new Vue({el: "#app",data: {users:[{name:'柳岩', gender:'女', age: 21},{name:'峰哥', gender:'男', age: 18},{name:'范冰冰', gender:'女', age: 24},{name:'刘亦菲', gender:'女', age: 18},{name:'古力娜扎', gender:'女', age: 25}]},})
</script>
数组角标
在遍历的过程中,如果我们需要知道数组角标,可以指定第二个参数:
语法
v-for="(item,index) in items"
- items:要迭代的数组
- item:迭代得到的数组元素别名
- index:迭代到的当前元素索引,从0开始。
示例
<ul><li v-for="(user, index) in users">{{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}</li></ul>
遍历对象
v-for除了可以迭代数组,也可以迭代对象。语法基本类似
语法:
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
- 1个参数时,得到的是对象的属性值
- 2个参数时,第一个是属性值,第二个是属性名
- 3个参数时,第三个是索引,从0开始
示例:
<div id="app"><ul><li v-for="(value, key, index) in user">{{index + 1}}. {{key}} - {{value}}</li></ul>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var vm = new Vue({el:"#app",data:{user:{name:'峰哥', gender:'男', age: 18}}})
</script>
key
当 Vue.js 用 v-for
正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
这个功能可以有效的提高渲染的效率。
但是要实现这个功能,你需要给Vue一些提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
属性。理想的 key
值是每项都有的且唯一的 id。
示例:
<ul><li v-for="(item,index) in items" :key=index></li>
</ul>
- 这里使用了一个特殊语法:
:key=""
我们后面会讲到,它可以让你读取vue中的属性,并赋值给key属性 - 这里我们绑定的key是数组的索引,应该是唯一的
v-if和v-show:判断
基本使用
v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。
语法:
v-if="布尔表达式"
示例:
<div id="app"><button v-on:click="show = !show">点我呀</button><br><h1 v-if="show">看到我啦?!</h1><h1 v-show="show">看到我啦?!show</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var app = new Vue({el: "#app",data: {show: true}})
</script>
与v-for结合
当v-if和v-for出现在一起时,v-for优先级更高。也就是说,会先遍历,再判断条件。
修改v-for中的案例,添加v-if:
<ul><li v-for="(user, index) in users" v-if="user.gender == '女'">{{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}</li></ul>
v-else
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div id="app"><h1 v-if="Math.random() > 0.5">看到我啦?!if</h1><h1 v-else>看到我啦?!else</h1>
</div>
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>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
v-show
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS 属性 display
。
示例:
<div id="app"><!--事件中直接写js片段--><button v-on:click="show = !show">点击切换</button><br/><h1 v-if="show">你好</h1></div><script src="./node_modules/vue/dist/vue.js"></script><script type="text/javascript">var app = new Vue({el:"#app",data:{show:true}})</script>
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>
绑定class样式
HTML:
<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>
渲染后的效果:(具有active和hasError的样式)
对象语法
我们可以传给 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不存在。
绑定style样式
数组语法
数组语法可以将多个样式对象应用到同一个元素上:
<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
计算属性
在插值表达式中使用js表达式是非常方便的,而且也经常被用到。
但是如果表达式的内容很长,就会显得不够优雅,而且后期维护起来也不方便,例如下面的场景,我们有一个日期的数据,但是是毫秒值:
data:{birthday:1529032123201 // 毫秒值
}
我们在页面渲染,希望得到yyyy-MM-dd的样式:
<h1>您的生日是:{{new Date(birthday).getFullYear() + '-'+ new Date(birthday).getMonth()+ '-' + new Date(birthday).getDay()}}
</h1>
在使用方法时需要加括号
虽然能得到结果,但是非常麻烦。
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();}}
})
- 计算属性本质就是方法,但是一定要返回数据。然后页面渲染时,可以把这个方法当成一个变量来使用。
页面使用:
<div id="app"><h1>您的生日是:{{birth}} </h1></div>
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要birthday
还没有发生改变,多次访问 birthday
计算属性会立即返回之前的计算结果,而不必再次执行函数。
watch
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>
组件化
在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。
但是如果每个页面都独自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同页面就可以共享这些组件,避免重复开发。
在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>
- 组件其实也是一个Vue实例,因此它在定义时也会接收:data、methods、生命周期函数等
- 不同的是组件不会与页面的元素绑定(不会相互影响),否则就无法复用了,因此没有el属性。
- 但是组件渲染需要html模板,所以增加了template属性,值就是HTML模板
- 全局组件定义完毕,任何vue实例都可以直接在HTML中通过组件名称来使用组件了。
- data必须是一个函数,不再是一个对象。
组件的复用
定义好的组件,可以任意复用多次:
<div id="app"><!--使用定义好的全局组件--><counter></counter><counter></counter><counter></counter>
</div>
你会发现每个组件互不干扰,都有自己的count值。怎么实现的?
组件的data属性必须是函数!
当我们定义这个 <counter>
组件时,它的data 并不是像之前直接提供一个对象:
data: {count: 0
}
取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {return {count: 0}
}
如果 Vue 没有这条规则,点击一个按钮就会影响到其它所有实例!
局部组件
一旦全局注册,就意味着即便以后你不再使用这个组件,它依然会随着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 // 将定义的对象注册为组件}
})
- components就是当前vue对象子组件集合。
- 其key就是子组件名称
- 其值就是组件对象名
- 效果与刚才的全局注册是类似的,不同的是,这个counter组件只能在当前的Vue实例中使用
组件通信
props(父向子传递)
- 父组件使用子组件时,自定义属性(属性名任意,属性值为要传递的数据)
- 子组件通过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>
props验证
我们定义一个子组件,并接收复杂数据:
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}}};
- 这个子组件可以对 items 进行迭代,并输出到页面。
- props:定义需要从父组件中接收的属性
- items:是要接收的属性名称
- type:限定父组件传递来的必须是数组
- default:默认值
- required:是否必须
- items:是要接收的属性名称
当 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>
子向父的通信
来看这样的一个案例:
<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>
- 子组件接收父组件的num属性
- 子组件定义点击按钮,点击后对num进行加或减操作
我们尝试运行,好像没问题,点击按钮试试:
子组件接收到父组件属性后,默认是不允许修改的。怎么办?
既然只有父组件能修改,那么加和减的操作一定是放在父组件:
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()函数,用来调用父组件绑定的函数
效果:
路由v-router
编写登录注册页面
一个页面,包含登录和注册,点击不同按钮,实现登录和注册页切换:
编写父组件
为了让接下来的功能比较清晰,我们先新建一个文件夹:src
然后新建一个HTML文件,作为入口:index.html
然后编写页面的基本结构:
<div id="app"><span>登录</span><span>注册</span><hr/><div>登录页/注册页</div>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">var vm = new Vue({el:"#app"})
</script>
编写登录注册组件
接下来我们来实现登录组件,以前我们都是写在一个文件中,但是为了复用性,开发中都会把组件放入独立的JS文件中,我们新建一个user目录以及login.js及register.js:
编写组件,这里我们只写模板,不写功能。
login.js内容如下:
const loginForm = {template:'\<div>\<h2>登录页</h2> \用户名:<input type="text"><br/>\密码:<input type="password"><br/>\</div>\'
}
register.js内容:
const registerForm = {template:'\<div>\<h2>注册页</h2> \用 户 名:<input type="text"><br/>\密  码:<input type="password"><br/>\确认密码:<input type="password"><br/>\</div>\'
}
在父组件中引用
<div id="app"><span>登录</span><span>注册</span><hr/><div><!--<loginForm></loginForm>--><!--疑问:为什么不采用上面的写法?由于html是大小写不敏感的,如果采用上面的写法,则被认为是<loginform></loginform>所以,如果是驼峰形式的组件,需要把驼峰转化为“-”的形式--><login-form></login-form><register-form></register-form></div>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script src="user/login.js"></script>
<script src="user/register.js"></script>
<script type="text/javascript">var vm = new Vue({el: "#app",components: {loginForm,registerForm}})
</script>
问题:
我们期待的是,当点击登录或注册按钮,分别显示登录页或注册页,而不是一起显示。
但是,如何才能动态加载组件,实现组件切换呢?
虽然使用原生的Html5和JS也能实现,但是官方推荐我们使用vue-router模块。
vue-router简介和安装
使用vue-router和vue可以非常方便的实现 复杂单页应用的动态路由功能。
官网:https://router.vuejs.org/zh-cn/
使用npm安装:npm install vue-router --save
在index.html中引入依赖:
<script src="../node_modules/vue-router/dist/vue-router.js"></script>
快速入门
新建vue-router对象,并且指定路由规则:
// 创建VueRouter对象
const router = new VueRouter({routes:[ // 编写路由规则{path:"/login", // 请求路径,以“/”开头component:loginForm // 组件名称},{path:"/register",component:registerForm}]
})
- 创建VueRouter对象,并指定路由参数
- routes:路由规则的数组,可以指定多个对象,每个对象是一条路由规则,包含以下属性:
- path:路由的路径
- component:组件名称
在父组件中引入router对象:
var vm = new Vue({el:"#app",components:{// 引用登录和注册组件loginForm,registerForm},router // 引用上面定义的router对象
})
页面跳转控制:
<div id="app"><!--router-link来指定跳转的路径--><span><router-link to="/login">登录</router-link></span><span><router-link to="/register">注册</router-link></span><hr/><div><!--vue-router的锚点--><router-view></router-view></div>
</div>
- 通过
<router-view>
来指定一个锚点,当路由的路径匹配时,vue-router会自动把对应组件放到锚点位置进行渲染 - 通过
<router-link>
指定一个跳转链接,当点击时,会触发vue-router的路由功能,路径中的hash值会随之改变
注意:单页应用中,页面的切换并不是页面的跳转。仅仅是地址最后的hash值变化。
事实上,我们总共就一个HTML:index.html
Vue总结
html模板
- 插值表达式:声明式渲染
- {{js表达式、数据模型}}:js表达式必须有返回值,出现插值闪烁
v-text:通常使用该方式
v-html:解析html,js,css。安全隐患
- {{js表达式、数据模型}}:js表达式必须有返回值,出现插值闪烁
- 双向渲染:双向绑定
- v-model=“数据模型”;在表单元素中使用才有意义
- 事件:v-on 简写@
- @click 点击事件
@contextMenu 右击事件,事件修饰符:.prevent:禁用默认事件
@keyup:键盘事件- .enter(13):回车键盘事件
组合事件
- .enter(13):回车键盘事件
- @click 点击事件
- v-for: 遍历集合或者对象
- 数组:v-for="(item,index) in items"
对象:v-for="(val, key, index) in user"
:key提高渲染速度
- 数组:v-for="(item,index) in items"
- v-if:判断
- v-if=“布尔表达式”:true-渲染,false-不渲染
v-show=“布尔表达式”:总是渲染:false-display:none
v-else-if=“布尔表达式”
v-else 一定要紧跟在if之后
- v-if=“布尔表达式”:true-渲染,false-不渲染
- v-bind:绑定属性:简写(
乐优商城个人笔记上-主要框架、基础知识、管理系统代码相关推荐
- 乐优商城学习笔记五-商品规格管理
0.学习目标 了解商品规格数据结构设计思路 实现商品规格查询 了解SPU和SKU数据结构设计思路 实现商品查询 了解商品新增的页面实现 独立编写商品新增后台功能 1.商品规格数据结构 乐优商城是一个全 ...
- 乐优商城个人笔记中-商城系统框架知识
搭建前台系统 静态资源 new project -> Static Web- Static Web 工程保存到hm49\code\leyou-portal 课程的leyou-portal文件解压 ...
- 乐优商城:笔记(六):上传微服务:LyUpload
文章目录 1 项目搭建 1.1 引入依赖 1.2 配置文件 1.3 启动类 2 实现文件上传功能 2.1 分布式文件上传系统 2.1.1 分布式文件系统 2.1.2 FastDFS 2.1.3 Fas ...
- 乐优商城学习笔记十五-搜索微服务(三)
3.页面分页效果 刚才的查询中,我们默认了查询的页码和每页大小,因此所有的分页功能都无法使用,接下来我们一起看看分页功能条该如何制作. 这里要分两步, 第一步:如何生成分页条 第二步:点击分页按钮,我 ...
- 乐优商城学习笔记十九-商品详情(二)
2.页面静态化 2.1.简介 2.1.1.问题分析 现在,我们的页面是通过Thymeleaf模板引擎渲染后返回到客户端.在后台需要大量的数据查询,而后渲染得到HTML页面.会对数据库造成压力,并且请求 ...
- 【javaWeb微服务架构项目——乐优商城day03】——(搭建后台管理前端,Vuetify框架,使用域名访问本地项目,实现商品分类查询,cors解决跨域,品牌的查询)
乐优商城day03 0.学习目标 1.搭建后台管理前端 1.1.导入已有资源 1.2.安装依赖 1.3.运行一下看看 1.4.目录结构 1.5.调用关系 2.Vuetify框架 2.1.为什么要学习U ...
- 乐优商城源码/数据库及笔记总结
文章目录 1 源码 2 笔记 2.1 项目概述 2.2 微服务 3 项目优化 4 项目或学习过程中涉及到的设计模式 5 安全问题 6 高内聚低耦合的体现 7 项目中待优化的地方 1 源码 Github ...
- 乐优商城笔记六:商品详情页
使用模板引擎 Thymeleaf + nginx 完成商品详情页静态化 完成乐优商城商品详情页 搭建商品详情页微服务 创建子工程 GroupId:com.leyou.service ArtifactI ...
- 【javaWeb微服务架构项目——乐优商城day15】——会调用订单系统接口,实现订单结算功能,实现微信支付功能
0.学习目标 会调用订单系统接口 实现订单结算功能 实现微信支付功能 源码笔记及资料: 链接:https://pan.baidu.com/s/1_opfL63P1pzH3rzLnbFiNw 提取码:v ...
- 【javaWeb微服务架构项目——乐优商城day05】——商品规格参数管理(增、删、改,查已完成),SPU和SKU数据结构,商品查询
乐优商城day05 0.学习目标 1.商品规格数据结构 1.1.SPU和SKU 1.2.数据库设计分析 1.2.1.思考并发现问题 1.2.2.分析规格参数 1.2.3.SKU的特有属性 1.2.4. ...
最新文章
- IOS使用正则表达式去掉html中的标签元素,获得纯文本
- 【 MATLAB 】filter 函数介绍 之 Filter Data in Sections
- Leetcode970. Powerful Integers强整数
- word树状分支图_交互设计技能 | 抛弃Word,试试用Excel和Xmind来整理思路吧
- 413 Request Entity Too Large
- class文件的产生过程
- setGeometry
- Spring aop优雅实现redis分布式锁 aop应用redis分布式锁
- 有滋有味了freeeim
- Java(多)线程中注入Spring的Bean
- 大多数物联网仍采用2.4GHz频段的原因
- java中多态含有math类_Java面试题汇总《Java基础、语法51-55》
- PAT 乙级A1025 适合当算法入门练习题做
- 如何用计算机辅助设计进行设计,计算机辅助设计的基本概念和特点
- ORACLE安装之环境搭建
- cpe动态ip,做端口映射方案
- RocketMQ——顺序消息
- Jxl解析Excel表格数据
- C++大数乘加减除比较操作集(含测试原码)
- 开源智能电子名片系统源码 含小程序完整前后端+搭建教程
热门文章
- 乐优商城学习笔记五-商品规格管理