1.前言
在【技术/前端】基于vite的react项目工程化(1) 中将项目所需要的各种工程化检测添加完成,接着来完善项目中所需要的基础类库,具体包括:
- 1.目录划分与创建
- 2.axios封装
- 3.antd组件库的引入
- 4.react-router路由库引入
- 5.zustand状态管理库引入
- 6.跨域处理
- 7.区分环境
- ...
2.目录划分与创建
为了更好管理代码,需要将特定功能代码进行划分管理
当前模版包括:
- .husky
- node_modules
- public
- src
- api: 后台api
- assets:静态资源
- components:通用组件
- constant: 常量
- config:公用配置
- hook:react的hooks
- layout:布局组件
- pages:具体页面
- router:路由
- store:状态管理
- types:类型文件
- utils:工具函数
- App.tsx:app组件
- main.tsx:文件挂载点
- vite-env.d.ts:vite声明文件
- index.html
- package.json
- pnpm-lock.yaml
- tsconfig.json
- tsconfig.node.json
- vite-plugin-eslint.d.ts
- vite.config.ts
- .editorconfig
- .eslintrc.cjs
- .gitignore
- .prettierrc.cjs
- .stylelintrc.json
- commitlint.config.js:规范git提交信息
- LICENSE
- README.md
3.axios封装
axios 是基于 promise 的 HTTP 客户端,支持发送,拦截,转换请求和响应
在进行http请求时,请求结果中会包含请求响应码, 其分类如下:
首先安装依赖 pnpm install axios
,然后在utils/request.ts
中封装,需要考虑以下问题:
- 1.前后端采用json通信
- 2.请求响应码在200-399之间的都属于正常请求,400-499之间属于请求出错,500-599属于服务器出错
首先创建axios实例,将其配置抽离到constants中
import axios from 'axios'
import { BASE_URL, REQUEST_TIMEOUT, TIMEOUT_ERRORMESSAGE } from '../constant'
const instance = axios.create({
baseURL: BASE_URL,
timeout: REQUEST_TIMEOUT,
timeoutErrorMessage: TIMEOUT_ERRORMESSAGE,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// constant.ts
export const BASE_URL = '/api'
export const REQUEST_TIMEOUT = 8000
export const TIMEOUT_ERRORMESSAGE = '请求超时,请稍后重试'
接着添加请求拦截器
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
// 2.请求拦截器
// 请求拦截器配置
const requestConfig = (config: InternalAxiosRequestConfig) => {
// 2.1 添加loading效果
// 2.2 添加ak用于鉴权
// 返回设置好的配置
return config
}
instance.interceptors.request.use(requestConfig, (error: AxiosError) => Promise.reject(error))
这里就需要封装loading效果,先在public/css/loading.css
中添加以下代码:
#loading {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 20px;
}
#loading .loading {
animation: rotate linear 3s infinite;
}
#loading .loading.spin {
animation: rotate linear 3s infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
接着在index.html
添加loading的div和引入css文件:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<link rel="stylesheet" href="css/loading.css">
</head>
<body>
<div id="root"></div>
<!-- loading效果 -->
<div id="loading" style="display: none">
<svg
t="1682858040467"
class="icon loading"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="7810"
width="64"
height="64"
>
<path
d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 1 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z"
fill="#1677ff"
p-id="7811"
></path>
</svg>
<p>Loading....</p>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
最后将效果实现在utils/loading.ts
:
let count = 0
let loadingTimeoutId: NodeJS.Timeout | number
export const showLoading = () => {
clearTimeout(loadingTimeoutId)
if (count === 0) {
const loading = document.getElementById('loading')
loading?.style.setProperty('display', 'flex')
}
count++
}
export const hideLoading = () => {
count--
if (count === 0) {
loadingTimeoutId = setTimeout(() => {
const loading = document.getElementById('loading')
loading?.style.setProperty('display', 'none')
}, 500)
}
}
然后还需要封装localstorage:
type Storable = string | number | boolean | null | { [key: string]: Storable } | Storable[]
function checkKey(key: string, action: string) {
if (!key) {
throw new Error(`${action}操作失败:缺少key`)
}
}
export default {
set(key: string, value: Storable) {
checkKey(key, 'set')
if (typeof value === 'undefined') {
throw new Error(`${key}值为undefined`)
}
let valueToStore: string
switch (typeof value) {
case 'object':
if (value === null) {
valueToStore = 'null'
} else {
valueToStore = JSON.stringify(value)
}
break
default:
valueToStore = value.toString()
}
localStorage.setItem(key, valueToStore)
},
get<T extends Storable = Storable>(key: string): T | undefined {
checkKey(key, 'get')
const rawValue = localStorage.getItem(key)
if (!rawValue) {
return undefined
}
try {
if (rawValue === 'true') return true as never as T
if (rawValue === 'false') return false as never as T
if (rawValue === 'null') return null as never as T
return JSON.parse(rawValue) as T
} catch (error) {
if (error instanceof SyntaxError) {
return rawValue as never as T
}
throw error
}
},
remove(key: string) {
checkKey(key, 'remove')
localStorage.removeItem(key)
},
clear(): void {
localStorage.clear()
},
getAllKeys(): string[] {
return Object.keys(localStorage)
},
has(key: string) {
checkKey(key, 'has')
return localStorage.getItem(key) !== null
}
}
接着就可以完善axios的请求拦截器了:
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { BASE_URL, REQUEST_TIMEOUT, TIMEOUT_ERRORMESSAGE } from '../constant'
import { showLoading } from './loading'
import storage from './localStorage'
// 1.创建axios实例
const instance = axios.create({
baseURL: BASE_URL,
timeout: REQUEST_TIMEOUT,
timeoutErrorMessage: TIMEOUT_ERRORMESSAGE,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 2.请求拦截器
// 请求拦截器配置
const requestConfig = (config: InternalAxiosRequestConfig) => {
// 2.1 InternalAxiosRequestConfig声明isShowLoading和isShowError
if (config.isShowLoading) showLoading()
// 2.2 添加ak用于鉴权
const token = storage.get('AccessToken')
if (token) config.headers!.Authorization = 'Bearer ' + token
// 返回设置好的配置
return config
}
instance.interceptors.request.use(requestConfig, (error: AxiosError) => Promise.reject(error))
接下来开始写响应拦截器:
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { BASE_URL, CODE_MESSAGE, REQUEST_TIMEOUT, SUCCESSCODE, TIMEOUT_ERRORMESSAGE } from '../constant'
// ...
// 响应拦截器
// 响应拦截器配置
const responseData = async ({ config, data, status, statusText }: AxiosResponse) => {
// 隐藏loading图标
hideLoading()
// 获得状态码
let code: number = data && data['code'] ? data['code'] : status
// 0-200内的状态码全设置成200
if (SUCCESSCODE.includes(data['code'])) code = 200
// 处理不同的状态码
switch (code) {
case 200:
return data
case 401:
// TODO: 刷新令牌
break
}
// 显示错误
if (config.isShowError === true) {
const errorMessage =
data && data['message'] ? data['message'] : CODE_MESSAGE[code] ? CODE_MESSAGE[code] : statusText
// TODO: 应该弹出提醒
console.log(errorMessage)
}
return Promise.reject(data)
}
instance.interceptors.response.use(responseData, (error: AxiosError) => {
return Promise.reject(error.message)
})
接着来导出请求方法:
import { IConfig, Result } from '../types'
// ...
export default {
get<T>(
url: string,
params?: object,
options: IConfig = { isShowLoading: true, isShowError: true }
): Promise<Result<T>> {
return instance.get(url, { params, ...options })
},
post<T>(url: string, params?: object, options: IConfig = { isShowLoading: true, isShowError: true }): Promise<T> {
return instance.post(url, params, options)
}
}
这里导入了2个类型定义:IConfig, Result
// 返回的结果类型
export interface Result<T> {
code: number
data: T
message: string
}
export interface IConfig {
isShowLoading?: boolean
isShowError?: boolean
}
此时可以来测试一下,在App.tsx
中编写测试代码:
import { useEffect } from 'react'
import request from './utils/request'
function App() {
useEffect(() => {
fetchPosts()
}, [])
const fetchPosts = async () => {
const response = await request.post('/user/login', { username: 'user1', password: '111111' })
console.log(response)
}
return <div className='test'>hello</div>
}
export default App
这里的路由是由nestjs生成的,这个以后再详细说,但如果直接运行就会报错
这是因为没有跨域,所以还需要配置一下vite跨域,具体如下:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: 'localhost',
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
// ...
})
可以看到此时使用了import path from 'path'
,会报类型错误,这需要安装对应依赖pnpm i @types/node -D
此时再来运行,结果如下:
这样就能将数据正常请求回来,但这里应该把请求的逻辑放在api文件夹下:
import { Result, userLoginParamType, userLoginResponseType } from '../types'
import request from '../utils/request'
// 用户登陆
export function userLoginApi(params: userLoginParamType): Promise<Result<userLoginResponseType>> {
return request.post('/user/login', params)
}
这里还需要把类型定义抽离到types文件夹中
// 用户登陆参数类型
export interface userLoginParam {
username: string
password: string
}
// 用户登陆返回结果类型
export interface userLoginResponseType {
data?: {
accessToken: string
refreshToken: string
}
}
而App.tsx文件中代码如下:
import { useEffect } from 'react'
import { userLoginApi } from './api'
function App() {
useEffect(() => {
fetchPosts()
}, [])
const fetchPosts = async () => {
const response = await userLoginApi({ username: 'user1', password: '111111' })
console.log(response)
}
return <div className='test'>hello</div>
}
export default App
结果如下:
4.引入antd
在开发过程中使用组件库能极大减少工作量,这里使用antd
首先安装antd: pnpm i antd
, 目前(2024/04/09)版本是^5.16.1
前面封装axios的responseData时候,显示错误是console.log(errorMessage)
可以改成antd的message
// request.ts
// ...
// 3.响应拦截器
// 响应拦截器配置
const responseData = async ({ data, status }: AxiosResponse) => {
// 此时应该是100 - 399之间的状态码
// 隐藏loading图标
hideLoading()
// 获得状态码
const code: number = data && data['code'] ? data['code'] : status
// 处理不同的状态码
switch (code) {
case 200:
return data
default:
return data
}
}
instance.interceptors.response.use(responseData, (error: AxiosError) => {
// 此时应该是400 - 599之间的状态码
// 在此拦截以后,后面使用时就不需要再try/catch了,只需要判断结果是不是undefined
hideLoading()
// 当请求失败且有响应时,处理状态码
if (error.response) {
// 对于400及以上的错误码进行处理
if (error.response.status >= 400) {
// 处理401刷新token
if (error.response.status === 401) {
// TODO:刷新token
}
// 弹窗错误消息
const errorMessage = CODE_MESSAGE[error.response.status] || '未知错误'
message.error(errorMessage)
}
} else {
// 发生了一些意外错误,如请求被阻止、取消或配置错误
message.error(CODE_MESSAGE[500])
}
// 错误在这里处理完成了不用往下传递了
// return Promise.reject(error)
})
//constant/index.ts
export const BASE_URL = '/api'
export const REQUEST_TIMEOUT = 8000
export const TIMEOUT_ERRORMESSAGE = '请求超时,请稍后重试'
export const SUCCESSCODE = [100, 399]
export const CODE_MESSAGE: { [key: number]: string } = {
200: '200 服务器成功返回请求的数据',
201: '201 新建或修改数据成功',
202: '202 一个请求已经进入后台排队(异步任务)',
204: '204 删除数据成功',
400: '400 发出的请求有错误',
401: '401 用户没有权限(令牌、用户名、密码错误)',
403: '403 用户得到授权,但是访问是被禁止的',
404: '404 发出的请求不存在,服务器没有进行操作',
406: '406 请求的格式不可得',
410: '410 请求的资源被永久删除,且不会再得到的',
422: '422 当创建一个对象时,发生一个验证错误',
500: '500 服务器发生错误,请检查服务器',
502: '502 网关错误',
503: '503 服务不可用,服务器暂时过载或维护',
504: '504 网关超时'
}
这里对响应拦截器重新进行了设计,当状态码为100-399之间时,代码会进入responseData,当状态码为400-599之间时,代码会进入后面的错误回调函数,将错误在此拦截后,后面使用时就不需要再try/catch了,只需要判断结果是不是undefined即可
接着来测试一下是否好用,先把后端服务器直接停掉,此时状态码必然是500,就不会往后传递了
重新启动后端服务器,但把访问的路径改错,效果如下:
import { Result, userLoginParamType, userLoginResponseType } from '../types'
import request from '../utils/request'
// 用户登陆
export function userLoginApi(params: userLoginParamType): Promise<Result<userLoginResponseType>> {
return request.post('/user/loginx', params)
}
再把路径改对,结果如下:
5.引入react-router
对于单页面应用,路由是实现切换不同页面的必要组件,本模版使用
react-router
首先安装依赖 pnpm i react-router-dom
,当前(2024/04/09)版本是"react-dom": "^18.2.0","react-router-dom": "^6.22.3"
接着就可以去引入了,在路由系统中,以下页面是基本页面,应创建在 src/pages
目录中:
- 欢迎页 :向用户呈现应用首页。
- 登录页面:为用户提供输入凭证以访问特定资源的界面。
- 403页面:当正在访问的用户没有足够权限时,展示此页面。
- 404页面:用户尝试访问不存在的路由时,展示此页面。
欢迎页面 (src/pages/Welcome/Welcome.tsx):
export function Welcome() {
return <div>欢迎页</div>;
}
登录页面 (src/pages/Login/Login.tsx):
export function Login() {
return <div>登录页</div>;
}
权限不足页面 (src/pages/403.tsx):
export default function Error403() {
return <div>403未授权</div>
}
页面未找到 (src/pages/404.tsx):
export default function Error404() {
return <div>404页面未找到</div>
}
接着在src/router/index.tsx
文件中定义 routes
配置数组,其中包含每个路由的路径和对应的页面组件。
import { Navigate, createBrowserRouter } from 'react-router-dom'
import Error403 from '@/pages/403.tsx'
import Error404 from '@/pages/404.tsx'
import { Welcome } from '@/pages/Welcome/Welcome.tsx'
import { Login } from 'src/pages/Login/Login'
const routes = [
{
path: '/',
element: <Navigate to='/welcome' />
},
{
path: '/welcome',
element: <Welcome />
},
{
path: '/login',
element: <Login />
},
{
path: '/403',
element: <Error403 />
},
{
path: '/404',
element: <Error404 />
},
{
path: '*',
element: <Navigate to={'/404'} />
}
]
export default createBrowserRouter(routes)
此时引入时报错,是因为别名@的类型没有匹配上:
// vite.config.ts
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
// ...
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
// ...
})
// tsconfig.json
{
// ...
"compilerOptions": {
/* 和vite中的alias配合,避免报类型错误 */
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
}
}
此时就正常了,有了路由配置文件之后,需要将其集成到React应用中。这是通过在 App.tsx
文件中导入RouterProvider
组件完成的。
import { RouterProvider } from 'react-router-dom'
import router from './router'
function App() {
return <RouterProvider router={router} />
}
export default App
此时访问一下: http://localhost:5173/welcome 一切正常
最后,为了提高用户体验,可以使用Ant Design的React UI库中的Result组件来优化403和404页面。
import { Button, Result } from 'antd'
import { useNavigate } from 'react-router-dom'
export default function Error403() {
const navigate = useNavigate()
const handleClick = () => {
navigate('/')
}
return (
<Result
status='403'
title='403'
subTitle='抱歉,您无权访问这个页面。'
extra={
<Button type='primary' onClick={handleClick}>
回到首页
</Button>
}
/>
)
}
import { Button, Result } from 'antd'
import { useNavigate } from 'react-router-dom'
export default function Error404() {
const navigate = useNavigate()
const handleClick = () => {
navigate('/')
}
return (
<Result
status='404'
title='404'
subTitle='抱歉,您访问的页面不存在。'
extra={
<Button type='primary' onClick={handleClick}>
回到首页
</Button>
}
/>
)
}
6.引入zustand
zustand 是一个简洁的状态管理库
首先安装依赖 pnpm i zustand
,当前(2024/04/09)版本为"zustand": "^4.5.2"
接着在store
目录下,创建user.ts
和index.ts
文件,index.ts
的代码只需将useUserStore
从user.ts
中导出即可:
export { useUserStore } from './user'
在user.ts
文件中,创建用户状态管理的核心代码:
// 引入zustand库和Immer中间件
import { create } from 'zustand'
type UserInfo = {
username: string
avatar: string
}
type Action = {
updateUserInfo?: (userInfo: UserInfo) => void
updateUserName: (username: string) => void
}
interface State {
userInfo: UserInfo
}
export const useUserStore = create<State & Action>()(
set => ({
userInfo: { username: '默认用户名', avatar: 'http://xxxx.com/yy.jpg' },
updateUserName: username =>
set(state => ({
...state, // 再次使用展开运算符复制所有既有状态
userInfo: {
...state.userInfo, // 复制 userInfo 对象内的其他属性
username: username // 只更新 username 属性
}
}))
})
)
接着在welcome
组件调用:
import { useUserStore } from '@/store'
export const Welcome = () => {
const { userInfo, updateUserName } = useUserStore()
const updateUserInfo = () => {
updateUserName('修改后的用户名')
}
return (
<div>
欢迎页{userInfo.username}
<button onClick={updateUserInfo}>更改用户信息</button>
</div>
)
}
7.区分开发环境
在dev/prod/stag环境下,配置的参数和资源不同,需要进行拆分
首先在项目根目录中创建三个文件,分别对应开发环境(.env.development
)、生产环境(.env.production
)和预发布环境(.env.stag
)。
NODE_ENV
:该变量用于设置当前的环境模式,例如开发模式(development)或生产模式(production)。VITE_BASE_API
服务器接口地址,根据不同的环境设置不同的url。
三个文件内容如下:
# .env.production
# 设置NODE_ENV环境模式
NODE_ENV=production
# 接口API地址
VITE_BASE_API=/api
# .env.development
# 设置NODE_ENV环境模式
NODE_ENV=development
# 接口API地址
VITE_BASE_API=http://localhost:5173/api
# .env.stag
# 设置NODE_ENV环境模式
NODE_ENV=stag
# 接口API地址
VITE_BASE_API = /api
接着所有的配置内容全放在src/config
文件夹,在config目录中创建3个文件:index.ts(集合了后面两个配置)
、axios.config.ts(axios配置)
、app.config.ts(应用级配置)
// index.config.ts(整合配置)
import axiosConfig from './axios.config.ts'
import appConfig from './app.config.ts'
export default {
...appConfig,
...axiosConfig
}
// app.config.ts(应用级配置)
export default {
// 应用的路由白名单,白名单内的路由可以在未登录的情况下访问
routeWhiteList: ['/login', '/register', '/callback', '/404', '/403']
}
// axios.config.ts(axios配置)
import { REQUEST_TIMEOUT, SUCCESSCODE } from '@/constant'
export default {
// API接口地址
baseURL: import.meta.env.VITE_BASE_API,
// 网络请求的超时时间
requestTimeout: REQUEST_TIMEOUT,
// 请求超时时的错误信息
timeoutErrorMessage: '请求超时,请稍后重试',
// 请求成功时服务端返回的状态码
successCode: SUCCESSCODE
}
现在就可以将request.ts/request.ts
配置抽离出来:
// ...
import axiosConfig from '@/config/axios.config'
// 1.创建axios实例
const instance = axios.create({
baseURL: axiosConfig.baseURL,
timeout: axiosConfig.requestTimeout,
timeoutErrorMessage: axiosConfig.timeoutErrorMessage,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// ...
同时为了验证axios请求没有问题,将原来App.tsx的请求内容写在welcome.tsx中:
import { userLoginApi } from '@/api'
import { useUserStore } from '@/store'
import { useEffect } from 'react'
export const Welcome = () => {
const { userInfo, updateUserName } = useUserStore()
const updateUserInfo = () => {
updateUserName('修改后的用户名')
}
useEffect(() => {
fetchPosts()
}, [])
const fetchPosts = async () => {
const response = await userLoginApi({ username: 'user1', password: '111111' })
console.log(response)
}
return (
<div>
欢迎页:{userInfo.username}
<button onClick={updateUserInfo}>更改用户信息</button>
</div>
)
}
结果如下: