溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

如何用RxJS實(shí)現(xiàn)Redux Form

發(fā)布時(shí)間:2020-10-04 19:53:52 來(lái)源:腳本之家 閱讀:147 作者:橘子小睿 欄目:web開(kāi)發(fā)

寫(xiě)在前面的話(huà)

看這篇文章之前,你需要掌握的知識(shí):

  • React
  • RxJS (至少需要知道 Subject 是什么)

背景

form 可以說(shuō)是 web 開(kāi)發(fā)中的最大的難題之一。跟普通的組件相比,form 具有以下幾個(gè)特點(diǎn):

1、更多的用戶(hù)交互。
這意味著可能需要大量的自定義組件,比如 DataPicker,Upload,AutoComplete 等等。

3、頻繁的狀態(tài)改變。
每當(dāng)用戶(hù)輸入一個(gè)值,都可能會(huì)對(duì)應(yīng)用狀態(tài)造成改變,從而需要更新表單元素或者顯示錯(cuò)誤信息。

3、表單校驗(yàn),也就是對(duì)用戶(hù)輸入數(shù)據(jù)的有效性進(jìn)行驗(yàn)證。
表單驗(yàn)證的形式也很多,比如邊輸入邊驗(yàn)證,失去焦點(diǎn)后驗(yàn)證,或者在提交表單之前驗(yàn)證等等。

4、異步網(wǎng)絡(luò)通信。
當(dāng)用戶(hù)輸入和異步網(wǎng)絡(luò)通信同時(shí)存在時(shí),需要考慮的東西就更多了。就比如 AutoComplete,需要根據(jù)用戶(hù)的輸入去異步獲取相應(yīng)的數(shù)據(jù),如果用戶(hù)每輸入一次就發(fā)起一次請(qǐng)求,會(huì)對(duì)資源造成很大浪費(fèi)。因?yàn)槊恳淮屋斎攵际?code>異步獲取數(shù)據(jù)的,那么連續(xù)兩次用戶(hù)輸入拿到的數(shù)據(jù)也有可能存在 "后發(fā)先至" 的問(wèn)題。

正因?yàn)橐陨线@些特點(diǎn),使 form 的開(kāi)發(fā)變得困難重重。在接下來(lái)的章節(jié)中,我們會(huì)將 RxJS 和 Form 結(jié)合起來(lái),幫助我們更好的去解決這些問(wèn)題。

HTML Form

在實(shí)現(xiàn)我們自己的 Form 組件之前,讓我們先來(lái)參考一下原生的 HTML Form。

保存表單狀態(tài)

對(duì)于一個(gè) Form 組件來(lái)說(shuō),需要保存所有表單元素的信息(如 value, validity 等),HTML Form 也不例外。

那么,HTML Form 將表單狀態(tài)保存在什么地方?如何才能獲取表單元素信息?

主要有以下幾種方法:

  • document.forms 會(huì)返回所有 <form> 表單節(jié)點(diǎn)。
  • HTMLFormElement.elements 返回所有表單元素。
  • event.target.elements 也能獲取所有表單元素。
document.forms[0].elements[0].value; // 獲取第一個(gè) form 中第一個(gè)表單元素的值

const form = document.querySelector("form");
form.elements[0].value; 

form.addEventListener('submit', function(event) {
 console.log(event.target.elements[0].value);
});

Validation

表單校驗(yàn)的類(lèi)型一般分為兩種:

內(nèi)置表單校驗(yàn)。默認(rèn)會(huì)在提交表單的時(shí)候自動(dòng)觸發(fā)。通過(guò)設(shè)置 novalidate 屬性可以關(guān)閉瀏覽器的自動(dòng)校驗(yàn)。

JavaScript 校驗(yàn)。

<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>

存在的問(wèn)題

定制化很難。 比如不支持 Inline Validation,只有 submit 時(shí)才能校驗(yàn)表單,且 error message 的樣式不能自定義。

難以應(yīng)對(duì)復(fù)雜場(chǎng)景。 比如表單元素的嵌套等。

