整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

正则表达式以及正则在JS表单校验中的应用

则表达式以及正则在JS表单校验中的应用

1. 正则表达式的引入

表单验证两种常用的方法:

<input type="submit" onclick="return  validate()"/>
<form action="/aa" onsubmit="return  validate()"/>

这里我们使用QQ号码作为表单验证来测试,我们知道QQ号码的规则还是比较复杂,但是这里我们仅仅使用以下三条规则来讲解:

1. qq号必须是5-10位

2. 数字0不可以作为qq号码的开头

3. QQ号码必须是纯数字组成

代码示例如下所示:

<input type="submit" onclick="return  validate()"/>
<form action="/aa" onsubmit="return  validate()"/>

当然除了在form表单中添加onsubmit事件来实现表单验证,我们还可以使用在submit类型的input标签中添加onclick事件来实现表单验证,代码如下所示:

<form action="server/server.html" method="get" >
    QQ账号: <input id="qq" type="text" placeholder="请输入QQ账号...">
  <span id="msg"></span> <br>
    <input type="submit" value="登录" onclick="return checkQQ();"> 
</form>

相信大家对上述代码都能够理解,但是大家应该也发现了使用传统的if-else来校验qq号码,代码非常冗余,而且可读性很差,何况才短短两条规则就写了这么多的判断,那真实的qq号码肯定规则更多,如果让你去验证邮箱,或者网址呢?所以我们非常有必要学习一门新的技术用于我们JS表单校验,它就是正则表达式了。

2. 正则表达式概述

正则表达式简介

为什么需要正则表达式?

1.本质是用来对复杂文本进行处理。

2.当然也可以用于数据校验

3.文本匹配、搜索、分割、替换等等

正则表达式的优势和用途?

一种强大而灵活的文本处理工具;

提供了一种紧凑的、动态的方式,能够以一种完全通用的方式来解决各种字符串处理(例如:验证、查找、替换等)问题;

大部分语言、数据库都支持正则表达式。

正则表达式定义:

正如他的名字一样是描述了一个规则,通过这个规则可以匹配一类字符串。

正则表达式的用处:

验证给定字符串是否符合指定特征,比如验证是否是合法的邮件地址。

用来查找字符串,从一个长的文本中查找符合指定特征的字符串。

用来替换,比普通的替换更强大,用来替换满足某个规则的字符串

用来分割,比普通的分割更强大,用来分割满足某个规则的字符串

3. 工具软件的使用

3.1. 工具软件RegexBuddy的使用

为了提高开发效率,一般都先在工具软件中测试正则表达式,通过测试后,才在程序中使用。

3.2 正则表达式图形化显示网站 Regexper

由于正则表达式非常灵活,正则表达式本身的可读性非常差,所以比较复杂的正则表达式阅读起来相对比较困难,所以我们可以借助一个图形化正则表达式网站来帮助我们分析一个相对复杂的正则表达式。

正则图形化显示网站:
//regexper.com
//regexr-cn.com

图形化显示结果如下所示:

4. 正则表达式规则

4.1 普通字符

字母、数字、汉字、下划线、以及没有特殊定义的标点符号,都是“普通字符”。表达式中的普通字符,在匹配一个字符串的时候,匹配与之相同的一个字符。例如字符a, b, c ,1,2,中等等。

4.2 转义字符

\n

代表换行符

\t

制表符

\

代表\本身

\^ ,$,\.,\(, \) , \{, \} , \? , \+ , \* , \| ,\[, \]

匹配这些字符本身

4.3 字符集合

[ ]方括号匹配方式,能够匹配方括号中任意一个字符

[ab5@]

匹配 "a" 或 "b" 或 "5" 或 "@"

[^abc]

匹配 "a","b","c" 之外的任意一个字符

[f-k]

匹配 "f"~"k" 之间的任意一个字母

[^A-F0-3]

匹配 "A"~"F","0"~"3" 之外的任意一个字符

显示效果如下所示:


注意: 正则表达式中的特殊符号,如果被包含于中括号中,则失去特殊意义,但 \ [ ] : ^ - 除外。

4.4 预定义字符

还有一些字符是正则表达式语法事先定义好的,有特殊的含义,能够与 ‘多种字符’ 匹配的表达式

\d

任意一个数字,0~9 中的任意一个

