1. 程式人生 > >靈雀雲前端: Angular 響應式表單

靈雀雲前端: Angular 響應式表單

Kubernetes 物件實戰
簡介
Kubernetes 叢集的使用者日常工作中經常需要與 Deployment 等 Kubernetes 物件接觸。熟悉 Kubernetes 的朋友都會知道,Kubernetes 的物件結構雖然重視可移植性,不同的物件有著相似的設計理念,但就算是最熟練的系統運維或者開發工程師也不一定能將 Kubernetes 的物件玩轉。
為了使使用者更方便的操作 Kubernetes 物件,靈雀雲的 Kubernetes 發行版前端介面提供了對使用者友好的 UI 表單解決方案。使用者可以通過 UI 表單更容易的編輯 Kubernetes 物件,同時還提供了 YAML 格式與 UI 表單實時互轉的功能。

讀者可以先從這個Deployment表單demo裡面感受一下實時互轉的效果(注:這個 demo 是為這篇文章單獨開發,與靈雀雲產品無關):
 

pic1.png




難點
YAML 是 Kubernetes 物件最常見的展現和修改形式。對於前端來說,假如我們需要同時支援表單和 YAML 的方式編輯 Kubernetes 資源,那麼最終我們都得落回到編輯 YAML 上。YAML 與表單資料的互轉,類似於序列化與反序列化的過程,而 YAML 就是我們需要關注的序列化後的資料。

實踐過程中,表單資料與 YAML 互轉有不少問題需要攻克。比如:
UI 表單狀態與 K8S 物件狀態不一定是完全一致的,比方說:
我們不會或者不需要通過 UI 編輯 K8S 的所有欄位,非 UI 可編輯欄位在互轉的時候可以得到正確保留
UI 展現的形式並非與 YAML 嚴格對應,比如 metadata 的 label 欄位在 YAML 中表現為一個 StringMap,但在 UI 上表現為陣列
針對實際業務場景,有時候會為 YAML 進行隱式的修改或者填充
表單欄位巢狀層次深,同時表單欄位之間可能有關聯性
區域性表單複用。 比如 Workload/Pod 相關的資源都可以編輯 PodSpec 或者 Container
實時同步表單與 YAML 內容,保證兩種資料表現形式在任何時間點都是一致的
考慮到正確性與可維護性,這個功能點驅使我們的表單實現方案必須往單項資料流方向靠攏。
思路推導 模板驅動表單


每個接觸 Angular 表單的開發者應該都接觸過這兩個不同的表單實現思路:模板驅動表單與響應式表單。有些開發者可能會把響應式表單與動態表單混淆,實際上這兩個概念沒有什麼聯絡。不熟悉的同學可以看看 Angular 官網這篇關於表單的介紹。官網給出了對於兩者的比較:
一般來說:

響應式表單更健壯:它們的可擴充套件性、可複用性和可測試性更強。 如果表單是應用中的關鍵部分,或者你已經準備使用響應式程式設計模式來構建應用,請使用響應式表單。

模板驅動表單在往應用中新增簡單的表單時非常有用,比如郵件列表的登記表單。它們很容易新增到應用中,但是不像響應式表單那麼容易擴充套件。如果你有非常基本的表單需求和簡單到能用模板管理的邏輯,請使用模板驅動表單。
用模板驅動表單寫前端表單確實很容易:給定任意一個數據物件,將需要的欄位與模板的表單控制元件通過[(ngModel)]進行資料繫結;根據實際需要,再繫結一下諸如required的表單驗證指令就完事了。鵝妹子嚶!

不過一旦這麼做,使用者就將資料的“權威”就交給了模板,脫離了資料的實際控制權,也就只能被動的接受來自於模板的資料更新、表單狀態與生命週期、資料驗證等事件。對於複雜表單的業務邏輯,你很難通過這種模式擴充套件到大規模而複雜的表單資料邏輯處理之中。
響應式表單與受控元件

使用 Angular 響應式表單對於初學者來說有些麻煩:為了維護表單的狀態,我們需要顯式地建立一套完整的表單控制器物件層級結構,並將此物件通過 FormGroup / FormControl 之類的指令繫結到模板上的表單控制元件上。初看 Angular 的響應式表單的思想,似乎有點違背如今 MV* 的設計模式,因為它把一些本來可以通過框架隱式管理的工作暴露給了開發者自己,額外的增加了不少工作量。

熟悉 React 的表單控制元件實現的人應該瞭解,React 有受控元件和非受控元件的概念。通過受控元件,使用者可以通過單項資料流的思路,掌握表單控制元件資料的實際控制權。不過對於實際的完整表單應用場景,使用者還需要處理表單的提交狀態、表單驗證邏輯等資訊。Angular 的響應式表單控制元件就提供了一套完整的解決方案,幫助開發者更可控的管理表單的狀態。
Angular 表單控制元件的根基:ControlValueAccessor
Angular 的表單控制元件的魔法與 React 的受控元件的思路十分類似,是典型的單項資料流的處理模式。假如一個元件提供了 NG_VALUE_ACCESSOR 令牌注入到模板的 DI 上下文,並實現了 ControlValueAccessor 介面,那麼這個元件就可以繫結任意 Angular 的表單指令。

