01-基本结构
目标:能够手动搭建登录界面的基本结构
分析说明:antd-mobile 中的 Form 组件用法与 antd 一样
步骤:
核心代码:
login/index.tsx 中:
import { Button, NavBar, Form, Input } from ‘antd-mobile’;
import styles from ‘./index.module.scss’;
const Login = () => {
return (
账号登录
extra={发送验证码}
>
{/ noStyle 表示不提供 Form.Item 自带的样式 /}
);
};
export default Login;
login/index.module.scss 中:
// 导入 1px 边框的样式文件
@import ‘@scss/hairline.scss’;
.root {
:global {
.login-form {
padding: 0 33px;
.title {
margin: 54px 0 13px 0;
}
.adm-list {
—align-items: end !important;
}
.adm-list-default {
border: none;
}
.adm-list-item {
padding: 0;
}
.login-item {
// 注意:因为 1px 边框样式使用了决定定位,所以,此处需要将设置为相对定位
position: relative;
// 为该元素设置 1px 边框
@include hairline(bottom, #f0f0f0);
> .adm-list-item-content {
height: 70px;
}
}
.adm-list-item-content {
position: relative;
border-bottom: none;
}
// 验证码
.adm-input-wrapper {
—placeholder-color: #a5a6ab;
}
.code-extra {
color: #595769;
font-size: 14px;
&-disabled {
color: #a5a6ab;
}
}
.adm-list-item-description {
position: absolute;
bottom: -25px;
}
.login-submit {
height: 50px;
margin-top: 38px;
border: none;
font-size: 16px;
background: linear-gradient(315deg, #fe4f4f, #fc6627);
}
}
}
}
02-表单校验
目标:能够为登录表单添加校验
核心代码:
login/index.tsx 中:
validateTrigger=”onBlur”
rules={[
{ required: true, message: ‘请输入手机号’ },
{
pattern: /^1[3-9]\d{9}$/,
message: ‘手机号格式错误’
}
]}
>
rules={[{ required: true, message: ‘请输入验证码’ }]}”
validateTrigger=”onBlur”
>
总结:
- 为 Form 表单添加 onFinish
- 创建 onFinish 函数,作为 Form 属性 onFinish 的值
- 指定函数 onFinish 的参数类型
- 通过参数获取到表单数据
核心代码:
login/index.tsx 中:
type LoginForm = {
mobile: string;
code: string;
};
const Login = () => {
const onFinish = (values: LoginForm) => {
console.log(values);
};
return
};
04-默认登录-登录逻辑
目标:能够在 Redux 中实现登录逻辑
分析说明:
实际项目开发中,通常都会为接口数据创建类型,这样,如果将来后端修改了接口数据,前端只需要修改接口数据的类型,
然后,所有用到该数据的地方都会有明确的错误提示,根据错误提示来进行修改即可。有利于项目功能的修改或重构
推荐按照以下步骤:
- 先按照接口的返回数据,准备 TS 类型
- 然后,在发送请求时,指定该请求的返回值类型
这样,在接下来的操作中,如果需要用到接口的数据,都会有类型提示了。
由于项目中的请求是通过 axios 处理的,所以,只需要为 axios 的请求方法指定类型即可:
- axios 的所有请求方法,都是泛型函数,通过泛型函数的泛型参数,来指定接口返回数据的类型
- 从哪获取 接口返回数据 的类型?文档
// 比如,以 post 请求为例:
const res = await axios.post
// res.data 的类型就是:ResponseDataType
步骤:
- 在 store/actions 中创建 login.ts 文件
- 创建 login 函数并导出
- 在函数中根据接口发送请求实现登录功能
- 在 login 的 reducer 中处理 login action
核心代码:
store/actions/login.ts 中:
import { RootThunkAction } from ‘@/types/store’;
import { http, setToken } from ‘@/utils’;
import type { Token } from ‘@/types/data’;
// login 函数的参数类型
type LoginParams = { mobile: string; code: string };
// login 接口的响应类型
type LoginResponse = {
message: string;
data: Token;
};
export const login = (values: LoginParams): RootThunkAction => {
return async (dispatch) => {
// 发送请求
const res = await http.post
// 拿到返回数据
const tokens = res.data.data;
// 设置本地token
setToken(tokens);
// 分发 action 将 token 保存到 redux state 中
dispatch({ type: ‘login/token’, payload: tokens });
};
};
store/reducers/login.ts 中:
import type { Token } from ‘@/types/data’;
import type { LoginAction } from ‘@/types/store’;
const initialState: Token = {
token: ‘’,
refresh_token: ‘’,
};
// 指定参数和返回值的类型
// 说明:此处明确指定返回值类型,可以在返回值与指定类型不一致时给出明确的错误提示
const login = (state = initialState, action: LoginAction): Token => {
switch (action.type) {
case ‘login/token’:
return action.payload;
default:
return state;
}
};
05-默认登录-组件登录逻辑
目标:能够调用登录逻辑实现登录并跳转到首页
步骤:
- 在 Login 组件中导入登录 action
- 在表单提交时,分发登录 action
- 登录成功后,展示成功提示
- 跳转到首页
核心代码:
pages/Login/index.tsx 中:
import { Toast } from ‘antd-mobile’;
import { useHistory } from ‘react-router-dom’;
import { useDispatch } from ‘react-redux’;
import { login } from ‘@/store/actions’;
const Login = () => {
const dispatch = useDispatch();
const history = useHistory();
const onFinish = async (values: LoginForm) => {
await dispatch(login(values));
// 登录成功提示
Toast.show({
content: ‘登录成功’,
duration: 600,
afterClose: () => {
// 返回首页
history.replace(‘/home’);
},
});
};
};
06-默认登录-异常处理
目标:能够处理登录时的异常
分析说明:
可以通过 try…catch 进行异常处理,其中 catch 的错误对象 e 的类型是:unknown
因此,要根据错误对象 e 进行异常处理,就需要先明确指定其类型,然后,才能对错误对象 e 进行操作
try {
// …
} catch (e) {
// e => unknown
const error = e as 具体的错误类型;
}
核心代码:
pages/Login/index.tsx 中:
import { AxiosError } from ‘axios’;
const onFinsih = async (values: LoginForm) => {
try {
await dispatch(login(values));
// 成功
Toast.show({
content: ‘登录成功’,
duration: 600,
afterClose: () => {
history.replace(‘/home’);
},
});
} catch (e) {
// 异常
// 如果异步操作失败了,会执行此处的错误处理
// 对于登录功能来说,出错了,通常是请求出问题了。
// 因此,此处将错误类型转为 AxiosError
const error = e as AxiosError<{ message: string }>;
Toast.show({
content: error.response?.data?.message,
duration: 1000,
});
}
};
07-默认登录-redux 获取 token
目标:能够实现刷新页面时在 redux 状态中拿到 token
分析说明:
问题:登录成功后,redux 状态中有 token 值。但是,刷新页面后,redux 中的 token 值没有了
原因说明:只在登录时,将 token 存储到 redux 状态中,没有处理刷新的情况
为了实现该功能,需要用到 createStore 的第二个参数:
// 第一个参数:reducer
// 第二个参数:初始状态
// 第三个参数:增强器,比如,中间件
createStore(reducer, [preloadedState], [enhancer]);
步骤:
- 在 store/index.ts 中导入 getToken 工具函数
- 创建 initialState 对象,将本地存储中保存的 token 放到该对象中
- 将 initialState 对象设置为 createStore 的第二个参数
核心代码:
store/index.ts 中:
const initialState = {
// 注意:此处的 login 属性是根据合并reducer时,login 的名称而来的
login: getToken(),
};
const store = createStore(rootReducer, initialState, middlewares);
utils/token.ts 中:
export const getToken = () =>
JSON.parse(
localStorage.getItem(GEEK_TOKEN_KEY) ??
‘{ “token”: “”, “refresh_token”: “” }’,
) as Token;
08-登录按钮启用或禁用
目标:能够根据表单验证是否成功来启用或禁用登录按钮
分析说明:
表单校验成功时,登录按钮为启用
表单校验失败或者用户还没有输入时,登录按钮为禁用
因此,需要动态控制登录按钮的状态,即:在用户输入的时候就进行校验
为了达到该目的,需要用到 shouldUpdate 属性,来在表单任意变化都对某一个区域进行渲染,达到实时校验的目的
参考 antd 文档:Form shouleUpdate参考 antd 示例:内联登录栏
// 可以通过 函数形式的children 来自定义渲染内容
{() => {
return (
);
}}
步骤:
- 创建登录表单的实例 form,来手动获取 Form 表单的校验状态等
- 将 form 设置为 Form 组件的 form 属性
- 使用一个函数的形式来渲染登录按钮
- 在该函数中处理是否禁用的逻辑
- 将是否禁用的值设置为 Button 按钮的 disabled 属性
核心代码:
const Login = () => {
// 创建 form 实例
const [form] = Form.useForm();
return (
{() => {
// isFieldsTouched(true) 检查是否所有字段都被操作过
const untouched = !form.isFieldsTouched(true);
// getFieldsError() 获取所有字段名对应的错误信息
const hasError =
form.getFieldsError().filter(({ errors }) => errors.length)
.length !== 0;
const disabled = untouched || hasError;
return (
);
};
// 上课分析的代码:
{() => {
// isFieldsTouched(true) 用来判断登录表单中的所有表单项是否被操作过
// 如果都操作过,结果为:true; 否则,为 false
// 如果只看该判断项,如果为 true 表示操作过,此时,才可能是不禁用
// 如果为 false 表示没有操作过(没有输入过内容),就应该是禁用
// console.log(‘登录按钮重新渲染了’, form.isFieldsTouched(true))
// console.log(form.getFieldsError())
// 获取校验失败的表单项
// const errors = form.getFieldsError().filter(item => item.errors.length > 0)
// 如果需要获取 表单校验 是否成功,只需要获取上述 errors 数组的长度
// 如果长度大于 0 说明有错误,表示:表单校验失败;否则,表单校验成功
// console.log(
// form.getFieldsError().filter(item => item.errors.length > 0)
// )
// 得到禁用状态
const disabled =
form.getFieldsError().filter((item) => item.errors.length > 0).length >
0 || !form.isFieldsTouched(true);
return (
09-动态获取验证码-拿到手机号码
目标:能够实现点击发送验证码时获取到手机号码
步骤:
- 给发送验证码绑定点击事件
- 在点击事件中获取到文本框的值
- 判断文本框的值是否为空
- 如果为空或手机号格式错误时,让文本框自动获得焦点
核心代码:
pages/Login/index.tsx 中:
import { useRef } from ‘react’
import { InputRef } from ‘antd-mobile/es/components/input’
const Login = () => {
const mobileRef = useRef
const onGetCode = () => {
// 拿到手机号
const mobile = (form.getFieldValue(‘mobile’) ?? ‘’) as string
// 判断手机号校验是否成功
const hasError = form.getFieldError(‘mobile’).length > 0
if (mobile.trim() === ‘’ || hasError) {
return mobileRef.current?.focus()
}
}
return (
// …
// …
extra={发送验证码}
>
// …
)
}
10-动态获取验证码-发送请求
目标:能够使用 redux 发送请求获取验证码
步骤:
- 在 Login 组件中导入获取验证码的 action
- 在获取验证码事件中分发获取验证码的 action
- 在 login action 中创建获取验证码的 action 并导出
- 发送请求获取验证码
核心代码:
pages/Login/index.tsx 中:
import { getCode, login } from ‘@/store/actions’;
const onGetCode = () => {
// …
dispatch(getCode(mobile));
};
actions/login.ts 中:
// 获取验证码
export const getCode = (mobile: string) => {
return async () => {
await http.get(/sms/codes/${mobile}
);
// 注意:验证码是发送到手机上的,因此,不需要更新Redux状态
};
};
总结-Form 提供的示例方法
Form 中提供的实例方法:参考 antd 的 Form 组件 API
- 获取表单中所有表单项的值:form.getFieldsValue()
- 获取表单中某个表单项的值:form.getFiledValue(name)
- 获取表单中所有表单项的错误:form.getFieldsError()
- 获取表单中某个表单项的错误:form.getFieldError(name)
- 手动对表单进行校验并在校验成功时,获取所有表单项的值:const values = await form.validateFields()
- 判断所有表单项是否被操作过(输入过内容):form.isFieldsTouched(true)
const [ form ] = Form.useForm()
11-验证码倒计时-开启倒计时
目标:能够在点击获取验证码时显示倒计时
步骤:
- 创建状态 timeLeft 倒计时数据
- 在点击获取验证码的事件处理程序中,更新倒计时时间并开启定时器
- 在定时器中,更新状态(需要使用回调函数形式的 setTimeLeft)
- 在开启定时器时,展示倒计时时间
核心代码:
pages/Login/index.tsx 中:
import { useState } from ‘react’;
const Login = () => {
const [timeLeft, setTimeLeft] = useState(0);
const onGetCode = () => {
// …
settimeLeft(5);
setInterval(() => {
setTimeLeft((timeLeft) => timeLeft - 1);
}, 1000);
};
return (
// …
// 判断是否开启定时器,没开启绑定事件,开启后去掉事件
onClick={timeLeft === 0 ? onGetCode : undefined}
>
{/ 判断是否开启定时器,没开启展示 发送验证码,开启后展示倒计时 /}
{timeLeft === 0 ? ‘发送验证码’ :
${timeLeft}s后重新获取
}}
>
);
};
12-验证码倒计时-清理定时器
目标:能够在倒计时结束时清理定时器
步骤:
- 通过 useRef Hook 创建一个 ref 对象,用来存储定时器 id
- 在开启定时器时,将定时器 id 存储到 ref 对象中
- 通过 useEffect Hook 监听倒计时的变化
- 判断倒计时时间是否为 0 ,如果为 0 就清理定时器
- 在组件卸载时(点击登录按钮,跳转到首页),清理定时器
const timerRef = useRef(-1);
const onGetCode = () => {
// …
// 注意:此处需要使用 window.setInterval
// 因为 setInterval 默认返回 NodeJS.Timeout,使用 window.setInterval 后,返回值才是 number 类型的数值
timerRef.current = window.setInterval(() => {
setTimeLeft((timeLeft) => timeLeft - 1);
}, 1000);
};
// 1. 监听倒计时变化,在倒计时结束时清理定时器
useEffect(() => {
if (timeLeft === 0) {
clearInterval(timerRef.current);
}
}, [timeLeft]);
// 2. 在组件卸载时清理定时器
useEffect(() => {
return () => {
// 组件卸载时清理定时器
clearInterval(timerRef.current);
};
}, []);