前端中臺系統常見問題剖析與解決方案

乾貨高能預警,此文章信息量巨大,大部份內容爲對現狀問題的思考和現有技術的論證。 感興趣的朋友能夠先收藏,而後慢慢研讀。此文凝結了我在中臺領域全部的思考和探索,相信讀完此文,可以讓你對中臺領域的常見業務場景和解決方法有着全新的認知。前端

此文轉載請註明出處。react

在2019年5月11日的那個週末,我在FDCon 2019大會上進行一次有關中臺領域的分享,分享的標題是《業務實現標準化在中臺領域的探索》,並在現場發佈了RCRE這個庫,並介紹瞭如何使用RCRE來解決中臺業務開發所面臨的各類問題。git

會後看了一些同窗的吐槽,多是我分享方式的問題,使得當時並無詳細的闡述RCRE產生的背景和緣由,以及當時所實際面臨的痛點,而是僅僅去介紹如何使用RCRE了,不免也被冠以出去打廣告的嫌疑。github

RCRE的誕生並非一蹴而就,而是我在這個領域多年摸爬滾打的精華。它每一行代碼都凝結着我從深坑中跳出來以後的思考,是下文介紹了全部問題和場景的解決方案。redux

初次公開分享不免會經驗不足,對在場觀衆的需求把控不清晰。現場演示代碼,可能並不能充分體現出這些API產生的背景和緣由。因此爲了知足當時你們的需求,因此這篇文章,不講代碼,只講思考和論證,來介紹當時我在中臺領域所面臨的問題,以及我針對這些問題的見解和思考和最後我爲何要在RCRE中設計這樣的功能來解決這些問題。後端

更完美的狀態管理方案

過去的幾年,中臺領域出現了不少很是優質的UI組件庫,好比Ant.Design, Element-UI等,這些組件庫解決了過去前端工程師所面臨的還原設計稿成本高的問題,經過採用統一的設計風格的UI組件,就能讓前端工程師無需再專一於切圖和寫CSS,而是更專一於頁面邏輯的實現。設計模式

在頁面邏輯的實現層面,UI組件的狀態管理也有了很大的發展。隨着Flux的提出,再到Redux,Mobx等,使用一個狀態管理庫來管理一個應用的狀態已經成爲了前端主流,甚至在最新的React中,還會有UseReducer這樣源自Redux的API出現。數組

而對於狀態管理,社區也衍生出兩種徹底不一樣的思路。bash

狀態管理的兩極分化

一種是以Redux爲主導的不可變數據流的方案,經過讓整個應用共享一個全局的Store,而且強調每一次數據更新都要保證State徹底不可變,以及徹底避免使用對象引用賦值的方式來更新狀態這樣的方式來保證對頁面的數據操做,讓整個應用具有可追溯,可回滾,可調試的特性。這樣的特性在面對代碼如山同樣的大型複雜應用,有着非同通常的優點,可以快速來定位和解決問題。網絡

不過Redux這樣的模式也存在必定的弊端,首當其中的就是它要求開發者要徹底按照官方所描述的那樣,寫大量的Action,Reducer這種的樣板代碼,會讓代碼行數大量膨脹,使得開發一個小功能變得很是繁瑣。使用單一的State來管理就須要開發者本身去完成State的結構設計,同時不可變數據狀態管理僅僅是Redux所強調的一種思想和要求而已,因爲並無提供有效避免對象引用賦值的解決方案,就須要開發者時刻遵照這種模式,以避免對不可變形成破壞。

所以Redux這種設計模式當然有效,可是過於繁瑣和強調模式也是它所存在的弊端。

而另一種則是與Redux徹底相反的思路,好比Mobx。它鼓勵開發者經過對象引用賦值來更改狀態。Mobx經過給對象添加Proxy的方式,得到了每一個用戶每一個React組件所依賴的屬性,這樣就拿到了對象和組件之間的屬性映射關係,這樣Mobx就能依據這些依賴關係,自動實現組件的更新。使用Mobx以後,State的不少細節都交給Mobx進行管理,也就不會有Redux那種State設計的工做了,同時也就不存在像Redux那樣,編寫大量的樣板代碼,而是直接修改狀態數據就能達到預期的效果。

