1.背景
为什么要是用vite来创建react项目,主要是cra已经不太适配react了,react官网也放弃了cra的创建,原因是react19和cra不太兼容。所以如果大家用的是18的话,还是可以继续用cra来创建的。参考官网
另外的话用vite默认创建的话就是不支持服务端渲染(SSR),没有seo(适合toB项目),如果项目要支持seo的话,需要配置,不如建议大家使用nextjs ,或者remix来创建项目(适合toC项目)
2.创建命令
vue
npm create vite@latest
下面跟着选就行了,输入项目名,选择react+ts(ts是否要看大家自己的选择,建议还是选上,ts是趋势。小项目或者时间紧的话可以直接使用js也可以。)



3.生成git仓库
由于这里生成的项目没有git仓库,所以我们git init来生成一个仓库。然后git add . ,git commit- m"" 来进行初次提交。


4.规划项目目录
在src文件夹下面建立这些文件夹,详细的作用对应如下:


5.删除多余的东西


main.tsx中一些删除掉的东西就没必要引入了
6.支持scss(其他的less等也可以,看自己熟悉哪个)
安装依赖
vue
npm i sass -D
测试一下有没有用

错误使用方式(同名类名会导致样式覆盖):

正确使用方式(改为css module方式,这样子父子组件类名相同也不会样式覆盖了):

7.加入UI组件库,安装antDesign
a.安装依赖
vue
npm i antd
b.测试(这样子你能在页面上看到蓝色的按钮就说明导入成功了)
vue
import { Button } from 'antd';
function App() {
return (
<>
<div className="ceshi">你好</div>
<Button type="primary">Button</Button>
</>
)
}
export default App
c.兼容react19,请查看官方文档(因为vite初始化的react项目目前是19版本的了,所以需要兼容)
安装依赖
vue
npm install @ant-design/v5-patch-for-react-19 --save
在main.tsx中导入
vue
import '@ant-design/v5-patch-for-react-19';

8.vite配置@别名
配置别名分为两种配置,1.项目支持使用@符号代表src 2.让vscode编辑器支持输入@符号时有下一个目录的提示
1.项目支持使用@符号代表src
a.先安装ts中支持path的依赖
<font style="color:rgb(6, 6, 7);">@types/node</font>(用于 TypeScript 环境中的 <font style="color:rgb(6, 6, 7);">path</font> 模块)。如果没有安装,可以通过以下命令安装:
vue
npm install --save-dev @types/node
b.配置vite.config.ts
vue
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path' // 导入 path 模块
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // 配置 '@' 别名指向项目根目录下的 src 文件夹
},
},
})
c.测试一下

2.让vscode编辑器支持输入@符号时有下一个目录的提示
可以看到上面截图有波浪线非常不舒服,并且输入@符号时没有下一个文件夹的提示
往这文件中(tsconfig.app.json)加入以下配置:
vue
"compilerOptions": {
"baseUrl": ".", // 设置基准路径
"paths": {
"@/*": ["src/*"] // 配置 '@' 别名指向 src 文件夹
},
}
说明
<font style="color:rgb(6, 6, 7);">tsconfig.json</font> 是项目的主配置文件,用于定义整个项目的 TypeScript 编译选项。:
<font style="color:rgb(6, 6, 7);">tsconfig.app.json</font> 用于专门配置客户端应用代码。它扩展或覆盖了 <font style="color:rgb(6, 6, 7);">tsconfig.json</font> 中的设置,以满足应用代码的特定需求。例如,你可以在这里设置 <font style="color:rgb(6, 6, 7);">baseUrl</font> 和 <font style="color:rgb(6, 6, 7);">paths</font> 来定义路径别名
<font style="color:rgb(6, 6, 7);">tsconfig.node.json</font> 用于配置 Node.js 环境中的 TypeScript,例如 Vite 的配置文件 <font style="color:rgb(6, 6, 7);">vite.config.ts</font>。它确保在 Node.js 环境中运行的代码(如构建脚本)使用正确的编译选项
成功的情况如下图(有路径提示了):

9.样式初始化(消灭浏览器差异以及盒子原有样式)
(1)normalize.css保证我们的代码在各浏览器的一致性
安装依赖
vue
npm i normalize.css
main.ts引入
vue
import 'normalize.css'
(2)消除盒子原有样式(简化版) 1.建立reset.scss,写入以下样式:
vue
* {
padding: 0;
margin: 0;
list-style: none;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
#root{
height: 100%;
}
2.在main.ts中引入:import '@/assets/css/reset.scss'
vue
import '@/assets/css/reset.scss'

10.router-配置基础路由页面
安装依赖
vue
npm install react-router-dom
在pages页面下建立这三个文件夹(Home,Layout,Login ,记得是index.tsx文件)并写入最简单的代码
vue
function Layout(){
return (
<div>Layout页面</div>
)
}
export default Layout

