前沿:思考一个问题,为啥要做笔记?
为了知识更有条理,为了自己学过之后下次遇到立刻可以想起来,即使想不起,也可以通过自己的笔记快速定位~ 毕竟互联网的知识迭代速度非常之快
笔记更是知识输入的一条路径,没有输入就难以自我成长~
一、项目简介
1、项目背景
市面上有5 种常见的电商模式 B2B、B2C、C2B、C2C、O2O;
1、B2B 模式
B2B (Business to Business), 是指商家与商家建立的商业关系。如:阿里巴巴
2、B2C 模式
B2C (Business to Consumer), 就是我们经常看到的供应商直接把商品卖给用户,即“商对客”模式,也就是通常说的商业零售,直接面向消费者销售产品和服务。如:苏宁易购、京东、天猫、小米商城
3、C2B 模式
C2B (Customer to Business),即消费者对企业。先有消费者需求产生而后有企业生产,即先有消费者提出需求,后有生产企业按需求组织生产
4、C2C 模式
C2C (Customer to Consumer) ,客户之间自己把东西放上网去卖,如:淘宝,闲鱼
5、O2O 模式
O2O 即Online To Offline,也即将线下商务的机会与互联网结合在了一起,让互联网成为线下交易的前台。线上快速支付,线下优质服务。如:饿了么,美团,淘票票,京东到家
谷粒商城是一个B2C 模式的电商平台,销售自营商品给客户
2、项目架构图
项目微服务架构图:
微服务划分图:
3、项目技术&特色
前后分离开发,并开发基于vue 的后台管理系统 SpringCloud 全新的解决方案 应用监控、限流、网关、熔断降级等分布式方案全方位涉及 透彻讲解分布式事务、分布式锁等分布式系统的难点 分析高并发场景的编码方式,线程池,异步编排等使用 压力测试与性能优化 各种集群技术的区别以及使用 CI/CD 使用 …4、项目前置要求
学习项目的前置知识
熟悉SpringBoot 以及常见整合方案 了解SpringCloud 熟悉git,maven 熟悉linux,redis,docker 基本操作 了解html,css,js,vue 熟练使用idea 开发项目二、分布式基础概念
1、微服务
微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API
。这些服务围绕业务能力来构建,并通过完全自动化部署
机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。
简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
2、集群&分布式&节点
集群是个物理形态,分布式是个工作方式。
只要是一堆机器,就可以叫集群,他们是不是一起协作着干活,这个谁也不知道:
《分布式系统原理与范型》定义:
“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
分布式系统(distributed system)是建立在网络之上的软件系统。
分布式是指将不同的业务分布在不同的地方。
集群指的是将几台服务器集中在一起,实现同一业务。
例如:京东是一个分布式系统,众多业务运行在不同的机器
,所有业务构成一个大型的业务集群
。每一个小的业务,比如用户系统,访问压力大的时候一台服务器是不够的。我们就应该将用户系统部署到多个服务器,也就是每一个业务系统也可以做集群化
;分布式中的每一个节点,都可以做集群。而集群并不一定就是分布式的。
节点:集群中的一个服务器
3、远程调用
在分布式系统中,各个服务可能处于不同主机,但是服务之间不可避免的需要互相调用,我们称为远程调用。
SpringCloud 中使用HTTP+JSON
的方式完成远程调用
4、负载均衡
分布式系统中,A 服务需要调用B 服务,B 服务在多台机器中都存在,A 调用任意一个服务器均可完成功能。
为了使每一个服务器都不要太忙或者太闲,我们可以负载均衡的调用每一个服务器,提升网站的健壮性。
常见的负载均衡算法:
轮询
:为第一个请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择,直到最后一个,然后循环。最小连接
:优先选择连接数最少,也就是压力最小的后端服务器,在会话较长的情况下可以考虑采取这种方式。散列
:根据请求源的IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器,可以考虑采取这种方式。 5、服务注册/发现&注册中心
A 服务调用B 服务,A 服务并不知道B 服务当前在哪几台服务器有,哪些正常的,哪些服务已经下线。解决这个问题可以引入注册中心;
如果某些服务下线,我们其他人可以实时的感知到其他服务的状态,从而避免调用不可用的服务
服务已上线就会被注册到注册中心,一下线就会从注册中心移除~
A服务想要调用B服务,就先去注册中心看一下哪些机器上有B服务,从而再根据策略进行调用,避免服务不可调用的情况
6、配置中心
每一个服务最终都有大量的配置,并且每个服务都可能部署在多台机器上。我们经常需要变更配置,我们可以让每个服务在配置中心获取自己的配置。
配置中心用来集中管理微服务的配置信息
7、服务熔断&服务降级
在微服务架构中,微服务之间通过网络进行通信,存在相互依赖,当其中一个服务不可用时,有可能会造成雪崩效应。要防止这样的情况,必须要有容错机制来保护服务。
1)、服务熔断
a. 设置服务的超时,当被调用的服务经常失败到达某个阈值,我们可以开启断路保护
机制,后来的请求不再去调用这个服务。本地直接返回默认的数据
2)、服务降级
a. 在运维期间,当系统处于高峰期,系统资源紧张,我们可以让非核心业务降级
运行。降级:某些服务不处理,或者简单处理【抛异常、返回NULL、调用Mock 数据、调用Fallback 处理逻辑】。
8、API 网关
在微服务架构中,API Gateway 作为整体架构的重要组件,它抽象了微服务中都需要的公共功能,同时提供了客户端负载均衡,服务自动熔断,灰度发布,统一认证,限流流控,日志统计
等丰富的功能,帮助我们解决很多API 管理难题。
三、环境搭建
1、安装linux 虚拟机
老师使用的是VirtualBox搭建的,中间可能会有好多问题吧~ 建议使用VMware+Centos搭建,具体文档可参考尚硅谷韩顺平老师的Linux课程~
2、安装docker
参考文档:https://blog.csdn.net/LXYDSF/article/details/121514373
3、docker 安装MySQL
① 下载镜像文件
docker pull mysql:5.7
② 创建并启动MySQL实例
docker run -p 3306:3306 --name mysql -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/data:/var/lib/docker_mysql -v /mydata/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
参数说明:
-p 3306:3306:将容器的3306 端口映射到主机的3306 端口
-v /mydata/mysql/conf:/etc/mysql:将配置文件夹挂载到主机
-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机
-e MYSQL_ROOT_PASSWORD=root:初始化root 用户的密码
③ 修改MySQL 配置
vi /mydata/mysql/conf/my.cnf[client]default-character-set=utf8[mysql]default-character-set=utf8[mysqld]init_connect='SET collation_connection = utf8_unicode_ci'init_connect='SET NAMES utf8'character-set-server=utf8collation-server=utf8_unicode_ciskip-character-set-client-handshakeskip-name-resolve
注意:解决MySQL 连接慢的问题
在配置文件中加入如下,并重启mysql
[mysqld]
skip-name-resolve #跳过域名解析
④通过容器的mysql 命令行工具连接
docker exec -it mysql mysql -uroot -p123456
⑤设置root 远程访问
grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option;flush privileges;
⑥进入容器文件系统
docker exec -it mysql /bin/bash
4、docker 安装Redis
① 下载镜像文件
docker pull redis
② 创建容器数据卷
mkdir -p /mydata/redis/conftouch /mydata/redis/conf/redis.conf
因为 Redis容器中 /etc/redis下面默认是没有redis.conf的,所以会导致挂在时 /mydata/redis/conf/redis.conf创建的会是一个目录。。。
解决办法:我们提前创建好配置文件
③ 创建并启动Redis实例
#创建并启动RedisL实例docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf
④使用redis 镜像执行redis-cli 命令连
docker exec -it redis redis-cli
5、开发环境统一
MavenIdea&VsCodeGit四、项目搭建
1、创建各个微服务项目
①从gitee 初始化一个项目
②使用SpringInitializer创建各个微服务项目
共同点:
1)、都需要web、openfeign工具
2)、每一个服务,包名com.atguigu.gulimall.xxx
(product/order/ware/coupon/member)
模块名:
gulimall-product
:商品服务
gulimall-ware
:仓储服务、
gulimall-order
:订单服务、
gulimall-coupon
:优惠券服务、
gulimall-member
:用户服务
注意:本次使用的Spring-boot版本为2.1.8.RELEASE
,SpringCloud版本为Greenwich.SR3
③整合项目
1、使gulimall聚合其他子项目
为gulimall父项目增加一个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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.atguigu.gulimall</groupId> <artifactId>gulimall</artifactId> <version>0.0.1-SNAPSHOT</version> <name>gulimall</name> <description>聚合服务</description> <packaging>pom</packaging> <modules> <module>gulimall-coupon</module> <module>gulimall-member</module> <module>gulimall-order</module> <module>gulimall-product</module> <module>gulimall-ware</module> </modules></project>
2、修改父项目的.gitignore
的模板,并提交到Github上
项目中的mvnw
mvnw.cmd
.mvn
/target/
.idea
均可以删掉,只用保留父项目的.gitignore
即可
2、数据库初始化
1、设置Docker容器开机自启动
[root@lxyStudy ~]# docker update redis --restart=alwaysredis[root@lxyStudy ~]# docker update mysql --restart=alwaysmysql
注意:前提需要Docker服务需要开启自启哦~
2、将资料中的SQL在数据库中执行
3、人人开源搭建后台管理系统
码云上搜“人人开源”,随便进一个项目,然后到它的主页
操作步骤:
1)克隆这两个项目。git clone xxxxxxxx
2)把renren-fast的后端项目里的.git删掉,然后整个项目拖进我们的guli后端项目中,并在pom里加上<module>renren-fast</module>
。然后启动该项目
3)在db文件夹下找到mysql.sql文件,导入gulimall_admin数据库。修改dev配置文件,如数据库的账号密码、库名
4)前端,把renren-fast-vue在VS Code中打开,npm install
,然后解决bug… 。之后npm run dev
启动项目
登录账号:admin 密码:admin
企业开发中不可能从0开始构建项目的,可以pull一个类似的开源项目,然后在里面增加一个我们需要的功能~
4、逆向工程搭建并生成所有微服务CRUD代码
①下载并导入 代码生成器:https://gitee.com/renrenio/renren-generator
② 创建gulimall-common项目,存放公共内容。如:下步导入生成CRUD代码使用的公共Utils、公共依赖:mysql-connector…
③ 修改renren-generator的application.yml,生成对应微服务的CRUD代码
运行以后会生成一个main文件夹和一堆sql文件。把生成的main文件夹复制到相应的模块中,替换原来src下的main。
resources中的src不要删除,后面会用到
修改renren-generator,对所有模块的CRUD代码进行生成~
④ 配置并测试每个项目
以商品模块为例:
applicaiton.ymlspring: datasource: username: root password: 123456 url: jdbc:mysql://192.168.174.128:3306/gulimall_pms driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus: mapper-locations: classpath:/mapper/**/*.xml #配置xml位置 global-config: db-config: id-type: auto #设置id自增
添加common依赖 <!--公共依赖--> <dependency> <groupId>com.atguigu.gulimall</groupId> <artifactId>gulimall-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
之后可以启动项目,然后调用某一个接口,如:http://localhost:8080/product/brand/list,判断是否访问成功
其他模块也进行如上的配置和修改~
⑤ 为每个服务配置端口
coupon 7000member 8000order 9000product 10000ware 11000
server: port: xxxx
目的:之后对相应服务进行横向扩展的话很方便,比如coupon在服务器上部署n个,则对应的端口分别为:7000、7001、7002、7003…
五、分布式组件
1、SpringCloud Alibaba 简介
1、简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
官方文档:https://github.com/alibaba/spring-cloud-alibaba
2、为什么使用
SpringCloud 的几大痛点
SpringCloud 部分组件停止维护和更新,给开发带来不便;SpringCloud 部分环境搭建复杂,没有完善的可视化界面,我们需要大量的二次开发和定制SpringCloud 配置复杂,难以上手,部分配置差别难以区分和合理应用SpringCloud Alibaba 的优势:
阿里使用过的组件经历了考验,性能强悍,设计合理,现在开源出来大家用成套的产品搭配完善的可视化界面给开发运维带来极大的便利搭建简单,学习曲线低。结合SpringCloud Alibaba 我们最终的技术搭配方案:
SpringCloud Alibaba - Nacos:注册中心(服务发现/注册)
SpringCloud Alibaba - Nacos:配置中心(动态配置管理)
SpringCloud - Ribbon:负载均衡
SpringCloud - Feign:声明式HTTP 客户端(调用远程服务)
SpringCloud Alibaba - Sentinel:服务容错(限流、降级、熔断)
SpringCloud - Gateway:API 网关(webflux 编程模式)
SpringCloud - Sleuth:调用链监控
SpringCloud Alibaba - Seata:原Fescar,即分布式事务解决方案
3、版本选择
由于Spring Boot 1 和Spring Boot 2 在Actuator 模块的接口和注解有很大的变更,且spring-cloud-commons 从1.x.x 版本升级到2.0.0 版本也有较大的变更,因此我们采取跟SpringBoot 版本号一致的版本:
1.5.x 版本适用于Spring Boot 1.5.x2.0.x 版本适用于Spring Boot 2.0.x2.1.x 版本适用于Spring Boot 2.1.x4、项目中的依赖
在gulimall-common 项目中引入如下。方便进行统一管理
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>
2、SpringCloud Alibaba-Nacos[作为注册中心]
Nacos 是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。他是使用java 编写。需要依赖java 环境。
Nacos Discovery Exaple:https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/readme-zh.md
1、引入依赖
修改common模块的 pom.xml 文件,引入 Nacos Discovery Starter。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
2、下载Nacos
参考文档:https://nacos.io/zh-cn/docs/quick-start.html
CSDN博客:https://blog.csdn.net/LXYDSF/article/details/122776664
3、添加配置
在application.yml中配置:
spring: cloud: nacos: discovery: server-addr: 192.168.174.128:8848 #自己的IP+端口 application: name: gulimall-order
为启动类添加 @EnableDiscoveryClient
注解,开启服务注册与发现功能
4、测试
启动服务,访问http://192.168.174.128:8848/nacos
3、Open-Feign远程调用
Feign是Netflix开发的声明式、模板化的HTTP客户端
, Feign可以帮助我们更快捷、优雅地调用HTTP API
。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。 举例:注册member模块。然后测试它和coupon模块间的互通。
1、引入依赖
我们想要会员服务远程调用优惠券服务,就要在会员服务的pom中引入open-feign依赖。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
2 在coupon中编写接口
@RequestMapping("/member/list")public R memberCoupons(){ CouponEntity coupon = new CouponEntity(); coupon.setCouponName("满100减10"); return R.ok().put("coupons",Arrays.asList(coupon));}
3、声明远程服务
在member服务的controller同级目录下新建一个feign目录,专门用来放远程调用的接口。
在该目录下新建一个接口,叫CouponFeignService
/** * 这是一个声明式的远程调用,告知SpringCloud这个接口需要远程服务 * @FeignClient 指定远程服务的name */@FeignClient("gulimall-coupon")public interface CouponFeignService { @RequestMapping("/coupon/coupon/member/list") R memberCoupons();}
4、主启动类里开启feign功能
为主启动类添加:@EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")
5、测试
在member服务的控制层写测试方法
@Autowired CouponFeignService couponFeignService; @RequestMapping("/coupons") public R test(){ MemberEntity member = new MemberEntity(); member.setNickname("张三"); R r = couponFeignService.memberCoupons(); return R.ok().put("member",member).put("coupons",r.get("coupons")); }
浏览器http://localhost:8000/member/member/coupons,有结果就是成功了~
4、SpringCloud Alibaba-Nacos[作为配置中心]
参考文档:https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md
1、引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
2、配置
增加bootstrap.properties
,并进行如下配置,以Coupon服务为例
# 服务名称spring.application.name=gulimall-coupon# 配置中心地址spring.cloud.nacos.config.server-addr=192.168.174.128:8848
3、写测试接口
application.properties增加数据:
coupon.user.name=张三coupon.user.age=20
这里我们使用 @Value 注解来将application.properties中写好的配置注入到控制层的 userName 和 age 字段
@Value("${coupon.user.name}")private String name;@Value("${coupon.user.age}")private Integer age;@RequestMapping("test")public R test(){ return R.ok().put("name",name).put("age",age);}
4、测试
访问:localhost:7000/coupon/coupon/test,便可以从网页中读取到配置文件中的内容
5、实际应用
如果直接在配置文件里改变量的值,那么每次修改后都要重新将项目部署上线,这样不好!可以利用nacos平台对我们的配置进行管理。
查看我们的启动日志,发现每次项目启动时都会从gulimall-coupon.properties
读取配置。
2021-06-10 19:19:26.694 INFO 2320 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='NACOS', propertySources=[NacosPropertySource {name='gulimall-coupon.properties'}]}
6、应用一:如何使用Nacos作为配置中心统一管理配置
在Nacos上创建gulimall-coupon.properties文件,内容和本地的那个相同@RefreshScope
打开动态刷新功能。进行测试:访问http://localhost:7000/coupon/coupon/test,发现可以读取到Nacos的gulimall-coupon.properties中的数据。数据修改后,重新访问,发现读取到的是最新的~ 7、应用二:实现环境配置的隔离。
企业开发中经常利用命名空间来做环境隔离:开发、测试、生产
然后使用哪个环境,就在bootstrap.properties里面添加:spring.cloud.nacos.config.namespace=xxxxxxx(命名空间id)
8、应用三:每个微服务之间隔离配置
每个微服务各自创建命名空间,使用Group来区分不同的环境
我们的项目采用的方案:每个微服务创建自己的命名空间,使用配置分组区分环境:dev、test、prod
9、应用四:使用配置集加载多个配置文件
如果配置比较多,我们可以把gulimall-properties分成:datasource.yml、mybatis.yml、other.yml…
# 命名空间spring.cloud.nacos.config.namespace=4aca1a7f-494c-438a-9399-e8d22d81624a# 分组[相当于指定默认gulimall-coupon.properties的group]spring.cloud.nacos.config.group=dev#加载配置文件spring.cloud.nacos.config.ext-config[0].data-id=datasource.ymlspring.cloud.nacos.config.ext-config[0].group=devspring.cloud.nacos.config.ext-config[0].refresh=truespring.cloud.nacos.config.ext-config[1].data-id=mybatis.ymlspring.cloud.nacos.config.ext-config[1].group=devspring.cloud.nacos.config.ext-config[1].refresh=truespring.cloud.nacos.config.ext-config[2].data-id=other.ymlspring.cloud.nacos.config.ext-config[2].group=devspring.cloud.nacos.config.ext-config[2].refresh=true
5、网关Gateway
1、简介
网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而springcloud gateway作为SpringCloud 官方推出的第二代网关框架,取代了Zuul 网关。
性能对比:
网关提供API 全托管服务,丰富的API 管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。
Spring Cloud Gateway 旨在提供一种简单而有效的方式来对API 进行路由,并为他们提供切面,例如:安全性,监控/指标和弹性等。
官方文档地址:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.3.RELEASE/single/spring-cloud-gateway.html
Spring Cloud Gateway 特点:
基于Spring5,支持响应式编程和SpringBoot2.0支持使用任何请求属性进行路由匹配特定于路由的断言和过滤器集成Hystrix 进行断路保护集成服务发现功能易于编写Predicates 和Filters支持请求速率限制支持路径重写思考:为什么使用API 网关?
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
客户端会多次请求不同的微服务,增加了客户端的复杂性。存在跨域请求,在一定场景下处理相对复杂。认证复杂,每个服务都需要独立认证。难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。某些微服务可能使用了防火墙/ 浏览器不友好的协议,直接访问会有一定的困难。以上这些问题可以借助API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由API 网关来做,这样既提高业务灵活性又不缺安全性。
使用API 网关后的优点如下:
易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。减少了客户端与各个微服务之间的交互次数。2、核心概念
路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter 组成。如果断言路由为真,则说明请求的URL 和配置匹配断言。Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是Spring5.0 框架中的ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于http request 中的任何信息,比如请求头和参数等。过滤器。一个标准的Spring webFilter。Spring cloud gateway 中的filter 分为两种类型的Filter,分别是Gateway Filter 和Global Filter。过滤器Filter 将会对请求和响应进行修改处理工作原理:
客户端向 Spring Cloud Gateway 发出请求。 如果 Gateway Handler Mapping 确定请求与路由匹配,则将其发送到 Gateway WebHandler。这个WebHandler 将请求交给一个过滤器链,请求到达目标服务之前,会执行所有过滤器的pre 方法。请求到达目标服务处理之后再依次执行所有过滤器的post 方法。
一句话:请求到达网关,断言判断这个请求是否符合路由规则,如果符合,则按照这个规则 经过一系列filter过滤最终 到指定地方===》满足某些断言(predicates)就路由到指定的地址(uri),使用指定的过滤器(filter)
3、使用
1、新建springboot模块,命名为gulimall-gateway
2、导入gateway依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId></dependency>
3、进行配置
application.ymlserver: port: 88spring: application: name: gulimall-gateway cloud: nacos: discovery: server-addr: 192.168.174.128:8848 namespace: d31f10b9-a4da-4cb1-ad4b-d76e31db9d09
bootstrap.properties ## 服务名称spring.application.name=gulimall-gateway# 配置中心地址spring.cloud.nacos.config.server-addr=192.168.174.128:8848# 配置namespacespring.cloud.nacos.config.namespace=d31f10b9-a4da-4cb1-ad4b-d76e31db9d09# 指定groupspring.cloud.nacos.config.group=dev# 指定后缀spring.cloud.nacos.config.file-extension=yml
使用配置中心,在项目上线后,就可以把application.yml移到Nacos中:gulimall-gatway.yml
4、配置路由并启动测试
参考官网教程,进行路由配置
spring: cloud: gateway: routes: - id: baidu_route uri: https://www.baidu.com/ predicates: - Query=url,baidu - id: qq_route uri: https://www.qq.com/ predicates: - Query=url,qq
访问:http://localhost:88/index.html?url=baidu --> 跳转到 www.baidu.com
访问:http://localhost:88/index.html?url=qq —>跳转到www.qq.com
过程中遇到的坑点:数据库报错
解决办法:
方法一:网关引入common时排除mybatis plus
<exclusion> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </exclusion>
方法二:注解里排除:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
4、补充:断言和路由器
断言:
过滤器:
六、前端开发基础知识
1、VSCode 使用
1、安装常用插件
安装以下基本插件
2、创建项目
vscode 很轻量级,本身没有新建项目的选项,创建一个空文件夹就可以当做一个项目
3、创建网页
创建文件,命名为index.html
快捷键! ,快速创建网页模板
h1 + 回车,自动补全标签
4、运行效果
如果使用live server,页面内容变化,保存以后,浏览器会自动变化;
2、ES6
1、简介
ECMAScript 6.0(以下简称ES6,ECMAScript 是一种由Ecma 国际(前身为欧洲计算机制造商协会,英文名称是European Computer Manufacturers Association)通过ECMA-262 标准化的脚本程序设计语言)是JavaScript 语言的下一代标准,已经在2015 年6 月正式发布了,并且从ECMAScript 6 开始,开始采用年号来做版本。即ECMAScript 2015,就是ECMAScript6。
它的目标,是使得JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。每年一个新版本。
2、什么是ECMAScript
来看下前端的发展历程:
web1.0 时代:最初的网页以HTML 为主,是纯静态的网页。网页是只读的,信息流只能从服务的到客户端单向流通。开发人员也只关心页面的样式和内容即可。web2.0 时代: 1995 年,网景工程师Brendan Eich 花了10 天时间设计了JavaScript 语言。1996 年,微软发布了JScript,其实是JavaScript 的逆向工程实现。1996 年11 月,JavaScript 的创造者Netscape 公司,决定将JavaScript 提交给标准化组织ECMA,希望这种语言能够成为国际标准。1997 年,ECMA 发布262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是1.0 版。JavaScript 和JScript 都是
ECMAScript
的标准实现者,随后各大浏览器厂商纷纷实现了ECMAScript
标准。 所以,ECMAScript 是浏览器脚本语言的规范,而各种我们熟知的js 语言,如JavaScript 则是规范的具体实现。
3、ES6 新特性
1、let 声明变量
// var 声明的变量往往会越域 // let 声明的变量有严格局部作用域 { var a = 1; let b = 2; } console.log(a); // 1 console.log(b); // ReferenceError: b is not defined // var 可以声明多次 // let 只能声明一次 var m = 1 var m = 2 let n = 3 // let n = 4 console.log(m) // 2 console.log(n) // Identifier 'n' has already been declared // var 会变量提升 // let 不存在变量提升 console.log(x); // undefined var x = 10; console.log(y); //ReferenceError: y is not defined let y = 20;
2、const 声明常量(只读变量)
// 1. 声明之后不允许改变 // 2. 一但声明必须初始化,否则会报错 const a = 1; a = 3; //Uncaught TypeError: Assignment to constant variable.
总结:
let(严谨) | var(不严谨) | const |
---|---|---|
局部变量 | 全局变量 | |
同个变量不能重复声明 | 可以多次声明 | 变量不能改 |
3、解构表达式
1)、数组解构
let arr = [1,2,3]; //以前我们想获取其中的值,只能通过角标。ES6 可以这样: const [x,y,z] = arr;// x,y,z 将与arr 中的每个位置对应来取值 // 然后打印 console.log(x,y,z);
2)、对象解构
const person = { name: "jack", age: 21, language: ['java', 'js', 'css'] } // 解构表达式获取值,将person 里面每一个属性和左边对应赋值 const { name, age, language } = person; // 等价于下面 // const name = person.name; // const age = person.age; // const language = person.language; // 可以分别打印 console.log(name); console.log(age); console.log(language); //扩展:如果想要将name 的值赋值给其他变量,可以如下,nn 是新的变量名 const { name: nn, age, language } = person; console.log(nn); console.log(age); console.log(language);
4、字符串扩展
1)、几个新的API
ES6 为字符串扩展了几个新的API:
includes()
:返回布尔值,表示是否找到了参数字符串。startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。 console.log(str.startsWith("hello"));//trueconsole.log(str.endsWith(".vue"));//trueconsole.log(str.includes("e"));//trueconsole.log(str.includes("hello"));//true
2)、字符串模板
模板字符串相当于加强版的字符串,用反引号`,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式。
// 1、多行字符串 let ss = ` <div> <span>hello world<span> </div> ` console.log(ss) // 2、字符串插入变量和表达式。变量名写在${} 中,${} 中可以放 入JavaScript 表达式。 let name = "张三"; let age = 18; let info = `我是${name},今年${age}了`; console.log(info) // 3、字符串中调用函数 function fun() { return "这是一个函数" } let sss = `O(∩_∩)O 哈哈~,${fun()}`; console.log(sss); // O(∩_∩)O 哈哈~,这是一个函数
5、函数优化
1)、函数参数默认值
//在ES6 以前,我们无法给一个函数参数设置默认值,只能采用变通写法: function add(a, b) { // 判断b 是否为空,为空就给默认值1 b = b || 1; return a + b; } // 传一个参数 console.log(add(10)); //现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值 function add2(a , b = 1) { return a + b; } // 传一个参数 console.log(add2(10));
2)、不定参数
不定参数用来表示不确定参数个数,形如,…变量名,由…加上一个具名参数标识符组成。具名参数只能放在参数列表的最后,并且有且只有一个不定参数
function fun(...values) { console.log(values.length) } fun(1, 2) //2 fun(1, 2, 3, 4) //4
3)、箭头函数
ES6 中定义函数的简写方式
//以前声明一个方法 // var print = function (obj) { // console.log(obj); // } // 可以简写为: var print = obj => console.log(obj); // 测试调用 print(100);
多个参数: // 两个参数的情况: var sum = function (a, b) { return a + b; } // 简写为: //当只有一行语句,并且需要返回结果时,可以省略{} , 结果会自动返回。 var sum2 = (a, b) => a + b; //测试调用 console.log(sum2(10, 10));//20 // 代码不止一行,可以用`{}`括起来 var sum3 = (a, b) => { c = a + b; return c; }; //测试调用 console.log(sum3(10, 20));//30
4)、实战:箭头函数结合解构表达式
//需求,声明一个对象,hello 方法需要对象的个别属性 //以前的方式: const person = { name: "jack", age: 21, language: ['java', 'js', 'css'] } function hello(person) { console.log("hello," + person.name) } //现在的方式 var hello2 = ({ name }) => { console.log("hello," + name) }; //测试 hello2(person);
6、对象优化
1)、新增的API
ES6 给Object 拓展了许多新的方法,如:
[[k1,v1],[k2,v2],...]
assign(dest, …src) :将多个src 对象的值拷贝到dest 中。(第一层为深拷贝,第二层为浅拷贝)
const person = { name: "jack", age: 21, language: ['java', 'js', 'css'] } console.log(Object.keys(person));//["name", "age", "language"] console.log(Object.values(person));//["jack", 21, Array(3)] console.log(Object.entries(person));//[Array(2), Array(2), Arra y(2)] const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; //Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。 Object.assign(target, source1, source2); console.log(target)//{a: 1, b: 2, c: 3}
2)、声明对象简写
const name = "张三"// 传统const person1 = { age: age, name: name }console.log(person1)// ES6:属性名和属性值变量名一样,可以省略const person2 = { age, name }console.log(person2) //{age: 23, name: "张三"}
3)、对象的函数属性简写
let person = { name: "jack", // 以前: eat: function (food) { console.log(this.name + "在吃" + food); }, // 箭头函数版:这里拿不到this eat2: food => console.log(person.name + "在吃" + food), // 简写版: eat3(food) { console.log(this.name + "在吃" + food); } } person.eat("apple");
4)、对象拓展运算符
拓展运算符(…)用于取出参数对象所有可遍历属性然后拷贝到当前对象。
// 1、拷贝对象(深拷贝) let person1 = { name: "Amy", age: 15 } let someone = { ...person1 } console.log(someone) //{name: "Amy", age: 15} // 2、合并对象 let age = { age: 15 } let name = { name: "Amy" } let person2 = { ...age, ...name } //如果两个对象的字段名重复,后面对象字 段值会覆盖前面对象的字段值 console.log(person2) //{age: 15, name: "Amy"}
7、map 和reduce
数组中新增了map 和reduce 方法。
1)、map
//map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。let arr = ['1', '20', '-5', '3'];console.log(arr)arr = arr.map(s => parseInt(s));console.log(arr)
2)、reduce
语法:arr.reduce(callback,[initialValue])
reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元
素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调
用reduce 的数组。callback
(执行数组中每个值的函数,包含四个参数)
1、previousValue
(上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue
(数组中当前被处理的元素)
3、index
(当前元素在数组中的索引)
4、array
(调用reduce 的数组)
initialValue (作为第一次调用callback 的第一个参数。)
const arr = [1,20,-5,3];//没有初始值:console.log(arr.reduce((a,b)=>a+b));//19console.log(arr.reduce((a,b)=>a*b));//-300//指定初始值:console.log(arr.reduce((a,b)=>a+b,1));//20console.log(arr.reduce((a,b)=>a*b,0));//-0
8、Promise
在JavaScript 的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript 的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现。一旦有一连串的ajax 请求a,b,c,d… 后面的请求依赖前面的请求结果,就需要层层嵌套。这种缩进和层层嵌套的方式,非常容易造成上下文代码混乱,我们不得不非常小心翼翼处理内层函数与外层函数的数据,一旦内层函数使用了上层函数的变量,这种混乱程度就会加剧…总之,这种层叠上下文
的层层嵌套方式,着实增加了神经的紧张程度。
案例:用户登录,并展示该用户的各科成绩。在页面发送两次请求:
查询用户,查询成功说明可以登录查询用户成功,查询科目根据科目的查询结果,获取去成绩分析:此时后台应该提供三个接口,一个提供用户查询接口,一个提供科目的接口,一个提供各科成绩的接口,为了渲染方便,最好响应json 数据。在这里就不编写后台接口了,而是提供三个json 文件,直接提供json 数据,模拟后台接口:
user.json:{ "id": 1, "name": "zhangsan", "password": "123456"}user_corse_1.json:{ "id": 10, "name": "chinese"}corse_score_10.json:{ "id": 100, "score": 90}
回调函数嵌套的噩梦:层层嵌套。
$.ajax({ url: "mock/user.json", success(data) { console.log("查询用户:", data); $.ajax({ url: `mock/user_corse_${data.id}.json`, success(data) { console.log("查询到课程:", data); $.ajax({ url: `mock/corse_score_${data.id}.json`, success(data) { console.log("查询到分数:", data); }, error(error) { console.log("出现异常了:" + error); } }); }, error(error) { console.log("出现异常了:" + error); } }); }, error(error) { console.log("出现异常了:" + error); }});
我们可以通过Promise 解决以上问题。
1)、Promise 语法
const promise = new Promise(function (resolve, reject) { // 执行异步操作 if (/* 异步操作成功*/) { resolve(value);// 调用resolve,代表Promise 将返回成功的结果 } else { reject(error);// 调用reject,代表Promise 会返回失败结果 } }); // 使用箭头函数可以简写为: const promise = new Promise((resolve, reject) =>{ // 执行异步操作 if (/* 异步操作成功*/) { resolve(value);// 调用resolve,代表Promise 将返回成功的结果 } else { reject(error);// 调用reject,代表Promise 会返回失败结果 } });
这样,在promise 中就封装了一段异步执行的结果。
2)、处理异步结果
如果我们想要等待异步执行完成,做一些事情,我们可以通过promise 的then 方法来实现。
如果想要处理promise 异步执行失败的事件,还可以跟上catch:
promise.then(function (value) { // 异步执行成功后的回调}).catch(function (error) { // 异步执行失败后的回调})
3)、Promise 改造以前嵌套方式
new Promise((resolve, reject) => { $.ajax({ url: "mock/user.json", success(data) { console.log("查询用户:", data); resolve(data.id); }, error(error) { console.log("出现异常了:" + error); } });}).then((userId) => { return new Promise((resolve, reject) => { $.ajax({ url: `mock/user_corse_${userId}.json`, success(data) { console.log("查询到课程:", data); resolve(data.id); }, error(error) { console.log("出现异常了:" + error); } }); });}).then((corseId) => { console.log(corseId); $.ajax({ url: `mock/corse_score_${corseId}.json`, success(data) { console.log("查询到分数:", data); }, error(error) { console.log("出现异常了:" + error); } });});
4)、优化处理
优化:通常在企业开发中,会把promise 封装成通用方法,如下:封装了一个通用的get 请求方法;
let get = function (url, data) { // 实际开发中会单独放到common.js 中 return new Promise((resolve, reject) => { $.ajax({ url: url, type: "GET", data: data, success(result) { resolve(result); }, error(error) { reject(error); } }); })}// 使用封装的get 方法,实现查询分数get("mock/user.json").then((result) => { console.log("查询用户:", result); return get(`mock/user_corse_${result.id}.json`);}).then((result) => { console.log("查询到课程:", result); return get(`mock/corse_score_${result.id}.json`)}).then((result) => { console.log("查询到分数:", result);}).catch(() => { console.log("出现异常了:" + error);});
通过比较,我们知道了Promise 的扁平化设计理念,也领略了这种上层设计
带来的好处。我们的项目中会使用到这种异步处理的方式;
9、模块化
1)、什么是模块化
模块化就是把代码进行拆分,方便重复利用。类似java 中的导包:要使用一个包,必须先
导包。而JS 中没有包的概念,换来的是模块。
模块功能主要由两个命令构成:export
和import
。
export
命令用于规定模块的对外接口。import
命令用于导入其他模块提供的功能。 2)、export
比如我定义一个js 文件:hello.js,里面有一个对象
const util = { sum(a,b){ return a + b; }}
我可以使用export 将这个对象导出:
const util = { sum(a,b){ return a + b; }}export {util};
当然,也可以简写为:
export const util = { sum(a,b){ return a + b; }}
export
不仅可以导出对象,一切JS 变量都可以导出。比如:基本类型变量、函数、数组、
对象。
当要导出多个值时,还可以简写。比如我有一个文件:user.js:
var name = "jack"var age = 21export {name,age}
省略名称
上面的导出代码中,都明确指定了导出的变量名,这样其它人在导入使用时就必须准确写出
变量名,否则就会出错。
因此js 提供了default
关键字,可以对导出的变量名进行省略
例如:
// 无需声明对象的名字export default { sum(a,b){ return a + b; }}
这样,当使用者导入时,可以任意起名字
3)、import
使用export
命令定义了模块的对外接口以后,其他JS 文件就可以通过import
命令加载这个模块。
例如我要使用上面导出的util:
// 导入utilimport util from 'hello.js'// 调用util 中的属性util.sum(1,2)
要批量导入前面导出的name 和age:
import {name, age} from 'user.js'console.log(name + " , 今年"+ age +"岁了")
但是上面的代码暂时无法测试,因为浏览器目前还不支持ES6 的导入和导出功能。除非借助于工具,把ES6 的语法进行编译降级到ES5,比如Babel-cli
工具。我们暂时不做测试,大家了解即可。
4、Node.js
前端开发,少不了node.js;Node.js 是一个基于Chrome V8 引擎的JavaScript 运行环境。http://nodejs.cn/api/
我们关注与node.js 的npm 功能就行;
NPM 是随同NodeJS 一起安装的包管理工具,JavaScript-NPM,Java-Maven;
1)、官网下载安装node.js,并使用node -v 检查版本
2)、配置npm 使用淘宝镜像
npm config set registry http://registry.npm.taobao.org/
3)、大家如果npm install 安装依赖出现chromedriver 之类问题,先在项目里运行下面命令
npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver
然后再运行npm install
5、Vue
1、MVVM 思想
M:即Model,模型,包括数据和一些 基本操作
V:即View,视图,页面渲染结果
VM:即View-Model,模型与视图间的双向操作(无需开发人员干涉)
在MVVM 之前,开发人员从后端获取需要的数据模型,然后要通过DOM 操作Model 渲染到View 中。而后当用户操作视图,我们还需要通过DOM 获取View 中的数据,然后同步到Model 中。
而MVVM 中的VM 要做的事情就是把DOM 操作完全封装起来,开发人员不用再关心Model
和View 之间是如何互相影响的:
把开发人员从繁琐的DOM 操作中解放出来,把关注点放在如何操作Model 上。
2、Vue 简介
Vue (读音/vjuː/,类似于view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
官网:https://cn.vuejs.org/
参考:https://cn.vuejs.org/v2/guide/
Git 地址:https://github.com/vuejs
尤雨溪,Vue.js 创作者,Vue Technology 创始人,致力于Vue 的研究开发。
3、入门案例
1)、安装
官网文档提供了3 中安装方式:
直接script 引入本地vue 文件。需要通过官网下载vue 文件。通过script 引入CDN 代理。需要联网,生产环境可以使用这种方式通过npm 安装。这种方式也是官网推荐的方式,需要nodejs 环境。本课程就采用第三种方式
2)、创建示例项目
新建文件夹hello-vue,并使用vscode 打开使用vscode 控制台,npm install -y;项目会生成package-lock.json 文件,类似于maven 项目的pom.xml 文件。使用npm install vue,给项目安装vue;项目下会多node_modules 目录,并且在下面有一个vue 目录。
3)、HelloWorld
在hello.html 中,我们编写一段简单的代码。
h2 中要输出一句话:xx 非常帅
。前面的xx
是要渲染的数据。
4)、vue 声明式渲染
<body> <div id="app"> <h1>{{name}},非常帅!!!</h1></div><script src="./node_modules/vue/dist/vue.min.js"></script><script> let vm = new Vue({ el:"#app", data:{ name: "张三" } });</script></body>
首先通过new Vue()来创建Vue 实例
然后构造函数接收一个对象,对象中有一些属性:
el:是element 的缩写,通过id 选中要渲染的页面元素,本例中是一个divdata:数据,数据是一个对象,里面有很多属性,都可以渲染到视图中name:这里我们指定了一个name 属性页面中的h2
元素中,我们通过{{name}}的方式,来渲染刚刚定义的name 属性。 打开页面查看效果:
更神奇的在于,当你修改name 属性时,页面会跟着变化:
5)、双向绑定
我们对刚才的案例进行简单修改:
<body> <div id="app"> <input type="text" v-model="num"> <h2> {{name}},非常帅!!!有{{num}}个人为他点赞。 </h2></div><script src="./node_modules/vue/dist/vue.js"></script><script> // 创建vue 实例 let app = new Vue({ el: "#app", // el 即element,该vue 实例要渲染的页面元素 data: { // 渲染页面需要的数据 name: "张三", num: 5 } });</script></body>
双向绑定:
效果:我们修改表单项,num 会发生变化。我们修改num,表单项也会发生变化。为了实
时观察到这个变化,我们将num 输出到页面。
我们不需要关注他们为什么会建立起来关联,以及页面如何变化,我们只需要做好数据和
视图的关联即可(MVVM)
6)、事件处理
给页面添加一个按钮:
<body> <div id="app"> <input type="text" v-model="num"> <button v-on:click="num++">关注</button><h2> {{name}},非常帅!!!有{{num}}个人为他点赞。 </h2></div><script src="./node_modules/vue/dist/vue.js"></script><script> // 创建vue 实例 let app = new Vue({ el: "#app", // el 即element,该vue 实例要渲染的页面元素 data: { // 渲染页面需要的数据 name: "张三", num: 5 } });</script></body>
这里用v-on
指令绑定点击事件,而不是普通的onclick
,然后直接操作num
简单使用总结:
1)、使用Vue 实例管理DOM
2)、 DOM 与数据/事件等进行相关绑定
3)、我们只需要关注数据,事件等处理,无需关心视图如何进行修改
4、概念
1、创建Vue 实例
每个Vue 应用都是通过用Vue 函数创建一个新的Vue 实例开始的:
let app = new Vue({});
在构造函数中传入一个对象,并且在对象中声明各种Vue 需要的数据和方法,包括:
el、data、methods等等。接下来我们一一介绍。
2、模板或元素
每个Vue 实例都需要关联一段Html 模板,Vue 会基于此模板进行视图渲染。
我们可以通过el 属性来指定。
例如一段html 模板:
<div id="app"></div>
然后创建Vue 实例,关联这个div
let vm = new Vue({ el: "#app"})
这样,Vue 就可以基于id 为app
的div 元素作为模板进行渲染了。在这个div 范围以外的部分是无法使用vue 特性的。
3、数据
当Vue 实例被创建时,它会尝试获取在data 中定义的所有属性,用于视图的渲染,并且监视data 中的属性变化,当data 发生改变,所有相关的视图都将重新渲染,这就是“响应式“系统。
<div id="app"> <input type="text" v-model="name" /> </div>let vm = new Vue({ el: "#app", data: { name: "刘德华" }})
name 的变化会影响到input
的值
input 中输入的值,也会导致vm 中的name 发生改变
4、方法
Vue 实例中除了可以定义data 属性,也可以定义方法,并且在Vue 实例的作用范围内使用。
<div id="app"> {{num}} <button v-on:click="add">加</button></div>let vm = new Vue({ el: "#app", data: { num: 0 }, methods: { add: function () { // this 代表的当前vue 实例 this.num++; } }})
5、安装vue-devtools 方便调试
将软件包中的vue-devtools 解压。
打开chrome 设置->扩展程序
开启开发者模式,并加载插件
打开浏览器控制台,选择vue
6、安装vscode 的vue 插件
5、指令
1、插值表达式
1)、花括号
格式:{{表达式}}
说明:
2)、插值闪烁
使用{{}}方式在网速较慢时会出现问题。在数据未加载完成时,页面会显示出原始的{{}}
,
加载完毕后才显示正确数据,我们称为插值闪烁。
我们将网速调慢一些,然后刷新页面,试试看刚才的案例:
3)、v-text 和v-html
可以使用v-text 和v-html 指令来替代{{}}
说明:
<div id="app"> v-text:<span v-text="hello"></span> <br /> v-html:<span v-html="hello"></span></div><script src="./node_modules/vue/dist/vue.js"></script><script> let vm = new Vue({ el: "#app", data: { hello: "<h1>大家好</h1>" } })</script>
效果:
并且不会出现插值闪烁,当没有数据时,会显示空白或者默认数据。
2、v-bind
html 属性不能使用双大括号形式绑定,我们使用v-bind 指令给HTML 标签属性绑定值;
而且在将v-bind
用于class
和style
时,Vue.js 做了专门的增强。
1)、绑定class
<div class="static" v-bind:class="{ active: isActive, 'text-danger' : hasError }"></div><script> let vm = new Vue({ el: "#app", data: { isActive: true, hasError: false } })</script>
2)、绑定style
v-bind:style
的对象语法十分直观,看着非常像CSS,但其实是一个JavaScript 对象。style
属性名可以用驼峰式(camelCase) 或短横线分隔(kebab-case,这种方式记得用单引号括起
来) 来命名。
例如:font-size–>fontSize
<div id="app" v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div><script> let vm = new Vue({ el: "#app", data: { activeColor: 'red', fontSize: 30 } })</script>
结果:
3)、绑定其他任意属性
<div id="app" v-bind:style="{ color: activeColor, fontSize: fontSize+ 'px' }"v-bind:user="userName"></div><script> let vm = new Vue({ el: "#app", data: { activeColor: 'red', fontSize: 30, userName: 'zhangsan' } })</script>//效果:<div id="app" user="zhangsan" style="color: red; font-size: 30px;"></div>
4)、v-bind 缩写
<div id="app" :style="{ color: activeColor, fontSize: fontSize +'px' }":user="userName"></div>
3、v-model
刚才的v-text、v-html、v-bind 可以看做是单向绑定,数据影响了视图渲染,但是反过来就不
行。接下来学习的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"> let vm = new Vue({ el: "#app", data: { language: [] } })</script>
多个CheckBox
对应一个model 时,model 的类型是一个数组,单个checkbox 值默认是boolean 类型
radio 对应的值是input 的value 值
text
和textarea
默认对应的model 是字符串
select
单选对应字符串,多选对应也是数组
效果:
4、v-on
1、基本用法
v-on 指令用于给页面元素绑定事件。
语法: v-on:事件名=“js 片段或函数名”
示例:
<div id="app"> <!--事件中直接写js 片段--> <button v-on:click="num++">点赞</button><!--事件指定一个回调函数,必须是Vue 实例中定义的函数--> <button v-on:click="decrement">取消</button><h1>有{{num}}个赞</h1></div><script src="../node_modules/vue/dist/vue.js"></script><script type="text/javascript"> let vm = new Vue({ el: "#app", data: { num: 100 }, methods: { decrement() { this.num--; //要使用data 中的属性,必须this.属性名 } } })</script>
另外,事件绑定可以简写,例如v-on:click='add'
可以简写为@click='add'
2、事件修饰符
在事件处理程序中调用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"> let app = new Vue({ el: "#app", data: { num: 100 }, methods: { decrement(ev) { // ev.preventDefault(); this.num--; } } })</script>
效果:右键“点赞”,不会触发默认的浏览器右击事件;右键“取消”,会触发默认的浏览器右击事件)
3、按键修饰符
在监听键盘事件时,我们经常需要检查常见的键值。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`4、组合按钮
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
<!-- Alt + C --><input @keyup.alt.67="clear"><!-- Ctrl + Click --><div @click.ctrl="doSomething">Do something</div>
5、v-for
遍历数据渲染页面是非常常用的需求,Vue 中通过v-for 指令来实现。
1、遍历数组
语法:v-for=“item in items”
<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"> let 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>
效果:
2、数组角标
在遍历的过程中,如果我们需要知道数组角标,可以指定第二个参数:
语法:v-for=“(item,index) in items”
<div id="app"> <ul> <li v-for="(user, index) in users"> {{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}} </li> </ul></div>
效果:
3、遍历对象
v-for 除了可以迭代数组,也可以迭代对象。语法基本类似
语法:
v-for=“value in object”
v-for=“(value,key) in object”
v-for=“(value,key,index) in object”
实例:
<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"> let vm = new Vue({ el: "#app", data: { user: { name: '张三', gender: '男', age: 18 } } })</script>
效果:
4、Key
用来标识每一个元素的唯一特征,这样Vue 可以使用“就地复用”策略有效的提高渲染的效率。
<ul> <li v-for="(item,index) in items" :key=”index”></li></ul><ul> <li v-for="item in items" :key=”item.id”></li></ul>
如果items 是数组,可以使用index 作为每个元素的唯一标识
如果items 是对象数组,可以使用item.id 作为每个元素的唯一标识
6、v-if 和v-show
1、基本用法
v-if,顾名思义,条件判断。当得到结果为true 时,所在的元素才会被渲染。
v-show,当得到结果为true 时,所在的元素才会被显示。
语法:v-if=“布尔表达式”, v-show=“布尔表达式”,
示例:
<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"> let app = new Vue({ el: "#app", data: { show: true } })</script>
效果:只显示女性
7、v-else 和v-else-if
v-else 元素必须紧跟在带v-if
或者v-else-if
的元素的后面,否则它将不会被识别。
<div id="app"> <button v-on:click="random=Math.random()">点我呀</button><span>{{random}}</span> <h1 v-if="random >= 0.75"> 看到我啦?!v-if >= 0.75</h1><h1 v-else-if="random > 0.5"> 看到我啦?!v-else-if > 0.5</h1><h1 v-else-if="random > 0.25"> 看到我啦?!v-else-if > 0.25</h1><h1 v-else> 看到我啦?!v-else </h1></div><script src="../node_modules/vue/dist/vue.js"></script><script type="text/javascript"> let app = new Vue({ el: "#app", data: { random: 1 } })</script>
6、计算属性和侦听器
1、计算属性(computed)
某些结果是基于之前数据实时计算出来的,我们可以利用计算属性。来完成
示例:
<div id="app"> <ul> <li>西游记:价格{{xyjPrice}},数量: <input type="number" v-model="xyjNum"></li> <li>水浒传:价格{{shzPrice}},数量: <input type="number" v-model="shzNum"></li> <li>总价:{{totalPrice}}</li> </ul></div><script src="../node_modules/vue/dist/vue.js"></script><script type="text/javascript"> let app = new Vue({ el: "#app", data: { xyjPrice: 56.73, shzPrice: 47.98, xyjNum: 1, shzNum: 1 }, computed: { totalPrice(){ return this.xyjPrice*this.xyjNum + this.shzPrice*th is.shzNum; } }, })</script>
效果:只要依赖的属性发生变化,就会重新计算这个属性
2、侦听(watch)
watch 可以让我们监控一个值的变化。从而做出相应的反应。
示例:
<div id="app"> <ul> <li>西游记:价格{{xyjPrice}},数量: <input type="number" v-model="xyjNum"></li><li>水浒传:价格{{shzPrice}},数量: <input type="number" v-model="shzNum"></li><li>总价:{{totalPrice}}</li>{{msg}}</ul></div><script src="../node_modules/vue/dist/vue.js"></script><script type="text/javascript"> let app = new Vue({ el: "#app", data: { xyjPrice: 56.73, shzPrice: 47.98, xyjNum: 1, shzNum: 1, msg:"" }, computed: { totalPrice(){ return this.xyjPrice*this.xyjNum + this.shzPrice*th is.shzNum; } }, watch: { xyjNum(newVal, oldVal){ if(newVal >= 3){ this.msg = "西游记没有更多库存了"; this.xyjNum = 3; }else{ this.msg = ""; } } } })</script>
3、过滤器(filters)
过滤器不改变真正的data
,而只是改变渲染的结果,并返回过滤后的版本。在很多不同的情况下,过滤器都是有用的,比如尽可能保持API 响应的干净,并在前端处理数据的格式。
示例:展示用户列表性别显示男女
<body> <div id="app"> <table> <tr v-for="user in userList"> <td>{{user.id}}</td><td>{{user.name}}</td><!-- 使用代码块实现,有代码侵入--> <td>{{user.gender===1? "男":"女"}}</td></tr></table></div></body><script src="../node_modules/vue/dist/vue.js"></script><script> let app = new Vue({ el: "#app", data: { userList: [ { id: 1, name: 'jacky', gender: 1 }, { id: 2, name: 'peter', gender: 0 } ] } });</script>
1、局部过滤器
注册在当前vue 实例中,只有当前实例能用
let app = new Vue({ el: "#app", data: { userList: [ { id: 1, name: 'jacky', gender: 1 }, { id: 2, name: 'peter', gender: 0 } ] }, // filters 定义局部过滤器,只可以在当前vue 实例中使用 filters: { genderFilter(gender) { return gender === 1 ? '男~' : '女~' } }});<!-- | 管道符号:表示使用后面的过滤器处理前面的数据--> <td>{{user.gender | genderFilter}}</td>
2、全局过滤器
// 在创建Vue 实例之前全局定义过滤器:Vue.filter('capitalize', function (value) { return value.charAt(0).toUpperCase() + value.slice(1)})任何vue 实例都可以使用: <td>{{user.name | capitalize}}</td>
过滤器常用来处理文本格式化的操作。过滤器可以用在两个地方:双花括号插值和v-bind表达式
7、组件化
在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。
例如可能会有相同的头部导航。
但是如果每个页面都独自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同页面就可以共享这些组件,避免重复开发。
在vue 里,所有的vue 实例都是组件
1、全局组件
我们通过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 }}})let app = new Vue({ el: "#app"})</script>
组件其实也是一个Vue 实例,因此它在定义时也会接收:data、methods、生命周期函数等不同的是组件不会与页面的元素绑定,否则就无法复用了,因此没有el 属性。但是组件渲染需要html 模板,所以增加了template 属性,值就是HTML 模板全局组件定义完毕,任何vue 实例都可以直接在HTML 中通过组件名称来使用组件了data 必须是一个函数,不再是一个对象。 2、组件的复用
定义好的组件,可以任意复用多次:
<div id="app"><!--使用定义好的全局组件--><counter></counter><counter></counter><counter></counter></div>
组件的data 属性必须是函数!
一个组件的data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝;
否则:
https://cn.vuejs.org/v2/guide/components.html#data-%E5%BF%85%E9%A1%BB%E6%98%AF%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0
3、局部组件
一旦全局注册,就意味着即便以后你不再使用这个组件,它依然会随着Vue 的加载而加载。
因此,对于一些并不频繁使用的组件,我们会采用局部注册。
我们先在外部定义一个对象,结构与创建组件时传递的第二个参数一致:
const counter = { template: '<button v-on:click="count++">你点了 我{{ count }} 次,我记住了.</button>',data() { return { count: 0 }}};// 然后在Vue 中使用它:let app = new Vue({ el: "#app", components: { counter: counter // 将定义的对象注册为组件 }})
components 就是当前vue 对象子组件集合。其key 就是子组件名称其值就是组件对象名效果与刚才的全局注册是类似的,不同的是,这个counter 组件只能在当前的Vue 实例中使用
简写:
let app = new Vue({ el: "#app", components: { counter // 将定义的对象注册为组件 }})
8、生命周期钩子函数
1、生命周期
每个Vue 实例在被创建时都要经过一系列的初始化过程:创建实例,装载模板,渲染模板等等。Vue 为生命周期中的每个状态都设置了钩子函数(监听函数)。每当Vue 实例处于不同的生命周期时,对应的函数就会被触发调用。
生命周期:你不需要立马弄明白所有的东西。
2、钩子函数
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:组件更新之后。<body> <div id="app"> <span id="num">{{num}}</span><button v-on:click="num++">赞!</button><h2> {{name}},非常帅!!!有{{num}}个人点赞。 </h2></div></body><script src="../node_modules/vue/dist/vue.js"></script><script> let app = new Vue({ el: "#app", data: { name: "张三", num: 100 }, methods: { show() { return this.name; }, add() { this.num++; } }, beforeCreate() { console.log("=========beforeCreate============="); console.log("数据模型未加载:" + this.name, this.num); console.log("方法未加载:" + this.show()); console.log("html 模板未加载: " + document.getElementById("num")); }, created: function () { console.log("=========created============="); console.log("数据模型已加载:" + this.name, this.num); console.log("方法已加载:" + this.show()); console.log("html 模板已加载: " + document.getElementById("num")); console.log("html 模板未渲染: " + document.getElementById("num").innerText); }, beforeMount() { console.log("=========beforeMount============="); console.log("html 模板未渲染: " + document.getElementById("num").innerText); }, mounted() { console.log("=========mounted============="); console.log("html 模板已渲染: " + document.getElementById("num").innerText); }, beforeUpdate() { console.log("=========beforeUpdate============="); console.log("数据模型已更新:" + this.num); console.log("html 模板未更新: " + document.getElementById("num").innerText); }, updated() { console.log("=========updated============="); console.log("数据模型已更新:" + this.num); console.log("html 模板已更新: " + document.getElementById("num").innerText); } });</script>
9、vue 模块化开发
1、npm install webpack -g
全局安装webpack
2、npm install -g @vue/cli-init
全局安装vue 脚手架
3、初始化vue 项目;
vue init webpack appname:vue 脚手架使用webpack 模板初始化一个appname 项目
4、启动vue 项目;
项目的package.json 中有scripts,代表我们能运行的命令
npm start = npm run dev:启动项目
npm run build:将项目打包
5、模块化开发
1、项目结构
运行流程
进入页面首先加载index.html 和main.js 文件。main.js 导入了一些模块【vue、app、router】,并且创建vue 实例,关联index.html页面的 元素。使用了router,导入了App 组件。并且使用标签引用了这个组件 第一次默认显示App 组件。App 组件有个图片和,所以显示了图片。但是由于代表路由的视图,默认是访问/#/路径(router 路径默认使用HASH 模式)。在router 中配置的/是显示HelloWorld 组件。所以第一次访问,显示图片和HelloWorld 组件。我们尝试自己写一个组件,并且加入路由。点击跳转。需要使用Go to Foo标签2、Vue 单文件组件
Vue 单文件组件模板有三个部分;
<template> <div class="hello"> <h1>{{ msg }}</h1></div></template><script> export default {name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' }}}</script> <style scoped> h1, h2 { font-weight: normal; }</style>
3、vscode 添加用户代码片段(快速生成vue 模板)
文件–>首选项–>用户代码片段–>点击新建代码片段–取名vue.json 确定
{ "生成vue 模板": { "prefix": "vue", "body": [ "<template>", "<div></div>", "</template>", "", "<script>", "//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json 文件,图片文件等等)", "//例如:import 《组件名称》from '《组件路径》';", "", "export default {", "//import 引入的组件需要注入到对象中才能使用", "components: {},", "props: {},", "data() {", "//这里存放数据", "return {", "", "};", "},", "//计算属性类似于data 概念", "computed: {},", "//监控data 中的数据变化", "watch: {},", "//方法集合", "methods: {", "", "},", "//生命周期- 创建完成(可以访问当前this 实例)", "created() {", "", "},", "//生命周期- 挂载完成(可以访问DOM 元素)", "mounted() {", "", "},", "beforeCreate() {}, //生命周期- 创建之前", "beforeMount() {}, //生命周期- 挂载之前", "beforeUpdate() {}, //生命周期- 更新之前", "updated() {}, //生命周期- 更新之后", "beforeDestroy() {}, //生命周期- 销毁之前", "destroyed() {}, //生命周期- 销毁完成", "activated() {}, //如果页面有keep-alive 缓存功能,这个函数会触发", "}", "</script>", "<style lang='scss' scoped>", "//@import url($3); 引入公共css 类", "$4", "</style>" ], "description": "生成vue 模板" }}
4、导入element-ui 快速开发
安装element-ui: npm i element-ui在main.js 中引入element-ui 就可以全局使用了。import ElementUI from ‘element-ui’
import ‘element-ui/lib/theme-chalk/index.css’
Vue.use(ElementUI)将App.vue 改为element-ui 中的后台布局添加测试路由、组件,测试跳转逻辑
(1) 、参照文档el-menu 添加router 属性
(2) 、参照文档el-menu-item 指定index 需要跳转的地址
6、Babel
Babel 是一个JavaScript 编译器,我们可以使用es 的最新语法编程,而不用担心浏览器兼容问题。他会自动转化为浏览器兼容的代码
7、Webpack
自动化项目构建工具。gulp 也是同类产品