Mobx這種的思想和Vue的機制很是相似,同時也都存在一樣的一個弊端——因爲沒有狀態的副本,沒法實現狀態的回滾。數據之間的關係捉摸不清,更新的實現徹底被隱藏在Mobx內部,對開發者不可見,當狀態複雜以後,就會形成調試困難,Bug難以復現的問題。

Mobx這種設計模式能在早期能極大提高開發效率,可是在項目後期就會給維護和調試形成必定的困難,形成效率的下降。

可見,在狀態管理不可變數據和可變數據都各有各的優缺點,貌似是魚和熊掌不可兼得。那麼問題來了,是否存在一種新的技術方案,可以結合Redux和Mobx的優勢呢?

有關Redux和Mobx之間對比的詳細的內容,能夠繼續看這篇文章:www.educba.com/mobx-vs-red…

簡單而又可靠的狀態管理

在大型複雜應用開發這種場景下,Redux可靠可是不簡單,Mobx簡單而又不可靠,所以就須要找到一種簡單而又可靠的狀態管理方法。

Redux的可靠在於它可以讓狀態可回溯,可監控,使用單一的狀態能下降模塊太多所帶來的複雜度。Mobx的簡單在於它使用方便,對寫代碼沒有太多要求,也不須要不少的代碼就能實現功能。

對於大型複雜應用來講,狀態可回溯,可監控這些特性是重中之重,有了它才能讓整個應用不會由於太複雜而失控。所以優化的方向就被轉化爲:可否借鑑Mobx這種簡單易用的思想,來下降Redux的使用成本。

在使用單向不可變數據流這種背景下,下降Redux的使用成本須要往如下三個方面發力:

  1. combineReducer的使用會讓開發更繁瑣,所以須要避免每次開發都須要進行State結構設計
  2. 每一次數據操做都要寫Action,Reducer也會讓開發更繁瑣,所以須要避免編寫大量的Action,Reducer
  3. 不是全部人寫的Reducer都能保證State修改不可變,所以須要一種替代方案來修改State

針對以上三個方面,我認爲能夠採起如下方法來進行解決:

  1. 利用將組件之間的結構關係映射到State,就能在一開始就推斷出State的結構,進而自動幫助開發者完成combineReducer這樣的操做。
  2. 將多個Action進行合併,爲開發者直接提供通用Action的方式,多個Action之間利用參數來進行區分,以解決Action過多的問題。
  3. 爲開發者封裝狀態操做的API,在內部實現不可變的數據操做,避免開發者直接接觸到State。

有了上面三個基本的思想,接下來就是要思考如何纔可以和現有的Redux架構進行整合。

像Mobx同樣去使用Redux

首先,第二個和第三個方法能夠被整合成一個API——一個通用,保證狀態不可變的狀態修改API。這樣就和Mobx直接改了數據狀態就更新的操做很相像了——調用這個API就把修改狀態搞定了。

而對於第一點,熟悉react-redux的同窗都知道,Redux中的State,是經過編寫mapStateToProps函數來將狀態映射到組件的Props上的。而mapStateToProps函數的參數倒是整個Redux的State,想要將它映射到組件中,還須要完成從State取值的操做。而當咱們在一開始設計狀態的時候,依然須要去想個名字來完成整個狀態的結構設計,先後一對比,仔細想一想後會發現,這一前一後都是須要一個Key才能完成,爲什麼不用同一個Key呢?這樣一個Key既能夠完成Redux的State中,每個Reducer的劃分,也能夠完成mapStateToProps的時候,屬性的讀取。

因此咱們只須要將這個的一個Key放到一個組件的屬性上,經過組件的掛載來完成過去須要combineReducer才能完成的狀態劃分,而後再mapStateToProps的時候,一樣依據這個屬性,完成State到Props的映射。並且經過這樣的方式,整個State的結構都徹底能夠在組件上進行控制了,也就不須要再去使用combineReducer這樣的API了。

經過上述的講述的方法,咱們就能夠將它們封裝起來,作成一個React組件,讓這個組件來幫助咱們管理狀態,而且經過這個組件的API來修改狀態。這也就是RCRE中,Container組件背後的思想。