在router页面下建立index.tsx文件写入以下代码
vue
import { createBrowserRouter } from 'react-router-dom'
import Home from '@/pages/Home'
import Layout from '@/pages/Layout'
import Login from '@/pages/Login'
const router = createBrowserRouter([
{
path: '/',
element: <Layout></Layout>,
},
{
path: '/home',
element: <Home></Home>,
},
{
path: '/login',
element: <Login></Login>,
},
])
export default router
在main.tsx的导入并使用(可以把app.tsx和index.scss删掉了,用不到了)
vue
import { createRoot } from 'react-dom/client'
import 'normalize.css'
import '@/assets/css/reset.scss'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router}></RouterProvider>
)
11.加入状态管理工具(这里以rtk+redux+react-redux为例)
a.安装依赖
vue
npm install @reduxjs/toolkit react-redux
b.在store文件夹下面建立modules文件夹(用于区分仓库)和index.tsx(仓库的统一出口),文件夹如下

c.如上图,在modules下建立user.tsx,写入以下代码
vue
import { createSlice } from '@reduxjs/toolkit'
export const userSlice = createSlice({
name: 'user',
initialState: {
value: 0,
userInfo: {},
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
setUserInfo: (state, action) => {
state.userInfo = action.payload
},
},
})
export const { increment, decrement, setUserInfo } = userSlice.actions
// 异步写法
const fetchUser = () => {
return async (dispatch: any) => {
const res = await Promise.resolve() // 假设这里去发了后台接口获取用户信息
dispatch(setUserInfo(res))
}
}
export { fetchUser }
export default userSlice.reducer
d.在router中的index.tsx中写入以下代码:
vue
// src/store.js
import { configureStore } from '@reduxjs/toolkit'
import userSlice from './modules/user'
const store = configureStore({
reducer: {
user: userSlice,
},
})
export default store
// 推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
e.在主应用中使用 Redux Store
在 <font style="color:rgba(0, 0, 0, 0.9);">src/main.tsx</font> 文件d中,使用 <font style="color:rgba(0, 0, 0, 0.9);">Provider</font> 组件将 Redux Store 传递给 React 组件树
vue
import { createRoot } from 'react-dom/client'
import 'normalize.css'
import '@/assets/css/reset.scss'
import '@ant-design/v5-patch-for-react-19'
import router from '@/router'
import { RouterProvider } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from '@/store'
createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<RouterProvider router={router}></RouterProvider>
</Provider>
)
f.在login页面中测试使用(异步和同步在react中调用时写法是一样的)
vue
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, fetchUser } from '@/store/modules/user'
import { RootState, AppDispatch } from '@/store'
function Login() {
// 使用 RootState 类型推断 state 的类型
const count = useSelector((state: RootState) => state.user.value)
// 使用 AppDispatch 类型推断 dispatch 的类型
const dispatch = useDispatch<AppDispatch>()
return (
<div>
Login页面
{count}
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(fetchUser())}>获取用户数据</button>
</div>
)
}
export default Login
12.加入axios进行发送请求
a.建立request.ts
具体的封装可看:https://blog.csdn.net/weixin_43239880/article/details/130143352?spm=1001.2014.3001.5501
javascript
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { message, Modal } from 'antd'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 扩展 axios 的配置类型,添加自定义属性
declare module 'axios' {
export interface AxiosRequestConfig {
meta?: {
responseAll?: boolean
}
}
}
// 定义后端返回的数据结构
interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // api 的 base_url,Vite 使用 import.meta.env
timeout: 5000, // 请求超时时间
})
// request 拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 如果登录了,有 token,则请求携带 token
const state = store.getState()
const userInfo = state.user?.userInfo as { token?: string } | undefined
const token = userInfo?.token || getToken()
if (token) {
// 让每个请求携带 token--['X-Token'] 为自定义 key 请根据实际情况自行修改
config.headers['X-Token'] = token
}
return config
},
(error: AxiosError) => {
// Do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
/**
* 下面的注释为通过 response 自定义 code 来标示请求状态,当 code 返回如下情况为权限有问题,登出并返回到登录页
* 如通过 xmlhttprequest 状态码标识 逻辑可写在下面 error 中
*/
(response: AxiosResponse<ApiResponse>) => {
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
message.error({
content: res.message || '请求失败',
duration: 5,
})
// 403:非法的 token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
Modal.confirm({
title: '确定登出',
content: '你已被登出,可以取消继续留在该页面,或者重新登录',
okText: '重新登录',
cancelText: '取消',
onOk: () => {
// 清除用户信息和 token
// 这里需要根据你的实际 Redux action 来调整
// store.dispatch(logout())
localStorage.removeItem('token')
location.reload() // 为了重新实例化 react-router 对象 避免 bug
},
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
// 默认只返回 data,不返回状态码和 message
// 通过 meta 中的 responseAll 配置来决定后台是否返回所有数据(包括状态码,message 和 data)
const isBackAll = response.config.meta?.responseAll
if (isBackAll) {
return res as unknown as AxiosResponse
} else {
return res.data as unknown as AxiosResponse
}
}
},
(error: AxiosError) => {
console.log('err' + error) // for debug
message.error({
content: error.message || '网络请求失败',
duration: 5,
})
return Promise.reject(error)
}
)
export default service
b.定义api文件user.ts:
javascript
import request from '@/utils/request'
import { SuccessResponse, PageParams, PageData } from './common-types'
// 定义用户相关的接口类型
export interface LoginParams {
username: string
password: string
}
export interface LoginResponse {
token: string
userInfo: {
id: number
username: string
avatar?: string
}
}
export interface UserInfo {
id: number
username: string
email?: string
avatar?: string
}
// ========== 场景1: 复杂返回数据,必须定义类型(推荐) ==========
/**
* 用户登录
* ✅ 返回数据复杂,定义类型可以获得完整的智能提示
*/
export function login(data: LoginParams) {
return request<LoginResponse>({
url: '/user/login',
method: 'post',
data,
})
}
/**
* 获取用户信息
* ✅ 返回数据有固定结构,定义类型
*/
export function getUserInfo() {
return request<UserInfo>({
url: '/user/info',
method: 'get',
})
}
// ========== 场景2: 简单返回数据,可以内联类型 ==========
/**
* 修改密码
* ✅ 返回数据简单,可以直接内联类型,不用单独定义
*/
export function changePassword(oldPwd: string, newPwd: string) {
return request<{ success: boolean }>({
url: '/user/password',
method: 'put',
data: { oldPwd, newPwd },
})
}
/**
* 检查用户名是否存在
* ✅ 返回单个布尔值,内联类型即可
*/
export function checkUsername(username: string) {
return request<{ exists: boolean }>({
url: '/user/check',
method: 'get',
params: { username },
})
}
// ========== 场景3: 使用通用类型 ==========
/**
* 删除用户
* ✅ 使用通用的 SuccessResponse 类型
*/
export function deleteUser(id: number) {
return request<SuccessResponse>({
url: `/user/${id}`,
method: 'delete',
})
}
/**
* 获取用户列表(分页)
* ✅ 使用通用的 PageData 泛型类型
*/
export function getUserList(params: PageParams) {
return request<PageData<UserInfo>>({
url: '/user/list',
method: 'get',
params,
})
}
// ========== 场景4: 不关心返回值,可以省略类型 ==========
/**
* 用户登出
* ⚠️ 不关心返回值,可以不定义类型(返回 unknown)
*/
export function logout() {
return request({
url: '/user/logout',
method: 'post',
})
}
/**
* 上报用户行为日志
* ⚠️ 不需要处理返回值,可以省略类型
*/
export function reportLog(action: string) {
return request({
url: '/log/report',
method: 'post',
data: { action, timestamp: Date.now() },
})
}
// ========== 场景5: 返回完整响应(包括 code 和 message) ==========
/**
* 获取用户信息(返回完整响应,包括 code 和 message)
* 💡 当需要获取后端返回的状态码和提示信息时使用
*/
export function getUserInfoWithFullResponse() {
return request<UserInfo>({
url: '/user/info',
method: 'get',
meta: {
responseAll: true, // 返回完整的响应数据
},
})
}
这是关于axios和ts使用的一些方案对比

最好的就是使用工具自动生成api的ts的类型,这样子就不用手动维护了。
现在我使用的是混合策略,关键是因为有时候后端没有swagger文档,或者很多项目不太规范
次优方案:混合策略
● 如果后端没有文档 → 使用这个
● 核心功能定义类型(登录、用户信息等)
● 次要功能简化处理(日志上报、简单操作等)
● 通用结构复用类型(分页、成功响应等)
13.加入代码检测工具lint-staged+husky
参考:https://blog.csdn.net/weixin_43239880/article/details/130263271?spm=1001.2014.3001.5501
1.安装依赖包:
javascript
npm i lint-staged --save-dev
2.在 package.json 中配置 lint-staged,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码
javascript
{
// ...
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix"
]
},
}
3.安装配置 husky
javascript
npm i husky --save-dev
4.在package.json中配置快捷命令,用来在安装项目依赖时生成 husky 相关文件
javascript
{
// ...
"scripts": {
// ...
"prepare": "husky && echo 'pnpm lint-staged' > .husky/pre-commit && chmod +x .husky/pre-commit"
},
}
5.有时候配置的命令可能随着版本升级会变,但是在git commit时出现这个检测就说明弄成功了
