前端表单开发经验

Share

前言

表单是前端开发中常见的场景,注册登录、调查问卷,乃至租赁一个云服务器都是在填写表单。

本文根据我的表单开发经验分享表单开发中,为什么需要表单库进行辅助。

原生表单

前端技术上,表单对应form标签,MDN有详细的介绍。

以一个注册表单为例,我们需要如下标签

这离用户体验还很远,想要完善它的功能,都要依靠JS,原生表单有很多能力缺口,例如:

  1. 无法展示错误信息

为了告知用户错误的原因,常见在输入框底部进行红色文案的提示,如下图。HTML会给校验失败的元素添加CSS 伪类 :invalid,但这还远远不够。

错误展示
  1. 不支持自定义校验

内置支持require、pattern正则这些能力,但确认密码的校验需要和第一次填写的密码进行对比,没有相关的属性可以用。

React中的表单

尝试在React下实现一个完整的注册表单。

我们维护了两个状态对象,一个formValue代表表单值,一个error用于渲染错误信息。每一个input输入框传入value与onChange使其受控,用户输入后会修改formValue并进行校验。同时提交时也会重新进行校验,满足条件会打印数据。

表单库

如果借助表单库会更简单么?使用React Hook Form,代码如下

可以看到只用了不到一半的代码实现了相同的功能。

表单库都做了哪些事情

从上面一个简单的例子可以看出表单库可以大大简化代码的编写,上述例子主要体现状态管理(formState和error)以及校验,下面将更体系介绍一个表单库可以提供的能力。

一、状态管理

  1. 表单值集中管理

表单在提交时需要上传一个对象,如

{
	"name": "pureink",
	"password": "fakePassword",
	"confirm_password": "fakePassword"
}

在上面的例子中,我们创建了一个formState状态,为每一个输入框绑定了值,同时在onChange时修改formState,表单库将接管这些状态的管理。

我们可以以一个整体对该数据进行处理,例如可以给表单赋予初始值

const {
	register,
	getFieldState,
	formState: { errors },
} = useForm({
	defaultValues: {
		name: "张三",
	},
});
  1. 辅助状态管理

表单中最重要的状态是值,但我们还需要其他状态辅助渲染

例如:

touched:一个全新的表单,在用户填写前,即使输入框校验为空,也不应该展示任何错误消息,touched可以帮助我们判断用户是否有输入操作

isValidating:对于异步校验的表单,你可能希望在校验时禁止用户的输入

error:校验错误的信息,例如渲染在输入框下的红色文案

类似的属性还有active、dirty、visited、pristine等等,对于每一个表单项,表单库都会管理以上状态。

  1. 值清空和重置

问卷中常见B问题只在A问题选中某一项时展示,对于会显示和隐藏的表单项,表单库可以处理其值的清空和重置。

下面例子中,勾选问题1后,填写问题2。此时取消勾选问题1并提交,将不会看到问题2的值。

(也支持不清空,例子中可以设置shouldUnregister为false)

二、校验

  1. 自定义校验

不受限于HTML的required和pattern,你可以编写任意的函数进行同步或异步的校验。

  1. 校验策略

支持表单级别校验(Form-Level),字段级别校验(Field-Level)

所有表单库都支持字段级别校验,你可以对每个字段指定校验规则,细粒度的进行控制

部分表单库提供表单级别校验,对于简单表单,可以简化校验的编写,如Final-Form

<Form
	onSubmit={onSubmit}
	validate={(values) => {
		const errors = {};
		if (!values.username) {
			errors.username = "Required";
		}
		if (!values.password) {
			errors.password = "Required";
		}
		if (!values.confirm) {
			errors.confirm = "Required";
		} else if (values.confirm !== values.password) {
			errors.confirm = "Must match";
		}
		return errors;
	}}
>
	xxx
</Form>
  1. 校验集成

如React-Hook-Form、Formik可以与Yup、Zod这样的校验规则库结合使用

const schema = yup
	.object({
		firstName: yup.string().required(),
		age: yup.number().positive().integer().required(),
	})
	.required();
  1. 校验时机

触发校验的时机不只是onChange, 例如注册表单中你可能需要发起请求判断用户名称是否重复,使用onChange可能会校验多次,可以选择blur(失焦)事件触发时校验。

