您好,登錄后才能下訂單哦!
看這篇文章之前,你需要掌握的知識:
form 可以說是 web 開發中的最大的難題之一。跟普通的組件相比,form 具有以下幾個特點:
1、更多的用戶交互。
這意味著可能需要大量的自定義組件,比如 DataPicker,Upload,AutoComplete 等等。
3、頻繁的狀態改變。
每當用戶輸入一個值,都可能會對應用狀態造成改變,從而需要更新表單元素或者顯示錯誤信息。
3、表單校驗,也就是對用戶輸入數據的有效性進行驗證。
表單驗證的形式也很多,比如邊輸入邊驗證,失去焦點后驗證,或者在提交表單之前驗證等等。
4、異步網絡通信。
當用戶輸入和異步網絡通信同時存在時,需要考慮的東西就更多了。就比如 AutoComplete,需要根據用戶的輸入去異步獲取相應的數據,如果用戶每輸入一次就發起一次請求,會對資源造成很大浪費。因為每一次輸入都是異步
獲取數據的,那么連續兩次用戶輸入拿到的數據也有可能存在 "后發先至" 的問題。
正因為以上這些特點,使 form 的開發變得困難重重。在接下來的章節中,我們會將 RxJS 和 Form 結合起來,幫助我們更好的去解決這些問題。
在實現我們自己的 Form 組件之前,讓我們先來參考一下原生的 HTML Form。
保存表單狀態
對于一個 Form 組件來說,需要保存所有表單元素的信息(如 value, validity 等),HTML Form 也不例外。
那么,HTML Form 將表單狀態保存在什么地方?如何才能獲取表單元素信息?
主要有以下幾種方法:
<form>
表單節點。document.forms[0].elements[0].value; // 獲取第一個 form 中第一個表單元素的值 const form = document.querySelector("form"); form.elements[0].value; form.addEventListener('submit', function(event) { console.log(event.target.elements[0].value); });
Validation
表單校驗的類型一般分為兩種:
內置表單校驗。默認會在提交表單的時候自動觸發。通過設置 novalidate
屬性可以關閉瀏覽器的自動校驗。
JavaScript 校驗。
<form novalidate> <input name='username' required/> <input name='password' type='password' required minlength="6" maxlength="6"/> <input name='email' type='email'/> <input type='submit' value='submit'/> </form>
存在的問題
定制化很難。 比如不支持 Inline Validation,只有 submit 時才能校驗表單,且 error message 的樣式不能自定義。
難以應對復雜場景。 比如表單元素的嵌套等。
Input 組件的行為不統一,從而難以獲取表單元素的值。 比如 checkbox 和 multiple select,取值的時候不能直接取 value,還需要額外的轉換。
var $form = document.querySelector('form'); function getFormValues(form) { var values = {}; var elements = form.elements; // elemtns is an array-like object for (var i = 0; i < elements.length; i++) { var input = elements[i]; if (input.name) { switch (input.type.toLowerCase()) { case 'checkbox': if (input.checked) { values[input.name] = input.checked; } break; case 'select-multiple': values[input.name] = values[input.name] || []; for (var j = 0; j < input.length; j++) { if (input[j].selected) { values[input.name].push(input[j].value); } } break; default: values[input.name] = input.value; break; } } } return values; } $form.addEventListener('submit', function(event) { event.preventDefault(); getFormValues(event.target); console.log(event.target.elements); console.log(getFormValues(event.target)); });
React Rx Form
感興趣的同學可以先去看一下源碼 https://github.com/reeli/react-rx-form
React 與 RxJS
RxJS 是一個非常強大的數據管理工具,但它并不具備用戶界面渲染的功能,而 React 卻特別擅長處理界面。那何不將它們的長處結合起來?用 React 和 RxJS 來解決我們的 Form 難題。既然知道了它們各自的長處,所以分工也就比較明確了:
RxJS 負責管理狀態,React 負責渲染界面。
設計思路
與 Redux Form 不同的是,我們不會將 form 的狀態存儲在 store 中,而是直接保存在 <Form/>
組件中。然后利用 RxJS 將數據通知給每一個 <Field/>
,然后 <Field/>
組件會根據數據去決定自己是否需要更新 UI,需要更新則調用 setState
,否則什么也不做。
舉個例子,假設在一個 Form 中有三個 Field (如下),當只有 FieldA 的 value 發生變化時, 為了不讓 <Form/> 和
其子組件也 re-render,Redux Form 內部需要通過 shouldComponentUpdate()
去限制。
// 偽代碼 <Form> <FieldA/> <FieldB/> <FieldC/> </Form>
而 RxJS 能把組件更新的粒度控制到最小,換句話說,就是讓真正需要 re-render 的 <Field/>
re-render,而不需要 re-render 的組件不重新渲染 。
核心是 Subject
從上面的設計思路可以總結出以下兩個問題:
第一個問題,需要的是一個 Observable 的功能,而且是能夠支持多播的 Observable。第二個問題需要的是一個 Observer 的功能。在 RxJS 中,既是 Observable 又是 Observer,而且還能實現多播的,不就是 Subject 么!因此,在實現 Form 時,會大量用到 Subject。
formState 數據結構
Form 組件中也需要一個 State,用來保存所有 Field 的狀態,這個 State 就是 formState。
那么 formState 的結構應該如何定義呢?
在最早的版本中,formState
的結構是長下面這個樣子的:
interface IFormState { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: TError; value: string; }; }
formState 是一個對象,它以 fieldName
為 key,以一個 保存了 Field 狀態的對象作為它的 value。
看起來沒毛病對吧?
但是。。。。。
最后 formState 的結構卻變成了下面這樣:
interface IFormState { fields: { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: string | undefined; }; }; values: { [fieldName: string]: any; }; }
Note: fields 中不包含 filed value,只有 field 的一些狀態信息。values 中只有 field values。
為什么呢???
其實在實現最基本的 Form 和 Field 組件時,以上兩種數據結構都可行。
那問題到底出在哪兒?
這里先買個關子,目前你只需要知道 formState 的數據結構長什么樣就可以了。
數據流
為了更好的理解數據流,讓我們來看一個簡單的例子。我們有一個 Form 組件,它的內部包含了一個 Field 組件,在 Field 組件內部又包含了一個 Text Input。數據流可能是像下面這樣的:
核心組件
首先,我們需要創建兩個基本組件,一個 Field 組件,一個 Form 組件。
Field 組件
Field 組件是連接 Form 組件和表單元素的中間層。它的作用是讓 Input 組件的職責更單一。有了它之后,Input 只需要做顯示就可以了,不需要再關心其他復雜邏輯(validate/normalize等)。況且,對于 Input 組件來說,不僅可以用在 Form 組件中,也可以用在 Form 組件之外的地方(有些地方可能并不需要 validate 等邏輯),所以 Field 這一層的抽象還是非常重要的。
利用 RxJS 的特性來控制 Field 組件的更新,減少不必要的 rerender。與 Form 進行通信。 當 Field 狀態發生變化時,需要通知 Form。在 Form 中改變了某個 Field 的狀態,也需要通知給 Field。
Form 組件
通知 Field 每一次 Form State 的變化。 在 Form 中會創建一個 formSubject$,每一次 Form State 的變化都會向 formSubject$ 上發送一個數據,每一個 Field 都會注冊成為 formSubject$ 的觀察者。也就是說 Field 知道 Form State 的每一次變化,因此可以決定在適當的時候進行更新。
當 FormAction 發生變化時,通知給 Field。 比如 startSubmit 的時候。
組件之間的通信
1、Form 和 Field 通信。
Context 主要用于跨級組件通信。在實際開發中,Form 和 Field 之間可能會跨級,因此我們需要用 Context 來保證 Form 和 Field 的通信。Form 通過 context 將其 instance 方法和 formState 提供給 Field。
2、Field 和 Form 通信。
Form 組件會向 Field 組件提供一個 d__ispatch__ 方法,用于 Field 和 Form 進行通信。所有 Field 的狀態和值都由 Form 統一管理。如果期望更新某個 Field 的狀態或值,必須 dispatch 相應的 action。
3、表單元素和 Field 通信
表單元素和 Field 通信主要是通過回調函數。Field 會向表單元素提供 onChange,onBlur 等回調函數。
接口的設計
對于接口的設計來說,簡單清晰是很重要的。所以 Field 只保留了必要的屬性,沒有將表單元素需要的其他屬性通過 Field 透傳下去,而是交給表單元素自己去定義。
通過 Child Render,將對應的狀態和方法提供給子組件,結構和層級更加清晰了。
Field:
type TValidator = (value: string | boolean) => string | undefined; interface IFieldProps { children: (props: IFieldInnerProps)=> React.ReactNode; name: string; defaultValue?: any; validate?: TValidator | TValidator[]; }
Form:
interface IRxFormProps { children: (props: IRxFormInnerProps) => React.ReactNode; initialValues?: { [fieldName: string]: any; } }
到這里,一個最最基本的 Form 就完成了。接下來我們會在它的基礎上進行一些擴展,以滿足更多復雜的業務場景。
FieldArray
FieldArray 主要用于渲染多組 Fields。
回到我們之前的那個問題,為什么要把 formState 的結構分為 fileds 和 values?
其實問題就出在 FieldArray,
FormValues
通過 RxJS,我們將 Field 更新的粒度控制到了最小,也就是說如果一個 Field 的 Value 發生變化,不會導致 Form 組件和其他 Feild 組件 rerender。
既然 Field 只能感知自己的 value 變化,那么問題就來了,如何實現 Field 之間的聯動?
于是 FormValues 組件就應運而生了。
每當 formValues 發生變化,FormValues 組件會就把新的 formValues 通知給子組件。也就是說如果你使用了 FormValues 組件,那么每一次 formValues 的變化都會導致 FormValues 組件以及它的子組件 rerender,因此不建議大范圍使用,否則可能帶來性能問題。
總之,在使用 FormValues 的時候,最好把它放到一個影響范圍最小的地方。也就是說,當 formValues 發生變化時,讓盡可能少的組件 rerender。
在下面的代碼中,FieldB 的顯示與否需要根據 FieldA 的 value 來判斷,那么你只需要將 FormValues 作用于 FIeldA 和 FieldB 就可以了。
<FormValues> {({ formValues, updateFormValues }) => ( <> <FieldA name="A" /> {!!formValues.A && <FieldB name="B" />} </> )} </FormValues>
FormSection
FormSection 主要是用于將一組 Fields group 起來,以便在復用在多個 form 中復用。主要是通過給 name
添加前綴來實現的。
那么怎樣給 Field 和 FieldArray 的 name 添加前綴呢?
我首先想到的是通過 React.Children 拿到子組件的 name,再和 FormSection 的 name 拼接起來。
但是,FormSection 和 Field 有可能不是父子關系!因為 Field 組件還可以被抽成一個獨立的組件。因此,存在跨級組件通信的問題。
沒錯!跨級組件通信我們還是會用到 context。不過這里我們需要先從 FormConsumer 中拿到對應的 context value,再通過 Provider 將 prefix 提供給 Consumer。這時 Field/FieldArray 通過 Consumer 拿到的就是 FormSection 中的 Provider 提供的值,而不再是由 Form 組件的 Provider 所提供。因為 Consumer 會消費離自己最近的那個 Provider 提供的值。
<FormConsumer> {(formContextValue) => { return ( <FormProvider value={{ ...formContextValue, fieldPrefix: `${formContextValue.fieldPrefix || ""}${name}.`, }} > {children} </FormProvider> ); }} </FormConsumer>
Unit Test
主要用于工具類方法。
Integration Test
主要用于 Field,FieldArray 等組件。因為它們不能脫離 Form 獨立存在,所以無法對其使用單元測試。
Note: 在測試中,無法直接修改 instance 上的某一個屬性,以為 React 將 props 上面的節點都設置成了 readonly (通過 Object.defineProperty 方法)。 但是可以通過整體設置 props 繞過。
instance.props = { ...instance.props, subscribeFormAction: mockSubscribeFormAction, dispatch: mockDispatch, };
Auto Fill Form Util
如果項目中的表單過多,那么對于 QA 測試來說無疑是一個負擔。這個時候我們希望能夠有一個自動填表單的工具,來幫助我們提高測試的效率。
在寫這個工具的時候,我們需要模擬 Input 事件。
input.value = 'v'; const event = new Event('input', {bubbles: true}); input.dispatchEvent(event);
我們的期望是,通過上面的代碼去模擬 DOM 的 input 事件,然后觸發 React 的 onChange 事件。但是 React 的 onChange 事件卻沒有被觸發。因此無法給 input 元素設置 value。
因為 ReactDOM 在模擬 onChange 事件的時候有一個邏輯:只有當 input 的 value 改變,ReactDOM 才會產生 onChange 事件。
React 16+ 會覆寫 input value setter,具體可以參考 ReactDOM 的 inputValueTracking。因此我們只需要拿到原始的 value setter,call 調用就行了。
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; nativeInputValueSetter.call(input, "v"); const event = new Event("input", { bubbles: true}); input.dispatchEvent(event);
打印 Log
在 Dev 環境中,可以通過 Log 來進行 Debug。目前在 Dev 環境下會自動打印 Log,其他環境則不會打印 Log。
Log 的信息主要包括: prevState, action, nextState。
Note: 由于 prevState, action, nextState 都是 Object,所以別忘了在打印的時候調用 cloneDeep,否則無法保證最后打印出來的值的正確性,也就是說最后得到的結果可能不是打印的那一時刻的值。
最后
這篇文章只講了關于 React Rx Form 的思路以及一些核心技術,大家也可以按照這個思路自己去實現一版。當然,也可以參考一下源碼,歡迎來提建議和 issue。Github 地址: https://github.com/reeli/react-rx-form
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。