首頁 > 軟體

微服務架構相關技術整理

2021-03-02 14:00:16

微服務整體框架

  • 開發前後臺分離:前臺與後臺之間,通過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 框架
如何選擇RPC框架

選擇一個rpc框架會基於多方面的考慮:框架特性、效能、成熟度、技術支援、社群活躍度等多個方面.最重要一點,這也是往往很多技術人員進入的誤區, "對於技術,不要為了使用而使用,用最簡單合適的技術實現解決問題才是正道" .架構是服務於業務的,能快速方便的滿足業務需求的架構才是好的架構.沒有最好的,只有適合自己的

Dubbo

  • Dubbo是一個開源分散式服務架構,阿里巴巴公司開源的一個高效能優秀的服務架構,使得應用可通過高效能的 RPC 實現服務的輸出和輸入功能,可以和Spring框架無縫整合.
  • Dubbo是一款高效能,輕量級的開源Java RPC框架,它提供了三大核心能力:面向介面的遠端方法呼叫,智慧容錯和負載均衡,以及服務自動註冊和發現
核心元件
  • Remoting: 網路通訊框架,實現了sync-over-asyncrequest-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本質上就是在JSDOM之間做了一個快取
    可以類比CPU和硬碟,既然硬碟這麼慢,我們就在它們之間加個快取:既然 DOM 這麼慢,
    我們就在它們JS和DOM之間加個快取.CPU(JS)只操作記憶體(Virtual 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物件
          }
    
  • 強烈注意
    • 元件內建的方法中的this為元件物件
    • 在元件中自定義的方法中的this為null
      1.強制繫結this
    this.change = this.change.bind(this);
    
    2.箭頭函數(ES6模組化編碼時才能使用)
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(): 必須重寫,返回一個自定義的虛擬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來處理
  • 注意:
    • 標籤必須有結束
    • 標籤的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
  • 在哪個方法去傳送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佔用率大,等等

IT145.com E-mail:sddin#qq.com