微服务整体框架
- 开发前后台分离:前台与后台之间,通过Restful风格接口通信(HTTP协议)
- 内部服务:Dubbo( RPC框架)
- 外部服务:SpringCloud Zuul(提供Restful API接口)
-
微服务应用开发
API Gateway
- API Gateway:网关,统一应用请求接口.API 网关在微服务们的最前端,让 API 网关变成由应用所发起的每个请求的入口,简化客户端实现和微服务应用程序间的沟通方式。
API Gateway两种方式:
-
单节点API Gateway
-
BFF (Backends for frontends) Gateway
API Gateway的作用
- 请求路由,版本控制: API Gateway 是微服务的入口,可以根据不同的请求路由到不同的服务上. 也可以进行路由的版本控制,这样即使后服务发生了变化,Gateway 的路径依然可以不改变
- 用户登录,权限认证: 客户端在与我们后端服务进行交互之前,由API Gateway先进行登录鉴权操作,这是后端所有的服务都需要有的共有逻辑
- 数据聚合: 由于不同的客户端往往需要的数据完全不同,而这些数据又是不同的 service 提供的,可以借助 Gateway 方便完成来自不同 service 的数据聚合
- 协议转换: 在项目实践中,CS(Client to Server)协议和SS(Server to Server)协议是不一样的,为了保证数据传输的可靠性,CS协议会有鉴权以及加密解密的逻辑,而在内部的SS协议则不需要这些逻辑,因此在 Gateway 我们需要有一个协议转换的过程
- 熔断,降级,限流: 通过API Gateway可以在监测到某个服务发生异常,或者当服务的流量超过服务的承载能力等情况时,可以采取相应的措施. 提高整个系统的容错性、稳定性
- 负载均衡: API Gateway知道所有服务实例的地址,可以根据不同服务采取不同的负载均衡策略
- 灰度发布: 灰度发布允许直接只导入指定量的流量请求到新的版本
API Gateway的架构
- 多网关集群(Backends for frontends): 针对不同的客户端,都有相应的网关层来接入.功能主要有:用户登录,鉴权,服务发现注册,协议转换,接口版本控制等以及监控,APM调用链,日志,流控策略等
- 聚合服务(Merge Service): 在某些客户端的需求中,需要从多个服务拉取数据,为了减少客户端的复杂度,以及加快客户端的访问速度,可以加一个聚合层,用来做聚合查询,在某些接口中可以把多个服务的数据一次性返回给客户端
- 仪表盘管理端(Dashboard): Dashboard 提供可视化的分析平台,包括服务的管理,监控数据报警配置,日志查询,灰度发布操作,API文档管理等
Eureka(服务发现框架)
- Eureka是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的. SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能
Eureka的两个组件
- Eureka Server: Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中看到. Eureka Server之间通过复制的方式完成数据的同步
- Eureka Client: 是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器
-
Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性
- 在应用启动后,将会向Eureka Server发送心跳, 如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除。
- Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性
RPC框架
RPC定义
- RPC(Remote Procedure Call Protocol): 远程过程调用协议,一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.也就是
客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样
- RPC是协议: 协议就是一套规范,目前典型的RPC实现包括:Dubbo,Thrift,GRPC,Hetty等.从目前技术的发展趋势来看,实现了RPC协议的应用工具往往都会附加其他重要功能
- 网络协议和网络IO模型对其透明: 既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。既然网络协议对其透明,那么调用过程中,使用的是哪一种网络IO模型调用者也不需要关心
- 信息格式对其透明: 我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的
-
应该有跨语言能力: 调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述
RPC主要组成部分
- Client: RPC协议的调用方.最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用.但实际情况来说Client或多或少的都需要指定RPC框架的一些细节
- Server: 在RPC规范中,这个Server并不是提供RPC服务器IP,端口监听的模块。而是远程服务方法的具体实现(在JAVA中就是RPC服务接口的具体实现).其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用
- Stub/Proxy: RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议,也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的
- Message Protocol: 一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式.RPC的消息管理层专门对网络传输所承载的消息信息进行编码和解码操作.目前流行的技术趋势是不同的RPC实现,为了加强自身框架的效率都有一套(或者几套)私有的消息格式
- Transfer/Network Protocol: 传输协议层负责管理RPC框架所使用的网络协议,网络IO模型. 传输层还需要统一RPC客户端和RPC服务端所使用的IO模型
- Selector/Processor: 存在于RPC服务端,用于服务器端某一个RPC接口的实现的特性(它并不知道自己是一个将要被RPC提供给第三方系统调用的服务).所以在RPC框架中应该有一种 "负责执行RPC接口实现" 的角色.包括:管理RPC接口的注册,判断客户端的请求权限,控制接口实现类的执行在内
- IDL: IDL(接口定义语言)并不是RPC实现中所必须的.但是需要跨语言的RPC框架一定会有IDL部分的存在.这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式.如果RPC实现没有考虑跨语言性,那么IDL部分就不需要包括,例如JAVA RMI因为就是为了在JAVA语言间进行使用,所以JAVA RMI就没有相应的IDL
不同的RPC框架实现都有一定设计差异。例如生成Stub的方式不一样,IDL描述语言不一样、服务注册的管理方式不一样、运行服务实现的方式不一样、采用的消息格式封装不一样、采用的网络协议不一样。但是基本的思路都是一样的,上图中的所列出的要素也都是具有的
影响RPC框架性能的因素
- 使用的网络IO模型: RPC服务器可以只支持传统的阻塞式同步IO,也可以做一些改进让RPC服务器支持非阻塞式同步IO,或者在服务器上实现对多路IO模型的支持.这样的RPC服务器的性能在高并发状态下,会有很大的差别.特别是单位处理性能下对内存,CPU资源的使用率
- 基于的网络协议: 一般来说可以选择让RPC使用应用层协议,例如HTTP或者HTTP/2协议,或者使用TCP协议.让RPC框架工作在传输层.工作在哪一层网络上会对RPC框架的工作性能产生一定的影响,但是对RPC最终的性能影响并不大.但是至少从各种主流的RPC实现来看,没有采用UDP协议做为主要的传输协议的
- 消息封装格式: 选择或者定义一种消息格式的封装,要考虑的问题包括:消息的易读性,描述单位内容时的消息体大小,编码难度,解码难度,解决半包/粘包问题的难易度. 当然如果您只是想定义一种RPC专用的消息格式,那么消息的易读性可能不是最需要考虑的.消息封装格式的设计是目前各种RPC框架性能差异的最重要原因,这就是为什么几乎所有主流的RPC框架都会设计私有的消息封装格式的原因.dubbo中消息体数据包含dubbo版本号,接口名称,接口版本,方法名称,参数类型列表,参数,附加信息
- 序列化和反序列化(Schema & Data Serialization): 序列化和反序列化,是对象到二进制数据的转换,程序是可以理解对象的,对象一般含有 schema 或者结构,基于这些语义来做特定的业务逻辑处理.
序列化框架一般会关注以下几点:
Encoding format:是human readable(是否能直观看懂 json)还是binary(二进制)
Schema declaration:也叫作契约声明,基于IDL,比如 Protocol Buffers/Thrift.还是自描述的,比如 JSON、XML.另外还需要看是否是强类型的
语言平台的中立性:比如Java的Native Serialization就只能自己玩,而Protocol Buffers可以跨各种语言和平台
新老契约的兼容性:比如IDL加了一个字段,老数据是否还可以反序列化成。
和压缩算法的契合度 :运行benchmark(基准)和实际应用都会结合各种压缩算法,例如gzip,snappy
性能 :这是最重要的,序列化,反序列化的时间,序列化后数据的字节大小是考察重点。
序列化方式非常多,常见的有Protocol Buffers,Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST
- 实现的服务处理管理方式: 在高并发请求下,如何管理注册的服务也是一个性能影响点.可以让RPC的Selector/Processor使用单个线程运行服务的具体实现(这意味着上一个客户端的请求没有处理完,下一个客户端的请求就需要等待). 也可以为每一个RPC具体服务的实现开启一个独立的线程运行(可以一次处理多个请求,但是操作系统对于“可运行的最大线程数”是有限制的). 也可以线程池来运行RPC具体的服务实现(目前看来,在单个服务节点的情况下,这种方式是比较好的). 还可以通过注册代理的方式让多个服务节点来运行具体的RPC服务实现
工业界的 RPC 框架
-
国内
- Dubbo: 来自阿里巴巴http://dubbo.I/O/
- Motan: 新浪微博自用https://github.com/weibocom/motan
- Dubbox: 当当基于 dubbo 的https://github.com/dangdangdotcom/dubbox
- rpcx: 基于 Golang 的https://github.com/smallnest/rpcx
-
国外
- Thrift from facebook:https://thrift.apache.org
- Avro from hadoop:https://avro.apache.org
- Finagle by twitter:https://twitter.github.I/O/finagle
- gRPC by Google:http://www.grpc.I/O(Google inside use Stuppy)
- Hessian from cuacho:http://hessian.caucho.com
- Coral Service inside amazon: not open sourced
如何选择RPC框架
选择一个rpc框架会基于多方面的考虑:框架特性、性能、成熟度、技术支持、社区活跃度等多个方面.最重要一点,这也是往往很多技术人员进入的误区, "对于技术,不要为了使用而使用,用最简单合适的技术实现解决问题才是正道" .架构是服务于业务的,能快速方便的满足业务需求的架构才是好的架构.没有最好的,只有适合自己的
Dubbo
- Dubbo是一个开源分布式服务框架,阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成.
- Dubbo是一款高性能,轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
核心组件
- Remoting: 网络通信框架,实现了sync-over-async和request-response消息机制
- RPC: 一个远程过程调用的抽象.支持负载均衡,容灾和集群功能
- Registry: 服务目录框架,用于服务的注册和服务事件发布和订阅
工作原理
Provider:暴露服务方称之为“服务提供者”
Consumer:调用远程服务方称之为“服务消费者”
Registry:服务注册与发现的中心目录服务称之为“服务注册中心”
Monitor:统计服务的调用次数和调用时间的日志服务称之为“服务监控中心”
连通性:
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示
服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销
服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销
注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
健壮性:
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
伸缩性:
注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者
Dubbo特性
- 面向接口代理的高性能RPC调用: 提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节
- 智能负载均衡: 内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量
- 服务自动注册与发现: 支持多种注册中心服务,服务实例上下线实时感知
- 高度可扩展能力: 遵循微内核+插件的设计原则,所有核心能力如Protocol,Transport,Serialization被设计为扩展点,平等对待内置实现和第三方实现
- 运行期流量调度: 内置条件,脚本等路由策略.通过配置不同的路由规则,轻松实现灰度发布,同机房优先等功能
- 可视化的服务治理与运维: 提供丰富服务治理,运维工具:随时查询服务元数据,服务健康状态及调用统计,实时下发路由策略,调整配置参数
使用示例
Zuul
-
Zuul是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用
-Zuul是一个基于JVM路由和服务端的负载均衡器,提供动态路由,监控,弹性,安全等边缘服务的框架,相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门
Zuul工作原理
-
过滤器机制
- Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行
1.Zuul的过滤器之间没有直接的相互通信,他们之间通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据 2.Zuul的过滤器是由Groovy写成,这些过滤器文件被放在Zuul Server上的特定目录下面,Zuul会定期轮询这些目录,修改过的过滤器会动态的加载到Zuul Server中以便过滤请求使用
-
标准过滤器类型:
Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期- PRE: 在请求被路由之前调用,利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- ROUTING: 请求路由到微服务,用于构建发送给微服务的请求,使用Apache HttpClient或Netfilx Ribbon请求微服务
- POST: 在路由到微服务以后执行,用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
- ERROR: 在其他阶段发生错误时执行该过滤器
-
内置的特殊过滤器:
- StaticResponseFilter: StaticResponseFilter允许从Zuul本身生成响应,而不是将请求转发到源
- SurgicalDebugFilter: SurgicalDebugFilter允许将特定请求路由到分隔的调试集群或主机
-
自定义的过滤器:
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。如STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务
-
过滤器的生命周期
Zuul请求的生命周期详细描述了各种类型的过滤器的执行顺序
-
过滤器调度过程
-
动态加载过滤器
Zuul的作用
Zuul可以通过加载动态过滤机制实现Zuul的功能:
- 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求
- 审查与监控: 在边缘位置追踪有意义数据及统计结果,得到准确的生产状态结论
- 动态路由: 以动态方式根据需要将请求路由至不同后端集群处
- 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平
- 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群
- 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近
Zuul与应用的集成方式
-
ZuulServlet - 处理请求(调度不同阶段的filters,处理异常等)
- 所有的Request都要经过ZuulServlet的处理,
- Zuul对request处理逻辑的三个核心的方法: preRoute(),route(), postRoute()
- ZuulServletZuulServlet交给ZuulRunner去执行。由于ZuulServlet是单例,因此ZuulRunner也仅有一个实例。ZuulRunner直接将执行逻辑交由FilterProcessor处理,FilterProcessor也是单例,其功能就是依据filterType执行filter的处理逻辑
-
FilterProcessor对filter的处理逻辑:
1.首先根据Type获取所有输入该Type的filter:List<ZuulFilter> list 2.遍历该list,执行每个filter的处理逻辑:processZuulFilter(ZuulFilter filter) 3.RequestContext对每个filter的执行状况进行记录,应该留意,此处的执行状态主要包括其执行时间、以及执行成功或者失败,如果执行失败则对异常封装后抛出 4.到目前为止,Zuul框架对每个filter的执行结果都没有太多的处理,它没有把上一filter的执行结果交由下一个将要执行的filter,仅仅是记录执行状态,如果执行失败抛出异常并终止执行
-
ContextLifeCycleFilter - RequestContext 的生命周期管理:
- ContextLifecycleFilter的核心功能是为了清除RequestContext;请求上下文RequestContext通过ThreadLocal存储,需要在请求完成后删除该对象RequestContext提供了执行filter Pipeline所需要的Context,因为Servlet是单例多线程,这就要求RequestContext即要线程安全又要Request安全。context使用ThreadLocal保存,这样每个worker线程都有一个与其绑定的RequestContext,因为worker仅能同时处理一个Request,这就保证了Request Context 即是线程安全的由是Request安全的。
- GuiceFilter - GOOLE-IOC(Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC).Guice非常小而且快.)
- StartServer - 初始化 zuul 各个组件(ioc,插件,filters,数据库等)
-
FilterScriptManagerServlet - uploading/downloading/managing scripts, 实现热部署
Filter源码文件放在zuul 服务特定的目录, zuul server会定期扫描目录下的文件的变化,动态的读取\编译\运行这些filter,如果有Filter文件更新,源文件会被动态的读取,编译加载进入服务,接下来的Request处理就由这些新加入的filter处理
React前端框架
React定义
- React前端框架是Facebook开源的一个js库,用于动态构建用户界面.
- React解决的问题:
- 数据绑定的时候,大量操作真实dom,性能成本太高
- 网站的数据流向太混乱,不好控制
- React 把用户界面抽象成一个个组件.如按钮组件 Button,对话框组件 Dialog,日期组件 Calendar.开发者通过组合这些组件,最终得到功能丰富,可交互的页面.通过引入 JSX 语法,复用组件变得非常容易,同时也能保证组件结构清晰.有了组件这层抽象,React 把代码和真实渲染目标隔离开来,除了可以在浏览器端渲染到 DOM 来开发网页外,还能用于开发原生移动应用
React核心
虚拟DOM是React的基石,React的核心是组件,React的精髓是函数式编程 ,在React中是单向响应的数据流
组件的设计目的是提高代码复用率,降低测试难度和代码复杂度:
提高代码复用率:组件将数据和逻辑封装,类似面向对象中的类
降低测试难度:组件高内聚低耦合,很容易对单个组件进行测试
降低代码复杂度:直观的语法可以极大提高可读性
React特点
- JSX: JSX 是 JavaScript 语法的扩展
- 组件: 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中
- 单向响应的数据流: React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单
- Declarative(声明式编码): React采用声明范式,可以轻松描述应用(自动dom操作)
- Component-Based(组件化编码)
- Learn Once,Write Anywhere(支持客户端与服务器渲染)
- 高效:React通过对DOM的模拟(虚拟dom),最大限度地减少与DOM的交互
1.虚拟(virtual)DOM, 不总是直接操作DOM,减少页面更新次数;
2.高效的DOM Diff算法, 最小化页面重绘;
- 灵活:React可以与已知的库或框架很好地配合
React的虚拟DOM
-
传统DOM更新
真实页面对应一个 DOM 树.在传统页面的开发模式中,每次需要更新页面时,都要手动操作 DOM 来进行更新
-
虚拟DOM
DOM操作非常昂贵.我们都知道在前端开发中,性能消耗最大的就是DOM操作,而且这部分代码会让整体项目的代码变得难以维护.React把真实DOM树转换成JavaScript对象树,也就是Virtual DOM
-
虚拟DOM定义:
- 一个虚拟DOM(元素)是一个一般的js对象,准确的说是一个对象树(倒立的)
- 虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应
- 如果只是更新虚拟DOM, 页面是不会重绘的
-
Virtual DOM算法步骤:
- 用JS对象树表示DOM树的结构.然后用这个树构建一个真正的DOM树插入到文档中
- 当状态变更的时候,重新构造一棵新的对象树.然后用新的树和旧的树进行比较,记录两棵树差异
- 把差异应用到真实DOM树上,视图就更新了
-
进一步理解:
- Virtual DOM本质上就是在JS和DOM之间做了一个缓存
可以类比CPU和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢, 我们就在它们JS和DOM之间加个缓存.CPU(JS)只操作内存(Virtual DOM,最后的时候再 把变更写入硬盘(DOM)
-
虚拟DOM定义:
- React提供了一些API来创建一种特别的一般js对象
//创建的就是一个简单的虚拟DOM对象
var element = React.createElement('h1', {id:'myTitle'}, 'hello');
- 虚拟DOM对象最终都会被React转换为真实的DOM
- 我们编码时基本只需要操作react的虚拟DOM相关数据,react会转换为真实DOM变化而更新界面
-
创建虚拟DOM的2种方式
- JSX方式
// jsx方式创建虚拟dom元素对象
const vDOM2 = <h3 id={myId.toUpperCase()}>{msg.toLowerCase()}</h3>
还有一种是纯JS,一般不使用:
// 纯JS方式
const msg = 'I like you';
const myId = 'atguigu';
const vDOM1 = React.createElement('h2',{id:myId},msg);
-
渲染虚拟DOM(元素)
- 语法: ReactDOM.render(virtualDOM,containerDOM)
- 作用: 将虚拟DOM元素渲染到真实容器DOM中显示
-
参数说明:
- 参数一: 纯js或jsx创建的虚拟DOM对象
- 参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)
// 渲染到真实的页面中
ReactDOM.render(vDOM1,document.getElementById('example1'));
ReactDOM.render(vDOM2,document.getElementById('example2'));
使用示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>02_JSX_DEMO</title>
</head>
<body>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<hr>
<div id="example1"></div>
<div id="example2"></div>
<script src="../js/react.js"></script>
<script src="../js/react-dom.js"></script>
<script src="../js/babel.min.js"></script>
<script type="text/babel">
/*
功能: 动态展示列表数据
*/
/*
技术点:
1). 使用JSX创建虚拟DOM
2). React能自动遍历显示数组中所有的元素
3). array.map()的使用
*/
//数据的数组
var names = ['Tom2', 'Jack2', 'Bob2'];
//数据的数组——>标签数组
var lis = [];
names.forEach((item,index)=>lis.push(<li key={index}>{item}</li>));
//创建虚拟的DOM
const ul=<ul>{lis}</ul>;
// 将虚拟的Dom渲染到页面中的某个DOM元素中
ReactDOM.render(ul,document.getElementById('example1'))
const ul2 = <ul>{names.map((name,index)=><li key={index}>{name}</li>)}</ul>
ReactDOM.render(ul2, document.getElementById('example2'))
</script>
</body>
</html>
React的组件
-
模块
- 什么是模块: 向外提供特定功能的js程序, 一般就是一个js文件
- 为什么要用模块: js代码越多越复杂了
- 使用模块的优势: 简化js的编写, 阅读, 提高运行效率
- 模块化: 当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
-
组件
- 什么是组件: 用来实现特定功能效果的代码集合(html/css/js)
- 为什么要用组件: 单个界面的功能更复杂
- 使用组件的优势: 复用, 简化项目编码, 提高运行效率
- 组件化: 当应用是以多组件的方式实现功能, 这样应用就是一个组件化的应用
-
自定义组件:
1. 定义组件
1.工厂(无状态)函数(简单组件,推荐使用)
// 方式一:工厂函数,推荐使用
function MyComponent() {
return <h2>工厂函数</h2>
}
2.ES6类语法
// 方式二:ES6类语法(复杂组件,推荐使用)
class MyComponent2 extends React.Component{
render(){
return <h2>ES6的语法</h2>
}
}
2. 渲染组件标签
//语法规则
ReactDOM.render(<MyComponent/>, document.getElementById('example'));
- ==注意==
- 返回的组件类必须首字母大写
- 虚拟DOM元素必须只有一个根元素
- 虚拟DOM元素必须有结束标签
- ReactDOM.render()渲染组件标签的基本流程:
1.React内部会创建组件实例对象;
2.得到包含的虚拟DOM并解析为真实DOM;
3.插入到指定的页面元素内部;
组件的三大属性
props属性
1.每个组件对象都会有props(properties的简写)属性
2.组件标签的所有属性都保存在props中
3.内部读取某个属性值:this.props.propertyName
4.作用: 通过标签属性从组件外向组件内传递数据(只读)
5.对props中的属性值进行类型限制和必要性限制:
// 对标签属性进行限制
Person.propTypes = {
name:React.PropTypes.string.isRequired,
sex:React.PropTypes.string,
age:React.PropTypes.number
}
6.扩展属性: 将对象的所有属性通过props传递
<Person {...person}/>
//具体如下:
ReactDOM.render(<Person {...person}/>,document.getElementById('example'))
7.默认属性值
// 指定属性的默认值
Person.defaultProps = {
sex:'男',
age:18
}
8.组件类的构造函数
constructor (props) {
super(props)
console.log(props) // 查看所有属性
}
refs属性
1.组件内的标签都可以定义ref属性来标识本身
2.在组件中可以通过this.refs.refName来得到对应的真实DOM对象
3.作用: 用于操作指定的ref属性的dom元素对象(表单标签居多)
-
事件处理
-
通过onXxx属性指定组件的事件处理函数(注意大小写)
- React使用的是自定义(合成)事件, 而不是使用的DOM事件
- React中的事件是通过委托方式处理的(委托给组件最外层的元素)
- 通过event.target得到发生事件的DOM元素对象
<input onFocus={this.handleClick}/> handleFocus(event) { event.target //返回input对象 }
-
通过onXxx属性指定组件的事件处理函数(注意大小写)
-
==强烈注意==
- 组件内置的方法中的this为组件对象
-
在组件中自定义的方法中的this为null
1.强制绑定this
2.箭头函数(ES6模块化编码时才能使用)this.change = this.change.bind(this);
state属性
- 组件被称为 "状态机" ,通过更新组件的状态值来更新对应的页面显示(重新渲染)
- 初始化状态:
constructor (props) {
super(props)
this.state = {
stateProp1 : value1,
stateProp2 : value2
}
}
- 读取某个状态值:
this.state.statePropertyName
- 更新状态->组件界面更新
this.setState({
stateProp1 : value1,
stateProp2 : value2
})
组件的生命周期
-
组件的三个生命周期状态:
- Mount: 插入真实 DOM
- Update: 被重新渲染
- Unmount: 被移出真实 DOM
-
React 为每个状态都提供了两种勾子(hook)函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用:
- componentWillMount()
- componentDidMount(): 已插入页面真实DOM,在render之后才会执行
- componentWillUpdate(object nextProps,object nextState)
- componentDidUpdate(object prevProps,object prevState)
- componentWillUnmount()
-
生命周期流程:
-
第一次初始化渲染显示:render()
- constructor(): 创建对象初始化state
- componentWillMount(): 将要插入回调函数
- render(): 用于插入虚拟DOM回调函数
- componentDidMount(): 已经插入回调函数.在此方法中启动定时器,绑定监听,发送Ajax请求
-
每次更新state:this.setSate()
- componentWillUpdate(): 将要更新回调函数
- render(): 更新,重新渲染
- componentDidUpdate(): 已经更新回调
-
删除组件
- ReactDOM.unmountComponentAtNode(div):移除组件
- componentWillUnmount():组件将要被移除回调
-
第一次初始化渲染显示:render()
-
常用的方法
- render(): 必须重写,返回一个自定义的虚拟DOM
- constructor(): 初始化状态,绑定this(可以箭头函数代替)
- componentDidMount(): 只执行一次,已经在DOM树中,适合启动,设置一些监听
-
==注意==
- 一般会在componentDidMount() 中:开启监听,发送ajax请求
- 可以在componentWillUnmount() 做一些收尾工作:停止监听
- 生命周期还有一个方法:componentWillReceiveProps()
React的函数式编程
- 函数式编程: 结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用
- 声明式编程: 只关注做什么,而不关注怎么做(流程),类似于填空题,数组中常见声明式方法:map() , forEach() ,find() ,findIndex()
- 命令式编程: 要关注做什么和怎么做(流程), 类似于问答题
var arr = [1, 3, 5, 7]
// 需求: 得到一个新的数组, 数组中每个元素都比arr中对应的元素大10: [11, 13, 15, 17]
// 命令式编程
var arr2 = []
for(var i =0;i<arr.length;i++) {
arr2.push(arr[i]+10)
}
console.log(arr2)
// 声明式编程
var arr3 = arr.map(function(item){
return item +10
})
// 声明式编程是建立命令式编程的基础上
React的JSX
-
JSX定义: JavaScript XML,react定义的一种类似于XML的JS扩展语法:XML+JS,用来创建react虚拟DOM(元素)对象.
-
注意:
1.它不是字符串, 也不是HTML/XML标签
2.它最终产生的就是一个JS对象
-
注意:
var ele = <h1>Hello JSX!</h1>;
-
JSX编码:
-
基本语法规则:
- 遇到 < 开头的代码, 以标签的语法解析:==html同名标签转换为html同名元素,其它标签需要特别解析==
- 遇到以 { 开头的代码,以JS的语法解析:==标签中的js代码必须用{}包含==
- js中直接可以套标签, 但标签要套js需要放在 { } 中
- 在解析显示js数组时,会自动遍历显示
- 把数据的数组转换为标签的数组
var liArr = dataArr.map(function(item, index){ return <li key={index}>{item}</li> })
-
babel.js的作用
- 浏览器的js引擎是不能直接解析JSX语法代码的,需要babel转译为纯JS的代码才能运行
- 只要用了JSX,都要加上type="text/babel",声明需要babel来处理
-
基本语法规则:
-
==<font color=red>注意:</font>==
- 标签必须有结束
- 标签的class属性必须改为className属性
- 标签的style属性值必须为: {{color:'red', width:12}}
React的其它操作
双向绑定
- React是一个单向数据流
- 可以自定义双向数据流组件(受控组件),需要通过onChange监听手动实现
<script type="text/babel">
class Control extends React.Component{
constructor(props){
super(props)
//初始化状态
this.state = {
msg:'ATGUIGU'
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event){
//得到最新的state的值
const msg=event.target.value;
// console.log(event.target)
// console.log(event.target.value)
//更新状态
this.setState({msg})
}
render(){
const {msg} = this.state
return(
<div>
<input type="text" value={msg} onChange={this.handleChange}/>
<p>{msg}</p>
</div>
)
}
}
ReactDOM.render(<Control/>,document.getElementById('example'))
</script>
React发送ajax请求
- React没有ajax模块,所以只能集成其它的js库(如jQuery/axios/fetch), 发送ajax请求
-
axios
- 封装XmlHttpRequest对象的ajax
- promise
- 可以用在浏览器端和服务器
-
fetch
- 不再使用XmlHttpRequest对象提交ajax请求
- fetch就是用来提交ajax请求的函数,只是新的浏览才内置了fetch
- 为了兼容低版本的浏览器,可以引入fetch.js
-
axios
-
在哪个方法去发送ajax请求:
- 只显示一次(请求一次): componentDidMount()
- 显示多次(请求多次): componentWillReceiveProps()
//做一个跳转页面
<script src="../js/react.js"></script>
<script src="../js/react-dom.js"></script>
<script src="../js/babel.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.16.2/axios.js"></script>
<script type="text/babel">
class UserLastGist extends React.Component {
constructor (props) {
super(props)
this.state = {
url: null
}
}
componentDidMount () {
// 发送ajax请求
const url = `https://api.github.com/users/${this.props.username}/gists`
axios.get(url)
.then(response => {
console.log(response)
// 读取响应数据
//0索引位代表最后更新的网页内容
const url = response.data[0].html_url
// 更新状态
this.setState({url})
})
.catch(function (error) {
console.log('----', error);
})
}
render () {
const {url} = this.state
if(!url) {
return <h2>loading...</h2>
} else {
return <p>{this.props.username}'s last gist is <a href={url}>here</a> </p>
}
}
}
UserLastGist.propTypes = {
username: React.PropTypes.string.isRequired
}
ReactDOM.render(<UserLastGist username="octocat"/>, document.getElementById('example'))
</script>
RESTful
- RESTful是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件. 它主要用于客户端和服务器交互类的软件. 可以使软件更简洁,更有层次,更易于实现缓存等机制
-
REST原则:
- 客户端和服务器之间的交互在请求之间是无状态的
- 分层系统
RESTful的关键
- 定义可表示流程元素或资源的对象: 在REST中,每一个对象都是通过URL来表示的,对象用户负责将状态信息打包进每一条消息内,以便对象的处理总是无状态的
- 组合管理及流程绑定
RESTful与 RPC
- RPC 样式的 Web 服务客户端将一个装满数据的信封:包括方法和参数信息, 通过 HTTP 发送到服务器。服务器打开信封并使用传入参数执行指定的方法。方法的结果打包到一个信封并作为响应发回客户端。客户端收到响应并打开信封。每个对象都有自己独特的方法以及仅公开一个 URI 的 RPC 样式 Web 服务,URI 表示单个端点。它忽略 HTTP 的大部分特性且仅支持 POST 方法
RESTful Web 服务的Java框架
-
Restlet
- 客户端和服务器都是组件, 组件通过连接器互相通信
- 该框架最重要的类是抽象类 Uniform 及其具体的子类 Restlet,该类的子类是专用类,比如 Application、Filter、Finder、Router 和 Route。这些子类能够一起处理验证、过滤、安全、数据转换以及将传入请求路由到相应资源等操作。Resource 类生成客户端的表示形式
- RESTful Web 服务也是多层架构:数据存储层,数据访问层,业务层,表示层
RESTful API
- RESTful:
URL定位资源,用HTTP动词(GET,POST,PUT,DELETE)描述操作
- RESTful API就是一套协议来规范多种形式的前端和同一个后台的交互方式.由SERVER来提供前端来调用,前端调用API向后台发起HTTP请求,后台响应请求将处理结果反馈给前端
RESTful API设计原则
- 资源: 首先是弄清楚资源的概念,资源总是要通过一种载体来反应它的内容.JSON是现在最常用的资源表现形式
- 统一接口: RESTful风格的数据元操CRUD(create,read,update,delete)分别对应HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,统一数据操作的接口
- URI: 可以用一个URI(统一资源定位符)指向资源,即每个URI都对应一个特定的资源.要获取这个资源访问它的URI就可以,因此URI就成了每一个资源的地址或识别符.一般的,每个资源至少有一个URI与之对应,最典型的URI就是URL
- 无状态:所有的资源都可以URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而变化。
有状态和无状态的区别:
例如要查询员工工资的步骤
第一步:登录系统。
第二步:进入查询工资的页面。
第三步:搜索该员工。
第四步:点击姓名查看工资。
这样的操作流程就是有状态的,查询工资的每一个步骤都依赖于前一个步骤,只要前置操作不成功,
后续操作就无法执行。如果输入一个URL就可以得到指定员工的工资,则这种情况就是无状态的,
因为获取工资不依赖于其他资源或状态,且这种情况下,员工工资是一个资源,由一个URL与之
对应可以通过HTTP中的GET方法得到资源,这就是典型的RESTful风格。
RESTful API设计规范
- URI语法
URI=scheme"://"authority"/"path["?"query]["#"fragment]
- scheme:指底层用的协议:http,https,ftp
- host:服务器的IP地址或者域名
- port:端口,http中默认80
- path:访问资源的路径,就是各种web 框架中定义的route路由
- query:为发送给服务器的参数
- fragment:锚点,定位到页面的资源,锚点为资源id
- 资源路径: rest资源的定义,即URL的定义,是最重要的;要设计出优雅的、易读的rest接口
- URL中不能有动词: 在Restful架构中,每个网址代表的是一种资源,所以网址中不能有动词,只能有名词,动词由HTTP的 get、post、put、delete 四种方法来表示
- URL结尾不应该包含斜杠 "/": ==URI中的每个字符都会计入资源的唯一身份的识别中==,这是作为URL路径中处理中最重要的规则之一,正斜杠"/"不会增加语义值,且可能导致混淆.RESTful API不允许一个尾部的斜杠,不应该将它们包含在提供给客户端的链接的结尾处.两个不同的URI映射到两个不同的资源.如果URI不同,那么资源也是如此,反之亦然.因此,RESTful API必须生成和传递精确的URI,不能容忍任何的客户端尝试不精确的资源定位.
- 正斜杠分隔符 "/" 必须用来指示层级关系: URI的路径中的正斜杠 "/" 字符用于指示资源之间的层次关系
- 应该使用连字符 "-" 来提高URL的可读性,而不是使用下划线 "_": 为了使URL容易让人们理解,要使用连字符 "-" 字符来提高长路径中名称的可读性
- URL路径中首选小写字母: RFC 3986将URI定义为区分大小写,但scheme 和 host components 除外
- URL路径名词均为复数: 为了保证url格式的一致性,建议使用复数形式
RESTful API对资源的操作
-
对于RESTful API资源的操作,由HTTP动词表示:
- get: 获取资源
- post: 新建资源
- put: 在服务器更新资源(向客户端提供改变后的所有资源)
-
delete: 删除资源
patch:在服务器更新资源(向客户端提供改变的属性),一般不用,用put
- 资源过滤: 在获取资源的时候,有可能需要获取某些“过滤”后的资源
例如指定前10行数据:
http://api.user.com/schools/grades/classes/boys?page=1&page-size=10
- 返回状态码,推荐标准HTTP状态码: 有很多服务器将返回状态码一直设为200,然后在返回body里面自定义一些状态码来表示服务器返回结果的状态码.由于RESTful API是直接使用的HTTP协议,所以它的状态码也要尽量使用HTTP协议的状态码
200 OK 服务器返回用户请求的数据,该操作是幂等的
201 CREATED 新建或者修改数据成功
204 NOT CONTENT 删除数据成功
400 BAD REQUEST 用户发出的请求有问题,该操作是幂等的
401 Unauthoried 表示用户没有认证,无法进行操作
403 Forbidden 用户访问是被禁止的
422 Unprocesable Entity 当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER ERROR 服务器内部错误,用户将无法判断发出的请求是否成功
503 Service Unavailable 服务不可用状态,多半是因为服务器问题,例如CPU占用率大,等等
有疑问加站长微信联系(非本文作者)