\w

任意一个字母数字下划线,也就是 A~Z,a~z,0~9,_ 中任意一个

\s

包括空格、制表符、换行符等空白字符的其中任意一个

.

小数点可以匹配除了换行符(\n)以外的任意一个字符

注意: 如果是\D,\W,\S表示相反的意思。

4.5 量词

{n}

表达式重复n次

{m,n}

表达式至少重复m次,最多重复n次

{m,}

表达式至少重复m次

?

匹配表达式0次或者1次,相当于 {0,1}

+

表达式至少出现1次,相当于 {1,}

*

表达式不出现或出现任意次,相当于 {0,}

显示效果如下所示:

表达式至少重复2次,最多重复4次

至少3次:

匹配表达式0次或者1次,相当于 {0,1}.

+至少一次

表达式不出现或出现任意次,相当于 {0,}

4.6 贪婪模式和非贪婪模式

匹配次数中的贪婪模式(匹配字符越多越好)

“{m,n}”, “{m,}”, “?”, “*”, “+”,具体匹配的次数随被匹配的字符串而定。这种重复匹配不定次数的表达式在匹配过程中,总是尽可能多的匹配。

匹配次数中的非贪婪模式(匹配字符越少越好)

在修饰匹配次数的特殊符号后再加上一个 "?" 号,则可以使匹配次数不定的表达式尽可能少的匹配,使可匹配可不匹配的表达式,尽可能地 "不匹配"。

4.7 边界字符

(本组标记匹配的不是字符而是位置,符合某种条件的位置)

^

与字符串开始的地方匹配

$

与字符串结束的地方匹配

\b

匹配一个单词边界

显示效果如下所示:


4.8 选择符和分组

表达式

作用

|

左右两边表达式之间 "或" 关系,匹配左边或者右边

( )

(1). 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰

(2). 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到

(3). 每一对括号会分配一个编号,使用 () 的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本

选择符操作显示效果如下所示:

分组:

4.9 反向引用(\nnn)

1. 每一对()会分配一个编号,使用 () 的捕获根据左括号的顺序从 1 开始自动编号

2. 通过反向引用,可以对分组已捕获的字符串进行引用。


5. 正则表达式的匹配模式

5.1 IGNORECASE 忽略大小写模式

1. 匹配时忽略大小写。

2. 默认情况下,正则表达式是要区分大小写的。

5.2 SINGLELINE 单行模式

1. 整个文本看作一个字符串,只有一个开头,一个结尾。

2.使小数点 "." 可以匹配包含换行符(\n)在内的任意字符。

5.3 MULTILINE 多行模式

1.每行都是一个字符串,都有开头和结尾。

2.在指定了 MULTILINE 之后,如果需要仅匹配字符串开始和结束位置,可以使用 \A 和 \Z

6.开发中使用正则表达式的流程

1. 分析所要匹配的数据,写出测试用的典型数据

2. 在工具软件中进行匹配测试

3. 在程序中调用通过测试的正则表达式


7. 课堂练习

电话号码验证

(1)电话号码由数字和"-"构成

(2)电话号码为7到8位(不加区号)

(3)如果电话号码中包含有区号,那么区号为三位或四位, 首位是0.

(4)区号用"-"和其他部分隔开

(5)移动电话号码为11位

(6)11位移动电话号码的第一位和第二位为"13“,”15”,”18”

0(\d{2,3})-\d{7,8}

(13)|(15)|(18)\d{9}


8. Java中使用正则表达式

相关类位于:java.util.regex包下面

类 Pattern

正则表达式的编译表示形式。

Pattern p = Pattern.compile(r); //建立正则表达式,并启用相应模式

类 Matcher

通过解释 Patterncharacter sequence 执行匹配操作的引擎

Matcher m = p.matcher(str); //匹配str字符串

编程中使用正则表达式常见情况:

验证表达式是否匹配整个字符串

验证表达式是否可以匹配字符串的子字符串

返回给定字符串中匹配给定正则表达式所有子字符串

替换给定字符串中匹配正则表达式的子字符串

根据正则表达式定义规则截取字符串


