Hooks + TS 搭建一个任务管理系统(一)-- 登录注册页面
📢 大家好,我是小丞同学,一名大二的前端爱好者
📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
📢 愿你忠于自己,热爱生活
💌 前言
这篇文章是这个专栏中的第一篇文章,因此就写点前言吧~,简单的介绍一下吧
最近刚学完 React 的一些基本内容,教学视频已经看完了,然后也学习了一下 TS 这门强类型的语言,对前端开发简直就是利器。同时也了解了一下 Hooks 的一些内容,但是对这部分掌握的不是很好,因此跟着视频利用 Hooks + TS4 + Router6
做了一个任务管理系统练练手。在做这个 hooks 的项目之前,也有跟着做过一个基于 React 16.4 版本 + Redux
实现的简书博客平台,对 Redux
也有一定的了解。
扯这么多,来说说这个项目吧!
这个项目是跟着视频做的并不是完全由我创新的 😥,因此如果文章有侵权行为的话,麻烦联系一下删除(应该不会吧,毕竟文章是我自己写的)
这个项目采用的技术栈是 React Hooks + TS4
主要实现的功能有 :用户登录注册,项目列表的展示,项目的 CRUD,项目详情展示,看板及任务组管理…
接下来的系列更文,将会围绕实现这些功能,以及在项目中遇到的难题,提出一些问题和解决方案。
强迫自己开启这个专栏是想要更加深入的理解,自己写的代码是什么意思,能够如何优化,了解更多代码上的细节,而不是跟着老师敲完代码就算了…,因此这个专栏会尽我所能将知识点囊括齐全!
高能预警:本项目采用了很多的 custom hook
,真的非常不错
下面开始今天的主题,实现登录注册页面
一、用状态驱动页面更新
为什么第一个要讲“用状态驱动页面更新”呢?
我们需要通过当前的登录状态,来展示不一样的页面。通过状态来做很多的事情…
首先我们需要通过 useState
,来创建两个状态,一个是 isRegister
用来标识是展示登录界面还是注册界面,当 isRegister
为 true
时展示注册页面
第二个状态是错误状态,用来接收登录页面的错误信息,当有错误发生时,都会丢到这个变量当中
// 标识当前是注册还是登录,false 表示当前是登录状态
const [isRegister, setIsRegister] = useState(false);
const [error, setError] = useState<Error | null>(null)
在上面的两行代码中,值得注意的是,通过 useState
创建的变量类型默认会是初始化时的类型
也就是说 isRegister
的类型会因为我们初始化时传的 false
变成 boolean
类型
而对于 error
而已,在不加泛型的情况下,它默认会是 null
类型,因此,在后面对它赋值 Error
对象类型时,会发生错误,因此在这里我们需要定义泛型 Error | null
这样 error
就能接收 Error
类型了~
现在我们的状态设置好了,接下来看看如何驱动页面更新呢,那一个例子讲讲
<Button type={'link'} onClick={() => setIsRegister(!isRegister)}>{isRegister ? '已经有账号了?直接登录' : '没有账号?注册新账号'}</Button>
这个是登录和注册切换的按钮,当点击这个按钮时,会触发 setIsRegister
改变 isRegister
的值,我们通过这个值的 true or false
来判断展示的内容
{/* 判断展示登录页面还是注册页面 */}
{
isRegister ? <RegisterScreen onError={setError} /> : <LoginScreen onError={setError} />
}
当为 true
的时候展示注册页面,在这里我们将两个页面抽象出了两个组件,将逻辑分开来,我们通过 props
向这两个组件传递了 onError
方法,在组件中可以通过调用这个方法来设置 error
状态的值,再展示到页面上
在这里值得我们注意的是,和类式组件不同,函数式组件会默认的接收 props
参数,因此我们不需要显式的去使用 props
我们可以直接在参数列表中解构出来,这样我们整个项目开发完成都不会见到一个 props
二、通过 Antd 布局页面
关于布局方面采用的是 flex
布局,主要是通过 Antd 组件来实现的
<ShadowCard>
<Title>
{
isRegister ? '请注册' : "请登录"
}
</Title>
<ErrorBox error={error} />
{/* 判断展示登录页面还是注册页面 */}
{
isRegister ? <RegisterScreen onError={setError} /> : <LoginScreen onError={setError} />
}
<Divider />
{/* 点击切换状态 */}
<Button type={'link'} onClick={() => setIsRegister(!isRegister)}>{isRegister ? '已经有账号了?直接登录' : '没有账号?注册新账号'}</Button>
</ShadowCard>
这里的 ShadowCard
其实是对 Antd
中的 Card
组件进行了加工,让它有了一些阴影,同时对它进行了一定的布局
// 组件加样式,给Card组件更改样式
const ShadowCard = styled(Card)`
width: 40rem;
min-height: 56rem;
padding: 3.2rem 4rem;
box-shadow: rgba(0,0,0,0.1) 0 0 10px;
text-align: center;
`
在 emotion
中,想要个 Antd 组件添加样式,我们只需要用 styled(组件名)
即可
对于登录和注册页面,采用的是 Antd 中的 Form
表单实现的,在控制好盒子大小后,基本不需要过多的布局
<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>
<LongButton loading={isLoading} htmlType={"submit"} type={"primary"}>登录</LongButton>
</Form>
对于登录注册的关键就是,通过前台认证之后,发送请求开启认证即可,关键就在于这个认证如何实现,当然如果只是简单的发请求是非常简单的,但是往后想想,我们会有很多个请求,如果我们每次都写一遍那串代码,那代码的冗余程度可想而知。
因此我们想在这里抽象出两个 custom hook
,一个用来获取数据,一个用来处理异步请求,写这两个之前,我们先写一个专门用来发送请求的文件,我们将我们关于登录注册的请求全部写在这个文件当中,再暴露出去,这样代码看起来思路更加清晰
三、编写 auth-provider 文件
我们在这个文件中来处理我们需要发送的相关请求,首先,由于我们需要实现刷新后仍保持登录状态的效果,我们需要设置 token
,并且对于 token
数据我们是放在本地存储当中的
// 保存本地存储中的 token 键
const localStorageKey = "__auth_provider_token__";
// 获取 token 的值
export const getToken = () => window.localStorage.getItem(localStorageKey);
通过封装一个函数用来获取我们本地的 token
值
export const handleUserResponse = ({ user }: { user: User }) => {
window.localStorage.setItem(localStorageKey, user.token || "");
return user;
};
通过这个函数来设置本地 token
,在登录注册后调用
处理登录请求
export const login = (data: { username: string; password: string }) => {
return fetch(`${apiUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(async (response) => {
if (response.ok) {
return handleUserResponse(await response.json());
} else {
throw Promise.reject(await response.json());
}
});
};
当我们在其他文件中调用这个 login
时就会返回这个 fetch
能够发送登录的请求,当成功返回结果时,就会调用前面的函数来设置一个本地的 token
值,用来保存用户的登录状态
这里有个比较重要的点:由于我们的请求都是异步的因此我们在 then
中需要采用 async await
的方式,优雅的解决这个由于异步造成的 undefined 的问题,对于其他注册和登出的请求也是如此
在编写好几个请求函数之后,我们需要编写一个 useAsync
函数用来专门处理异步请求
四、编写 useAsync 发送异步请求
我们已经能够发送请求获取登录信息了,为什么我们还需要再编写一个这样的 custom hook
呢?
首先,我们在上面确实是能够满足我们最基本的业务需求了,我们编写这个 custom hook
能够帮我们将这个异步函数给具体化,什么是具体化呢?
我们先来看看这个 custom hook
返回的结果
return {
// 请求状态
isIdle: state.stat === 'idle',
isLoading: state.stat === 'loading',
isError: state.stat === 'error',
isSuccess: state.stat === 'success',
// run 接收一个promise 对象,返回执行结果
run,
setData,
setError,
// retry 被调用重新执行 run,让state 更新
retry,
...state
}
看到这些返回的结果,相信已经有了一定的想法,我们可以通过这个 hook
来直视到异步函数的执行过程,而且又能将过程抽象在这个 hook 当中,在外部,我们只需要 run
一下,就能得到结果,这不正是我们想要的吗?
我们不想关注异步的细节,什么 then
啊,async
啊,这些我们都不想关心,我们想要的是,执行后的结果,因此这个 hook 需要帮我们解决这些问题!这在优化我们代码中起着非常重要的作用
对于这个 hook 的实现,比较复杂,类型复杂,
interface State<D> {
error: Error | null;
// 返回的数据
data: D | null;
// 请求过程状态
stat: 'idle' | 'loading' | 'error' | 'success'
}
首先我们定义初始化状态的接口
初始化我们的初始状态
// 初始状态
const defaultInitialState: State<null> = {
stat: 'idle',
data: null,
error: null
}
我们先写一个 hook 来帮我们判断组件是否卸载
// 用这个dispatch 会帮我们判断 mountedRef 组件是否被卸载
const useSafeDispatch = <T>(dispatch: (...args: T[]) => void) => {
const mountedRef = useMountedRef()
return useCallback((...args: T[]) => (mountedRef.current ? dispatch(...args) : void 0), [dispatch, mountedRef])
}
当我们使用这个 hook 时,将会接收到当前组件的状态,当组件被卸载后,我们就不需要再将数据返回了,如果返回的话,就会造成数据无法渲染的情况从而报错,因此,我们编写这个 hook 也是出于这样的考虑
我们通过监听 safeDispatch
的变化来该判断当前的状态,同时我们可以通过 setData
来传递返回的数据,再通过 safeDispatch
来发送 dispatch
设置响应
const safeDispatch = useSafeDispatch(dispatch)
// 正常响应时的数据处理
const setData = useCallback((data: D) => safeDispatch({
data,
stat: 'success',
error: null
}), [safeDispatch])
// 发生错误时的错误处理
const setError = useCallback((error: Error) => safeDispatch({
error,
stat: 'error',
data: null
}), [safeDispatch])
当然还有一些其他的状态也需要这样编写,基本一致
在这里我们开始编写我们的 run
函数,这个函数是主入口,用于触发异步请求,首先从我们的调用上来看 run(login(values))
我们只想传递一个 promise
对象就能获得所有的结果,
首先我们需要先判断一下,传入的对象是不是 promise
对象,如果不是则直接抛出错误
当进入 run
函数后,我们需要将 stat
状态置为 loading
状态,这样我们可以通过这个值来实现请求 loading 的效果,
最后我们返回一个 promise
对象的执行结果,在这个返回当中有很多值得探讨的地方
为了获取到传入的 promise
对象抛出的错误,我需要使用 then
中的第二个参数来接收这 错误对象,再返回这个错误,才能使用 catch
获取,正常情况下,catch
获取不到这个错误
// run是主入口,触发异步请求
// 采用useCallback,只有依赖中的数据发生变化的时候,run才会被重新定义
const run = useCallback((promise: Promise<D>, runConfig?: { retry: () => Promise<D> }) => {
// 如果传入的不是 promise,直接 throw
if (!promise || !promise.then) {
throw new Error('请传入 Promise 类型数据')
}
// 定义重新刷新一次,返回一个有上一次 run 执行时的函数
setRetry(() => () => {
if (runConfig?.retry) {
run(runConfig?.retry(), runConfig)
}
})
// 如果是 promise 则设置状态,开始 loading
safeDispatch({ stat: 'loading' })
// 返回一个promise对象处理数据
return promise
.then(data => {
// 成功则处理stat
// 判断组件状态
setData(data)
return data
}, async (err) => {
// 接收到扔来的错误,再扔一下
return Promise.reject(await err)
})
.catch(error => {
// 错误抛出了,但是接不住
setError(error)
if (config.throwOnError) {
return Promise.reject(error)
}
return Promise.reject(error)
})
}, [config.throwOnError, safeDispatch, setData, setError])
在这个 hook
中有太多值得我们学习的地方
首先当我们的 custom hook
返回的值是一个函数时,我们最好用 useCallback
来包一下,这样能解决无限循环的问题
在我们的请求当中需要对异步情况做出特别的处理,利用 async
来解决这些问题
对于数据的类型,需要我们对泛型有很清晰的认识
五、编写 useAuth 获取用户信息
在编写好 useAsync
hook 后,我们需要 通过 useAuth
来获取用户的信息,主要是依赖于 useAsync
,这也能体现出 useAsync
的巨大威力
在这个 custom hook
当中,我们会采用 useAsync
暴露的方法,同时也会采用到 react-query
处理缓存,利用 context
来实现数据共享
export const useAuth = () => {
// 由于在使用 context 时,需要在子节点中声明一下这个 context
const context = React.useContext(AuthContext)
// 如果这个 context 不存在
if (!context) {
throw new Error('useAuth必须在 context 中使用')
}
// 返回这个 context 数据中心
return context
}
当我们调用这个 hook 的时候,就会返回这个 context 对象 ,AuthContext
,当然不会这么简单,关键在于我们如何将这些数据存储在 context
当中
我们编写一个 AuthProvider
方法
export const AuthProvider = ({ children }: { children: ReactNode }) => {
// 设置一个user变量 ,由于user 的类型由初始化的类型而定,但不能是 null ,我们需要进行类型断言
// const [user, setUser] = useState<User | null>(null)
const { data: user, error, isLoading, isIdle, isError, run, setData: setUser } = useAsync<User | null>()
const queryClient = useQueryClient()
// 设置三个函数 登录 注册 登出
// setUser 是一个简写的方式 原先是:user => setUser(user)
const login = (form: AuthForm) => auth.login(form).then(setUser)
const register = (form: AuthForm) => auth.register(form).then(setUser)
const logout = () => auth.logout().then(() => {
setUser(null)
// 清除数据缓存
queryClient.clear()
})
// 当组件挂载时,初始化 user
useMount(() => {
run(bootstrapUser())
})
// 当初始化和加载中的时候显示loading
if (isIdle || isLoading) {
return <FullPageLoading />
}
if (isError) {
return <FullPageErrorFallback error={error} />
}
// 返回一个 context 容器
return <AuthContext.Provider children={children} value={{ user, login, logout, register }} />
}
当我们这个方法返回了一个 provider
容器,这需要我们对 context
有一定的了解,我们需要使用 provider
来包裹数据共享的范围,只有在这个范围内的元素才能使用这些数据
这里的意思是,所有的子元素都能够使用这个 context
容器 ,我们在使用的时候
<AuthProvider>
{children}
</AuthProvider>
这样所有的子元素都能共享它的 context
容器
接下来我们看看这个函数都写了什么,首先我们调用 useAsync
解构出了它的部分返回结果,这些都是我们后面可能会用到的
在这里我们对当前的状态进行了判断
// 当初始化和加载中的时候显示loading
if (isIdle || isLoading) {
return <FullPageLoading />
}
if (isError) {
return <FullPageErrorFallback error={error} />
}
当状态为 loading
时我们展示一个加载框,当 error
时,展示一个错误提示框
// 当组件挂载时,初始化 user
useMount(() => {
run(bootstrapUser())
})
在组件刚挂载时,我们先检查是否存在 token
如果有,我们就对他进行自动登录
// 保持用户登录状态,在组件挂载的时候就调用
const bootstrapUser = async () => {
let user = null
// 从本地取出 token
const token = auth.getToken()
if (token) {
// 如果有值,就去发送请求获得 user 信息
const data = await http('me', { token })
user = data.user
}
// 返回 user
return user
}
同时我们还将 auth-provider
中编写的三个方法,一同存放到了 context
容器当中,这样我们可以在外部调用
<AuthContext.Provider children={children} value={{ user, login, logout, register }} />
这里的 value 设置的就是它的 context 容器中的值
通过编写这个 custom hook 我们对 useAsync
有了更好的理解,同时也学会了如何使用 context
来进行数据的共享
六、按钮触发函数执行
在编写完了前面的几个 custom hook
之后,我们已经将数据接口转到了 context
当中,因此我们在调用里面的内容时,只需要调用 useAuth
来解构出对应的数据即可
// login.tsx
const { login } = useAuth()
// 采用 useAsync 来封装异步请求,添加loading
const { run, isLoading } = useAsync(undefined, { throwOnError: true })
我们得到了 login 函数,同时也得到了 isLoading
的状态
当表单提交时,会触发 Form
组件中的 onFinish
事件,我们给他绑定了一个 handleSubmit
方法,用于发送请求
const handleSubmit = async (values: { username: string, password: string }) => {
// 采用 antd 组件库后代码优化
// 这里的catch 会捕获错误,调用 onError 这个函数相当于是 error => onError(error)
// 由于在index中传入的props是,onError={setError} 因此就相当于 setError(error)
run(login(values)).catch(onError)
}
就这样我们就能够成功的发送请求,并且返回结果,当有错误发生时,会触发 catch
中的 onError
设置 index
中的 error
状态,显示在页面当中
📌 总结
在这个登录注册页面当中,我们可以学到以下几点
- context 状态管理
- custom hook 在 react 中的强大威力
- 当 custom hook 返回函数时,需要使用 useCallback 包裹
- 多利用解构赋值,来优化代码
- useState 设置的变量,类型会跟随初始值的类型
- 对于不同的事务,我们最好能分离出来写,这样我们的主文件思路会非常清晰
- 利用 CSS in JS 解决样式混乱的问题
最后,可能在很多地方讲诉的不够清晰,请见谅
💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
- 点赞
- 收藏
- 关注作者
评论(0)