react@18 + vite@4 + react-router@6 + zustand + antd@5 + axios 搭建后台管理系统
项目目录结构一览:
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
│ └── favicon.svg
├── src
│ ├── App.tsx
│ ├── api
│ ├── assets
│ ├── components
│ ├── hooks
│ ├── main.tsx
│ ├── pages
│ ├── router
│ ├── stores
│ ├── styles
│ ├── utils
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
创建vite项目
pnpm create vite
选择 React + Typescript
在 vite.config.ts 中简单的配置下路径别名,注意引入 path 时报错,需要安装下 @types/node:pnpm add @types/node -D
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'node:path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'~': path.resolve(__dirname, 'src'),
},
},
})
还需要配置一个地方:tsconfig.json,解决ts中使用别名报错,且会有代码提示
// tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": "./",
"paths": {
"~/*": ["src/*"]
}
}
}
添加路由
pnpm add react-router-dom@latest
在 src 目录下新建 pages 和 router 文件夹
在 pages 下新增页面级组件 Home/index.tsx, About/index.tsx 添加内容如下:
// src/pages/Home/index.tsx
const Home = () => {
return <div>Home Page</div>
}
export default Home
// src/pages/About/index.tsx
const About = () => {
return <div>About Page</div>
}
export default About
在 router 目录下创建 index.tsx 文件,内容如下:
// src/router/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
import Home from '~/pages/Home'
import About from '~/pages/About'
const routes: RouteObject[] = [
{
path: '/',
children: [
{
index: true,
element: <Navigate to="/home" replace />,
},
{
path: 'home',
element: <Home />,
},
{
path: 'about',
element: <About />,
},
],
},
]
export default createBrowserRouter(routes, {
basename: '/',
})
再修改下 src/App.tsx 内容:
import { RouterProvider } from 'react-router-dom'
import router from './router'
const App = () => {
return <RouterProvider router={router} />
}
启动一下项目,在地址栏手动输了不同的路由 /home,/about,能渲染对应的组件说明 router 就配置好了
改进一下组件懒加载
router 目录下新建 lazyLoad.tsx 文件:
// router/lazyLoad.tsx
import { Suspense } from 'react'
const lazyLoad = (Component: React.LazyExoticComponent<() => JSX.Element>) => {
return (
<Suspense>
<Component />
</Suspense>
)
}
export default lazyLoad
修改 router/index.tsx:
// router/index.ts
import { lazy } from 'react'
import { createBrowserRouter, Navigate } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
import lazyLoad from './lazyLoad'
const Home = lazy(() => import('~/pages/Home'))
const About = lazy(() => import('~/pages/About'))
const routes: RouteObject[] = [
{
path: '/',
children: [
{
index: true,
element: <Navigate to="/home" replace />,
},
{
path: 'home',
element: lazyLoad(Home),
},
{
path: 'about',
element: lazyLoad(About),
},
],
},
]
export default createBrowserRouter(routes, {
basename: '/',
})
添加 zustand
Zustand 一款简洁好用的状态管理库
pnpm add zustand
在 src 目录下新建 stores 文件夹,添加 counter.ts 文件:
import { create } from 'zustand'
interface CounterState {
counter: number
increase: (by: number) => void
}
const useCounterStore = create<CounterState>()((set) => ({
counter: 0,
increase: (by) => set((state) => ({ counter: state.counter + by })),
}))
export default useCounterStore
在组件中使用(拿 Home 组件举例):
import useCounterStore from '~/stores/counter'
const Home = () => {
const counter = useCounterStore((state) => state.counter)
const increase = useCounterStore((state) => state.increase)
return (
<div>
<div>Home Page</div>
<button onClick={() => increase(1)}> counter: {counter} </button>
</div>
)
}
export default Home
这时你可以在 home 页面上点击 button 便可看到 counter 的变化。
添加 Antd
pnpm add antd
修改 App.tsx
// App.tsx
import { RouterProvider } from 'react-router-dom'
import { ConfigProvider } from 'antd'
import { useGlobalStore } from '~/stores'
import router from './router'
import dayjs from 'dayjs'
import zhCN from 'antd/locale/zh_CN'
import 'dayjs/locale/zh-cn'
import 'antd/dist/reset.css'
dayjs.locale('zh-cn')
const App = () => {
const { primaryColor } = useGlobalStore()
return (
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: primaryColor,
},
}}
>
<RouterProvider router={router} />
</ConfigProvider>
)
}
export default App
这里用到了 dayjs 需要安装一下:pnpm add dayjs
添加了全局的主题色,需要在 stores 下新建 global.ts 文件,在里面维护了全局主题色 primaryColor,添加持久化缓存
// src/stores/global.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface GlobalState {
primaryColor: string
setColor: (color: string) => void
}
const useGlobalStore = create<GlobalState>()(
persist(
(set) => ({
primaryColor: '#00b96b',
setColor: (color) => set(() => ({ primaryColor: color })),
}),
{
name: 'primaryColor',
// partialize 过滤属性,存储哪些字段到localStorage
partialize: (state) =>
Object.fromEntries(Object.entries(state).filter(([key]) => ['primaryColor'].includes(key))),
}
)
)
export default useGlobalStore
为了方便倒出管理,在 stores 下新建 index.ts,统一倒出:
export { default as useCounterStore } from './counter'
export { default as useGlobalStore } from './global'
在 src 目录下新建 components/Layout/index.tsx
// src/components/Layout/index.tsx
import { useState } from 'react'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import { Button, Layout, Menu } from 'antd'
import type { MenuProps } from 'antd'
const { Header, Sider, Content } = Layout
const items: MenuProps['items'] = [
{
label: 'home',
path: '/home',
},
{
label: 'about',
path: '/about',
},
].map((nav) => ({
key: nav.path,
icon: null,
label: nav.label,
}))
const BasicLayout = () => {
const navigate = useNavigate()
const { pathname } = useLocation()
const [collapsed, setCollapsed] = useState(false)
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
navigate(key)
}
return (
<Layout style={{ height: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
<div
style={{
height: 32,
margin: 16,
background: 'rgba(0, 0, 0, 0.2)',
zIndex: 200,
}}
/>
<Menu
mode="inline"
defaultSelectedKeys={[pathname]}
items={items}
onClick={handleMenuClick}
/>
</Sider>
<Layout style={{ display: 'flex', flexDirection: 'column' }}>
<Header style={{ background: '#fff', padding: 0 }}>
<Button type="text" onClick={() => setCollapsed(!collapsed)}>
collapsed
</Button>
</Header>
<Content style={{ padding: '16px', flex: 1, overflowY: 'auto' }}>
<Outlet />
</Content>
</Layout>
</Layout>
)
}
export default BasicLayout
修改 router/index.tsx
// src/router/index.tsx
import Layout from '~/components/Layout'
{
path: '/',
element: <Layout />,
children: [
// ...
]
}
再重置一下浏览器的一些默认样式,新建 src/styles/index.scss,可以安装下 sass:pnpm add sass -D
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
background: #efefef;
}
ul,
ol {
list-style: none;
}
img,
svg {
display: inline-block;
}
a:focus,
a:active,
div:focus {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
在 main.tsx 中引入一下:import '~/styles/index.scss',大功告成
现在项目呈现效果如下,点击切换和刷新都👌
后续就自己加业务组件了,丰富项目了,这里就不细说了。
封装 Axios
pnpm add axios
在 src 中添加 utils/request.ts 封装一下 axios
// src/utils/request.ts
import axios from 'axios'
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios'
import { useUserInfoStore } from '~/stores'
class Request {
private instance: AxiosInstance
// 存放取消请求控制器Map
private abortControllerMap: Map<string, AbortController>
constructor(config: CreateAxiosDefaults) {
this.instance = axios.create(config)
this.abortControllerMap = new Map()
// 请求拦截器
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (config.url !== '/login') {
const token = useUserInfoStore.getState().userInfo?.token
if (token) config.headers!['x-token'] = token
}
const controller = new AbortController()
const url = config.url || ''
config.signal = controller.signal
this.abortControllerMap.set(url, controller)
return config
}, Promise.reject)
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
const url = response.config.url || ''
this.abortControllerMap.delete(url)
if (response.data.code !== 1000) {
return Promise.reject(response.data)
}
return response.data
},
(err) => {
if (err.response?.status === 401) {
// 登录态失效,清空userInfo,跳转登录页
useUserInfoStore.setState({ userInfo: null })
window.location.href = `/login?redirect=${window.location.pathname}`
}
return Promise.reject(err)
}
)
}
// 取消全部请求
cancelAllRequest() {
for (const [, controller] of this.abortControllerMap) {
controller.abort()
}
this.abortControllerMap.clear()
}
// 取消指定的请求
cancelRequest(url: string | string[]) {
const urlList = Array.isArray(url) ? url : [url]
for (const _url of urlList) {
this.abortControllerMap.get(_url)?.abort()
this.abortControllerMap.delete(_url)
}
}
request<T>(config: AxiosRequestConfig): Promise<T> {
return this.instance.request(config)
}
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
}
export const httpClient = new Request({
timeout: 20 * 1000,
baseURL: import.meta.env.VITE_API_URL,
})
// src/api/testApi/index.ts
import { httpClient } from '~/utils/request'
export const testApi = {
getData: () => {
return httpClient.get('/test')
},
}
import { testApi } from '~/api/testApi'
useEffect(() => {
testApi.getData().then(res=>{
console.log(res)
})
}, [])
添加.env配置文件
在项目目录下新建 .env.development 和 .env.production
// .env.development
VITE_APP_ENV = 'development'
VITE_BASE_URL = '/'
VITE_API_URL = '/'
// .env.production
VITE_APP_ENV = 'development'
VITE_BASE_URL = '/'
VITE_API_URL = '/'
这样在可以替换 router/index.tsx 中的 basename 和 utils/request.ts 中的 baseURL
// ...
const router = createBrowserRouter(routes, {
basename: import.meta.env.VITE_BASE_URL,
})
export const httpClient = new Request({
timeout: 20 * 1000,
baseURL: import.meta.env.VITE_API_URL,
})
转载自:https://juejin.cn/post/7237840998985072698