創建自己的 React 驗證庫:基礎知識(第 1 部分)

已發表: 2022-03-10
快速總結↬有沒有想過驗證庫是如何工作的? 這篇文章將告訴你如何一步一步地為 React 構建你自己的驗證庫。 下一部分將添加一些更高級的功能,最後一部分將重點改善開發者體驗。

我一直認為表單驗證庫非常酷。 我知道,這是一種利基興趣——但我們經常使用它們! 至少在我的工作中——我所做的大部分工作是使用依賴於早期選擇和路徑的驗證規則構建或多或少複雜的表單。 了解表單驗證庫的工作方式至關重要。

去年,我寫了一個這樣的表單驗證庫。 我將其命名為“校準”,您可以在此處閱讀介紹性博客文章。 這是一個很好的庫,提供了很大的靈活性,並且使用的方法與市場上的其他庫略有不同。 不過,還有很多其他很棒的庫——我的庫正好滿足我們的要求。

今天,我將向你展示如何為 React 編寫你自己的驗證庫。 我們將逐步完成該過程,您將在我們進行過程中找到 CodeSandbox 示例。 到本文結束時,您將知道如何編寫自己的驗證庫,或者至少對其他庫如何實現“驗證的魔力”有更深入的了解。

  • 第 1 部分:基礎知識
  • 第 2 部分:功能
  • 第 3 部分:體驗
跳躍後更多! 繼續往下看↓

第 1 步:設計 API

創建任何庫的第一步是設計如何使用它。 它為接下來的許多工作奠定了基礎,在我看來,這是您將在您的圖書館中做出的最重要的決定。

創建一個“易於使用”且足夠靈活以允許未來改進和高級用例的 API 非常重要。 我們將努力實現這兩個目標。

我們將創建一個接受單個配置對象的自定義鉤子。 這將允許在不引入破壞性更改的情況下傳遞未來的選項。

關於鉤子的注意事項

Hooks 是一種全新的 React 編寫方式。 如果你過去寫過 React,你可能不認識其中的一些概念。 在這種情況下,請查看官方文檔。 它寫得非常好,並帶您了解您需要了解的基礎知識。

我們現在將調用我們的自定義鉤子useValidation 。 它的用法可能看起來像這樣:

 const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);

config對象接受一個fields屬性,它為每個字段設置驗證規則。 此外,它接受表單提交時的回調。

fields對象包含我們要驗證的每個字段的鍵。 每個字段都有自己的配置,其中每個鍵是驗證器名稱,每個值是該驗證器的配置屬性。 另一種寫法是:

 { fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }

我們的useValidation鉤子將返回一個帶有一些屬性的對象—— getFieldPropsgetFormPropserrors 。 前兩個函數是 Kent C. Dodds 所說的“prop getter”(有關這些的精彩文章,請參見此處),用於獲取給定表單字段或表單標籤的相關 props。 errors屬性是一個包含任何錯誤消息的對象,每個字段都鍵入。

這種用法如下所示:

 const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };

好吧! 所以我們已經確定了 API。

  • 請參閱 CodeSandbox 演示

請注意,我們還創建了useValidation掛鉤的模擬實現。 目前,它只是返回一個帶有我們需要的對象和函數的對象,所以我們不會破壞我們的示例實現。

存儲表單狀態

我們需要做的第一件事是將所有表單狀態存儲在我們的自定義掛鉤中。 我們需要記住每個字段的值、任何錯誤消息以及表單是否已提交。 我們將為此使用useReducer鉤子,因為它允許最大的靈活性(和更少的樣板)。 如果您曾經使用過 Redux,您會看到一些熟悉的概念——如果沒有,我們將在進行過程中進行解釋! 我們將從編寫一個 reducer 開始,它被傳遞給useReducer鉤子:

 const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }

什麼是減速機?

reducer 是一個函數,它接受值對象和“動作”並返回值對象的增強版本。

動作是帶有type屬性的純 JavaScript 對象。 我們使用switch語句來處理每種可能的操作類型。

“值的對象”通常被稱為狀態,在我們的例子中,它是我們驗證邏輯的狀態。

我們的狀態由三部分數據組成—— values (我們的表單字段的當前值)、 errors (當前的錯誤消息集)和一個標誌isSubmitted ,指示我們的表單是否至少提交了一次。

為了存儲我們的表單狀態,我們需要實現我們的useValidation鉤子的幾個部分。 當我們調用我們的getFieldProps方法時,我們需要返回一個包含該字段值的對象,一個更改處理程序用於何時更改,以及一個名稱屬性來跟踪哪個字段是哪個。

 function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };

getFieldProps方法現在返回每個字段所需的道具。 當一個 change 事件被觸發時,我們確保該字段在我們的驗證配置中,然後告訴我們的 reducer 發生了一個change操作。 reducer 將處理對驗證狀態的更改。

  • 請參閱 CodeSandbox 演示

驗證我們的表格

我們的表單驗證庫看起來不錯,但在驗證表單值方面做得併不多! 讓我們解決這個問題。