9. JAVASCRIPT中使用正则表达式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!--         QQ账号的验证: 
           1.qq号必须是5-10位 
  2.数字0不可以作为qq号码的开头
     -->     <form action="server/server.html" method="get" onsubmit="return checkQQ()">
        QQ账号: <input id="qq" type="text" placeholder="请输入QQ账号..."> <span id="msg"></span> <br>
        <input type="submit" value="登录">
    </form>    
<script>
        function checkQQ() {            var qq = document.getElementById("qq").value;
            var msg = document.getElementById("msg");
            // 默认qq号码是合格的
            var flag = true;
            // 保证5-10位
            if(qq.length >= 5 && qq.length <= 10) {
                // 保证不是0开头
                if(qq.charAt(0) != 0) {
                    // 保证是数字
                    for(var i = 0; i < qq.length; i++) {
 var ch = qq.substr(i,1);
                        if(!(ch >= '0' && ch <= '9')) {
                            flag = false; 
   msg.innerText = "XXX QQ号码必须是数字!!!"                            
msg.style.color = "red";
                            break;    
                    }      
              }      
          } else {          
          flag = false;   
                 msg.innerText = "XXX QQ号码不能以数字0开头!!!"
                    msg.style.color = "red";  
              }      
      } else {       
         flag = false;   
             msg.innerText = "XXX QQ号码的位数必须在5~10之间!!!"
                msg.style.color = "red";
            }       
     return flag;  
      } 
   </script>
</body>
</html>

Flags可选项常见值:

g (全文查找出现的所有pattern)(没有g只会匹配一次)

i (忽略大小写)

<form action="server/server.html" method="get" >
    QQ账号: <input id="qq" type="text" placeholder="请输入QQ账号...">
  <span id="msg"></span> <br>
    <input type="submit" value="登录" onclick="return checkQQ();"> 
</form>

RegExp对象常用方法:

exec(): 返回的是一个数组。该数组包含了匹配该模式的第一个子字符串以及该子字符串中匹配相关分组的字符串。比如:

var re = new RegExp("(\\d+)([a-z]+)","ig");
var result = re.exec("33ff55tt77uu88yy");  

返回的数组为:[33ff,33,ff]

test(): 返回一个 Boolean 值,它指出在被查找的字符串中是否存在模式匹配的子字符串

//test 查找是否 存在的字符串
var str1 = "abc123abc123";
//判断字符串是否都是数字
var reg3 = /^\d+$/;
var flag = reg3.test(str1);
console.log(flag);

字符串中常用的使用正则表达式的方式:

match():使用正则表达式模式对字符串执行查找,并将符合该模式的所有子字符串包含到数组中返回。

var re = new RegExp("(\\d+)([a-z]+)","ig");
var reg = /(\d+)([a-z]+)/gi;
var t = "33ff55tt77uu88yy".match(re);

结果为数组:[33ff,55tt,77uu,88yy]

search(): 返回与正则表达式查找内容匹配的第一个子字符串的位置

var reg = /\d+/g; // 数组正则
var index = "33ff55tt77uu88yy".search(reg);
split(regex):按照指定的模式切割字符串,返回一个数组。
var t = "asdfd33ff55tt77uu88yy".split(/\d+/);
replace(): 替换
var t = "asdfd3355tt77uu88yy".replace(/\d+/g,"#");
代码示例如下所示:
// 替换
// 指定为global模式,否则只替换第一个子字符串
var reg4= /\d+/g;
var str2 = str1.replace(reg4,"#");
console.log(str2);

10. 针对表单域的验证,封装一个通用的函数

我们可以编写一个简单的用户名验证的方法,代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>   
 <meta charset="UTF-8">  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">  
  <title>Js表单正则验证</title>
</head>
<body> 
   <form action="server/server.html" method="get">
用户名:<input type="text" id="nameId" name="name" onblur="checkName();"><span id="msg"></span><br>
密码: <input type="password" name="pwd"><br>
        <input type="submit">  
  </form> 
   <script>
        /*
             表单验证:
            实现思路: 
                1.给input标签添加onblur事件
                2.获取input标签输入的内容
                3.判断是否输入合法
  4.dom操作修改页面提示信息    
     */
         function checkName() {
            
var nameInputValue = document.getElementById("nameId").value;
            var msg = document.getElementById("msg");
            var reg = /^[\u4e00-\u9fa5]{2,10}$/; 
            if(reg.test(nameInputValue)) {
                msg.innerText = "√ 用户名合法!!!"
                msg.style.color = "red";
            } else {
                msg.innerText = "X 用户名必须是2-10个中文字符!!!"
                msg.style.color = "red"; 
           }   
      }   
 </script>
