创建自己的 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 部分:体验