rn_for_openharmony_表单验证这事儿,看着简单做起来烦

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony
注册功能写完了,测试的时候发现各种问题:用户名为空能提交、密码只输了一个字符也能提交、两次密码不一样也能提交……
表单验证,必须安排上。
我们要验证什么
WanAndroid 的注册接口有几个要求:
用户名不能为空,长度有限制。
密码不能为空,长度至少 6 位。
确认密码要和密码一致。
这些验证可以在前端做,也可以依赖后端。但前端验证能给用户更快的反馈,不用等网络请求返回才知道哪里填错了。
最基础的验证
先看我们现有的验证代码:
const handleSubmit = async () => {
if (!username || !password) {
Alert.alert('提示', '请输入用户名和密码');
return;
}
if (isRegister && password !== repassword) {
Alert.alert('提示', '两次密码不一致');
return;
}
// 提交逻辑...
};
这段代码做了两个验证:
第一个验证:非空检查
!username || !password 检查用户名和密码是否为空。这里用到了 JavaScript 的类型转换特性:空字符串 '' 在布尔上下文中是 false,所以 !'' 等于 true。
逻辑运算符 || 是短路求值,只要左边为 true 就不会计算右边。所以如果用户名为空,直接触发验证失败,不会再检查密码。
第二个验证:密码一致性检查
isRegister && password !== repassword 这个表达式有两层含义:
isRegister确保只在注册模式下检查,登录模式不需要确认密码password !== repassword用严格不等于比较两个字符串
验证失败的处理
Alert.alert('提示', '请输入用户名和密码') 弹出系统原生对话框,第一个参数是标题,第二个是内容。
return 语句很关键,它阻止函数继续执行。没有这个 return,验证失败后还会继续执行提交逻辑。
这个验证能用,但有几个问题。
问题一:空格也能通过
用户输入一堆空格,!username 是 false,验证通过了。但空格用户名肯定是不行的。
解决方法是用 trim() 去掉首尾空格:
if (!username.trim() || !password.trim()) {
Alert.alert('提示', '请输入用户名和密码');
return;
}
trim() 方法详解
trim() 是字符串的内置方法,它会返回一个新字符串,去掉原字符串首尾的空白字符(空格、制表符、换行符等)。
举几个例子:
' hello '.trim()返回'hello'' '.trim()返回''(空字符串)'hello'.trim()返回'hello'(没有空格就原样返回)
注意 trim() 不会修改原字符串,而是返回新字符串。JavaScript 的字符串是不可变的。
更进一步,可以在提交前统一处理:
const handleSubmit = async () => {
const trimmedUsername = username.trim();
const trimmedPassword = password.trim();
if (!trimmedUsername || !trimmedPassword) {
Alert.alert('提示', '请输入用户名和密码');
return;
}
// 用 trimmedUsername 和 trimmedPassword 提交
};
为什么要用新变量
这里用 trimmedUsername 和 trimmedPassword 两个新变量,而不是直接修改 state。原因有两个:
setUsername(username.trim())是异步的,调用后username不会立刻变化,后面的验证还是用的旧值- 保持输入框显示用户实际输入的内容,只在提交时处理空格,用户体验更好
问题二:密码太短
用户输入一个字符的密码,验证通过了。但这种密码太弱,容易被破解。
加一个长度验证:
if (password.length < 6) {
Alert.alert('提示', '密码至少6位');
return;
}
字符串的 length 属性
password.length 返回字符串的字符数。注意是字符数,不是字节数。对于中文、emoji 等多字节字符,一个字符就是一个长度单位。
比如:
'abc'.length是 3'你好'.length是 2'😀'.length是 2(emoji 在 JavaScript 里可能占 2 个长度单位,这是 UTF-16 编码的特性)
WanAndroid 的接口要求密码至少 6 位,我们在前端也做同样的限制。前后端保持一致,避免前端验证通过但后端拒绝的情况。
问题三:用户名太短或太长
用户名也应该有长度限制:
if (username.trim().length < 3) {
Alert.alert('提示', '用户名至少3个字符');
return;
}
if (username.trim().length > 20) {
Alert.alert('提示', '用户名不能超过20个字符');
return;
}
为什么要限制长度
最小长度限制:太短的用户名没有辨识度,比如 a、ab 这种。而且容易被恶意注册占用。
最大长度限制:太长的用户名会导致 UI 显示问题,比如用户名显示区域放不下,要么截断要么换行,都不好看。数据库字段通常也有长度限制。
链式调用
username.trim().length 是链式调用:先调用 trim() 返回去掉空格的新字符串,再访问这个新字符串的 length 属性。这样写比分两行更简洁。
问题四:一次只提示一个错误
现在的验证是串行的,第一个验证失败就 return 了,用户只能看到一个错误。
如果用户名为空、密码太短、两次密码不一致,用户要提交三次才能看到所有错误。体验不好。
可以改成收集所有错误,一次性提示:
const handleSubmit = async () => {
const errors: string[] = [];
if (!username.trim()) {
errors.push('请输入用户名');
} else if (username.trim().length < 3) {
errors.push('用户名至少3个字符');
}
if (!password) {
errors.push('请输入密码');
} else if (password.length < 6) {
errors.push('密码至少6位');
}
if (isRegister && password !== repassword) {
errors.push('两次密码不一致');
}
if (errors.length > 0) {
Alert.alert('提示', errors.join('\n'));
return;
}
// 提交逻辑...
};
数组收集错误
const errors: string[] = [] 声明一个字符串数组,用来收集所有错误信息。TypeScript 的类型注解 string[] 表示这是一个字符串数组。
每个验证失败时,用 errors.push() 把错误信息添加到数组末尾,而不是直接 return。
else if 的作用
注意用户名的验证用了 else if:先检查是否为空,不为空才检查长度。这样避免了重复提示,比如用户名为空时不会同时提示"请输入用户名"和"用户名至少3个字符"。
join() 方法
errors.join('\n') 把数组元素用指定的分隔符连接成字符串。'\n' 是换行符,所以每个错误占一行。
比如 ['错误1', '错误2'].join('\n') 返回 '错误1\n错误2',在弹窗里显示为两行。
最终检查
if (errors.length > 0) 检查是否有错误。数组的 length 属性返回元素个数,大于 0 说明有错误,弹窗提示并 return。
问题五:没有实时反馈
现在的验证是在点击提交按钮时才触发。用户填完整个表单,点提交,才发现第一个输入框就填错了。
更好的体验是实时验证:输入框失去焦点时就验证,有错误立刻提示。
const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = useState('');
const validateUsername = (value: string) => {
if (!value.trim()) {
setUsernameError('请输入用户名');
} else if (value.trim().length < 3) {
setUsernameError('用户名至少3个字符');
} else {
setUsernameError('');
}
};
<TextInput
value={username}
onChangeText={setUsername}
onBlur={() => validateUsername(username)}
/>
{usernameError ? <Text style={styles.error}>{usernameError}</Text> : null}
错误状态管理
每个输入框对应一个错误状态:usernameError、passwordError。初始值是空字符串,表示没有错误。
验证函数根据验证结果设置错误信息,验证通过时设置为空字符串清除错误。
onBlur 事件
onBlur 在输入框失去焦点时触发。"失去焦点"就是用户点击了输入框外面的地方,或者切换到下一个输入框。
这个时机很合适:用户输入完一个字段,准备输入下一个字段时,验证上一个字段。不会在用户还在输入时就报错。
条件渲染错误提示
{usernameError ? <Text>...</Text> : null} 是条件渲染的三元表达式写法。usernameError 有值时渲染 Text 组件,没值时渲染 null(什么都不显示)。
也可以用短路写法:{usernameError && <Text>...</Text>},效果一样。
错误提示的样式:
error: {
color: '#ff4444',
fontSize: 12,
marginTop: -12,
marginBottom: 12,
marginLeft: 4
}
样式细节解释
color: '#ff4444'红色,醒目地提示错误fontSize: 12比正文小一号,不喧宾夺主marginTop: -12负边距,让错误提示紧贴输入框。因为输入框有marginBottom: 16,不用负边距的话错误提示会离输入框太远marginBottom: 12和下一个输入框保持间距marginLeft: 4稍微缩进,和输入框内的文字对齐
问题六:输入时清除错误
用户看到错误提示后开始修改,但错误提示还在,感觉怪怪的。
应该在用户输入时清除错误:
const handleUsernameChange = (value: string) => {
setUsername(value);
if (usernameError) {
setUsernameError('');
}
};
<TextInput
value={username}
onChangeText={handleUsernameChange}
onBlur={() => validateUsername(username)}
/>
为什么要清除错误
用户看到"用户名至少3个字符"的错误后,开始补充输入。如果错误提示一直显示,用户会困惑:我已经在改了,为什么还报错?
在用户输入时立刻清除错误,给用户一个"我知道你在修改"的反馈。等输入框失去焦点时再重新验证。
条件判断的优化
if (usernameError) 先判断是否有错误,有才清除。虽然直接 setUsernameError('') 也能工作,但会触发不必要的重新渲染。React 的 setState 即使设置相同的值也会触发渲染(除非用了 memo 等优化)。
事件处理函数的命名
handleUsernameChange 这个命名遵循 React 的惯例:handle + 事件名。一看就知道是处理用户名变化的函数。
输入框样式变化
有错误时,输入框的边框可以变成红色,更醒目:
<TextInput
style={[
styles.input,
{backgroundColor: theme.bg, color: theme.text, borderColor: usernameError ? '#ff4444' : theme.border}
]}
// ...
/>
动态样式的写法
style 属性接受一个数组,React Native 会把数组里的样式对象合并。后面的样式会覆盖前面的同名属性。
styles.input 是基础样式,后面的对象是动态样式。borderColor 根据 usernameError 是否有值来决定用红色还是主题色。
三元表达式
usernameError ? '#ff4444' : theme.border 是三元表达式:条件 ? 真值 : 假值。
usernameError 是字符串,非空字符串在布尔上下文中是 true,空字符串是 false。所以有错误信息时用红色,没有时用主题边框色。
提交按钮的禁用状态
如果表单有错误,提交按钮可以禁用:
const isFormValid = username.trim().length >= 3 &&
password.length >= 6 &&
(!isRegister || password === repassword);
<TouchableOpacity
style={[styles.btn, {backgroundColor: isFormValid ? theme.accent : theme.border}]}
onPress={handleSubmit}
disabled={!isFormValid || loading}
>
表单有效性判断
isFormValid 是一个布尔值,综合判断表单是否可以提交:
username.trim().length >= 3:用户名去掉空格后至少 3 个字符password.length >= 6:密码至少 6 位(!isRegister || password === repassword):这个稍微复杂,我们拆开看
逻辑运算符的组合
!isRegister || password === repassword 的意思是:
- 如果是登录模式(
!isRegister为 true),整个表达式为 true,不需要检查确认密码 - 如果是注册模式(
!isRegister为 false),需要password === repassword为 true
这是一个常见的逻辑技巧:!条件 || 检查 等价于"如果条件成立,则必须通过检查"。
按钮的视觉反馈
backgroundColor: isFormValid ? theme.accent : theme.border:表单有效时用强调色(通常是蓝色或绿色),无效时用边框色(通常是灰色)。
灰色按钮在视觉上告诉用户"这个按钮现在不能点"。
disabled 属性
disabled={!isFormValid || loading} 禁用按钮的两种情况:
- 表单无效时禁用,防止提交无效数据
- 正在加载时禁用,防止重复提交
不过这种方式有个问题:用户不知道为什么按钮是灰的。所以实时错误提示还是要有的。
完整的验证逻辑
把上面的优化整合起来:
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [repassword, setRepassword] = useState('');
const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [repasswordError, setRepasswordError] = useState('');
const validateUsername = (value: string) => {
const trimmed = value.trim();
if (!trimmed) {
setUsernameError('请输入用户名');
return false;
}
if (trimmed.length < 3) {
setUsernameError('用户名至少3个字符');
return false;
}
if (trimmed.length > 20) {
setUsernameError('用户名不能超过20个字符');
return false;
}
setUsernameError('');
return true;
};
const validatePassword = (value: string) => {
if (!value) {
setPasswordError('请输入密码');
return false;
}
if (value.length < 6) {
setPasswordError('密码至少6位');
return false;
}
setPasswordError('');
return true;
};
const validateRepassword = (value: string) => {
if (value !== password) {
setRepasswordError('两次密码不一致');
return false;
}
setRepasswordError('');
return true;
};
const handleSubmit = async () => {
const isUsernameValid = validateUsername(username);
const isPasswordValid = validatePassword(password);
const isRepasswordValid = isRegister ? validateRepassword(repassword) : true;
if (!isUsernameValid || !isPasswordValid || !isRepasswordValid) {
return;
}
setLoading(true);
// 提交逻辑...
};
验证函数的返回值
每个验证函数返回 boolean:验证通过返回 true,失败返回 false。
这样设计的好处是可以在提交时统一调用所有验证函数,根据返回值判断是否继续。
提前返回模式
验证函数里用了"提前返回"模式:每个条件检查失败就立刻 return false,不用写一堆 else if。代码更扁平,更易读。
最后一行 return true 只有在所有检查都通过后才会执行。
提交时的验证调用
const isUsernameValid = validateUsername(username);
const isPasswordValid = validatePassword(password);
const isRepasswordValid = isRegister ? validateRepassword(repassword) : true;
注意这里没有用短路求值(&&),而是把每个验证结果存到变量里。这样即使第一个验证失败,后面的验证也会执行,所有错误都会显示出来。
如果用 validateUsername(username) && validatePassword(password),第一个失败就不会执行第二个了。
条件验证
isRegister ? validateRepassword(repassword) : true:只在注册模式下验证确认密码,登录模式直接返回 true。
后端验证也不能少
前端验证是为了用户体验,但不能完全依赖前端。用户可以绕过前端直接调接口,所以后端也要做验证。
WanAndroid 的接口会返回错误信息:
const register = async (username: string, password: string, repassword: string): Promise<boolean> => {
try {
const res = await userApi.register(username, password, repassword);
if (res.errorCode === 0) {
setIsLoggedIn(true);
setUserInfo(res.data);
return true;
} else {
Alert.alert('注册失败', res.errorMsg || '请检查输入信息');
return false;
}
} catch (e) {
Alert.alert('错误', '网络错误');
return false;
}
};
Promise 返回类型
Promise<boolean> 表示这个异步函数返回一个 Promise,resolve 的值是 boolean 类型。调用方可以用 await 获取这个 boolean 值。
错误码判断
res.errorCode === 0 是 WanAndroid 接口的约定:errorCode 为 0 表示成功,非 0 表示失败。
不同的接口可能有不同的约定,有的用 code,有的用 status,有的用 HTTP 状态码。要看具体接口文档。
错误信息的容错
res.errorMsg || '请检查输入信息':如果后端返回了错误信息就用后端的,没返回就用默认的。
|| 运算符在左边是 falsy 值(null、undefined、空字符串等)时返回右边的值。
try-catch 捕获网络错误
try-catch 捕获的是网络层面的错误,比如断网、超时、服务器无响应等。这些错误会导致 fetch 抛出异常。
接口返回的业务错误(errorCode 非 0)不会抛异常,需要在 try 块里判断处理。
res.errorMsg 是后端返回的错误信息,比如"用户名已存在"。这种错误前端没法验证,只能靠后端。
用户名已存在的处理
用户名已存在是常见的注册失败原因。后端返回错误后,可以把错误显示在用户名输入框下方:
const register = async (...) => {
const res = await userApi.register(username, password, repassword);
if (res.errorCode !== 0) {
if (res.errorMsg?.includes('已存在') || res.errorMsg?.includes('已注册')) {
setUsernameError('用户名已被注册');
} else {
Alert.alert('注册失败', res.errorMsg || '请检查输入信息');
}
return false;
}
// ...
};
可选链操作符
res.errorMsg?.includes('已存在') 用了可选链 ?.。如果 res.errorMsg 是 null 或 undefined,不会报错,直接返回 undefined。
这比 res.errorMsg && res.errorMsg.includes('已存在') 更简洁。
字符串的 includes 方法
includes() 检查字符串是否包含指定子串,返回 boolean。比 indexOf() !== -1 更直观。
智能错误展示
根据错误信息判断是不是用户名重复,是的话显示在输入框下方,比弹窗更直观。用户一眼就知道是用户名的问题,不用看弹窗再去找哪个输入框。
密码强度提示
可以加一个密码强度指示器,告诉用户密码是弱、中、强:
const getPasswordStrength = (pwd: string) => {
if (pwd.length < 6) return {level: 0, text: ''};
let score = 0;
if (pwd.length >= 8) score++;
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score++;
if (/\d/.test(pwd)) score++;
if (/[^a-zA-Z0-9]/.test(pwd)) score++;
if (score <= 1) return {level: 1, text: '弱', color: '#ff4444'};
if (score <= 2) return {level: 2, text: '中', color: '#ffaa00'};
return {level: 3, text: '强', color: '#00aa00'};
};
const strength = getPasswordStrength(password);
{password.length >= 6 && (
<Text style={{color: strength.color, fontSize: 12, marginBottom: 8}}>
密码强度:{strength.text}
</Text>
)}
正则表达式检测
/[a-z]/.test(pwd):检查是否包含小写字母/[A-Z]/.test(pwd):检查是否包含大写字母/\d/.test(pwd):检查是否包含数字,\d等价于[0-9]/[^a-zA-Z0-9]/.test(pwd):检查是否包含特殊字符,^在方括号里表示"非"
评分机制
每满足一个条件加 1 分:长度 >= 8、大小写混合、包含数字、包含特殊字符。
根据分数返回不同的强度等级和颜色。这个算法比较简单,实际项目可能需要更复杂的评估。
条件渲染
password.length >= 6 && (...) 只有密码达到最小长度才显示强度提示。太短的密码显示"弱"没意义,用户还在输入呢。
这个功能不是必须的,但能引导用户设置更安全的密码。
当前项目的验证代码
我们项目里的验证比较简洁:
const handleSubmit = async () => {
if (!username || !password) {
Alert.alert('提示', '请输入用户名和密码');
return;
}
if (isRegister && password !== repassword) {
Alert.alert('提示', '两次密码不一致');
return;
}
setLoading(true);
let success = false;
if (isRegister) {
success = await register(username, password, repassword);
} else {
success = await login(username, password);
}
setLoading(false);
if (success) {
setUsername('');
setPassword('');
setRepassword('');
onClose();
}
};
代码流程分析
- 先做前端验证,失败就 return
- 设置 loading 状态,显示加载动画
- 根据模式调用 register 或 login
- 关闭 loading 状态
- 成功则清空表单并关闭弹窗
let 声明的使用
let success = false 用 let 而不是 const,因为后面要重新赋值。const 声明的变量不能重新赋值。
await 的使用
success = await register(...) 等待异步操作完成,获取返回值。如果不用 await,success 会是一个 Promise 对象,不是 boolean。
成功后的清理
清空三个输入框的状态,调用 onClose() 关闭弹窗。这样下次打开弹窗时是干净的。
够用,但不够完善。如果你的项目对用户体验要求更高,可以参考上面的优化方案。
验证的时机选择
总结一下验证的几个时机:
输入时(onChange)
适合格式化输入,比如手机号自动加空格、银行卡号每四位加空格。不适合验证,因为用户还没输完,过早报错会打断用户。
失焦时(onBlur)
适合单个字段的验证。用户输完一个字段,切换到下一个字段时验证上一个。这个时机比较自然,不会打断用户输入。
提交时(onSubmit)
最后一道防线。即使前面的验证被跳过(比如用户直接点提交按钮),提交时也要验证一遍。这是必须有的。
实时验证(onChange + 防抖)
有些场景需要实时验证,比如检查用户名是否已被注册。但要加防抖,不然每输入一个字符就发一次请求,太浪费了。
我们的项目用的是提交时验证,简单直接。如果想要更好的体验,可以加上失焦时验证。
表单验证看着简单,细节还挺多的。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)