Input 組件的行為不統(tǒng)一,從而難以獲取表單元素的值。 比如 checkbox 和 multiple select,取值的時(shí)候不能直接取 value,還需要額外的轉(zhuǎn)換。

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

感興趣的同學(xué)可以先去看一下源碼 https://github.com/reeli/react-rx-form

React 與 RxJS

RxJS 是一個(gè)非常強(qiáng)大的數(shù)據(jù)管理工具,但它并不具備用戶(hù)界面渲染的功能,而 React 卻特別擅長(zhǎng)處理界面。那何不將它們的長(zhǎng)處結(jié)合起來(lái)?用 React 和 RxJS 來(lái)解決我們的 Form 難題。既然知道了它們各自的長(zhǎng)處,所以分工也就比較明確了:

RxJS 負(fù)責(zé)管理狀態(tài),React 負(fù)責(zé)渲染界面。

設(shè)計(jì)思路

與 Redux Form 不同的是,我們不會(huì)將 form 的狀態(tài)存儲(chǔ)在 store 中,而是直接保存在 <Form/> 組件中。然后利用 RxJS 將數(shù)據(jù)通知給每一個(gè) <Field/> ,然后 <Field/> 組件會(huì)根據(jù)數(shù)據(jù)去決定自己是否需要更新 UI,需要更新則調(diào)用 setState ,否則什么也不做。

舉個(gè)例子,假設(shè)在一個(gè) Form 中有三個(gè) Field (如下),當(dāng)只有 FieldA 的 value 發(fā)生變化時(shí), 為了不讓 <Form/> 和
其子組件也 re-render,Redux Form 內(nèi)部需要通過(guò) shouldComponentUpdate() 去限制。

// 偽代碼
<Form>
  <FieldA/>
  <FieldB/>
  <FieldC/>
</Form>

而 RxJS 能把組件更新的粒度控制到最小,換句話(huà)說(shuō),就是讓真正需要 re-render 的 <Field/> re-render,而不需要 re-render 的組件不重新渲染 。

核心是 Subject

從上面的設(shè)計(jì)思路可以總結(jié)出以下兩個(gè)問(wèn)題:

  • Form 和 Field 是一對(duì)多的關(guān)系,form 的狀態(tài)需要通知給多個(gè) Field。
  • Field 需要根據(jù)數(shù)據(jù)去修改組件的狀態(tài)。

第一個(gè)問(wèn)題,需要的是一個(gè) Observable 的功能,而且是能夠支持多播的 Observable。第二個(gè)問(wèn)題需要的是一個(gè) Observer 的功能。在 RxJS 中,既是 Observable 又是 Observer,而且還能實(shí)現(xiàn)多播的,不就是 Subject 么!因此,在實(shí)現(xiàn) Form 時(shí),會(huì)大量用到 Subject。

formState 數(shù)據(jù)結(jié)構(gòu)

Form 組件中也需要一個(gè) State,用來(lái)保存所有 Field 的狀態(tài),這個(gè) State 就是 formState。

那么 formState 的結(jié)構(gòu)應(yīng)該如何定義呢?

在最早的版本中,formState 的結(jié)構(gòu)是長(zhǎng)下面這個(gè)樣子的:

interface IFormState {
 [fieldName: string]: {
  dirty?: boolean;
  touched?: boolean;
  visited?: boolean;
  error?: TError;
  value: string;
 };
}

formState 是一個(gè)對(duì)象,它以 fieldName 為 key,以一個(gè) 保存了 Field 狀態(tài)的對(duì)象作為它的 value。

看起來(lái)沒(méi)毛病對(duì)吧?

但是。。。。。

最后 formState 的結(jié)構(gòu)卻變成了下面這樣:

interface IFormState {
 fields: {
  [fieldName: string]: {
   dirty?: boolean;
   touched?: boolean;
   visited?: boolean;
   error?: string | undefined;
  };
 };
 values: {
  [fieldName: string]: any;
 };
}

Note: fields 中不包含 filed value,只有 field 的一些狀態(tài)信息。values 中只有 field values。

為什么呢???

其實(shí)在實(shí)現(xiàn)最基本的 Form 和 Field 組件時(shí),以上兩種數(shù)據(jù)結(jié)構(gòu)都可行。