例如Final-Form和Formik都支持validateOnBlur参数。

  1. 主动触发

例如注册表单中,输入了同样的密码和确认密码,此时如果再次更改密码,确认密码的校验并不会运行,因为并没有对它进行直接的更改,触发其onChange事件。

表单库支持主动触发一个字段的校验,如React Hook FormFormik

Final-Form使用了另一种方式支持这种场景,本文不进行深入。

  1. 提交联动

表单进行提交时,需要对每一项内容进行校验,一些表单库还会提供滚动、focus到第一个错误表单项的能力。

  1. 渲染

一些表单库如Formik可以和第三方组件库如Antd简单结合,错误时输入框会变红等。

三、联动

多字段表单中各个字段经常互相关联,例如选择地区时,需要根据省市区的顺序进行选择,前一个选择影响后面的可选项。

你可以选择自定义onChange方法,当值变更时进行操作,也可以使用useEffect触发。

Formily这样的库支持一对一、一对多、多对一等方式的联动处理,开发效率和维护性都更高。

四、其他

  1. 性能

使用React Context管理表单值会导致任何一个表单项值变化,整个表单都会重新渲染一遍。表单库提供监听的功能,避免重复的渲染。

如Formik的FastField,Final Form的subscription

  1. 表单提交处理

除了可以直接拿到整个表单的值、触发整体校验以外,你可以感知表单是否在提交展示Loading动画,或禁用输入框避免用户此时输入等。

  1. 数组或嵌套字段

对于复杂字段的处理,如Final Form对于数组能力的支持

import { createForm } from "final-form";
import arrayMutators from "final-form-arrays";

// Create Form
const form = createForm({
	mutators: { ...arrayMutators },
	onSubmit,
});

// push
form.mutators.push("customers", { firstName: "", lastName: "" });

// pop
const customer = form.mutators.pop("customers");
  1. 动态化配置

如Formily这样完备的库支持JSON Shcema形式配置表单。

总结

表单因为多字段下状态管理和校验复杂导致代码量很多,借助表单库可以提升很多效率。

了解一个库的设计目的才能更好的使用工具,本文旨在说明为什么需要一个表单库 / 表单库做了什么事情。

下一篇文章会对React生态下流行的表单库进行多维度对比。

Read more

WASM初探

前言 最近开发上线了xhair.pro, 其核心的数据是通过爬虫下载.dem文件并使用一些开源的解析库如demoinfocs-golang,demoparser解析获取最终的数据 在本地下载解析上传可以随意配置环境,但浏览器无法直接运行Golang或Rust的代码。想要将解析的能力制作成web应用,需要使用WebAssembly技术。 WASM WebAssembly 是一种新的编码方式,可以在现代的 Web 浏览器中运行——它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。 我们可以把Rust、Go等语言编译成.wasm后缀的二进制文件,浏览器可以通过WebAssembly的API加载该模块,并调用相关能力 实践 不同编程语言需要通过不同的方式编译至wasm,我尝试了Go与Rust。 Golang实践 1. 首先需要配置Go的环境 go 安装 2. 初始化项目 mkdir w

By pureink

家用服务器公网访问方案

前言 因为ipv4地址数量受限,家庭宽带几乎都没有公网IP。在局域网内可以使用类似192.168.0.100这样的内网IP进行访问,但是想要在外访问或是提供给朋友使用是不行的。 方案一:内网穿透 可以使用具备公网IP的服务器进行中转,用户访问具备公网IP的服务器,该公网服务器再与家庭服务器通信,最终用户访问到家庭内网。 方案对比 方案 价格 优缺点 使用免费服务如ngrok、cloudflare tunnel 无 速度受限,部分功能需要收费如绑定域名 购买云服务器搭建 中 价格高,需要部署维护 购买FRP服务如sakura frp 低 有流量和带宽限制 个人建议 临时需求可以使用ngrok,例如给朋友看正在开发的网站 长期需求建议购买FRP服务,服务器安装frp软件后,可以通过网页配置隧道如下图,可以使用香港的服务器省去域名的备案,但不太适合云盘等大流量应用。 方案二:DDNS 内网穿透需要付费,并且有带宽和流量限制,额外的一个中转服务器也会影响访问速度。 好在ipv6已经逐渐普及,若服务端和客户端同时支持ipv6网络,

By pureink