微服務整體架構
- 開發前背景分離:前台與背景之間,通過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工作原理
- 過濾器機制
1.Zuul的過濾器之間沒有直接的互相通信,他們之間通過一個RequestContext的靜态類來進行資料傳遞的。RequestContext類中有ThreadLocal變量來記錄每個Request所需要傳遞的資料 2.Zuul的過濾器是由Groovy寫成,這些過濾器檔案被放在Zuul Server上的特定目錄下面,Zuul會定期輪詢這些目錄,修改過的過濾器會動态的加載到Zuul Server中以便過濾請求使用
- StaticResponseFilter: StaticResponseFilter允許從Zuul本身生成響應,而不是将請求轉發到源
- SurgicalDebugFilter: SurgicalDebugFilter允許将特定請求路由到分隔的調試叢集或主機
- PRE: 在請求被路由之前調用,利用這種過濾器實作身份驗證、在叢集中選擇請求的微服務、記錄調試資訊等
- ROUTING: 請求路由到微服務,用于建構發送給微服務的請求,使用Apache HttpClient或Netfilx Ribbon請求微服務
- POST: 在路由到微服務以後執行,用來為響應添加标準的HTTP Header、收集統計資訊和名額、将響應從微服務發送給用戶端等
- ERROR: 在其他階段發生錯誤時執行該過濾器
-
标準過濾器類型:
Zuul大部分功能都是通過過濾器來實作的。Zuul中定義了四種标準過濾器類型,這些過濾器類型對應于請求的典型生命周期
- 内置的特殊過濾器:
-
自定義的過濾器:
除了預設的過濾器類型,Zuul還允許我們建立自定義的過濾器類型。如STATIC類型的過濾器,直接在Zuul中生成響應,而不将請求轉發到後端的微服務
- Zuul提供了一個架構,可以對過濾器進行動态的加載,編譯,運作
-
過濾器的生命周期
Zuul請求的生命周期較長的描述了各種類型的過濾器的執行順序
微服務架構相關技術整理 - 過濾器排程過程
微服務架構相關技術整理 - 動态加載過濾器
微服務架構相關技術整理
Zuul的作用
Zuul可以通過加載動态過濾機制實作Zuul的功能:
- 驗證與安全保障: 識别面向各類資源的驗證要求并拒絕那些與要求不符的請求
- 審查與監控: 在邊緣位置追蹤有意義資料及統計結果,得到準确的生産狀态結論
- 動态路由: 以動态方式根據需要将請求路由至不同後端叢集處
- 壓力測試: 逐漸增加指向叢集的負載流量,進而計算性能水準
- 負載配置設定: 為每一種負載類型配置設定對應容量,并棄用超出限定值的請求
- 靜态響應處理: 在邊緣位置直接建立部分響應,進而避免其流入内部叢集
- 多區域彈性: 跨越AWS區域進行請求路由,旨在實作ELB使用多樣化并保證邊緣位置與使用者盡可能接近
Zuul與應用的內建方式
- ZuulServlet - 處理請求(排程不同階段的filters,處理異常等)
- ContextLifecycleFilter的核心功能是為了清除RequestContext;請求上下文RequestContext通過ThreadLocal存儲,需要在請求完成後删除該對象RequestContext提供了執行filter Pipeline所需要的Context,因為Servlet是單例多線程,這就要求RequestContext即要線程安全又要Request安全。context使用ThreadLocal儲存,這樣每個worker線程都有一個與其綁定的RequestContext,因為worker僅能同時處理一個Request,這就保證了Request Context 即是線程安全的由是Request安全的。
- 所有的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 的生命周期管理:
- 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
微服務架構相關技術整理 可以類比CPU和硬碟,既然硬碟這麼慢,我們就在它們之間加個緩存:既然 DOM 這麼慢, 我們就在它們JS和DOM之間加個緩存.CPU(JS)隻操作記憶體(Virtual DOM,最後的時候再 把變更寫入硬碟(DOM)
微服務架構相關技術整理 - Virtual DOM本質上就是在JS和DOM之間做了一個緩存
- 用JS對象樹表示DOM樹的結構.然後用這個樹建構一個真正的DOM樹插入到文檔中
- 當狀态變更的時候,重新構造一棵新的對象樹.然後用新的樹和舊的樹進行比較,記錄兩棵樹差異
- 把差異應用到真實DOM樹上,視圖就更新了
- 一個虛拟DOM(元素)是一個一般的js對象,準确的說是一個對象樹(倒立的)
- 虛拟DOM儲存了真實DOM的層次關系和一些基本屬性,與真實DOM一一對應
- 如果隻是更新虛拟DOM, 頁面是不會重繪的
- 虛拟DOM定義:
- Virtual 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(元素)
- 參數一: 純js或jsx建立的虛拟DOM對象
- 參數二: 用來包含虛拟DOM元素的真實dom元素對象(一般是一個div)
- 文法: ReactDOM.render(virtualDOM,containerDOM)
- 作用: 将虛拟DOM元素渲染到真實容器DOM中顯示
- 參數說明:
// 渲染到真實的頁面中
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.工廠(無狀态)函數(簡單元件,推薦使用)
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元素對象(表單标簽居多)
- 事件處理
<input onFocus={this.handleClick}/> handleFocus(event) { event.target //傳回input對象 }
- React使用的是自定義(合成)事件, 而不是使用的DOM事件
- React中的事件是通過委托方式處理的(委托給元件最外層的元素)
- 通過onXxx屬性指定元件的事件處理函數(注意大小寫)
- 通過event.target得到發生事件的DOM元素對象
- 強烈注意
2.箭頭函數(ES6子產品化編碼時才能使用)this.change = this.change.bind(this);
- 元件内置的方法中的this為元件對象
-
在元件中自定義的方法中的this為null
1.強制綁定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()
- 生命周期流程:
- componentWillUpdate(): 将要更新回調函數
- render(): 更新,重新渲染
- componentDidUpdate(): 已經更新回調
- 删除元件
- ReactDOM.unmountComponentAtNode(div):移除元件
- componentWillUnmount():元件将要被移除回調
- constructor(): 建立對象初始化state
- componentWillMount(): 将要插入回調函數
- render(): 用于插入虛拟DOM回調函數
- componentDidMount(): 已經插入回調函數.在此方法中啟動定時器,綁定監聽,發送Ajax請求
- 第一次初始化渲染顯示:render()
- 每次更新state:this.setSate()
- 常用的方法
- 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編碼:
var liArr = dataArr.map(function(item, index){ return <li key={index}>{item}</li> })
- 浏覽器的js引擎是不能直接解析JSX文法代碼的,需要babel轉譯為純JS的代碼才能運作
- 隻要用了JSX,都要加上type="text/babel",聲明需要babel來處理
- babel.js的作用
-
遇到 < 開頭的代碼, 以标簽的文法解析:
html同名标簽轉換為html同名元素,其它标簽需要特别解析
-
遇到以 { 開頭的代碼,以JS的文法解析:
标簽中的js代碼必須用{}包含
- 基本文法規則:
- js中直接可以套标簽, 但标簽要套js需要放在 { } 中
- 在解析顯示js數組時,會自動周遊顯示
- 把資料的數組轉換為标簽的數組
-
- 标簽必須有結束
- 标簽的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請求
- 不再使用XmlHttpRequest對象送出ajax請求
- fetch就是用來送出ajax請求的函數,隻是新的浏覽才内置了fetch
- 為了相容低版本的浏覽器,可以引入fetch.js
- 封裝XmlHttpRequest對象的ajax
- promise
- 可以用在浏覽器端和伺服器
- axios
- fetch
- 在哪個方法去發送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
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占用率大,等等