likes
comments
collection
share

react@18 + vite@4 + react-router@6 + zustand + antd@5 + axios 搭建后台管理系统

作者站长头像
站长
· 阅读数 74

项目地址github.com

项目目录结构一览:

├── 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项目

Vite v4

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/*"]
    }
  }
}

添加路由

React Router v6

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

Ant Design

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',大功告成

现在项目呈现效果如下,点击切换和刷新都👌

react@18 + vite@4 + react-router@6 + zustand + antd@5 + axios 搭建后台管理系统

后续就自己加业务组件了,丰富项目了,这里就不细说了。

封装 Axios

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,
})

项目地址github.com

转载自:https://juejin.cn/post/7237840998985072698
评论
请登录