Featured image of post PC 前端技术选型:antd + unocss + react-router + redux toolkit

PC 前端技术选型:antd + unocss + react-router + redux toolkit

PC 端前端技术栈选型指南:Ant Design 组件库、UnoCSS 原子化 CSS、react-router v6 路由、Redux Toolkit 状态管理、RTK Query 请求层。

选型原则

PC 后台管理类项目(ERP / CRM / 数据可视化)的核心需求:

  1. 组件库丰富:表格、表单、图表、上传
  2. CSS 工程化:原子化、tree-shake
  3. 路由层级清晰:嵌套、权限、懒加载
  4. 状态管理可控:服务器状态、客户端状态分离
  5. 请求层统一:缓存、轮询、乐观更新

依赖选型

类别选型备选
UI 库antd + @ant-design/pro-componentsElement Plus / Arco Design
图表@ant-design/chartsECharts / Recharts
路由react-router v6tanstack/router
状态@reduxjs/toolkit + react-reduxzustand / jotai
请求rtk queryswr / tanstack-query
CSSunocsstailwindcss / styled-components
图标@ant-design/iconsreact-icons

一、Ant Design 5.x

Ant Design 是阿里 2015 年起的 React UI 库,国内 PC 端事实标准。pro-components 是中后台高级组件:

  • ProTable:内置搜索、分页、列控制
  • ProForm:表单 + 校验 + 联动
  • ProLayout:完整后台布局
1
2
pnpm add antd @ant-design/icons
pnpm add @ant-design/pro-components
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { Button, Table, ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { ProTable } from '@ant-design/pro-components';

<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#1890ff' } }}>
  <ProTable
    columns={columns}
    request={async (params) => fetchList(params)}
    rowKey="id"
    search={{ labelWidth: 'auto' }}
  />
</ConfigProvider>

二、@ant-design/charts

封装 G2Plot 的 React 组件:

1
pnpm add @ant-design/charts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Line, Column, Pie } from '@ant-design/charts';

<Line
  data={salesData}
  xField="month"
  yField="value"
  point={{ size: 5, shape: 'diamond' }}
  smooth
/>

<Pie data={pieData} angleField="value" colorField="type" radius={0.8} innerRadius={0.5} />

三、react-router v6

react-router v6(2019 重写)API 收敛明显:

1
pnpm add react-router-dom
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/main.tsx
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'users', element: <UserList /> },
      { path: 'users/:id', element: <UserDetail /> },
      {
        path: 'admin',
        element: <RequireAuth roles={['admin']}><Admin /></RequireAuth>,
      },
    ],
  },
  { path: '/login', element: <Login /> },
  { path: '*', element: <NotFound /> },
]);

<RouterProvider router={router} />
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Layout.tsx
import { Link, Outlet, useNavigate } from 'react-router-dom';

export const Layout = () => (
  <div>
    <nav>
      <Link to="/">首页</Link>
      <Link to="/users">用户</Link>
    </nav>
    <Outlet />
  </div>
);

四、Redux Toolkit + RTK Query

Redux Toolkit(RTK)是 Redux 官方推荐的现代写法,消除了 boilerplate

1
pnpm add @reduxjs/toolkit react-redux

Store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { userApi } from './api/user';
import counterReducer from './slices/counter';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    [userApi.reducerPath]: userApi.reducer,
  },
  middleware: (getDefault) => getDefault().concat(userApi.middleware),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Slice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/store/slices/counter.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; },
    incrementBy: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;

RTK Query(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/store/api/user.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const userApi = createApi({
  reducerPath: 'userApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['User'],
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => 'users',
      providesTags: ['User'],
    }),
    getUser: builder.query<User, number>({
      query: (id) => `users/${id}`,
    }),
    createUser: builder.mutation<User, Partial<User>>({
      query: (body) => ({
        url: 'users',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['User'],
    }),
  }),
});

export const { useGetUsersQuery, useGetUserQuery, useCreateUserMutation } = userApi;
1
2
3
// 使用
const { data: users, isLoading, refetch } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();

RTK Query 自带能力

  • 自动缓存
  • 轮询(pollingInterval: 5000
  • 乐观更新(onQueryStarted
  • 自动重试
  • 预取(initiate

五、UnoCSS 原子化 CSS

UnoCSS 是 2022 年起的新一代原子化 CSS 引擎,比 Tailwind 更快、配置更灵活

1
2
3
pnpm add -D unocss
pnpm add @unocss/reset
pnpm add @unocss/preset-wind3

uno.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { defineConfig, presetWind3 } from 'unocss';

export default defineConfig({
  presets: [presetWind3()],
  theme: {
    colors: {
      primary: '#1890ff',
    },
  },
});
1
2
3
4
<div className="flex items-center gap-4 p-4 bg-white">
  <h1 className="text-2xl font-bold text-primary">标题</h1>
  <Button className="ml-auto">操作</Button>
</div>

六、完整工程目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
src/
├── api/                  # 业务 API 抽象
│   ├── user.ts
│   └── order.ts
├── pages/                # 页面
│   ├── Home/
│   ├── UserList/
│   └── UserDetail/
├── components/           # 业务组件
├── layouts/              # 布局
├── hooks/                # 自定义 hooks
├── utils/                # 工具函数
├── store/                # Redux
│   ├── index.ts
│   ├── slices/
│   └── api/
├── routes/               # 路由配置
├── types/                # TS 类型
├── App.tsx
└── main.tsx

七、权限管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/components/RequireAuth.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';

export const RequireAuth = ({ roles, children }) => {
  const user = useSelector((s) => s.user);
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (roles && !roles.includes(user.role)) {
    return <Navigate to="/403" replace />;
  }

  return children;
};

八、构建与优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react(), unocss()],
  build: {
    target: 'es2020',
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom', 'react-router-dom'],
          antd: ['antd', '@ant-design/icons'],
          charts: ['@ant-design/charts'],
        },
      },
    },
  },
});

下一步

  • 想把同一套代码同时跑 PC / Mobile / Desktop,看 2020-04-15《FSS 全栈脚手架》
  • 想把 redux 替换成 zustand(更轻量),看 Zustand 官方文档
  • 想要 SSR / SSG(Next.js / Remix),看 Next.js 官方

参考资料

  • Ant Design:https://ant.design/
  • Pro Components:https://procomponents.ant.design/
  • @ant-design/charts:https://charts.ant.design/
  • react-router:https://reactrouter.com/
  • Redux Toolkit:https://redux-toolkit.js.org/
  • UnoCSS:https://unocss.dev/
使用 Hugo 构建
主题 StackJimmy 设计