</body>
</html>

这里我们通过输入框失去焦点来对输入的内容进行验证,验证的步骤也非常简单,步骤基本如下所示:

1. 给表单域添加onblur失去焦点事件

2. 获取表单域输入框的内容

3. 通过正则表达式判断输入是否合法

4. 根据是否合法显示不同的页面提示信息

通过观察我们不难发现,不仅验证用户名,验证邮箱,密码,身份证等等基本上步骤都是一样的,但是有一些例如正则表达式或者显示的正确或者错误信息是变化的,所以我们可以对验证的方法进行封装,将变化的抽取成为形参,从而用于其他的表单校验,代码如下所示:

现代web开发中,表单是用户与网站互动的重要方式之一。HTML5为表单提交提供了强大的功能和丰富的输入类型,让收集和验证用户输入数据变得更加容易和安全。本文将详细介绍HTML5表单的各个方面,包括基本结构、输入类型、验证方法和提交过程。

基本结构

HTML表单由<form>标签定义,它可以包含输入字段、标签、按钮等元素。一个基本的表单结构如下所示:

<form action="/submit_form" method="post">
  <label for="name">姓名:</label>
  <input type="text" id="name" name="name" required>
  
  <label for="email">电子邮箱:</label>
  <input type="email" id="email" name="email" required>
  
  <input type="submit" value="提交">
</form>

在这个例子中,表单有两个输入字段:姓名和电子邮箱。每个输入字段都有一个<label>标签,这不仅有助于用户理解输入的内容,也有助于屏幕阅读器等辅助技术。<form>标签的action属性定义了数据提交到服务器的URL,method属性定义了提交数据的HTTP方法(通常是post或get)。

输入类型

HTML5提供了多种输入类型,以支持不同的数据格式和设备。

文本输入

<!-- 单行文本 -->
<input type="text" name="username" placeholder="请输入用户名" required>

<!-- 密码 -->
<input type="password" name="password" required minlength="8">

<!-- 邮箱 -->
<input type="email" name="email" required placeholder="example@domain.com">

<!-- 搜索框 -->
<input type="search" name="search" placeholder="搜索...">

数值输入

<!-- 数值 -->
<input type="number" name="age" min="18" max="100" step="1" required>

<!-- 滑动条 -->
<input type="range" name="volume" min="0" max="100" step="1">

<!-- 电话号码 -->
<input type="tel" name="phone" pattern="^\+?\d{0,13}" placeholder="+8613800000000">

日期和时间输入

<!-- 日期 -->
<input type="date" name="birthdate" required>

<!-- 时间 -->
<input type="time" name="appointmenttime">

<!-- 日期和时间 -->
<input type="datetime-local" name="appointmentdatetime">

选择输入

<!-- 复选框 -->
<label><input type="checkbox" name="interest" value="coding"> 编程</label>
<label><input type="checkbox" name="interest" value="music"> 音乐</label>

<!-- 单选按钮 -->
<label><input type="radio" name="gender" value="male" required> 男性</label>
<label><input type="radio" name="gender" value="female"> 女性</label>

<!-- 下拉选择 -->
<select name="country" required>
  <option value="china">中国</option>
  <option value="usa">美国</option>
</select>

特殊输入

<!-- 颜色选择器 -->
<input type="color" name="favcolor" value="#ff0000">

<!-- 文件上传 -->
<input type="file" name="resume" accept=".pdf,.docx" multiple>

验证方法

HTML5表单提供了内置的验证功能,可以在数据提交到服务器之前进行检查。

必填字段

<input type="text" name="username" required>

正则表达式

<input type="text" name="zipcode" pattern="\d{5}(-\d{4})?" title="请输入5位数的邮政编码">

数值范围

<input type="number" name="age" min="18" max="99">

长度限制

<input type="text" name="username" minlength="4" maxlength="8">

表单提交

当用户填写完表单并点击提交按钮时,浏览器会自动检查所有输入字段的有效性。如果所有字段都满足要求,表单数据将被发送到服务器。否则,浏览器会显示错误信息,并阻止表单提交。

<input type="submit" value="提交">

