在web應用中,複雜表單這類web應用富交互元素多,業務邏輯複雜,犬牙交錯,且需求變化頻繁。及容易成為晦澀和幽暗之地,也經常是各種代碼壞味道的來源。針對這種典型的複雜應用,本文以淘寶機票訂單為例提出一種架構模式梳理和消化表單帶來的複雜性。
模塊和組件劃分解決複雜表單的的第一步,劃分模塊。
概念上,為了復用和解耦方便,應將模塊按照功能的內聚程度進行劃分。強相關,頻繁溝通和交互的功能應該歸為一個模塊。模塊間儘量不存在依賴關係。也就是常說的「高內聚,低耦合」。
如下圖所示,淘寶機票訂單頁面主要有被分為7個主要模塊。
模塊劃分完畢,下一步確認組成模塊的組件。
關於模塊和組件的區分。一般按照以下三個緯度考量。
是否有業務邏輯參與。
是否包含html。
是否具備一定獨立性。
「模塊」,定義為一個包含」html」、」css(圖片被認為是css的一部分)「、」javascript」的代碼集。模塊的應用方式多為通過web模板技術(如:velocity、freemarker、php)。因為包含了html,使得模塊必須通過服務端合併加載並且最終推送到用戶瀏覽器。此外,「模塊」還是具備一定獨立業務和交互的集合,最好可以被其他頁面引用。良好的獨立性也可以幫助協同開發,在實際開發中可多人可以並行開發多個獨立模塊,提高效率。
「組件」,定義為一個僅包含」css」和」javascript」的代碼集。正因為不包含html,所以組件可通過javascript異步加載。因為這種可異步加載的特性,組件在復用方面的容易性遠超模塊。組件沒有業務邏輯或者僅有少部分公共業務邏輯。業務邏輯越多,組件的可復用性就越低。
模塊、組件間通訊組件/模塊劃分的目的是將彼此間相對獨立的功能分離,前面通過模塊和組件的劃分解決了分離問題。實際中,模塊之間存在協作關係。模塊間應以一種輕量的方式協作。一般的為了更好的分離和解耦,可以考慮用廣播的方式在模塊間溝通,考慮使用事件的方式在組件間通訊。
如下圖所示,淘寶機票訂單頁面的數據流向。
不同模塊在後期均有可能擴展小功能。例如不定期的活動優惠等。事件廣播可以讓不同模塊/組件間新增功能影響面縮小。在淘寶機票訂單中應用中,使用廣播組件通訊主要用來完成以下意圖。
1、知會。
知會的特性在於異步通訊。廣播發起方只需要放出事件,無需等待其他關注者完成處理。稱為異步廣播。例如表單模塊的內容變更需要知會到顯示訂單金額的模塊,顯示訂單金額的模塊接受事件後需要更改金額。
基於這種方式的通訊,各模塊之需要做好自己的事情,外部關注的事件廣播出去即可。異步廣播還有一個好處是系統堅固性比較強,廣播發送者不會因為事件監聽者的使用不當而異常。
2、請求數據
例如,模塊6(負責提交)需要在被點擊後從模塊2(乘機人表單),模塊4(聯繫地址)、模塊7(金額計算)。獲取具體數據提交。請求數據的場景特性在於,廣播發起者需要等待事件處理者完成處理後再繼續下一步行為。稱為同步廣播。
基於此機制。提交模塊只需要負責綜合校驗,浮層,網絡請求及異常處理。而具體請求的內容由其他模塊決定。對後續模塊的擴充起到了很好的左右。
複雜組件拆分模塊和組件劃分完畢後,可能會發現某些組件非常複雜,幾乎佔據了整個web應用一半以上的代碼。這部分組件由純js實現,並且使用javascript模塊加載器加載。
同一個組件大量代碼糾結在一起,最終還是會導致架構腐化。因此,複雜組件需要進一步拆分。在淘寶機票訂單中,乘機人信息組件是一個複雜組件。如下圖所示:
拆分這類輸入型的複雜組件,一般來說有兩種思路方式。
縱切,組件樹型式。
將組件進一步劃分為更細力度的輸入組件,將每個輸入域作為一個單獨組件。最終形成一個組件樹。
這樣的組織方式結構嚴謹層級清晰,最大的優點是很容易支持欄位擴展。
但考慮如下場景,為了儘量友好的提示用戶,需要在輸入域外的某處增加提示幫助。
這種場景下組件樹的組織方式每次在面對變化時就會略顯手忙腳亂。難道把每個地方出現的tip都座位獨立組件看待嗎?
欄位級的適普性降低了適應細節調整的能力,付出的代價在於界面體驗。
橫切,AOP式。
將所有輸入域抽象的看待為同一個組件。按照組件的富應用特性分層看待。在本例中,乘機人組件被按照從簡單到複雜分為3個切面。
切面1-基礎展現層只負責最基礎的可完成輸入的表單控制項,及基礎dom管理。
切面2-富展現層負責修飾base層的基礎html控制項,形成富輸入控制項。
切面3-校驗層負責對base層的輸入數據進行業務級校驗。
未來,如果新增tip或者其他業務邏輯,增加一個新切面即可,完全或者很少需要修改老代碼文件。
淘寶機票訂單採用了AOP這種方式,從最終代碼量上來看,可以看出複雜度被比較均衡的分布到不同文件中去。
同樣,這種方式也有局限,如果需要擴展欄位,那將是一個災難,你有可能需要到每一個切面裡面去做修改。
有句老話說的好,沒有最優方案,只有最適合的解決方案,任何解決方案,都需要放到具體場景中去評判。事實上,對這個問題的進一步研究,可以發現以下規律。
對於一個組件、模塊,同時追求簡單設計、適普性(欄位級擴充)、界面體驗是不可能的。如果場景需要適應欄位靈活擴展,那就採用縱切的模式。如果使用場景需欄位確定,需要更多細節控制力度,那就橫切,AOP式。如果兩者都要兼顧,就需要引入複雜設計,綜合運用橫切和縱切。但是這樣形成的最終設計會很複雜,開發和可維護性上會有代價付出。
對於淘寶機票這類網際網路應用,使用了橫切的方式來拆分組件,因為在這個場景中,欄位的數量是相對固定的,而圍繞固定數量欄位的優化需求是層出不窮的。然而在企業內網應用或者網站後臺web應用中,欄位的變化會比較頻繁。建議主要採用縱切的思路劃分。
表單校驗有表單的地方就有校驗。項目初期,校驗的功能總是不起眼。等待項目後期時候經常會發現校驗已經佔據了巨大工作量並且成為海量bug的源頭。因此校驗是一種典型的容易被輕視單又蘊含巨大工作量的事情,需要特別對待,專門設計。
一般來說,這根據校驗根據其複雜度可以分為以下兩類:
格式校驗
格式校驗一般是校驗用戶輸入的格式是否滿足要求,比如是否數字、電話號碼、郵箱等等。此類校驗的特點是校驗域單一,一般只對一個input或者某個組件的value進行檢查。格式類校驗應與與用戶展現非常接近,一種非常好的做法是將此類校驗信息直接描述在html標籤屬性中。html5中input的pattern屬性就是一種基於這種思想的解決方案。
邏輯校驗
邏輯校驗是滿足格式校驗後,繼續進行的與業務相關的校驗,例如是否存在相同用戶名,輸入的生日是否和身份證號不符等等。此類校驗的一般涉及多個輸入域,要綜合處用戶的輸入內容一起校驗。此類校驗邏輯複雜,不適合寫在html中。
目前有很多流行的form校驗框架解決校驗問題,如何引入合適的校驗框架,先從理解校驗這件事的過程開始。
典型的一個校驗過程如下,用戶在某個input處完成輸入,應用在某個時刻被觸發校驗,可以是失去焦點或者keyup或者其他。被觸發的校驗過程找到此處input所需要的校驗規則(有時候這個規則被直接寫在html中)判單正確與否,如果正確,可能有提示,如果錯誤,可能也有提示。
從以上場景的描述中,可以找到校驗的幾個關鍵環節。這裡局部採用一下管理學上經典的5w1h問題分析方法來分析問題
who: 哪個輸入控制項的內容需要校驗。這是框架是解決不了的。要對哪個輸入域做校驗應該是應用傳遞進入的。
when: 何時被觸發校驗。比如說是「who」失去焦點時。變化太多,框架解決不了。只能被動觸發。
what: 做什麼校驗。有時候這個」what」被寫在html中。基本上,所有格式校驗都是固定的,這個問題應框架解決。但框架應預留接口做更加複雜的業務校驗。
how: 校驗完畢後的動作。框架不能決定做什麼,但是在校驗結果出來後,框架應能知會到外部調用者。
在設計框架或者選擇已有框架時,首先要區分框架的邊界,簡單來說,就是做什麼和不做什麼。框架應實現相對固定的業務流程。同時對可變部分預留足夠的靈活性。
一個通用的校驗框架一定是不含界面部分的。界面是多變和難以窮舉的,是用tip顯示錯誤,還是在輸入域附近顯示,是否需要動畫,是否需要修改輸入域的視覺狀態,這些可變化的部分應為框架外部內容,由更專業的tip組件或者popup來完成。框架只應該負責在校驗完成時候知會相關組件完成顯示錯誤提示等若干事情。
基於以上的分析,校驗框架應該具備以下規格
1. 解決what問題。內置了各種格式校驗規則,如電話號碼、e-mail等.並且能夠靈活定義新的邏輯校驗。
2. 解決who問題。說明如何根據輸入的字符真正找到who對應的value。並且能夠對於這個who使用哪些校驗規則
3. 解決when問題。提供一個觸發校驗的方法。
4. 解決how問題。產生校驗結果後能夠知會外部的功能框架。
在淘寶機票訂單應用中,依據上述原則自行設計了一個Validator框架,接口定義如下,Validator是校驗框架對象。
在構造函數中提供表格化的校驗邏輯定義型式。如下圖所示,傳遞如下結構,定義每個欄位對應的校驗方式。在下圖中,定義每行為一個field,每個field有若干rule,每個rule可以是框架內置的格式校驗,也可以是自定義的邏輯校驗,實際上是函數名。
Validator框架提供validate()方法,validate方法有兩個行為,如果不指定參數,將依次執行完所有field的校驗,並且將最終結果返回。如果執行一個field name,框架將只校驗field name對應的輸入域。
一旦執行validate()方法,無論校驗結果如何,框架均向其觀察者發送事件』onValidate』。以便觸發後續動作。
一些輔助參數,需要提供一個從field name找到輸入域value的function。
總結在處理複雜表單時,首先通過合理模塊、組件劃分,將複雜度分散。然後利用詳細和廣播機制解決分散的模塊和組件間通問題。接著,過於複雜的組件要考慮進一步拆分,具體拆分的方式有縱切和橫切兩種,根據具體使用場景決定。最後,不要小看了校驗,需要特別對待,專門設計。
來源:http://ued.taobao.org/blog/2012/11/refactor-complex-form/
1. 回復「m」可以查看歷史記錄;
2. 回復「h」或者「幫助」,查看幫助;
開發者已開通多個微信群交流學習,請加若飛微信:13511421494 進群
開發者:KaiFaX