那問(wèn)題到底出在哪兒?

這里先買(mǎi)個(gè)關(guān)子,目前你只需要知道 formState 的數(shù)據(jù)結(jié)構(gòu)長(zhǎng)什么樣就可以了。

數(shù)據(jù)流

如何用RxJS實(shí)現(xiàn)Redux Form

為了更好的理解數(shù)據(jù)流,讓我們來(lái)看一個(gè)簡(jiǎn)單的例子。我們有一個(gè) Form 組件,它的內(nèi)部包含了一個(gè) Field 組件,在 Field 組件內(nèi)部又包含了一個(gè) Text Input。數(shù)據(jù)流可能是像下面這樣的:

  • 用戶(hù)在輸入框中輸入一個(gè)字符。
  • Input 的 onChange 事件會(huì)被 Trigger。
  • Field 的 onChange Action 會(huì)被 Dispatch。
  • 根據(jù) Field 的 onChange Action 對(duì) formState 進(jìn)行修改。
  • Form State 更新之后會(huì)通知 Field 的觀察者。
  • Field 的觀察者將當(dāng)前 Field 的 State pick 出來(lái),如果發(fā)現(xiàn)有更新則 setState ,如果沒(méi)有更新則什么都不做。
  • setState 會(huì)使 Field rerender ,新的 Field Value 就可以通知給 Input 了。

核心組件

首先,我們需要?jiǎng)?chuàng)建兩個(gè)基本組件,一個(gè) Field 組件,一個(gè) Form 組件。

Field 組件

Field 組件是連接 Form 組件和表單元素的中間層。它的作用是讓 Input 組件的職責(zé)更單一。有了它之后,Input 只需要做顯示就可以了,不需要再關(guān)心其他復(fù)雜邏輯(validate/normalize等)。況且,對(duì)于 Input 組件來(lái)說(shuō),不僅可以用在 Form 組件中,也可以用在 Form 組件之外的地方(有些地方可能并不需要 validate 等邏輯),所以 Field 這一層的抽象還是非常重要的。

  • 攔截和轉(zhuǎn)換。 format/parse/normalize。
  • 表單校驗(yàn)。 參考 HTML Form 的表單校驗(yàn),我們可以把 validation 放在 Field 組件上,通過(guò)組合驗(yàn)證規(guī)則來(lái)適應(yīng)不同的需求。
  • 觸發(fā) field 狀態(tài)的 改變(如 touched,visited)
  • 給子組件提供所需信息。 向下提供 Field 的狀態(tài) (error, touched, visited...),以及用于表單元素綁定事件的回調(diào)函數(shù) (onChange,onBlur...)。
利用 RxJS 的特性來(lái)控制 Field 組件的更新,減少不必要的 rerender。

與 Form 進(jìn)行通信。 當(dāng) Field 狀態(tài)發(fā)生變化時(shí),需要通知 Form。在 Form 中改變了某個(gè) Field 的狀態(tài),也需要通知給 Field。

Form 組件

  • 管理表單狀態(tài)。 Form 組件將表單狀態(tài)提供給 Field,當(dāng) Field 發(fā)生變化時(shí)通知 Form。
  • 提供 formValues。
  • 在表單校驗(yàn)失敗的時(shí)候,阻止表單的提交。
通知 Field 每一次 Form State 的變化。 在 Form 中會(huì)創(chuàng)建一個(gè) formSubject&dollar;,每一次 Form State 的變化都會(huì)向 formSubject&dollar; 上發(fā)送一個(gè)數(shù)據(jù),每一個(gè) Field 都會(huì)注冊(cè)成為 formSubject&dollar; 的觀察者。也就是說(shuō) Field 知道 Form State 的每一次變化,因此可以決定在適當(dāng)?shù)臅r(shí)候進(jìn)行更新。
當(dāng) FormAction 發(fā)生變化時(shí),通知給 Field。 比如 startSubmit 的時(shí)候。

組件之間的通信

1、Form 和 Field 通信。