可以使用JavaScript来自定义验证或处理提交事件:

document.querySelector('form').addEventListener('submit', function(event) {
  // 检查表单数据
  if (!this.checkValidity()) {
    event.preventDefault(); // 阻止表单提交
    // 自定义错误处理
  }
  // 可以在这里添加额外的逻辑,比如发送数据到服务器的Ajax请求
});

完整例子

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表单提交并显示JSON</title>
</head>
<body>

<!-- 表单定义 -->
<form id="myForm">
  <label for="name">姓名:</label>
  <input type="text" id="name" name="name">
  <br>

  <label for="email">电子邮件:</label>
  <input type="email" id="email" name="email">
  <br>

  <input type="button" value="提交" onclick="submitForm()">
</form>

<script>
// JavaScript函数,处理表单提交
function submitForm() {
  // 获取表单元素
  var form = document.getElementById('myForm');
  
  // 创建一个FormData对象
  var formData = new FormData(form);
  
  // 创建一个空对象来存储表单数据
  var formObject = {};
  
  // 将FormData转换为普通对象
  formData.forEach(function(value, key){
    formObject[key] = value;
  });
  
  // 将对象转换为JSON字符串
  var jsonString = JSON.stringify(formObject);
  
  // 弹出包含JSON字符串的对话框
  alert(jsonString);
  
  // 阻止表单的默认提交行为
  return false;
}
</script>

</body>
</html>

在这个例子中:

  1. 我们定义了一个包含姓名和电子邮件输入字段的表单,以及一个按钮,当点击按钮时会调用submitForm函数。
  2. 在submitForm函数中,我们首先获取表单元素并创建一个FormData对象。然后,我们遍历FormData对象,将表单数据复制到一个普通的JavaScript对象formObject中。
  3. 接着,我们使用JSON.stringify方法将formObject转换成JSON字符串。
  4. 最后,我们使用alert函数弹出一个包含JSON字符串的对话框。

注意,这个例子中我们使用了type="button"而不是type="submit",因为我们不希望表单有默认的提交行为。我们的JavaScript函数submitForm会处理所有的逻辑,并且通过返回false来阻止默认的表单提交。如果你想要使用type="submit",你需要在<form>标签上添加一个onsubmit="return submitForm()"属性来代替按钮上的onclick事件。

结论

HTML5的表单功能为开发者提供了强大的工具,以便创建功能丰富、用户友好且安全的网站。通过使用HTML5的输入类型和验证方法,可以确保用户输入的数据是有效的,同时提高用户体验。随着技术的不断进步,HTML5表单和相关API将继续发展,为前端工程师提供更多的可能性。

者:郜克帅

原文:https://dev.to/ajones_codes/a-better-guide-to-forms-in-react-47f0?utm_source=newsletter.reactdigest.net&utm_medium=newsletter&utm_campaign=a-better-guide-to-forms-in-react

React 生态拥有丰富的库、文章、视频和几乎你能想到的所有 Web 领域的资料。然而,随着时间的推移,这些资料许多都已经过时,无法满足现代最佳实践的要求了。

最近,我在开发一个 AI 项目,里面有许多复杂的动态表单。在研究了许多优秀的 React 表单指南之后,我意识到,大多数构建表单的资源都已经过时了,而且往往已经过时很多年。

本文将介绍 React 中构建表单的现代最佳实践、如何去构建动态表单、 RSC(React Server Components)的表单等等。最后,在理解了这些之后,我将解释我在其他指南中发现的不足,并根据我使用 React 的经验提出建议。

受控与非受控

理解 React 中表单的关键点在于 “受控” 与 “非受控” 的概念,这是 React 中构建表单的两种不同的方法。

受控表单使用 state 存储每个 input 的值,然后在每次渲染时通过 value 属性设置对应 input的值。如果其他函数更新了这些 state,同样的,对应 input 的值也会立刻改变。

如果你的代码没有渲染 Form,但相关的 state 并不会消失,仍然存在于我们的运行时上下文中。

受控表单往往给予了我们更大的选择,例如比较复杂的、非 HTML 标准的表单校验,如检查密码强度和对用户手机号进行格式化。

它们看起来往往是这个样子的:

import React, { useState } from 'react'