ControlValueAccessor 最關鍵的有兩點: registerOnChange 和 writeValue,這兩個函式分別對應了單項資料流從表單內到外和從表單外到內兩個方向的資料變化。

registerOnChange:初始化表單的過程中 Angular 會通過此介面請求目標元件註冊一個 onChange 回撥。使用者可以通過這回調,從內到外,將表單控制元件的資料更新事件發射到控制元件外部,更新表單控制元件物件的資料。
writeValue:Angular 的表單控制元件物件更新時會主動呼叫此函式。可以看成外部的資料狀態流入表單內部。使用者可以自定義這次資料更新的作用,繫結到元件內部模板的表單控制元件上。
問題發散
聰明的朋友一定會注意到,ControlValueAccessor 介面並沒有要求 onChange 與 writeValue 呼叫的時候表單資料格式需要與輸入一致。因此們可以在一個業務表單控制元件元件內實現區域性的資源物件與UI表單資料轉換的邏輯。比方說上面提到的,我們可以通過它實現一個鍵值對錶單控制元件。
 

pic2.jpg





它對外暴露為正常的鍵值對控制元件,值型別為 { [key: string]: string }。 資料由外到內時,可以通過 writeValue 將鍵值對通過 Object.entries 改變為 [string, string] 的陣列,最後將繫結到表單內部的 FormArray 控制元件上;同時將內部狀態改變時,在呼叫 onChange 之前將 [string, string][] 轉化為外部的 { [key: string]: string } 物件型別。

具體實現細節詳見:https://github.com/pengx17/k8s ... -form
通過這個思路,我們可以繼續引申:
由於有了這樣的資料轉化思路, 對於每一個表單控制元件,我們可以通過 onChange 和 writeValue 這兩個介面進行資料與表單的模型變化,實現UI的資料模型和實際對外暴露的資料模型的不一致需求。

通過遞迴,表單控制元件元件的內部模板的表單控制元件元件還可以是用同樣方式進行實現。這樣,我們就可以把問題拆解為一個個子表單。通過對於子表單的組合和巢狀,我們可以最終實現一個複雜的表單樹。

內嵌表單的實現隔離了複雜表單的實現邏輯。每一個子表單控制元件雖然對外暴露是一個表單控制元件資料,但其內部是一個完整的表單。它的內部處理邏輯,比如表單的錯誤處理、資料轉換等,父級(host)元件可以完全不瞭解。同時由於K8S的設計,一些子表單是可以在不同的資源裡複用的,也就減輕了我們的開發成本。

表單控制元件本身在提供 K8S 資料的同時,也可以表現為一個獨立的 K8S 物件資源。我們可以把區域性相關的業務邏輯完整的封裝在此表單控制元件元件內部,做到神行合一。這點很重要,通過這一點,我們可以更容易的劃分出 K8S 資源的問題範圍,更快做出程式碼結構的判斷,減少開發的腦力負擔和維護成本。坊間也有其他開發者傾向於將業務處理邏輯獨立出來,不放到元件內部,這樣元件就可以只負責薄薄一層檢視渲染邏輯。我認為不是不可行,不過在複雜表單元件巢狀和複用角度,可能本文采用的方式更容易維護。

由於上述實現思路過程有比較規範的思路,我們可以設計出來一個標準的開發 Kubernetes 資源物件表單的實現正規化。這個正規化可以大大降低開發人員對於開發、維護複雜表單實現的思維負擔。有好的正規化的時候可以獲得如下開發紅利:

不管任何模式的複雜表單,可以立刻開始著手開發
強調開發體驗的共識、抽象
避免開發出新的錯誤型別
Kubernetes物件的響應式表單開發正規化
中心思想
我總結了這個開發正規化裡幾個關鍵點:
神形合一:元件即是資源,也是表單控制元件
分形:區域性子物件表單元件處理與整體物件表單元件處理保持一致
遞迴: 由於分形的特性,我們可以用遞迴的方式自上而下,用統一的方式處理表單元件
問題隔離:一次只處理一個問題
響應式表單:嚴格執行單向資料流,同步處理,以達到實時同步的目的
流程
為任意一個 Kubernetes 物件開發表單的過程可以總結如下:
學習目標 Kubernetes 物件的基本功能, 對它的 YAML Schema 有基本概念。
由於我們前端人員對於 YAML 欄位的高透明度和充分的修改靈活度, 我們需要了解相關 k8s 物件的業務/特性.
書寫目標 API 物件 TypeScript 的型別 ( interface / type 等)。
拆解 k8s 物件型別成一系列子物件,為每個可複用的子物件封裝為單獨的表單元件。
比如 PodSpec, Container, Env 等
為拆解出來的每個子物件表單元件實現表單到物件的互轉。
組合子物件表單,最終組合成完整的 K8S 物件表單
稍後我們會以部署表單為例,詳細說明流程細節。
用例分析: Deployment 表單
熟悉 Deployment 物件的結構
首先參考官網對於 Deployment 的 API 文件,輸出一套 TypeScript 的介面,方便後續參照:
 