Mobx的簡單不光光在於開發者不須要思考如何去更新和管理狀態,它還有一個很大的優點在於,你能夠再任何一個地方均可以直接去修改狀態。相比目前Redux中,一些值和函數都採用props進行傳遞這種繁瑣的方式,Mobx這樣的功能會讓人感受方便很多。

所以,即便如今有了Container這種能夠幫助咱們自動管理狀態的組件以外,咱們還須要一種相似於Mobx這樣,能夠繞過props也能傳遞數據和方法的設計。

React在16版本推出了新的Context API,這也是所官方推薦的一種跨props傳遞數據的解決方案。所以咱們能夠利用這個API,來實如今Container組件內部的任何一個地方,均可以自由讀取狀態和修改狀態。也就是RCRE中,ES組件背後的思想。

總結一下,解決Redux使用成本高的問題的核心就在於,找出那些能夠被重複利用,差別性不是特別大的地方,再加以封裝,就能獲得很是不錯的效果。

總結來看的話,整個模型就可使用下面的圖來進行歸納。

image-20190520154645038

解決組件聯動所帶來的複雜性

寫過中臺類型系統的人都知道,凡是涉及到組件聯動的需求,項目排期必定很長。由於一旦頁面中的組件有了關係,那麼就得花費大量時間來處理每一次聯動背後,每一個組件的更新,組件的數據狀態建立與銷燬,稍有不注意,就有可能寫出聯動以後組件沒有正確更新,或者是組件銷燬數據沒有銷燬的Bug。

當天天維護的就是這樣一個包含數不清的聯動關係的大型系統,每個Bug所帶來的損失都不可估量的時候,這背後的難度也就可想而知了。

組件聯動的本質

組件聯動自己並不複雜,咱們能夠把它簡單描述爲:當一個組件更新以後修改全局的狀態,其餘組件須要根據狀態來作出相應的反應。同時組件修改狀態並不必定是同步操做,它還有多是異步的操做,好比調用一個接口。組件修改狀態它僅僅是一個單向的操做,是很容易被理解的,而你們都以爲開發帶有組件聯動的功能很複雜的緣由是在於,當這個組件完成了狀態更新以後,究竟有哪些組件會所以而聯動,將是一件很複雜的事情。

單向數據流思想的價值所在

在過去MVC架構的應用中,這樣的場景是很是難以處理的。由於組件與組件之間的通訊是經過發佈訂閱這種模式進行的,當組件之間關係複雜以後,就會造成一種網狀的依賴結構,在這種結構下,暫且不說能不能理清它們之間的關係,光是可能出現的環形依賴所形成的死循環,就已經讓開發者抓狂。

React的單向數據流思想,我認爲就是應對這種問題最好的方法。由於在單向數據流的架構下,組件之間的關係從過去的網狀結構,轉變成了樹狀結構。在樹狀結構模型下,組件與組件之間只存在,父子關係與兄弟關係這兩種狀況,並且尚未環形依賴。這就大大簡化了關係複雜所產生的一系列問題,讓整個組件結構一直都能保持穩定。

image-20190520154951688

每一個組件都要管好本身

接下來就是要思考,當一個組件更新的時候,該如何去更新其餘組件了。

當場景很複雜的時候,咱們是很難搞清楚一個組件的更新究竟要觸發哪些組件,那麼最好的辦法就是讓每一個組件本身主動對當前的狀況作出反應。

這也就是不難理解,React爲每一個組件都提供了生命週期函數這樣的功能了。當組件開始聯動的時候,咱們不須要分析出一個組件究竟須要影響哪些組件,而是讓每個組件都管好本身就行了,就像父母和老師常常就對孩子說,管好你本身,你已是個大人了。

經過一個組件的觸發,來帶動組件父級的更新,父級再進而帶動其全部組件的更新,而後每一個子組件更新的時候去檢查數據並做出相應的反應,就能以可持續的方式來實現組件聯動。

經過結合生命週期和組件狀態來提高效率

當組件被其餘組件所影響的時候,組件大體分爲三種不一樣的狀態:

  1. 組件掛載
  2. 組件更新
  3. 組件銷燬

一個完備的業務組件,想要去支持被其餘組件聯動觸發的話,除了組件的基礎渲染結構,仍是須要在以上三個方面添加針對這個組件的一些實現。不過當系統中有不少不少的組件的時候,反覆爲每一個組件都實現上訴三個方面的功能,就顯得有些重複性勞動。

所以咱們就須要想個辦法不去單獨爲每一個組件都編寫這些邏輯,而是尋找到一種更爲通用的方法。首先,咱們須要先對這三個方面的功能進行更爲細緻的分析。

組件的掛載的時候,除了要初始化一些私有的數據和狀態以外,可能和其餘組件產生影響的就是這個組件的默認值了,當組件初始化的時候,就要馬上將組件初始化寫入到狀態中,來完成一些特定業務需求所須要的初始默認值。

當組件被更新的時候,若是整個組件渲染的數據徹底是來自於props,是個徹底的受控組件的話,正常狀況下是不須要作任何處理的。

當組件被銷燬的時,若是業務有需求,是須要自動將這個組件所帶有的狀態也一併在狀態中刪除。

經過以上分析,能夠看出,在生命週期內所實現了和狀態有關的操做,都是對某個指定的Key執行新增或者刪除相關的操做。因此要想提高效率,就只須要將這個Key也做爲組件的一個屬性,而後就能夠在底層實現通用的掛載邏輯和銷燬邏輯,實現簡單的配置就完成了生命週期和組件狀態的整合。

這些思考,均可以在RCRE中的ES組件中找到對應的實現:

  1. 執行狀態操做的Key: name屬性
  2. 組件初始化的默認值: defaultValue屬性
  3. 控制組件是否須要銷燬時自動清除數據: clearWhenDestory屬性

接口調用的通用模式

接口調用在常規的中臺應用中很常見,任何涉及增刪改查的應用都是須要依賴一些後端接口。

在一些簡單的場景,你可能只須要在某個回調函數內調用fetch就能拿到接口的數據。

不過對於較爲複雜的場景和中大型應用,接口的調用就更須要規範化。所以纔會有利用Action來調用接口的方案出現。不過當場景愈來愈複雜,好比一個Action調用多個接口這種狀況,redux-thunk這種簡單的方案就會顯得力不從心,所以社區又出現了redux-saga這種能夠支持多接口並行調用等更高級的庫出現。不過redux-saga的學習成本並不低,甚相當於什麼是saga,還專門有一篇論文來解釋,耗費這麼多精力來學習各類庫和概念,等真正要在業務中實際應用的時候,仍是一頭霧水。沒有任何開發經驗的同窗,依然很難處理好如何調用接口這個問題。

所以關於異步獲取數據,我認爲須要用一種更爲簡單傻瓜的設計,提供一種可以覆蓋多種業務場景的統一方法,來幫助開發者快速理解並完成它們須要的功能。

和接口相關的常見業務場景

針對這樣的問題,從業務角度來進行思考是一個很是不錯的方向,在這個方向努力,就能實現快速解決業務中那些常見場景下的功能需求。

首先,須要來分析一下,在中臺系統中,和異步獲取數據相關的一些常見功能:

  1. 由各類參數和條件觸發的查詢
  2. 頁面一開始初始化所需的數據
  3. 組件聯動時須要的數據
  4. 並行調用無依賴的接口
  5. 串行調用相互依賴的接口

以上的三個方面,幾乎就囊括了常規那些中臺業務需求中除了表單驗證以外須要接口的場景了。接下來,就是要從這些功能中,找出它們的共同點,這樣才能作出更爲通用的設計,來應對不一樣需求變動所帶來的不肯定性。

接口參數和接口觸發的關係

對於不同的業務功能,接口參數和組件的觸發的時機是可變的,它取決於當前業務所須要的字段和每一個UI組件所觸發的回調函數。不變的是每一次接口的請求,都將伴隨着組件的更新,畢竟拿到接口數據以後,必然要更新組件才能將接口數據傳遞給其餘組件。

所以對於第一類功能,無論頁面中的組件是如何變化,只要這個組件可以觸發接口,那麼它必然會影響到接口請求的參數,不然沒有參數變動的請求是不會知足與當前的業務需求的。所以關鍵點就在於參數的變動和請求接口之間的關係:

參數變化,觸發接口
參數不變,不觸發接口
複製代碼

剛好的是,任何狀態的更新都將觸發容器組件的更新,進而更新整個應用的組件。所以咱們能夠利用這樣的一個特性 —— 在容器組件上掛載鉤子來自動觸發接口,而且在請求以前,讀取最新的狀態來動態的去計算接口的參數,進而判斷出是否須要觸發接口。

所以咱們就能夠很巧妙的設計出這樣的一套觸發流程:

各類不一樣的操做更新了狀態 --> 容器組件更新 --> 從新計算接口參數 --> 斷定並觸發接口
複製代碼

接口初始化多樣性所帶來的問題

對於第二類功能,在最簡單的狀況下,頁面初始化的時候,它所依賴的接口是無條件觸發的。可是現實並非如此,由於某些接口的初始化是存在條件的,它多是依賴某個組件的數據,也有多是依賴某個接口。

不過在平常業務開發中,只有最簡單的場景下,接口的調用是放置在componentDidMount這類生命週期內部,其餘帶有條件的接口初始化調用,是沒法放置在componentDidMount內部的,而是分散在其餘地方。壞的狀況就是被放置在某個組件的回調函數內,等接口調用完再執行下一次操做,好的狀況就是會封裝一個Redux middleware, 經過全局攔截的方法來調用。

仔細想一想的話,就會發現這樣的作法會有不少弊端,第一點是接口的調用不夠集中,它是分散的,這樣就會給大型應用的代碼管理形成很大的障礙。第二點是接口的調用都須要一個特定的前置條件,這樣的前置條件多是取決於代碼調用的位置,也有多是來自於一大堆if else的判斷,這些都對如何管理和組織接口形成了很大的難題。

不過若是咱們將視野放寬,從關注如何去調用一個接口,放大到組件的狀態和接口之間的關係,就會發現此類問題,都能使用上面所推導出的觸發流程來解決。

經過將可以觸發接口請求的數據都存入到State中,而且在每一個接口上添加一些觸發的附加條件,就能複用上面那個觸發流程模型:

普通組件掛載 --> 組件初始化數據 --> 狀態更新 --> 容器組件更新 --> 接口斷定是否知足請求條件 --> 從新計算接口參數 --> 斷定並觸發接口
複製代碼

這樣的話,咱們就可使用同一種機制和模型,來完成第一類和第二類場景下有關接口的需求。

複雜的組件聯動所形成的開發成本劇增

組件聯動是中臺領域中一個比較複雜的場景了,由於它涉及一個組件的數據變動對其餘組件的狀態影響。

當頁面中一個組件的數據發生了變動,若是有一些組件和這個組件存在聯動的話,那麼全部涉及的組件都將全部反應,反應的行爲一般包括新組件的掛載,現有組件的更新,以及組件的銷燬等。組件之間的聯動關係並非固定的,而是徹底取決於當前的業務邏輯。若是在如此複雜的組件關係中,還須要去調用新的接口,例如須要請求接口來爲新出現的下拉選項組件提供數據,那麼在何處調用這個接口,就又是一個值得推敲的問題了。

組件聯動以後去調用接口,並非僅僅在新組件的componentDidMount中寫入接口調用那麼簡單,由於這個接口調用,不必定是在當前組件掛載完畢以後就知足請求的條件,有可能新的接口調用,是須要兩個以上的接口都完成掛載並初始化數據以後才能發起請求。這樣的話,接口的調用就只能被移植到狀態更新以後,而後再單獨編寫一些斷定才能解決。

今後可見,組件的聯動和特定的接口觸發條件會急劇增大完成需求的難度。若是咱們將上面所介紹的機制拿來和現有的場景進行對比後發現,組件的聯動帶來的接口觸發,也只不過是個紙老虎。

組件的聯動,必然會涉及狀態。無論是一對一的聯動,仍是一對多的聯動,都離不開背後對組件狀態的修改。狀態可以時刻反映出當前組件的狀況。

由於組件的聯動只不過是多個組件狀態的變動,因此咱們依然能夠採用上面所介紹的模型來解決這樣的一類問題:

A組件被觸發 --> 狀態更新 --> B組件和C組件作出反應 --> 狀態更新 --> 容器組件更新 --> 接口斷定是否知足請求條件 --> 從新計算接口參數 --> 斷定並觸發接口
複製代碼

這樣的話,咱們就可使用同一種機制和模型,完成一二三類場景下有關接口的需求。

如何處理接口之間的關係

當應用複雜起來以後,不光組件之間存在不少的關係,接口與接口之間也是。而每個接口的請求,都是須要消耗必定的網絡時間。可是接口與接口是否存在關聯,是徹底取決於當前的業務需求和數據現狀。當接口觸發的條件並非來自於其餘接口返回的數據,咱們能夠認爲接口與接口之間不存在關聯。

若是不使用任何async await或者是redux-saga這樣的工具的話,在一個函數內調用多個接口很容易出現callback hell的狀況,也給接口的管理形成必定的負擔。

可是,若是咱們仔細研究的話,會發現每一個接口在最後,都會將返回數據或者一部分寫入到狀態中。那麼若是咱們給每個接口進行命名,讓接口返回以後,將返回數據寫入到這個名字爲Key的值中。那麼就能夠直接在狀態中經過斷定這個名字是否存在來斷定接口已經成功返回,這樣就和判斷其餘組件的值是否在狀態中沒有任何區別。

有了以上的基礎,那麼斷定接口是否返回就和斷定組件同樣簡單,所以就能夠將它囊括到接口斷定是否知足條件中去。

總結

要想將調用接口這麼複雜的事情作到傻瓜化,就須要找出不一樣場景下,這些操做的共同點,找出共同點就能設計一個通用的模型來解決一系列的問題,實現應對多種不一樣場景下的接口需求。這也是RCRE中Container組件的DataProvider功能的背後的思想。

流程式任務管理

在中臺系統中,還有一類特殊的業務功能是很難被一種模型所歸納的——由用戶行爲所觸發了線性交互邏輯。

這種類型的業務功能有一些比較明顯的特色:

  1. 它並不複雜,一般是一連串操做的組合
  2. 它由用戶行爲所觸發,也可能會涉及一些連續交互的功能。
  3. 徹底由業務邏輯所主導,並無太多的共同點

一般狀況下,這類邏輯就大量分散在系統的各個組件內部,看上去像是某些事件的回調函數。可是隨着需求的不斷迭代,就會讓組件變得很是膨脹,以致於會影響整個組件的可維護性。

因爲每一個功能都是徹底按照需求所定製化開發的,在一些常見業務功能都被高度封裝的狀況下,多個功能之間的銜接,依然須要工程師人工編寫代碼來進行完成。

這樣就會形成一個問題——功能的複用程度並非特別高,由於有至關一部分的代碼都是膠水代碼,是沒法被複用的。因此想要提高總體的代碼複用性,就須要去思考,如何才能減小膠水代碼的開發。

分析交互邏輯的內部細節

假若仔細去分析以後就會發現,組合通用邏輯的膠水代碼,無論是執行同步的操做仍是異步的操做,它都是以線性的方式去執行,相比組件與組件之間的關係來講,交互邏輯這類代碼的結構都比較簡單,它們都是在上一個操做完成以後才能去執行下一個操做,當中間遇到了一些執行錯誤或者異常時,都是退出這個操做就結束了。

task1 --> task2 --> task3 --> task4
複製代碼

因此,這個問題就能夠被轉變爲如何找到一種可以去結構化同步或者異步操做的機制。

多個異步操做可使用Promise進行串行調用,同步的操做也能夠被包裝成馬上返回的異步操做。因此可使用Promise來將異步和同步之間的差別進行打平。

串行調用在程序的世界中是很是常見的操做,例如reduce函數,就是一個很是好的例子。若是可以將每個操做的調用,放置在一個數組中,那麼就可使用一次調用,來進行批處理操做。

批處理的數據來源

對於每一個交互邏輯來講,它都須要讀取一些參數來完成它的工做。好比發起請求須要參數,彈出確認框須要提示信息,數據驗證須要輸入數據。這些操做的數據來源有多是來自於用戶觸發事件時的事件對象,也有多是來自於當前整個應用中狀態的數據,也有多是來自於上一個操做的返回值。

因此若是要作這樣的一套批處理機制,讓每個操做都能很順暢的運行的話,那麼封裝全部來源的數據就是一件頗有必要的事情了。

所以就須要在調用每個操做所封裝的函數以前,把當前全部的數據信息都收齊起來,組裝成一個對象傳入到函數中,來知足不一樣的業務需求所須要的數據。

每個操做在執行的過程當中,都有可能讀取如下來源的數據:

  1. 上一個操做的返回值
  2. 事件觸發的時候,傳遞的值
  3. 全局應用的狀態

固然,批處理還須要具有錯誤能力——當任何一個操做返回的異常,整個操做就會直接被終止。

配置聚合和控制中心

任何零散的事物要想有組織的進行工做,就必需要有控制中心。

在過去,處理交互邏輯是很是的分散的,即便如今有了相似於reduce的批處理操做,若是它依然是散步在一些鮮爲人知的角落,這依然沒法解決分散所致使的混亂問題。因此咱們還須要將批處理的配置聚合在一塊兒,並放置在最顯眼固定的位置,讓每個人都知道想要找到這段邏輯是如何工做的,就須要看這裏就夠了。

所以就須要思考,這樣的一個包含全部操做的信息的控制中心,應該放置在哪裏比較好。

頁面中的組件,都是以樹狀的結構進行組織的,那麼無論這個頁面中組件的數量有多大,這些組件必定都會有一個最頂層的父級組件。因此這個站在金字塔最頂層的組件,就是放置控制中心的最佳選擇,怎麼看起來感受和現實世界中的狀況差很少(笑。

而在React應用中,直接和狀態通訊的容器組件,就是聚合配置信息的組件了。也就是爲何在RCRE中,Task功能是做爲Container組件的一個屬性的存在。

而流程式任務管理,正是RCRE的任務組功能背後的思想,經過這樣的一套機制,就能過去分散的交互邏輯,有跡可循,易於調整。

更便捷的表單驗證

表單一直都是中臺領域中開發成本高的表明。它含有數不清的交互場景,也是業務需求最頻繁改動的重災區。

實現單個表單驗證並非很難的一件事情。表單驗證的目的就是要去驗證用戶輸入的組件數據,經過驗證數據的合法性來給予用戶一些反饋。所以表單驗證就只有2個功能,第一是組件數據的改變觸發驗證,第二是將驗證結果反饋給用戶。

頁面中的數據是多變的,實現一個全面的數據驗證功能,光在組件的onChange事件內添加鉤子來觸發驗證是遠遠不夠的,由於組件的數據不光來自於本身,還有可能會來自於其餘組件。除此以外,針對頁面輸入框這種特殊的組件,觸發表單驗證還有onBlur事件這樣特殊的交互。

所以實現數據的驗證功能就須要圍繞三個方面來開展,第一是onChange事件的觸發,第二是組件所讀取的數據發生改變時觸發,第三是onBlur事件這種特殊場景。

而對於頁面中的結果反饋,由於它涉及到組件的渲染,全部是須要經過一個統一的狀態來進行控制,這樣才能經過組件渲染到頁面上,進而給予用戶提示。

因此總結來看,實現一個組件的驗證功能不光光是一個簡單的數據校驗邏輯,而是要去完成如下的工做:

  1. 對數據的校驗邏輯
  2. onChange事件鉤子
  3. onBlur事件的鉤子
  4. 組件更新時對數據變動的判斷
  5. 存儲表單驗證狀態的State
  6. 展示錯誤信息的組件

以上就是完成一個組件驗證所須要的工做了,可是這並非最煩人的地方,最讓開發者頭疼的,是以上這些工做,每一個須要被驗證的組件都要完成,那麼須要去寫的代碼可就多了去了。

利用狀態來驅動表單驗證

仔細觀察這些觸發表單的場景以後會發現,上訴2,3,4點的是業務中最多見的應用場景,同時這三點的背後,也和狀態的更新徹底保持一致。由於不管是onChange事件仍是onBlur事件,仍是對數據變動的判斷,都是先有組件的狀態變動,再有的驗證,所以充分利用這個特性來節省工做量,就是解決2,3,4這三類問題的突破點。

表單驗證和組件狀態變動是同步變動的,那麼只須要在組件變動的不一樣生命週期和回調函數內,添加觸發表單驗證邏輯的鉤子,就能很好的讓表單也跟着組件的狀態一塊兒變化。

表單驗證的常見業務場景

經過上的分析和方法,表單驗證能夠被狀態自動觸發,因此咱們能夠把經過狀態來觸發表單驗證全部的場景都列舉出來:

  1. 組件觸發onBlur事件來觸發驗證
  2. 組件觸發onChange事件來觸發驗證
  3. 經過一個接口來驗證數據
  4. 組件被其餘組件所聯動來觸發驗證
  5. 特殊驗證場景,好比特定的驗證邏輯
  6. 組件被刪除也要同步清空組件的驗證狀態

同時,除了和狀態之間的關係,表單還有一些它所特有的場景:

  1. 經過點擊提交按鈕,在發送請求以前觸發全部組件的驗證
  2. 跳過被禁用按鈕的驗證功能
  3. 多組件之間的驗證相互互斥

同時表單的禁用特性,還會和組件聯動有關:經過一個組件,來控制另一個組件的禁用屬性,進而操做驗證狀態。

提供表單特有場景下的支持

根據以上的分析,表單有三個特有的場景須要被支持。對於第一個場景,點用戶點擊了提交按鈕的時候,最外層的Form組件會觸發onSubmit事件,所以能夠爲開發者提供一個封裝好的回調函數給開發者使用。在這個回調函數內部,須要去依次去觸發每一個組件的驗證功能,來進行全局的校驗,來確保提交的時候,每一項都驗證經過。

在表單中,被禁用的組件是不須要驗證功能的,由於用戶沒法更改組件的輸入,那麼驗證也就沒有了意義,所以還須要專門監控組件的disabled屬性以便當組件被設置爲禁用的時候,馬上充值組件的驗證狀態。

對於組件驗證互斥這種特殊的驗證邏輯,咱們能夠將它看做是一種將組件狀態和驗證狀態進行整合的功能。由於要想實現驗證互斥,就必需要去讀取其餘組件的驗證狀態,並將自身取反,所以就只須要給開發提供一個能夠自定義擴展驗證的功能就足以,具體的專門邏輯實現交給開發者處理。不過這裏須要注意的是,在提供自定義驗證的同時,還要給開發者提供讀取全局狀態的能力,由於實現這種功能不只要讀取自身的數據,而是要讀取來自其餘組件的數據,這是一個須要注意的地方。

在RCRE中,組件都已經徹底具有此類功能,可以自動幫助開發者完成那些由各類狀態變動而觸發的表單驗證場景。

表單自身的私有狀態

因爲表單也須要來存儲當前的驗證信息和錯誤信息,所以表單也須要和組件的同樣,須要持有一些狀態。

所以想要節省開發表單時,驗證和錯誤信息的開發工做量,就須要爲開發者提供一個通用的狀態存儲功能。同時表單的狀態並非相似於組件的狀態那種,會有聯動的功能,每一個組件的驗證都是相互獨立,只爲當前組件所負責。

所以就能夠直接使用React State這種輕量級的狀態管理功能來完成組件驗證狀態的持有,經過將其封裝成一個React組件,就能方面開發者進行使用,這也就是RCRE中RCREForm />組件背後的思想。

除了一個存儲整個表單狀態的組件,每一個組件的驗證狀態還須要實時同步到這樣的統一存儲區域。所以就須要像上文所介紹的和組件通信的機制相似,採用React Context API來實現組件驗證狀態和之間的通信,以完成表單驗證狀態的同步,這就是RCRE中組件背後的思想。

有了這兩個機制,開發者就不須要手動去編寫實現來維護表單的驗證狀態了,因此對於上述第五點和第六點所帶來的重複性工做也就迎刃而解。

寫在最後

這篇文章全部的內容,就是RCRE這個庫背後全部的設計思路和思想了,想必你看到這裏也可以理解爲何會有RCRE這樣的庫誕生了。若是有興趣想繼續瞭解這個項目,能夠點擊下面這個連接:

github.com/andycall/RC…

若是有任何問題,歡迎在下方留言,我儘量將內容作到更好。

下載
相關文章
相關標籤/搜索