我們將驗證每個更改事件的所有字段。 這聽起來可能不是很有效,但在我遇到的實際應用程序中,這並不是一個真正的問題。

請注意,我們並不是說您必須在每次更改時顯示每個錯誤。 我們將在本文後面重新討論如何僅在您提交或導航離開字段時顯示錯誤。

如何選擇驗證器功能

對於驗證器,有大量的庫可以實現您需要的所有驗證方法。 如果你願意,你也可以自己寫。 這是一個有趣的運動!

對於這個項目,我們將使用我前段時間編寫的一組驗證器—— calidators 。 這些驗證器具有以下 API:

 function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;

換句話說,每個驗證器都接受一個配置對象並返回一個完全配置的驗證器。 當使用一個值調用函數時,如果該值無效,則返回message prop,如果有效則返回null 。 您可以通過查看源代碼來了解其中一些驗證器是如何實現的。

要訪問這些驗證器,請使用npm install calidators安裝calidators包。

驗證單個字段

還記得我們傳遞給useValidation對象的配置嗎? 它看起來像這樣:

 { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }

為了簡化我們的實現,假設我們只有一個字段要驗證。 我們將遍歷字段配置對象的每個鍵,並一個一個地運行驗證器,直到我們發現錯誤或完成驗證。

 import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }

在這裡,我們編寫了一個函數validateField ,它接受要驗證的值和該字段的驗證器配置。 我們遍歷所有驗證器,將驗證器的配置傳遞給它們,然後運行它。 如果我們收到錯誤消息,我們會跳過其餘的驗證器並返回。 如果沒有,我們嘗試下一個驗證器。

注意:在驗證器 API 上

如果您選擇具有不同 API 的不同驗證器(例如非常流行的validator.js ),您的這部分代碼可能看起來有點不同。 然而,為了簡潔起見,我們讓這部分成為留給讀者的練習。

注意:在 for...in 循環中

以前從未使用過for...in循環? 沒關係,我也是第一次! 基本上,它遍歷對像中的鍵。 你可以在 MDN 上閱讀更多關於它們的信息。

驗證所有字段

現在我們已經驗證了一個字段,我們應該能夠輕鬆地驗證所有字段。

 function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }

我們編寫了一個函數validateFields ,它接受所有字段值和整個字段配置。 我們遍歷配置中的每個字段名稱,並使用其配置對象和值驗證該字段。

下一篇:告訴我們的減速機

好的,所以現在我們有了這個函數來驗證我們所有的東西。 讓我們把它拉到我們代碼的其餘部分!

首先,我們將向validationReducer添加一個validate操作處理程序。

 function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }

每當我們觸發validate操作時,我們將狀態中的錯誤替換為與操作一起傳遞的任何內容。

接下來,我們將從useEffect掛鉤觸發我們的驗證邏輯:

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

這個useEffect鉤子在我們的state.fieldsconfig.fields發生變化時運行,除了在第一次掛載時。

當心錯誤

上面的代碼中有一個非常微妙的錯誤。 我們已經指定我們的useEffect鉤子應該只在state.fieldsconfig.fields改變時重新運行。 事實證明,“改變”並不一定意味著價值的改變! useEffect使用Object.is來確保對象之間的相等性,這反過來又使用引用相等性。 也就是說——如果你傳遞一個具有相同內容的新對象,它就不一樣了(因為對象本身是新的)。

state.fieldsuseReducer返回,這保證了我們的引用相等性,但是我們的config是在我們的函數組件中內聯指定的。 這意味著在每次渲染時都會重新創建對象,這反過來會觸發上面的useEffect

為了解決這個問題,我們需要使用 Kent C. Dodds 的use-deep-compare-effect庫。 你用npm install use-deep-compare-effect安裝它,然後用這個替換你的useEffect調用。 這確保我們進行深度相等檢查而不是引用相等檢查。

您的代碼現在將如下所示:

 import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

關於 useEffect 的說明

事實證明, useEffect是一個非常有趣的函數。 如果您有興趣了解有關此鉤子的所有信息,Dan Abramov 寫了一篇關於useEffect錯綜複雜的非常好的長篇文章。

現在事情開始看起來像一個驗證庫!

  • 請參閱 CodeSandbox 演示

處理表單提交

我們基本表單驗證庫的最後一部分是處理提交表單時發生的事情。 現在,它重新加載頁面,沒有任何反應。 這不是最優的。 當涉及到表單時,我們希望阻止默認的瀏覽器行為,而是自己處理它。 我們將這個邏輯放在getFormProps prop getter 函數中:

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };

我們將getFormProps函數更改為返回一個onSubmit函數,只要觸發了submit DOM 事件,就會觸發該函數。 我們阻止默認的瀏覽器行為,調度一個動作來告訴我們的 reducer 我們提交了,並調用提供的onSubmit回調與整個狀態 - 如果它提供了。

概括

在那裡! 我們創建了一個簡單、可用且非常酷的驗證庫。 不過,在我們能夠主宰互聯網之前,還有大量工作要做。

  • 第 1 部分:基礎知識
  • 第 2 部分:功能
  • 第 3 部分:體驗