Context 主要用于跨級(jí)組件通信。在實(shí)際開(kāi)發(fā)中,F(xiàn)orm 和 Field 之間可能會(huì)跨級(jí),因此我們需要用 Context 來(lái)保證 Form 和 Field 的通信。Form 通過(guò) context 將其 instance 方法和 formState 提供給 Field。

2、Field 和 Form 通信。

Form 組件會(huì)向 Field 組件提供一個(gè) d__ispatch__ 方法,用于 Field 和 Form 進(jìn)行通信。所有 Field 的狀態(tài)和值都由 Form 統(tǒng)一管理。如果期望更新某個(gè) Field 的狀態(tài)或值,必須 dispatch 相應(yīng)的 action。

3、表單元素和 Field 通信

表單元素和 Field 通信主要是通過(guò)回調(diào)函數(shù)。Field 會(huì)向表單元素提供 onChange,onBlur 等回調(diào)函數(shù)。

接口的設(shè)計(jì)

對(duì)于接口的設(shè)計(jì)來(lái)說(shuō),簡(jiǎn)單清晰是很重要的。所以 Field 只保留了必要的屬性,沒(méi)有將表單元素需要的其他屬性通過(guò) Field 透?jìng)飨氯?,而是交給表單元素自己去定義。

通過(guò) Child Render,將對(duì)應(yīng)的狀態(tài)和方法提供給子組件,結(jié)構(gòu)和層級(jí)更加清晰了。

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;
 }
}

到這里,一個(gè)最最基本的 Form 就完成了。接下來(lái)我們會(huì)在它的基礎(chǔ)上進(jìn)行一些擴(kuò)展,以滿(mǎn)足更多復(fù)雜的業(yè)務(wù)場(chǎng)景。

Enhance

FieldArray

FieldArray 主要用于渲染多組 Fields。

回到我們之前的那個(gè)問(wèn)題,為什么要把 formState 的結(jié)構(gòu)分為 fileds 和 values?

其實(shí)問(wèn)題就出在 FieldArray,

  • 初始長(zhǎng)度由 initLength 或者 formValues 決定。
  • formState 整體更新。

FormValues

通過(guò) RxJS,我們將 Field 更新的粒度控制到了最小,也就是說(shuō)如果一個(gè) Field 的 Value 發(fā)生變化,不會(huì)導(dǎo)致 Form 組件和其他 Feild 組件 rerender。

既然 Field 只能感知自己的 value 變化,那么問(wèn)題就來(lái)了,如何實(shí)現(xiàn) Field 之間的聯(lián)動(dòng)?

于是 FormValues 組件就應(yīng)運(yùn)而生了。

每當(dāng) formValues 發(fā)生變化,F(xiàn)ormValues 組件會(huì)就把新的 formValues 通知給子組件。也就是說(shuō)如果你使用了 FormValues 組件,那么每一次 formValues 的變化都會(huì)導(dǎo)致 FormValues 組件以及它的子組件 rerender,因此不建議大范圍使用,否則可能帶來(lái)性能問(wèn)題。

總之,在使用 FormValues 的時(shí)候,最好把它放到一個(gè)影響范圍最小的地方。也就是說(shuō),當(dāng) formValues 發(fā)生變化時(shí),讓盡可能少的組件 rerender。

在下面的代碼中,F(xiàn)ieldB 的顯示與否需要根據(jù) FieldA 的 value 來(lái)判斷,那么你只需要將 FormValues 作用于 FIeldA 和 FieldB 就可以了。

<FormValues>
  {({ formValues, updateFormValues }) => (
    <>
      <FieldA name="A" />
      {!!formValues.A && <FieldB name="B" />}
    </>
  )}
</FormValues>

FormSection

FormSection 主要是用于將一組 Fields group 起來(lái),以便在復(fù)用在多個(gè) form 中復(fù)用。主要是通過(guò)給 name添加前綴來(lái)實(shí)現(xiàn)的。

那么怎樣給 Field 和 FieldArray 的 name 添加前綴呢?

我首先想到的是通過(guò) React.Children 拿到子組件的 name,再和 FormSection 的 name 拼接起來(lái)。

