最近跟着黑马学习了一个项目,他采用传统的javascript的写法,当然我是理解项目的基础上用tpescript语法改写了极客园项目,采取了react+typescript+redux+antd实现的一个项目,本文接下来会详细讲解每一步,包括核心思想以及实现流程,从0开始手把手教你做极客园项目,总体篇幅较长,所以本文是先讲解了登录功能的实现源码地址极客园项目github
使用react脚手架快速创建项目
npx create-react-app jikeyuan --template typescript
右击项目的文件夹,在终端中打开输入以下命令安装项目相关的依赖
1.sass:用于在React项目中使用Sass(Syntactically Awesome Stylesheets)进行样式编写。Sass是一种CSS预处理器,它扩展了CSS的功能,并提供了更多的灵活性和可重用性。
2.react-redux:React-Redux是一个用于在React应用中管理全局状态的库。它提供了一种将Redux状态管理与React组件结合使用的方式,使得在React应用中更容易管理和共享状态。
3.@craco/craco:Craco是一个用于扩展Create React App(CRA)配置的工具。它允许开发人员在不弹出CRA配置的情况下,自定义和扩展React应用的构建和开发配置。
4.@reduxjs/toolkit:Redux Toolkit是一个官方维护的Redux工具集,旨在简化和提高Redux的开发效率。它提供了一些实用的函数和工具,使得在Redux应用中编写和管理状态更加简单和直观。
5.axios:Axios是一个基于Promise的HTTP客户端,用于在浏览器和Node.js中进行HTTP请求。它提供了一种简洁而强大的方式来处理异步请求,并支持拦截器、取消请求等功能。
6.antd:Ant Design是一个流行的React UI组件库,提供了一套美观、易用且高度可定制的UI组件,可以帮助开发人员快速构建用户界面。
7.react-router-dom:React Router是一个用于在React应用中进行路由管理的库。react-router-dom是React Router的DOM绑定版本,它提供了一些用于创建和管理路由的组件和API,使得在React应用中实现路由功能更加简单和灵活。
npm install sass react-redux @craco/craco @reduxjs/toolkit axios antd react-router-dom
apis:放置接口
assets:存放静态资源
components:通用的组件
pages:页面的组件
router:路由
store:redux状态
util:封装函数
在根目录下创建一个craco.config.js文件,配置别名路径将src/替换为@/
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src')
},
}
};
在tsconfig.json的"compilerOptions"中添加
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
},
在package.json文件中script中用craco替代
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},
在pages里创建两个文件夹Layout和Login,每个文件夹包括一个index.tsx文件,输入代码rafce回车快速初始化组件,然后在路由文件里创建一个index.tsx文件,表示路由路径组件
import { createBrowserRouter } from "react-router-dom";
import Layout from "@/pages/Layout";
import Login from "@/pages/Login";
const router=createBrowserRouter([
{
path:'/',
element:<Layout/>
},
{
path:'/login',
element:<Login/>
}
])
export default router
在根组件index.tsx中修改代码,将路由配置到入口文件挂载
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import router from './router';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<RouterProvider router={router}/>
);
使用antd的 Card, Form, Input, Button组件,配合scss样式
import './index.scss'
import { Card, Form, Input, Button} from 'antd'
import logo from '@/assets/logo.png'
const Login = () => {
return (
<div className="login">
<Card className="login-container">
<img className="login-logo" src={logo} alt="" />
{/* 登录表单 */}
<Form>
<Form.Item>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item>
<Input size="large" placeholder="请输入验证码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login
查阅antd的item组件的文档,在item上添加name属性和rule属性,name和后端接口数据的字段名保持相同,rule则是添加校验规则,required代表这项是否需要必填,message表示输出错误的信息,这里判断输入为空和一个正则表达式来判断手机号是否符合格式
<Form.Item name="mobile"
rules={[{ required: true, message: '手机号不能为空' }, { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="code"
rules={[{ required: true, message: '验证码不能为空' }]}>
<Input size="large" placeholder="请输入验证码" />
</Form.Item>
form表单上添加一个属性onfinish,表示点击登录后获取表单里的输入信息,添加onFinish函数,在控制台测试是否能够收集
<Form onFinish={onFinish}>
export interface logindata {
moblie: string,
code: string
}
const Login = () => {
const onFinish=(values:logindata)=>{
console.log(values)
}
项目中会发送很多请求,为了方便统一管理和复用,所以封装请求会简化代码量,在utils里创建request.tsx
封装包括1.根域名配置,2.超时时间,3.请求拦截器和响应拦截器
//axios封装处理
//1.根域名配置,2.超时时间,3.请求拦截器/响应拦截器
import axios from 'axios'
const request =axios.create({
baseURL:'http://geek.itheima.net/v1_0',//根域名
timeout:5000,//超时时间五秒
})
request.interceptors.request.use((config)=> {
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
request.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export{request}
在utils里添加一个中转导出index.tsx文件,所有utils里的工具函数都导入进index.tsx文件和导出
import { request } from "./request"
export {
request,
}
token用于标识用的身份和权限,当用户登录后,后端会返回一个token用于前端识别用户,在store文件夹创建一个hook.tsx文件和一个index.tsx文件和一个modules文件夹,modules文件夹里创建一个user.tsx文件用于管理用户登录状态
hook.tsx
导出自定义的钩子函数useAppDispatch和useAppSelector,对useDispatch和useSelector的封装
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
index.tsx
创建和配置Redux store,并导出RootState和AppDispatch类型
import { configureStore } from "@reduxjs/toolkit";
import userReducer from './module/user'
const store=configureStore(
{
reducer:{
user:userReducer
}
}
)
export type RootState = ReturnType<typeof store.getState>
// 推断出类型: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
export default store
user.tsx
创建和配置一个名为user的Redux模块,定义了一个同步的reducer函数setToken用于修改user模块的token状态
import { logindata } from "@/pages/Login";
import { request } from "@/utils";
import { Dispatch, createSlice } from "@reduxjs/toolkit";
const userStore = createSlice({
name: 'user',
initialState: {
token: ''
},
reducers: {
setToken(state, action) {
state.token = action.payload
}//同步修改
}
})
const { setToken } = userStore.actions
const userReducer = userStore.reducer
export { setToken}
export default userReducer
配置好完成后,在根组件index.tsx文件中把状态添加上包裹路由出口实现状态管理
root.render(
<Provider store={store}>
<RouterProvider router={router}/>
</Provider>
);
在user.tsx文件中添加一个异步函数获取token
const fetchLogin: (loginForm: logindata) => (dispatch: Dispatch) => Promise<void> = (loginForm) => {
return async (dispatch: Dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setToken(res.data.token))
}
}
添加导出
export { setToken, fetchLogin }
在Login的index.tsx函数中使用useAppDispatch函数调用状态里的异步方法,并使用useNavigate实现登录成功跳转界面
const navigate = useNavigate()
const dispatch = useAppDispatch()
const onFinish = async (values: logindata) => {
await dispatch(fetchLogin(values))
navigate('/')
message.success('登录成功')
};
可以看到在使用正确的手机号和验证码登录后,发送的请求报文包括我们表单输入的数据,响应报文中包括返回的token,并且在redux里也成功存入了token
上述功能虽然能够在用户登录后根据token来确定用户身份,但是刷新界面后,会发现redux里的token变成了空串,这是因为我们刷新时,token回到了最初的状态,要想token刷新时仍然存在,使用localstorage技术在本地添加缓存可以有效实现token的持久化,在utils工具函数内封装一个token.tsx文件,用于token在localstorage的存取删方法
const TOKENKEY = 'token_key'
function setToken(token: string) {
localStorage.setItem(TOKENKEY, token)/键、值
}
function getToken() {
return localStorage.getItem(TOKENKEY)
}
function removeToken() {
localStorage.removeItem(TOKENKEY)
}
export { setToken, getToken, removeToken }
同样的,在中转导出index.tsx里添加
//中转导出
import { request } from "./request";
import { getToken, removeToken, setToken } from "./token";
export {
request,
getToken,
setToken,
removeToken
}
在user.tsx里重新设置状态,当localstorage里有token时拿localstorage里的token,没有则设为空串,并且在同步存入状态里添加同步存入token到localstorage里
import { setToken as _setToken, getToken } from "@/utils";
const userStore = createSlice({
name: 'user',
initialState: {
token: getToken() || ''
},
reducers: {
setToken(state, action) {
state.token = action.payload
_setToken(action.payload)
}
}
})
经过测试,页面刷新后可以正常获取到token到redux的状态中
在axios请求头注入token后,向后端发送数据时就会携带token用于识别当前用户,在request.tsx里的请求拦截器添加请求头携带token
request.interceptors.request.use((config)=> {
const token=getToken()
if(token){
config.headers.Authorization=`Bearer ${token}`//根据后端要求拼接
}
return config
}, (error)=> {
return Promise.reject(error)
})
利用高阶组件HOC component判断组件是否有token,有则跳转到路由组件,没有则返回登录界面
在components里添加一个AuthRouter.tsx文件
import { getToken } from "@/utils";
import { ReactNode } from "react";
import { Navigate } from "react-router-dom";
interface AuthRouterProps{
children: ReactNode;
};
function AuthRouter({ children }: AuthRouterProps) {
const token = getToken()
if (token) {
return <>{children}</>
} else {
return <Navigate to={'/login'} replace />//重定向到login
}
}
export default AuthRouter
在router里用高阶组件包裹路由组件
{
path:'/',
element:<AuthRouter><Layout/></AuthRouter>
},
当我们把localstorage里的token删除时,意味着还没有获取用户身份,此刻刷新界面时,根据重定向到登录界面
登录界面包括的逻辑功能很多,包括封装函数,拦截器,redux管理状态和localstorage本地缓存存储token,以及高阶组件和重定向,几乎适用于所有登录功能的实现,后续会根据学习进度继续更新后续的极客园内容,希望大家点点赞,点点关注