$ npm create vite@latest react-admin -- --template react-ts
$ npm install
- 项目根目录下创建
.editorconfig
文件 - 配置文件内容
root = true // 顶层配置
[*] // 所有文件
charset = utf-8 // 编码格式
indent_style = space // 缩进风格
indent_size = 2 // 缩进大小
end_of_line = lf // 换行符
insert_final_newline = true // 文件末尾插入新行
trim_trailing_whitespace = true // 删除行尾空格
[*.md] // 所有md文件
trim_final_newline = false // 不删除行尾空格
trim_trailing_whitespace = false // 不删除行尾空格
- 安装插件
- VS Code: EditorConfig for VS Code
- 安装依赖
$ npm install --save-dev prettier
- 项目根目录下创建
.prettierrc.cjs
文件 - 配置文件内容
module.exports = {
"printWidth": 120, // 每行代码长度(默认80)
"singleQuote": true, // 使用单引号(默认false)
"trailingComma": "none", // 末尾逗号(默认none)
"endOfLine": "auto", // 结尾是 \n \r \n\r auto
"semi": false, // 句尾添加分号(默认true)
"tabWidth": 2, // tab缩进大小(默认是2)
"useTabs": true, // 使用tab缩进(默认false)
"bracketSpacing": true, // 对象字面量的大括号间增加空格(默认true)
"jsxBracketSameLine": true, // jsx标签的反尖括号是否单独一行(默认false)
"arrowParens": "avoid", // 箭头函数参数为单个时是否省略括号(默认avoid)
}
- 安装插件
- VS Code: Prettier - Code formatter
- 局部保存格式化:项目根目录下创建
.vscode
文件
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
- 安装依赖eslint,prettier
$ npm install -D eslint prettier eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier
- 项目根目录下创建
.eslintrc.cjs
文件 - 配置文件内容
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',// 推荐规则集
'plugin:@typescript-eslint/recommended', // 官方推荐的规则集
'plugin:react/recommended', // 官方推荐的react规则集
'plugin:react-hooks/recommended',// 官方推荐的react-hooks规则集
'plugin:prettier/recommended' // prettier推荐的规则集
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh','react','@typescript-eslint','prettier'],
rules: {
"react/react-in-jsx-scope": "off",
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
- 项目根目录下创建
vite.config.ts
文件 - 安装依赖
$ npm install path
- 配置文件内容
import path from 'path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
- tsconfig.json 配置别名映射
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
::: tip 针对 ES Modules 引入__dirname 的解决方案 如果使用 "type": "module",改用以下方式:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
:::
- api: 存放接口请求
- components: 存放公用组件
- config: 存放配置文件
- layout: 存放布局组件
- router: 存放路由配置
- store: 存放状态管理
- utils: 存放工具函数
- types: 存放类型定义
- hooks: 存放自定义hooks
- styles: 存放全局样式
- views: 存放页面组件
- 下载依赖
$ npm install antd
- 定制主题
ConfigProvider
- 获取主题变量
- 下载依赖
$ npm install tailwindcss @tailwindcss/vite
- 配置 Vite 插件
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
- 在src目录下创建
styles/index.css
文件,并写入以下内容
@import "tailwindcss";
- 安装依赖
$ npm install sass -D
- 在
vite.config.ts
中配置scss
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/index.scss";`, // 全局引入样式文件
},
},
},
})
- 在
src/styles/index.scss
中写入全局样式
// 浏览器填充文本框背景色修改
.ant-input:-webkit-autofill,
.ant-input:-webkit-autofill:hover,
.ant-input:-webkit-autofill:focus,
.ant-input:-webkit-autofill:active {
transition-delay: 999999999s;
transition: color 999999999s ease-out, background-color 999999999s ease-out;
-webkit-transition-delay: 999999999s;
-webkit-transition: color 999999999s ease-out, background-color 999999999s ease-out;
-webkit-text-fill-color: #fff;
}
.ant-input:-webkit-autofill {
box-shadow: 0 0 0px 1000px #ffffff inset !important; //改成你想要的背景色
-webkit-text-fill-color:#000000 !important;
}
- css module 引入scss文件
// 创建'./index.module.scss'
// 修改第三方组件样式:global
:global(.globalStyle) {
color: blue;
}
- 安装依赖
$ npm install normalize.css
- 在
src/styles/index.css
中引入样式文件
import 'normalize.css'
- 安装React-Router
$ npm install react-router-dom
- 项目根目录下创建
src/router/index.tsx
文件
import { useRoutes, RouteObject, Navigate } from 'react-router-dom'
import Login from '@/views/Login.tsx'
import NotFound from '@/views/NotFound.tsx'
import Home from '@/views/Home/index.tsx'
import Layout from '@/layout/index.tsx'
// 定义路由配置数组
const router: RouteObject[] = [
{
path: '/',
element: <Layout />,
children: [
{ path: 'home', element: <Home /> },
{ path: 'about', element: <div>about</div> }
]
},
{ path: '/login', element: <Login /> },
{ path: '/404', element: <NotFound /> },
{ path: '*', element: <Navigate to="/404" /> }
]
// 创建路由组件
function Router() {
return useRoutes(router)
}
export default Router
// 在App.tsx中使用
import { BrowserRouter } from 'react-router-dom'
import Router from './router'
// API+组件化创建的路由
function App() {
return (
<BrowserRouter>
<Router />
</BrowserRouter>
)
}
export default App
$ npm install axios
import axios from 'axios'
import {getItem,clearCache} from '@/utils/storage'
import { showLoading, hideLoading } from '@/utils/loading/index'
import { AxiosRequestConfig } from 'axios'// 扩展 AxiosRequestConfig 类型定义
declare module 'axios' {
interface AxiosRequestConfig {
hideLoading?: boolean | undefined
}
}
// 创建axios实例
const VITE_BASE_API = import.meta.env.VITE_BASE_API || '/'
// 1.定义pendingRequests作为Map,存储请求的key和对应的cancel函数。
const pendingRequests = new Map()
// 2.生成唯一的key,用于取消重复请求。
const generateRequestKey = (config:AxiosRequestConfig) => {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 3. 添加请求到pendingRequests的函数。
const addPendingRequest = (config:AxiosRequestConfig) => {
const requestKey = generateRequestKey(config)
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pendingRequests.has(requestKey)) {
pendingRequests.set(requestKey, cancel)
}
})
}
// 4. 移除等待中的请求
const removePendingRequest = (config:AxiosRequestConfig) => {
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
const cancelRequest = pendingRequests.get(requestKey)
cancelRequest && cancelRequest(requestKey)
pendingRequests.delete(requestKey)
}
}
// 5.创建axios实例,配置baseURL、超时等。
const service = axios.create({
baseURL: VITE_BASE_API,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
},
timeout: 50000
})
// 6. request拦截器
service.interceptors.request.use(
config => {
// 7.请求开始时开启全局loading
!config.hideLoading && showLoading()
// 8.处理重复请求(取消之前的)
removePendingRequest(config)
addPendingRequest(config)
// 9.添加认证头(Token)等信息。
const token = getItem('token') || ''
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
},
// 10.请求失败处理逻辑
error => {
return Promise.reject(error)
}
)
// 11. respone拦截器
service.interceptors.response.use(
// 12. 响应成功处理逻辑
response => {
// 13. 请求成功时关闭全局loading
hideLoading()
// 14. 移除等待中的请求
removePendingRequest(response.config)
// 15. 如果是blob类型,直接返回response对象
if (response.config.responseType === 'blob') {
return response.data
} else {
const res = response.data
if (res.code === 0) {
return res
} else if (res.code === -1) {
window.$message.error(res.msg || 'Error')
return res
} else if (res.code === -2) {
window.$message.error('登录超时,请重新登录', 3,() => { clearCache() })
return res
} else {
window.$message.error(res.msg || 'Error')
return Promise.reject()
}
}
},
// 16. 响应失败处理逻辑
error => {
// 17. 请求失败时关闭全局loading
hideLoading()
// 18. 移除等待中的请求
error.config && removePendingRequest(error.config)
// 19. 处理取消请求的错误
if (axios.isCancel(error)) {
return Promise.reject({ message: '请求被取消' })
} else {
const { status } = error.response
if (status) {
switch (status) {
case 401:
window.$message.error('登录超时,请重新登录', 3,() => { clearCache() })
break
case 404:
window.$message.error('路径未找到',3)
break
case 403:
window.$message.error('权限不足',3)
break
case 400:
window.$message.error('错误的请求',3)
break
case 500:
window.$message.error('服务器异常',3)
break
default:
window.$message.error('请求错误,请稍后再试……',3)
break
}
} else {
window.$message.error('网络错误,请稍后再试……',3)
}
}
return Promise.reject(error)
}
)
export default service
import request from '@/utils/request.ts'
export function userLogin() {
return request({
url: '/user/test',
method: 'get'
})
}
在vite.config.ts
中配置代理
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 目标服务器地址
changeOrigin: true, // 是否改变源
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
logLevel: 'debug', // 日志级别
},
},
},
})
- 在
utils
目录下创建loading/index.js
- 封装loading组件的展示和隐藏
import ReactDOM from 'react-dom/client'
import Loading from './loading'
let count = 0
export const showLoading = () => {
if (count === 0) {
const spinWrapDiv = document.createElement('div')
spinWrapDiv.setAttribute('id', 'root-loading')
document.body.appendChild(spinWrapDiv)
ReactDOM.createRoot(spinWrapDiv).render(<Loading />)
}
count++
}
export const hideLoading = () => {
if (count < 0) return
count--
if (count === 0) {
const spinWrapDiv = document.getElementById('root-loading') as HTMLDivElement
document.body.removeChild(spinWrapDiv)
}
}
- 在
loading/loading.tsx
中创建组件
import { Spin } from 'antd'
import './loading.scss'
function Loading({ tip = 'loading' }: { tip?: string }) {
return <Spin size="large" tip={tip} wrapperClassName="requiest-loading" fullscreen={true} />
}
export default Loading
- 在
loading/loading.scss
中创建样式
#root-loading{
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
}
- 在
utils
目录下创建storage/index.ts
- 将本项目所有的存储放在统一的key下
const BASE_KEY = 'REACT_ADMIN_CACHE'
// 1.sessionStorage存储
export function setItem(key: string, data: any) {
// 1. 读取已有数据
const oldData = JSON.parse(sessionStorage.getItem(BASE_KEY) as string)
// 2. 合并新旧数据
if (oldData) {
const newData = { ...oldData, [key]: data }
return sessionStorage.setItem(BASE_KEY, JSON.stringify(newData))
} else {
sessionStorage.setItem(BASE_KEY, JSON.stringify({ [key]: data }))
}
}
// 2.sessionStorage获取
export function getItem(key: string) {
const oldData = JSON.parse(sessionStorage.getItem(BASE_KEY) as string)
if (oldData) {
const result = oldData[key]
try {
return JSON.parse(result as string)
} catch (error) {
return result as string
}
} else {
return null
}
}
// 3.sessionStorage移除
export function removeItem(key: string) {
const oldData = JSON.parse(sessionStorage.getItem(BASE_KEY) as string)
if (oldData && oldData[key]) {
delete oldData[key]
return sessionStorage.setItem(BASE_KEY, JSON.stringify(oldData))
} else {
return null
}
}
// 清楚缓存
export function clearCache() {
sessionStorage.removeItem(BASE_KEY)
}
- 在
utils/global/index.tsx
中定义全局message、modal、notification
import { App } from 'antd'
import type { MessageInstance } from 'antd/es/message/interface'
import type { ModalStaticFunctions } from 'antd/es/modal/confirm'
import type { NotificationInstance } from 'antd/es/notification/interface'
let newMessage: MessageInstance
let notification: NotificationInstance
let modal: Omit<ModalStaticFunctions, 'warn'>
const msgKey = ['info', 'success', 'error', 'warning','loading']
const message : { [key: string]: any } = {}
declare global {
interface Window {
$message: typeof message;
$modal: typeof modal;
$notification: typeof notification;
}
}
export default function AdtdGlobal(){
const staticFunction = App.useApp()
newMessage = staticFunction.message
modal = staticFunction.modal
notification = staticFunction.notification
msgKey.forEach(key => {
message[key] = (content:any,duration?:number,onClose?:()=>void)=>{
newMessage.destroy()
newMessage[key as keyof typeof newMessage](content as any,duration,onClose)
}
})
window.$message = message
window.$modal = modal
window.$notification = notification
return null
}
- 在
App.tsx
中引入
import { BrowserRouter } from 'react-router-dom'
import { App as AntdApp, ConfigProvider,theme } from 'antd'
import Router from './router'
import AdtdGlobal from '@/utils/global'
// API+组件化创建的路由
function App() {
return (
<ConfigProvider
theme={{
token: {
colorPrimary: "#ed7c0b",
borderRadius: 4,
},
algorithm: theme.defaultAlgorithm,
}}
>
<AntdApp>
<AdtdGlobal />
<BrowserRouter>
<Router />
</BrowserRouter>
</AntdApp>
</ConfigProvider>
)
}
export default App
- 使用全局message
window.$message.success('登录成功')
- vite 将环境变量暴漏在
import.meta.env
上 - 在
.env
文件中配置开发环境变量
# 设置NODE_NEV环境模式
VITE_NODE_NEV = dev
# 前端端口
VITE_PORT = 9001
# 反向代理地址
VITE_PROXY_URL = http://localhost:3000/api
# 设置接口的api地址
VITE_BASE_API = '/api'
- 在
.env.stag
文件中配置测试环境变量 - 在
.env.production
文件中配置生产环境变量 - 在
package.json
中配置环境变量
"scripts": {
"dev": "vite",
"build:stag": "vite build --mode stag",
"build:prod": "vite build --mode prod",
}
- 在
src/config/index.ts
中配置 - 封装运行时环境配置
type ENV = 'dev' | 'stag' | 'prod'
const env = import.meta.env.VITE_NODE_NEV as ENV
const config = {
dev: {
name: '开发环境'
},
stag: {
name: '测试环境'
},
prod: {
name: '生产环境'
}
}
export default config[env]
::: tip 编译时环境配置和运行时环境配置的区别
- 编译时环境配置:在项目构建阶段就已经确定了,比如
VITE_PORT
,VITE_PROXY_URL
等 - 运行时环境配置:在项目运行阶段才能确定,比如
VITE_NODE_NEV
,VITE_BASE_API
等 - 运行时环境配置更容易做动态切换,比如在开发阶段和测试阶段的接口地址不一样。 :::
- 安装依赖
$ npm install react-redux @reduxjs/toolkit
$ npm install @types/redux --save-dev
- 创建store文件夹,在store文件夹中创建modules文件
- 在modules文件中创建模块,比如
user.ts
- 在store文件夹中创建index.ts文件,引入所有模块
- 在项目main.tsx中引入store,并使用Provider包裹App组件
- 使用useSelector和useDispatch钩子函数获取状态值和分发action对象
- AuthRoute组件封装路由守卫逻辑
import {Navigate,useLocation} from 'react-router-dom'
import {useSelector,useDispatch} from 'react-redux'
import { ReactNode, useEffect } from 'react'
import {RootState} from '@/store'
import {UserState} from '@/store/modules/user'
import {SetLasterRoute} from '@/store/modules/route'
function AuthRoute ({children}:{children:ReactNode}){
const location = useLocation()
const dispatch = useDispatch()
// 记录最后一次访问的路由信息
useEffect(()=>{
dispatch(SetLasterRoute(location))
},[location])
// 获取用户登录状态信息
const user = useSelector<RootState,UserState>(state => state.user)
const {token} = user
if(token){
return <>{children}</>
}else{
return <Navigate to="/login" replace/>
}
}
export default AuthRoute
- 在App.tsx中使用AuthRoute组件包裹需要鉴权的路由
{
path: '/',
element: <AuthRoute><Layout /></AuthRoute>,
children: [
{ element: <Home />, index: true },
{ path: 'test', element: <Test /> },
{ path: 'about', element: <div>关于我们</div> }
]
}
- 封装login页面守卫逻辑
// LoginGuard.tsx
import { ReactNode, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { UserState } from '@/store/modules/user'
import {RouteState} from '@/store/modules/route'
import { RootState } from '@/store'
const LoginGuard = ({ children }:{children:ReactNode}) => {
const navigate = useNavigate()
const { token } = useSelector<RootState,UserState>(state => state.user)
const {lasterRouter} = useSelector<RootState,RouteState>(state => state.route)
// 登录成功后跳转至最后一次访问的路由信息
useEffect(() => {
if (token) {
navigate(lasterRouter?.pathname || '/', { replace: true })
}
}, [token, navigate])
// 如果没有登录,则渲染login页面组件,否则不渲染
return !token ? children : null
}
export default LoginGuard