文章目录
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
| 项 | 版本 |
|---|---|
| react & react-dom | ^18.2.0 |
| react-router & react-router-dom | ^6.11.2 |
| antd | ^4.24.8 |
| @commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
| eslint-config-prettier | ^8.6.0 |
| husky | ^8.0.3 |
| lint-staged | ^13.1.2 |
| prettier | 2.8.4 |
| json-server | 0.17.2 |
| craco-less | ^2.0.0 |
| @craco/craco | ^7.1.0 |
| qs | ^6.11.0 |
| dayjs | ^1.11.7 |
| react-helmet | ^6.1.0 |
| @types/react-helmet | ^6.1.6 |
| react-query | ^6.1.0 |
| @welldone-software/why-did-you-render | ^7.0.1 |
| @emotion/react & @emotion/styled | ^11.10.6 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
六、用户体验优化 - 加载中和错误状态处理
1~2
3.登录注册页面 Loading 和 Error 状态处理,与 Event Loop 详解
列表页的 异步状态 弄完,接下来是登录注册页了
修改 src\unauthenticated-app\index.tsx(新增 error 状态处理,将 error j监听操作 交给 登录注册页):
...
import { Card, Button, Divider, Typography } from "antd";
...
export const UnauthenticatedApp = () => {
...
const [error, setError] = useState<Error | null>(null);
return (
<Container>
...
<ShadowCard>
<Title>{isRegister ? "请注册" : "请登录"}</Title>
{ error ? <Typography.Text type="danger">{error.message}</Typography.Text> : null }
{isRegister ? <Register onError={setError}/> : <Login onError={setError}/>}
<Divider />
...
</ShadowCard>
</Container>
);
};
...
修改 src\unauthenticated-app\login.tsx(传入 onError 并在异步操作后 catch 中使用):
...
export const Login = ({onError}: { onError: (error: Error) => void }) => {
...
const handleSubmit = (values: { username: string; password: string }) => {
login(values).catch(e => onError(e))
};
...
};
...
同理修改 src\unauthenticated-app\register.tsx:
...
export const Register = ({onError}: { onError: (error: Error) => void }) => {
...
const handleSubmit = (values: { username: string; password: string }) => {
register(values).catch(e => onError(e))
};
...
};
...
使用非预设用户名密码检验:没反应。。。但是控制台打印出了刚输入的用户名和密码。。。
通过登录的调用链可以找到 导致这个问题的原因:src\auth-provider.ts
!res.ok时,返回了Promise.reject(data),而data是请求入参,这显然不是预想的效果(注册同理),修改这部分为Promise.reject(await res.json())
修改后再次检验,成了!
Promise.catch 固然好用,但接下来换个思路,使用 try..catch 并引出 Event Loop。
先修改 src\unauthenticated-app\login.tsx 试试水:
...
export const Login = ({onError}: { onError: (error: Error) => void }) => {
...
const handleSubmit = (values: { username: string; password: string }) => {
try {
// login(values).catch(e => onError(e))
login(values);
} catch(e: Error | any) {
onError(e)
}
};
...
};
...
控制台输出正常,但是界面没有效果。。。
问题出在 login 是异步操作,程序中会优先执行同步操作,然后才会异步操作,所以 onError 优先执行,并没有拿到后端返回的报错信息
再次修改 src\unauthenticated-app\login.tsx (使用 async await 处理异步操作):
...
export const Login = ({onError}: { onError: (error: Error) => void }) => {
...
const handleSubmit = async (values: { username: string; password: string }) => {
try {
// login(values).catch(e => onError(e))
await login(values);
} catch(e: Error | any) {
onError(e)
}
};
...
};
...
这样便正常啦!
接下来给注册页新增确认密码功能
修改 src\unauthenticated-app\register.tsx (新增确认密码的 Form.Item 和 相关处理逻辑):
...
export const Register = ({onError}: { onError: (error: Error) => void }) => {
const { register, user } = useAuth();
const handleSubmit = ({ cpassword, ...values }: { username: string, password: string, cpassword: string }) => {
if (cpassword === values.password) {
register(values).catch(e => onError(e));
} else {
onError(new Error('请确认两次的输入密码相同'))
return
}
};
return (
<Form onFinish={handleSubmit}>
<Form.Item
name="username"
rules={[{ required: true, message: "请输入用户名" }]}
>
<Input placeholder="用户名" type="text" id="username" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: "请输入密码" }]}
>
<Input placeholder="密码" type="password" id="password" />
</Form.Item>
<Form.Item
name="cpassword"
rules={[{ required: true, message: "请确认密码" }]}
>
<Input placeholder="确认密码" type="password" id="cpassword" />
</Form.Item>
<Form.Item>
<LongButton htmlType="submit" type="primary">
注册
</LongButton>
</Form.Item>
</Form>
);
};
再接着为 登录注册页 添加异步状态 Loading 的处理:
...
import { useAsync } from "utils/use-async";
export const Login = ({onError}: { onError: (error: Error) => void }) => {
const { login, user } = useAuth();
const { run, isLoading } = useAsync()
const handleSubmit = async (values: { username: string; password: string }) => {
try {
// login(values).catch(e => onError(e))
await run(login(values))
} catch(e: Error | any) {
onError(e)
}
};
return (
<Form onFinish={handleSubmit}>
...
<Form.Item>
<LongButton loading={isLoading} htmlType="submit" type="primary">
登录
</LongButton>
</Form.Item>
</Form>
);
};
...
检验一下,没有效果,但是控制台抛出 400 错误了,排查一下
try..catch中的onError没接收到,唯一的变数就是这个run了- 查看一下,果然报错被
run内部消化了,没有正常抛出(将catch到的error throw或是用Promise.reject包裹返回都是可以的,建议使用后者)
修改 src\utils\use-async.ts:
...
export const useAsync = <D>(initialState?: State<D>) => {
...
// run 来触发异步请求
const run = (promise: Promise<D>) => {
...
return promise
.then(...)
.catch((error) => {
// catch 会消化异常,如果不主动抛出,外面是接收不到异常的
setError(error);
// return error; // 原代码
// throw error;
return Promise.reject(error);
});
};
...
};
检验一下,正常 catch 并 展示报错信息
- try…catch only works for runtime errors (try…catch 只能处理有效代码之中的异常)
- try…catch works synchronously(try…catch 只能处理同步代码之中的异常)
问题是解决了,但这样 try…catch 还是有些拖泥带水的感觉,继续优化:
修改 src\utils\use-async.ts(增加是否抛出异常的配置,来合理化逻辑):
...
const defaultConfig = {
throwOnError: false
}
export const useAsync = <D>(initialState?: State<D>, initialConfig?: typeof defaultConfig) => {
const config = {...defaultConfig, ...initialConfig}
...
// run 来触发异步请求
const run = (promise: Promise<D>) => {
...
return promise
.then((data) => {
setData(data);
return data;
})
.catch((error) => {
// catch 会消化异常,如果不主动抛出,外面是接收不到异常的
setError(error);
return config.throwOnError ? Promise.reject(error) : error;
});
};
...
};
修改 src\unauthenticated-app\login.tsx (传入 { throwOnError: true }):
...
export const Login = ({onError}: { onError: (error: Error) => void }) => {
...
const { run, isLoading } = useAsync(undefined, { throwOnError: true })
...
};
...
同理修改 src\unauthenticated-app\register.tsx :
...
export const Register = ({onError}: { onError: (error: Error) => void }) => {
...
const { run, isLoading } = useAsync(undefined, { throwOnError: true })
const handleSubmit = async ({ cpassword, ...values }: { username: string, password: string, cpassword: string }) => {
if (cpassword === values.password) {
try {
await run(register(values))
} catch (e: Error | any) {
onError(e)
}
} else {
onError(new Error('请确认两次的输入密码相同'))
return
}
};
return (
<Form onFinish={handleSubmit}>
...
<Form.Item>
<LongButton loading={isLoading} htmlType="submit" type="primary">
注册
</LongButton>
</Form.Item>
</Form>
);
};
最后收尾,修改 src\unauthenticated-app\index.tsx (切换登录和注册时,error 清空):
...
export const UnauthenticatedApp = () => {
const [isRegister, setIsRegister] = useState(false);
const [error, setError] = useState<Error | null>(null);
return (
<Container>
...
<ShadowCard>
...
<Button type="link" onClick={() => { setIsRegister(!isRegister); setError(null) }}>
切换到{isRegister ? "已经有账号了?直接登录" : "没有账号?注册新账号"}
</Button>
</ShadowCard>
</Container>
);
};
...
检验效果,完美!
拓展学习(引用自:高薪之路—前端面试精选集-慕课专栏)
js 是单线程的,异步在 js 中是反直觉的存在
判断打印顺序:
console.log('script start')
setTimeout(function(){
console.log('setTimeout');
},0);
new Promise(function(resolve){
console.log('promise1');
resolve();
console.log('promise2');
}).then(function(){
console.log('promise then');
});
console log('script end');
打印顺序:
script start
promise1
promise2
script end
promise then
setTimeout
因为JavaScript中有2种任务:
- 宏任务(macro-task):同步 script(整体代码),setTimeout 回调函数,setlnterval 回调函数,l/O,Ul rendering;
- 微任务(micro-task):process.nextTick,Promise 回调函数, Object.observe,MutationObserver
其执行的顺序是这样的:
- 首先 JavaScript 引擎会执行一个宏任务,注意这个宏任务一般是指主干代码本身,也就是目前的同步代码;
- 执行过程中如果遇到微任务,就把它添加到微任务任务队列中;
- 宏任务执行完成后,立即执行当前微任务队列中的微任务,直到微任务队列被清空;
- 微任务执行完成后,开始执行下一个宏任务;
- 如此循环往复,直到宏任务和微任务被清空。
部分引用笔记还在草稿阶段,敬请期待。。。
本文详细介绍了使用React、Hook和TypeScript构建项目的过程,涵盖了项目初始化、列表实现、强类型使用、JWT鉴权、异步请求处理、CSS-in-JS样式添加以及登录注册页面的加载和错误状态处理。通过实例分析了EventLoop的工作原理,并展示了如何优化用户体验,包括错误捕获和加载状态的显示。此外,还讨论了异步操作的管理,如try...catch与async/await的使用。
4897

被折叠的 条评论
为什么被折叠?



