01-基本结构

目标:能够手动搭建登录界面的基本结构
分析说明:antd-mobile 中的 Form 组件用法与 antd 一样
步骤

  1. 完成导航栏和登录标题 - NavBar 组件
  2. 添加登录表单 - Form 组件

核心代码
login/index.tsx 中:
import { Button, NavBar, Form, Input } from ‘antd-mobile’;

import styles from ‘./index.module.scss’;

const Login = () => {
return (





账号登录








className=”login-item”
extra={发送验证码}
>



{/ noStyle 表示不提供 Form.Item 自带的样式 /}

block
type=”submit”
color=”primary”
className=”login-submit”
>
登 录





);
};

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 中:


name=”mobile”
validateTrigger=”onBlur”
rules={[
{ required: true, message: ‘请输入手机号’ },
{
pattern: /^1[3-9]\d{9}$/,
message: ‘手机号格式错误’
}
]}
>

name=”code”
rules={[{ required: true, message: ‘请输入验证码’ }]}”
validateTrigger=”onBlur”
>

总结

  • 注意:不要忘记给每个需要校验的 Form.Item 添加 name 属性

    03-获取登录表单数据

    目标:能够拿到手机号和验证码数据
    步骤
  1. 为 Form 表单添加 onFinish
  2. 创建 onFinish 函数,作为 Form 属性 onFinish 的值
  3. 指定函数 onFinish 的参数类型
  4. 通过参数获取到表单数据

核心代码
login/index.tsx 中:
type LoginForm = {
mobile: string;
code: string;
};
const Login = () => {
const onFinish = (values: LoginForm) => {
console.log(values);
};

return

;
};

04-默认登录-登录逻辑

目标:能够在 Redux 中实现登录逻辑
分析说明
实际项目开发中,通常都会为接口数据创建类型,这样,如果将来后端修改了接口数据,前端只需要修改接口数据的类型,
然后,所有用到该数据的地方都会有明确的错误提示,根据错误提示来进行修改即可。有利于项目功能的修改或重构
推荐按照以下步骤:

  1. 先按照接口的返回数据,准备 TS 类型
  2. 然后,在发送请求时,指定该请求的返回值类型

这样,在接下来的操作中,如果需要用到接口的数据,都会有类型提示了。
由于项目中的请求是通过 axios 处理的,所以,只需要为 axios 的请求方法指定类型即可:

  • axios 的所有请求方法,都是泛型函数,通过泛型函数的泛型参数,来指定接口返回数据的类型
  • 从哪获取 接口返回数据 的类型?文档

// 比如,以 post 请求为例:
const res = await axios.post();

// res.data 的类型就是:ResponseDataType
步骤

  1. 在 store/actions 中创建 login.ts 文件
  2. 创建 login 函数并导出
  3. 在函数中根据接口发送请求实现登录功能
  4. 在 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(‘/authorizations’, values);
// 拿到返回数据
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-默认登录-组件登录逻辑

目标:能够调用登录逻辑实现登录并跳转到首页
步骤

  1. 在 Login 组件中导入登录 action
  2. 在表单提交时,分发登录 action
  3. 登录成功后,展示成功提示
  4. 跳转到首页

核心代码
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]);
步骤

  1. 在 store/index.ts 中导入 getToken 工具函数
  2. 创建 initialState 对象,将本地存储中保存的 token 放到该对象中
  3. 将 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 (

);
}}

步骤

  1. 创建登录表单的实例 form,来手动获取 Form 表单的校验状态等
  2. 将 form 设置为 Form 组件的 form 属性
  3. 使用一个函数的形式来渲染登录按钮
  4. 在该函数中处理是否禁用的逻辑
  5. 将是否禁用的值设置为 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 (
block
type=”submit”
color=”primary”
className=”login-submit”
disabled={disabled}
>
提交

);
}}


);
};

// 上课分析的代码:

{() => {
// 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 (
disabled={disabled}
block
type=”submit”
color=”primary”
className=”login-submit”
>
登 录

);
}}
;

09-动态获取验证码-拿到手机号码

目标:能够实现点击发送验证码时获取到手机号码
步骤

  1. 给发送验证码绑定点击事件
  2. 在点击事件中获取到文本框的值
  3. 判断文本框的值是否为空
  4. 如果为空或手机号格式错误时,让文本框自动获得焦点

核心代码
pages/Login/index.tsx 中:
import { useRef } from ‘react’
import { InputRef } from ‘antd-mobile/es/components/input’

const Login = () => {
const mobileRef = useRef(null)

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 (
// …

// …
className=”login-item”
extra={发送验证码}
>


// …
)
}

10-动态获取验证码-发送请求

目标:能够使用 redux 发送请求获取验证码
步骤

  1. 在 Login 组件中导入获取验证码的 action
  2. 在获取验证码事件中分发获取验证码的 action
  3. 在 login action 中创建获取验证码的 action 并导出
  4. 发送请求获取验证码

核心代码
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

  1. 获取表单中所有表单项的值:form.getFieldsValue()
  2. 获取表单中某个表单项的值:form.getFiledValue(name)
  3. 获取表单中所有表单项的错误:form.getFieldsError()
  4. 获取表单中某个表单项的错误:form.getFieldError(name)
  5. 手动对表单进行校验并在校验成功时,获取所有表单项的值:const values = await form.validateFields()
  6. 判断所有表单项是否被操作过(输入过内容):form.isFieldsTouched(true)

const [ form ] = Form.useForm()

11-验证码倒计时-开启倒计时

目标:能够在点击获取验证码时显示倒计时
步骤

  1. 创建状态 timeLeft 倒计时数据
  2. 在点击获取验证码的事件处理程序中,更新倒计时时间并开启定时器
  3. 在定时器中,更新状态(需要使用回调函数形式的 setTimeLeft)
  4. 在开启定时器时,展示倒计时时间

核心代码
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 (
// …
extra={
// 判断是否开启定时器,没开启绑定事件,开启后去掉事件
onClick={timeLeft === 0 ? onGetCode : undefined}
>
{/ 判断是否开启定时器,没开启展示 发送验证码,开启后展示倒计时 /}
{timeLeft === 0 ? ‘发送验证码’ : ${timeLeft}s后重新获取}

}
>
);
};

12-验证码倒计时-清理定时器

目标:能够在倒计时结束时清理定时器
步骤

  1. 通过 useRef Hook 创建一个 ref 对象,用来存储定时器 id
  2. 在开启定时器时,将定时器 id 存储到 ref 对象中
  3. 通过 useEffect Hook 监听倒计时的变化
  4. 判断倒计时时间是否为 0 ,如果为 0 就清理定时器
  5. 在组件卸载时(点击登录按钮,跳转到首页),清理定时器

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);
};
}, []);