但是,F(xiàn)ormSection 和 Field 有可能不是父子關(guān)系!因?yàn)?Field 組件還可以被抽成一個(gè)獨(dú)立的組件。因此,存在跨級(jí)組件通信的問(wèn)題。

沒(méi)錯(cuò)!跨級(jí)組件通信我們還是會(huì)用到 context。不過(guò)這里我們需要先從 FormConsumer 中拿到對(duì)應(yīng)的 context value,再通過(guò) Provider 將 prefix 提供給 Consumer。這時(shí) Field/FieldArray 通過(guò) Consumer 拿到的就是 FormSection 中的 Provider 提供的值,而不再是由 Form 組件的 Provider 所提供。因?yàn)?Consumer 會(huì)消費(fèi)離自己最近的那個(gè) Provider 提供的值。

<FormConsumer>
 {(formContextValue) => {
  return (
   <FormProvider
    value={{
     ...formContextValue,
     fieldPrefix: `${formContextValue.fieldPrefix || ""}${name}.`,
    }}
   >
    {children}
   </FormProvider>
  );
 }}
</FormConsumer>

測(cè)試

Unit Test

主要用于工具類(lèi)方法。

Integration Test

主要用于 Field,F(xiàn)ieldArray 等組件。因?yàn)樗鼈儾荒苊撾x Form 獨(dú)立存在,所以無(wú)法對(duì)其使用單元測(cè)試。

Note: 在測(cè)試中,無(wú)法直接修改 instance 上的某一個(gè)屬性,以為 React 將 props 上面的節(jié)點(diǎn)都設(shè)置成了 readonly (通過(guò) Object.defineProperty 方法)。 但是可以通過(guò)整體設(shè)置 props 繞過(guò)。

instance.props = {
 ...instance.props,
 subscribeFormAction: mockSubscribeFormAction,
 dispatch: mockDispatch,
};

Auto Fill Form Util

如果項(xiàng)目中的表單過(guò)多,那么對(duì)于 QA 測(cè)試來(lái)說(shuō)無(wú)疑是一個(gè)負(fù)擔(dān)。這個(gè)時(shí)候我們希望能夠有一個(gè)自動(dòng)填表單的工具,來(lái)幫助我們提高測(cè)試的效率。

在寫(xiě)這個(gè)工具的時(shí)候,我們需要模擬 Input 事件。

input.value = 'v';
const event = new Event('input', {bubbles: true});
input.dispatchEvent(event);

我們的期望是,通過(guò)上面的代碼去模擬 DOM 的 input 事件,然后觸發(fā) React 的 onChange 事件。但是 React 的 onChange 事件卻沒(méi)有被觸發(fā)。因此無(wú)法給 input 元素設(shè)置 value。

因?yàn)?ReactDOM 在模擬 onChange 事件的時(shí)候有一個(gè)邏輯:只有當(dāng) input 的 value 改變,ReactDOM 才會(huì)產(chǎn)生 onChange 事件。

React 16+ 會(huì)覆寫(xiě) input value setter,具體可以參考 ReactDOM 的 inputValueTracking。因此我們只需要拿到原始的 value setter,call 調(diào)用就行了。

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, "v");

const event = new Event("input", { bubbles: true});
input.dispatchEvent(event);

Debug

打印 Log

在 Dev 環(huán)境中,可以通過(guò) Log 來(lái)進(jìn)行 Debug。目前在 Dev 環(huán)境下會(huì)自動(dòng)打印 Log,其他環(huán)境則不會(huì)打印 Log。
Log 的信息主要包括: prevState, action, nextState。

Note: 由于 prevState, action, nextState 都是 Object,所以別忘了在打印的時(shí)候調(diào)用 cloneDeep,否則無(wú)法保證最后打印出來(lái)的值的正確性,也就是說(shuō)最后得到的結(jié)果可能不是打印的那一時(shí)刻的值。

最后

這篇文章只講了關(guān)于 React Rx Form 的思路以及一些核心技術(shù),大家也可以按照這個(gè)思路自己去實(shí)現(xiàn)一版。當(dāng)然,也可以參考一下源碼,歡迎來(lái)提建議和 issue。Github 地址: https://github.com/reeli/react-rx-form

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI