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

blank

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

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

此文轉載請註明出處。

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

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

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

初次公開分享難免會經驗不足,對在場觀眾的需求把控不清晰。現場演示代碼,可能並不能充分體現出這些API產生的背景和原因。所以為了滿足當時大家的需求,所以這篇文章,不講代碼,只講思考和論證,來介紹當時我在中台領域所面臨的問題,以及我針對這些問題的看法和思考和最後我為什麼要在RCRE中設計這樣的功能來解決這些問題。

更完美的狀態管理方案

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

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

而對於狀態管理,社區也衍生出兩種完全不同的思路。

狀態管理的兩極分化

一種是以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之間對比的詳細的內容,可以繼續看這篇文章: educba.com/mobx-vs-redu

簡單而又可靠的狀態管理

在大型複雜應用開發這種場景下,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使用成本高的問題的核心就在於,找出那些可以被重複利用,差異性不是特別大的地方,再加以封裝,就能得到非常不錯的效果。

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

blank

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

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

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

組件聯動的本質

組件聯動本身並不復雜,我們可以把它簡單描述為:當一個組件更新之後修改全局的狀態,其他組件需要根據狀態來做出相應的反應。同時組件修改狀態並不一定是同步操作,它還有可能是異步的操作,比如調用一個接口。組件修改狀態它僅僅是一個單向的操作,是很容易被理解的,而大家都覺得開髮帶有組件聯動的功能很複雜的原因是在於,當這個組件完成了狀態更新之後,究竟有哪些組件會因此而聯動,將是一件很複雜的事情。

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

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

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

blank

每個組件都要管好自己

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

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

這也就是不難理解,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中, <RCREFormItem />組件都已經完全具備此類功能,能夠自動幫助開發者完成那些由各種狀態變更而觸發的表單驗證場景。

表單自身的私有狀態

由於表單也需要來存儲當前的驗證訊息和錯誤訊息,因此表單也需要和組件的一樣,需要持有一些狀態。

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

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

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

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

寫在最後

這篇文章所有的內容,就是RCRE這個庫背後所有的設計思路和思想了,想必你看到這裡也能夠理解為什麼會有RCRE這樣的庫誕生了。如果有興趣想繼續了解這個項目,可以點擊下面這個鏈接:

如果有任何問題,歡迎在下方留言,我盡可能將內容做到更好。

What do you think?

Written by marketer

blank

那些年的體驗技術部

ECMAScript 和TypeScript概述