function ControlledForm() {
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const handleSubmit = () => {
    sendInputValueToApi(value).then(() => /** 业务逻辑... */);
  };

  return (
    <>
      <input type="text" value={value} onChange={handleChange} />
      <button onClick={handleSubmit}>send</button>
    </>
  )
}

注意,用 <form>input 包裹起来并且给 input 一个命名从语义上来讲更加准确,但是这不是必需的。

因为数据已经保存在 state 中,所以我们并不需要真正的 onSubmit事件,而且在按钮点击时,我们也并不需要直接访问 input 的值。

这种方式有一些不足之处:

  1. 你可能不想要每次用户输入时都去重新渲染组件。
  2. 你需要写许多代码去管理复杂的表单,因为随着表单规模的增长,会导致出现大量的 statesetSate,从而使代码变的非常臃肿。
  3. 构建动态表单将变的非常困难,因为你无法在条件判断中使用像 useStatehooks。为了修复这个问题,你可能需要:
  1. 整个表单的值将存储在一个巨大的对象中,然而这会导致所有的子组件将在任一其他组件变化时全部重新渲染,因为我们更新的方式是 setState({ ...preState, field: newValue })。要解决上述的问题,唯一的办法就是缓存,但这又会增加大量的代码。
  1. 在大型表单例如表格和 Excel 中,这会导致性能问题。
import React, { useState } from "react";

function CumbersomeForm() {
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: "",
    email: "",
    address: "",
    // ... 可能会有更多的值
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prevState) => ({ ...prevState, [name]: value }));
  };

  return (
    <>
      <label>First Name:</label>
      <input
        type="text"
        name="firstName"
        value={formData.firstName}
        onChange={handleChange}
      />
      <label>Last Name:</label>
      <input
        type="text"
        name="lastName"
        value={formData.lastName}
        onChange={handleChange}
      />
      <label>Email:</label>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <label>Address:</label>
      <input
        type="text"
        name="address"
        value={formData.address}
        onChange={handleChange}
      />
      {/* ... 可能会有更多的字段 */}
    </>
  );
}

与受控表单不同的是,非受控表单不在 state 中存储表单的值。相反,非受控表单使用原生 HTML 内置的 <form> 的功能和 JavaScript 去管理数据。

举例来说,浏览器会帮我们管理状态,我们无需在每次 input 改变时使用 setState 更新 state 并把 state 设置到 inputvalue 属性上,我们的组件不再需要或使用这些 state

当组件渲染时,React 会将 onSubmit 监听器添加到表单上。当提交按钮被点击时,我们的 handleSubmit 函数会被执行。与使用 state相比,它更接近于不使用任何 JavaScript 的普通 HTML 表单的工作方式。

function UncontrolledForm() {

  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);
    const inputValue = formData.get('inputName');

    sendInputValueToApi(inputValue).then(() => /* 业务逻辑... */)  
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="inputName" />
      <button type="submit">Send</button>
    </form>
  );
}

使用非受控表单的一个好处就是会减少大量的冗余代码:

// 受控
const [value, setValue] = useState('')

const handleChange = (event) => {
  setValue(event.target.value);
}
...
<input type="text" value={value} onChange={handleChange} />

// 非受控
<input type="text" name="inputName" />

即便只有 1 个 input,区别也是非常显著的,当有许多 input 时,效果会更加明显:

function UncontrolledForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const { name, email, address } = Object.fromEntries(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>First Name:</label>
      <input type="text" name="firstName" />

      <label>Last Name:</label>
      <input type="text" name="lastName" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Address:</label>
      <input type="text" name="address" />
      {/* ... 可能会有更多的字段 */}
      <button type="submit">Submit</button>
    </form>
  );
}

非受控表单与受控表单相比,没有许多冗余的代码,并且我们不需要手动管理许许多多的 state 或一个巨大的对象。事实上,这里根本没有 state。这个表单可以有成百上千个子组件但它们不会导致彼此重新渲染。使用这种方式,会让表单性能变的更好、减少大量的冗余代码并且使我们代码的可读性更强。

非受控表单的不足之处是你无法直接访问每个 input 的值。这会使自定义校验变的棘手。例如你需要在用户输入手机号的时候格式化手机号。

注意事项

不要使用useRef