pic3.jpg



}
部署表單拓撲
對於部署表單,我們拆分為3個主要表單:
 

pic4.jpg



art: http://asciiflow.com/
K8S 資源物件表單控制元件元件 - 模板
最外層元件,物件的使用者可以依然使用模板驅動表單,將檢視雙向繫結到資料上:

<deployment-form [(ngModel)]="deployment"></deployment-form>
內部模板書寫上比較容易:由普通表單控制元件 (如select, input等) 和其他子物件表單控制元件(如pod-spec-form)組成為一個單獨的表單。

部署模板使用響應式表單:
 

pic5.jpg



K8S 資源物件表單控制元件元件 - 控制器
資源物件元件控制器(也就是 TS 部分)的職責如下:
對外暴露為一個單獨的表單控制元件
Host 模板可以繫結表單相關指令到物件表單控制元件
對內表現為一個完整的表單元件
根據檢視創建出一個表單控制元件樹
協同各個表單控制元件,響應資料變化
使用單向資料流處理流入表單的資料
使用單向資料流處理流出表單的資料
元件初始化時,需要生成一個響應式表單控制元件樹。根據實戰,我總結如下經驗:
有且只有一個根部 form 控制元件物件, 根據情況可能是 FormGroup 、FormArray、FormControl。但最終都要繫結到模板的 FormGroupDirective 指令上。
FormGroup 物件結構一般與當前物件 schema 結構相似,這樣可以
通過 form.patchValue 來設定表單資料
在控制器或者模板裡更容易的與原始資料進行對照
在模板內可以組合使用 formGroupName, formControlName 等指令綁定於響應表單控制元件
比如對於部署表單,我們需要生成這樣結構的表單控制元件:
 

pic6.jpg



控制元件需要對外暴露為一個普通的表單控制元件,同時將內部表單的錯誤向上傳遞到 Host 上的 NgControl 指令上。最關鍵的就是要實現 ControlValueAccessor 介面:

writeValue: 由外部寫入內部時,需要將資源物件適配為表單可用的模型結構。
大部分時候表單的 FormModel 與資源物件的 schema 一致。
假如業務需要,比如 k8s 的 metadata.labels 欄位是 { [key: string]: string } 鍵值對映物件,但在檢視中他的表單模型是鍵值對陣列 [string, string][],可以在這個階段進行資料適配。
onChange: 由內部寫回外部時,需要將表單模型適配為資源物件模型,同時將 UI 不可見的欄位寫回資源物件模型中。
同時由於實現的原因,需要監聽上層模板的 Form 指令,以得到提交巢狀模板的功能
setFormByResource 和 setResourceByForm
剛才提到,為表單設定資源物件資料時可以直接通過呼叫 form.patchValue(formModel) ,使得一個結構化的表單被能快速的填充。 有一個問題是,Angular 限制呼叫 patchValue 方法時 formModel 的 schema 必須是 form 結構的一個子集, 但通常來講 form 的控制器結構有時候不需要覆蓋完整的 schema (比如 status 欄位等)。

我設計了 setFormByResource 函式解決這個問題,方法是通過遍歷表單層級裡面所有的控制器,以控制器所在的路徑作為線索查詢資源物件上的相應的值, 然後設定到表單控制器上;同時在 form 的某個控制器是 FormArray 的情況下,根據資料來源的大小進行伸縮。

而 setResourceByForm 函式與 setFormByResource 作用相反。 在表單資料寫回資源物件時,利用它遍歷表單層級控制器,將值設定到資源物件上。通過 setResourceByForm, 我們還可以做到從 UI 資料寫回資源物件時,不去觸碰 UI 表單沒有的欄位,避免了資料轉化過程中資料可能會丟失的情況。
ng-resource-form-util 資源表單輔助工具庫
開表單的單項資料流基本上可以用一張簡單的圖表示:
 

pic7.jpg



由於控制器大多數情況下使用方式和行為高度相似,於是靈雀雲前端將表單的這些功能和行為抽象、封裝到了 BaseResourceFormComponent 基類內,並將 程式碼開源在此。
上面的流程裡還剩一些關鍵細節遺漏。比如說:
表單是如何處理表單驗證,或者甚至是非同步表單驗證邏輯,並向上傳遞錯誤狀態的?
本文表單與資源物件互轉實際上解決的是表單資料與資源物件(JSON)之間資料流傳遞的過程。但在最外層,資源物件實際表現為 YAML 字串,而不是 JSON 物件。
你可以通過 DEMO 和 DEMO 的原始碼繼續瞭解一個比較完整的解決方案是怎樣的。
寫在最後
基於本文表單開發正規化,靈雀雲的前端開發可以非常快速的進行 K8S 相關資源物件表單的實現,並且得到 YAML 與資源物件互轉的需求實現。

本文介紹了一種通用的基於 Angular 響應式表單編輯 Kubernetes 物件的實現思路與正規化。實際上,這個思路並不只侷限於 Angular 或者 Kubernetes 物件,讀者甚至可以根據自己的需要,將此文章的思路使用 final form 帶入到 React 或者 Vue 應用之中。