许多文章推荐在非受控表单的每个 input 上使用一个 ref 而不是使用 new FormData(),我认为原因是 FormData API 很少人知道。然而,它在大约十年前已经成为了一个标准并且已经被所有主流浏览器支持。

我强烈建议你不要为表单使用 useRef,因为它会像 useState 一样引入许多相同的问题和冗余的代码。

然而,确实有一些场景,ref 可以帮助我们。

  1. 聚焦字段时
function MyForm() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <form>
      <input ref={inputRef} type="text" />
      <button type="button" onClick={focusInput}>
        Focus Input
      </button>
    </form>
  );
}
  1. 调用子组件的方法时
const ChildComponent = React.forwardRef((props, ref) => (
  <input ref={ref} type="text" />
));

function MyForm() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <form>
      <ChildComponent ref={inputRef} />
      <button type="button" onClick={focusInput}>
        Focus Input
      </button>
    </form>
  );
}
  1. 其他的例如保存 useEffect 的前一个值或测量一个元素大小时

混合受控与非受控

在许多场景中,你可能需要控制一个或更多的 input,当用户在输入手机号码时对其进行格式化是一个非常棒的例子。在这些场景中,即便你正在使用非受控表单,你也可以使用一个受控的 input。在这种情况下,也不要使用 state 去访问 input 的值,继续使用 new FormData(...),仅仅使用 state 去管理相关输入的展示。

function MixedForm() {
  const [phoneNumber, setPhoneNumber] = useState("");

  const handlePhoneNumberChange = (event) => {
    // 格式化手机号
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <input
        type="tel"
        name="phoneNumber"
        value={phoneNumber}
        onChange={handlePhoneNumberChange}
      />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

function formatPhoneNumber(number) {
  return number.replace(/\D/g, "").slice(0, 10);
}

注意:尽量减少 state,在这个例子中,你不会既想要一个保存原始电话号码的 useState,又想要一个用于格式化电话号码的 useState,并且因为同步它们还会带来多余的重新渲染的效果。

谈到重新渲染优化,我们可以将受控 input 抽离出来以此来减少重新渲染对表单其余部分的影响。

const PhoneInput = () => {
  const [phoneNumber, setPhoneNumber] = useState("");

  const handlePhoneNumberChange = (event) => {
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };

  return (
    <input
      type="tel"
      name="phoneNumber"
      value={phoneNumber}
      onChange={handlePhoneNumberChange}
    />
  );
};

function MixedForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <PhoneInput />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

function formatPhoneNumber(number) {
  return number.replace(/\D/g, "").slice(0, 10);
}

如果你用过受控 input,那么看完上面代码之后,你可能会想:“没有传递任何 setStateref, 父组件是如何知道子组件的值”。为了理解这个问题,请记住,当 React 代码被渲染成 HTML 时,浏览器只会看到 Form 和它里面的 inputs,包括 <PhoneInput /> 渲染的 input

我们的组件组合方式对我们渲染的 HTML 没有功能上的影响。因此,那个 input 的值会像其他字段一样被包含在 FormData 中。这就是组件组合和封装的力量。我们可以将重新渲染控制在最小影响范围内,与此同时,DOM 依然像原生 HTML 一样呈现。

等等... 我如何在非受控 input 中做校验?

考虑到这个问题的并非只有你一个!当在提交前需要校验时,React 开发者往往会倾向于去使用受控组件。

许多开发者并没有意识到,你并不需要 React 或自定义的 JavaScript 做这些校验。事实上,有一些原生的属性已经支持了这些事情。请参阅 MDN 查看更多的细节:https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation

在不使用任何 JavaScript 的前提下,你可以设置 input 必填、设置长度限制和用正则表达式设置格式要求。

错误处理

在相关的讨论中,在我们需要在客户端展示错误信息的时候,开发者通常会选择受控表单来解决这个问题。然而,我会优先选择使用非受控组件并在我的 onSubmit 函数里面做校验和错误管理,而不是使用受控组件并在每次 input 改变时更新对应的 state。这种方式可以尽量减少 statesetState 的数量。

function UncontrolledForm() {
  const [errors, setErrors] = useState({});

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    let validationErrors = {};

    // 自定义校验:确保邮箱的域名是:"example.com"
    const email = formData.get("email");
    if (email && !email.endsWith("@example.com")) {
      validationErrors.email = "Email must be from the domain example.com.";
    }

    if (formData.get("phoneNumber").length !== 10) {
      validationErrors.phoneNumber = "Phone number must be 10 digits.";
    }

    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
    } else {
      console.log(Array.from(formData.entries()));
       // 清空之前的值
      setErrors({});
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" required />
      {errors.name && <div className="error">{errors.name}</div>}

      <label>Email (must be @example.com):</label>
      <input type="email" name="email" required />
      {errors.email && <div className="error">{errors.email}</div>}

      <label>Phone Number (10 digits):</label>
      <input type="tel" name="phoneNumber" required pattern="\d{10}" />
      {errors.phoneNumber && <div className="error">{errors.phoneNumber}</div>}

      <button type="submit">Submit</button>
    </form>
  );
}

export default UncontrolledForm;

服务端组件中的 Form

React Server Components(RSC) 使用服务端框架去渲染部分组件,通过这种办法可以减少浏览器访问你网站时下载的 JavaScript 的数量。这可以显著地提升你网站的性能。

RSC 对我们编写表单的方式有很大的影响。因为,对于首次渲染来说,如果我们没有使用 state,它们可以在服务端被渲染并不附带任何 JavaScript 文件给浏览器。这意味着,非受控表单即使在没 JavaScript的情况下也可以交互,意味着它们可以更早的工作而不用等待 JavaScript 去下载然后运行。这可以让你的网站体验更加丝滑。

使用 Next.js,你可以在你的表单中使用 Server Actions,因此你不需要去为了你的表单交互写一个 API。你需要准备的只是一个事件处理函数。你可以在 Next.js 的文档中找到关于这个主题的更多内容或者观看是 Lee 的视频。

如果你要在 RSC 中混合一些受控表单,请确保把它们抽离为单独的客户端组件,就像上面的 <PhoneInput /> 一样。这可以尽可能的减少需要打包的 JavaScript 文件。

// page.jsx
import { PhoneInput } from "./PhoneInput";

export default function Page() {
  async function create(formData: FormData) {
    "use server";

    // ... use the FormData
  }

  return (
    <form action={create}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <PhoneInput />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

// PhoneInput.jsx
"use client";

function formatPhoneNumber(number) {
  return number.replace(/\D/g, "").slice(0, 10);
}

import { useState } from "react";

export const PhoneInput = () => {
  const handlePhoneNumberChange = (event) => {
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };
  const [phoneNumber, setPhoneNumber] = useState("");

  return (
    <input
      type="tel"
      name="phoneNumber"
      value={phoneNumber}
      onChange={handlePhoneNumberChange}
    />
  );
};

Form 库

React 生态中有许多为受控表单设计的优秀的库。最近我一直在使用 React Hook Form 来处理这些应用,不过我更倾向于使用非受控表单,因为不需要额外的库来管理表单状态。(一些流行的库:React Hook FormFormikInformed

总结、对比和推荐

因为 Google 搜索 react forms 时排名靠前的文章令人感到困惑、过时或具有误导性,因此我写了本文。

  • 其中一篇文章说:“React 中更通用的方式是受控表单”,我不认为受控或非受控谁更通用。实际上,正如上文所述,这两种类型都有其用武之地。事实上,许多旧文章都推荐使用受控表单,同时理由同样含糊不清或具有误导性。
  • 没有一篇排名靠前的文章使用 FormData。对于非受控表单,至少两篇文章推荐使用 useRef,这同样会让你的代码变的不灵活且臃肿。
  • 一些排名靠前的文章仍然在使用类组件,没有提到函数式组件。

一些总结性的看法:

  1. 以我的经验来看,许多表单都是受控和不受控混合的。我们之所以有这两种选择,是因为我们有灵活性,不应该教条主义。我们可以使用同时使用它们,就像上面的 RSC 例子一样。
  2. 时至今日,我更偏爱于使用非受控表单,我认为这会简化代码结构并优化性能。
  3. 认真的说,在 onSubmit 函数中使用 new FormData(...)而不要使用 useRef
  4. 封装和组合受控表单去尽量减少 state 更新对其他组件的影响,并依靠组合后的 DOM 来处理提交事件。

作者:郜克帅

来源:微信公众号:Goodme前端团队

出处:https://mp.weixin.qq.com/s/JikF87PYtxnb9uxEtTtGNA