react-admin:前端项目搭建

2,872 阅读6分钟

react-admin是一个开箱即用的中大型后台管理系统,不仅只有前端解决方案,更是提供了基于nestjs的后端解决方案。

前端项目地址

后端项目地址

前端基于vite5、react、react-router、zustand、antd5搭建的中后台管理系统

后端基于nestjs、typeorm、mysql等技术栈

此专栏记录从0-1实现一个完整的中后台管理系统的开发过程

现已完成功能

  • [ √] 登录页(三种登录风格可选)
  • [ √] 明暗主题切换
  • [ √] 国际化
  • [ √] 面包屑
  • [ √] 多标签页(多种标签风格可选)
  • [ √] KeepAlive缓存
  • [ √] 图标展示及图标选择组件
  • [ √] 全局菜单搜索

后续计划

  • 后端项目搭建
  • 后端登录实现
  • 后端组织、菜单、用户、角色管理开发

1. 创建vite项目

pnpm create vite react-admin --template react-swc-ts

2. 配置路径别名

  1. 安装@types/node
pnpm add @types/node -D
  1. 修改vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'node:path';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
});
  1. 修改tsconfig.json
// tsconfig.json
{
  "compilerOptions": {
    // ...
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

3. 添加路由

  1. 安装react-router-dom
pnpm add react-router-dom@latest
  1. src目录下新建pagesrouter文件夹
  2. pages目录下新建页面组件home/index.tsxabout/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;
  1. 创建src/components/LazyLoadComponent/index.tsx
import { Suspense } from 'react';

import AppLoading from '../app/AppLoading';

const LazyLoadComponent = ({
  Component,
}: {
  Component: React.LazyExoticComponent<() => JSX.Element>;
}) => {
  return (
    <Suspense fallback={<AppLoading />}>
      <Component />
    </Suspense>
  );
};

export default LazyLoadComponent;
  1. router目录下创建index.tsxroutes.tsx
// src/router/routes.tsx
import { lazy } from 'react';
import { Navigate } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import LazyLoadComponent from '@/components/LazyLoadComponent';

const routes: RouteObject[] = [
  {
    path: '/',
    children: [
      {
        index: true,
        element: <Navigate to="/home" replace />,
      },
      {
        path: '/home',
        element: (
          <LazyLoadComponent Component={lazy(() => import('@/pages/home'))} />
        ),
      },
      {
        path: '/about',
        element: (
          <LazyLoadComponent Component={lazy(() => import('@/pages/about'))} />
        ),
      },
    ],
  },
];
export default router;
// src/router/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom';
import routes from './routes';

const router = createBrowserRouter(routes, {
  basename: '/react',
});
export default router;
  1. 修改src/App.tsx
// src/App.tsx
import { RouterProvider } from 'react-router-dom';
import router from './router';

const App = () => {
  return <RouterProvider router={router} />;
};

4. 添加zustand

  1. 安装zustand依赖
pnpm add zustand
  1. src目录下新建store文件夹,添加counter.ts
  2. 在组件中使用
import useCounterStore from '@/store/counter';
const Home = () => {
  // const counter = useCounterStore((state) => state.counter)
  // const increase = useCounterStore((state) => state.increase);
  const { counter, increase } = useCounterStore();

  return (
    <div>
      <div>Home Page</div>
      <button onClick={() => increase(1)}>counter: {counter}</button>
    </div>
  );
};

export default Home;

5. 添加antd

  1. 安装依赖antddayjs
pnpm add antd
pnpm add dayjs
  1. 创建src/store/index.ts
import { create } from 'zustand';

interface GlobalState {
  primaryColor: string;
}

const useGloabalState = create<GlobalState>()(() => ({
  primaryColor: '#00b96b',
}));

export default useGloabalState;
  1. 修改src/App.tsx
import { RouterProvider } from 'react-router-dom';
import { ConfigProvider } from 'antd';
import useGlobalStore from '@/store';
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;
  1. src/pages/home/index.tsx里引入组件
import { Button } from 'antd';

import useCounterStore from '@/store/counter';
const Home = () => {
  // const counter = useCounterStore((state) => state.counter)
  // const increase = useCounterStore((state) => state.increase);
  const { counter, increase } = useCounterStore();

  return (
    <div>
      <div>Home Page</div>
      <button onClick={() => increase(1)}>counter: {counter}</button>
      <Button type="primary">Primary</Button>
    </div>
  );
};

export default Home;

6. 配置环境变量

新建 .env(所有环境生效).env.development(开发环境配置) .env.production(生产环境配置)

  1. 定义变量 以 VITE_ 为前缀定义变量
# .env
VITE_APP_TITLE = 'react后台管理系统'
VITE_BASE_API_PREFIX = '/api'
# .env.development
# public path
VITE_PUBLIC_PATH = /react-admin
VITE_APP_PORT = 5173
# .env.production
# public path
VITE_PUBLIC_PATH = /react-admin
VITE_APP_PORT = 8000
  1. 定义类型 修改vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_PUBLIC_PATH: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_APP_PORT: string;
  readonly VITE_BASE_API_PREFIX: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
  1. 使用变量 替换src/router/index.tsx中的basename,替换index.html中的title
import { createBrowserRouter } from 'react-router-dom';
import routes from './routes';

const router = createBrowserRouter(routes, {
  basename: import.meta.env.VITE_PUBLIC_PATH,
});

export default router;
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>%VITE_APP_TITLE%</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
  1. vite.config.ts中使用环境变量 修改默认导出为一个函数,使用loadEnv读取环境变量,并使用环境变量
import { ConfigEnv, defineConfig, loadEnv, UserConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'node:path';

// https://vitejs.dev/config/
export default ({ mode }: ConfigEnv): UserConfig => {
  const envConfig = loadEnv(mode, process.cwd());
  return defineConfig({
    plugins: [react()],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      },
    },
    base: envConfig.VITE_PUBLIC_PATH,
    server: {
      port: Number(envConfig.VITE_APP_PORT),
      proxy: {
        '/api': {
          target: 'http://localhost:3000'
          changeOrigin: true,
        },
      },
    },
  });
};

7. 配置请求

  1. 安装依赖
pnpm add axios
  1. 新建src/utils/axios.ts
import axios from 'axios';
import storage from '@/utils/storage'
const instance = axios.create({
  baseURL: import.meta.env.VITE_BASE_API_PREFIX,
  headers: {
    'Content-Type': 'application/json',
  },
});

instance.interceptors.request.use(
  (config) => {
    // Add authorization token if available
    const token = storage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    Promise.reject(error);
  },
);

instance.interceptors.response.use(
  (response) => {
    if (response.status >= 200 && response.status < 300) {
      response.data.success = true;
      return response.data;
    } else {
      return response.data;
    }
  },
  (error) => {
    if (error.response) {
      if (error.response.data) {
        error.response.data.success = false;
      }
      return error.response?.data || error;
    }
  },
);
  1. 新建src/services/user/index.tssrc/services/user/interface.tssrc/types/custom-types.d.ts
// src/types/custom-types.d.ts
export interface IBasicResponse<T> {
  error_code: string;
  message: string;
  data: T;
  success: boolean;
}

export interface IPaging {
  page_no: number;
  page_size: number;
  total: number;
}
export interface IPagingResponse<T> extends IBasicResponse<T> {
  data: {
    paging: IPaging;
    data: T[];
  };
}
// src/services/user/index.ts
import axios from '@/utils/axios';
import { ILoginParams, ILoginResponse } from './interface';
import { IBasicResponse } from '@/types/custom-types';

export function login(params: ILoginParams) {
  return axios<ILoginResponse, IBasicResponse<ILoginResponse>, ILoginParams>(
    '/login',
    {
      method: 'POST',
      data: params,
    },
  );
}
// src/services/user/interface.ts
export interface ILoginParams {
  username: string;
  password: string;
  captcha: string;
}

export interface ILoginResponse {
  access_token: string;
  refresh_token: string;
}

8. 配置prettiereslintstylelint

  1. 安装依赖
pnpm create @eslint/config@latest
pnpm add @eslint/eslintrc -D
pnpm add eslint-plugin-import -D # eslint import规则
pnpm add prettier -D
## prettier用于格式化代码配合vscode插件,不在eslint中使用
# pnpm add eslint-plugin-prettier -D # eslint prettier
# pnpm add eslint-config-prettier -D # 解决eslint与prettier冲突

  1. 创建 .prettierrc
{
  "endOfLine": "auto",
  "printWidth": 120,
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "bracketSpacing": true
}
  1. 修改eslint.config.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { fixupConfigRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import pluginJs from '@eslint/js';
// import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
import pluginEslintImport from 'eslint-plugin-import';
// import pluginEslintPrettier from 'eslint-plugin-prettier/recommended';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
import tsEslint from 'typescript-eslint';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const flatCompat = new FlatCompat({
  baseDirectory: __dirname, // 可选;默认值: process.cwd()
  resolvePluginsRelativeTo: __dirname, // 可选
  recommendedConfig: pluginJs.configs.recommended, // 默认使用 "eslint:recommended"
});

export default [
  {
    ignores: ['.idea', '.vscode', '**/dist/'],
    files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
    languageOptions: {
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },
      },
      // 全局变量
      globals: { ...globals.node, ...globals.es2022, ...globals.browser },
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    plugins: {
      import: pluginEslintImport,
      'react-hooks': pluginReactHooks,
    },
  },
  pluginJs.configs.recommended,
  ...tsEslint.configs.recommended,
  // pluginEslintPrettier, // 更新这里用prettier只做格式化不加入格式检查
  // ...fixupConfigRules(pluginReactConfig),
  // 因为v9变化较大,为了兼容之前的 config,官方提供了转换整个旧的 config 的方法
  ...fixupConfigRules(
    flatCompat.config({
      extends: ['plugin:react/recommended', 'plugin:react/jsx-runtime'],
      plugins: ['react', 'react-refresh'],
    }),
  ),
  {
    rules: {
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      '@typescript-eslint/no-explicit-any': 'error',
      'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
       // 引入type时使用 type 关键字如: import { type xx } from 'xxx'
      '@typescript-eslint/no-import-type-side-effects': 'error',
      '@typescript-eslint/consistent-type-imports': 'error',
       // 引入type另起一行而不是跟现有的import一起使用如: import type { xx } from 'xx'
      'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
      //import导入顺序规则
      'import/order': [
        'error',
        {
          //按照分组顺序进行排序
          groups: [
            'builtin',
            'external',
            'parent',
            'sibling',
            'index',
            'internal',
            'object',
            'type',
          ],
          //通过路径自定义分组
          pathGroups: [
            {
              pattern: 'react*', //对含react的包进行匹配
              group: 'builtin', //将其定义为builtin模块
              position: 'before', //定义在builtin模块中的优先级
            },
            {
              pattern: '@/components/**',
              group: 'parent',
              position: 'before',
            },
            {
              pattern: '@/utils/**',
              group: 'parent',
              position: 'after',
            },
            {
              pattern: '@/services/**',
              group: 'parent',
              position: 'after',
            },
          ],
          //将react包不进行排序,并放在前排,可以保证react包放在第一行
          pathGroupsExcludedImportTypes: ['react'],
          'newlines-between': 'always', //每个分组之间换行
          //根据字母顺序对每个组内的顺序进行排序
          alphabetize: {
            order: 'asc',
            caseInsensitive: true,
          },
        },
      ],
    },
  },
];
  1. 新建.prettierignore
# 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist
  1. 配置stylelint
  • 安装依赖
pnpm create stylelint
pnpm add stylelint-config-prettier -D
  • 修改.stylelintrc.json
{ "extends": ["stylelint-config-standard", "stylelint-config-prettier"] }
  1. vscode保存自动格式化
{
  "workbench.iconTheme": "material-icon-theme",
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "files.autoSave": "onFocusChange",
  "eslint.codeActionsOnSave.rules": null,
  "editor.tabSize": 2,
  "editor.fontSize": 14,
  "editor.formatOnSave": true,
  "eslint.validate": [
    "javascript",
    "typescript",
    "vue",
    "typescriptreact",
    "javascriptreact"
  ],
  "stylelint.validate": ["css", "less", "scss", "vue"],
  "editor.codeActionsOnSave": {
    "source.fixAll": "always",
    "source.fixAll.eslint": "always",
    "source.fixAll.stylelint": "always"
  },
  "files.exclude": {
    "**/.git": false
  },
  "prettier.useEditorConfig": false,
  "eslint.useFlatConfig": true,
  "eslint.workingDirectories": ["eslint.config.js"],
  "stylelint.packageManager": "pnpm",
  "stylelint.config": {}
}

9. 配置huskylint-staged@commitlint/cli

husky:一个为git客户端增加hook的工具 lint-staged:仅对git代码暂存区文件进行处理,配合husky使用 @commitlint/cli:让commit信息规范化

  1. 安装依赖
pnpm add husky -D
pnpm add lint-staged -D
pnpm add @commitlint/cli -D
pnpm add @commitlint/config-conventional -D
  1. 配置husky
# 生成 .husky 的文件夹
pnpm exec husky init

# 添加 hooks,会在 .husky 目录下生成一个 pre-commit 脚本文件
echo "npx --no-install lint-staged" > .husky/pre-commit

# 添加 commit-msg
echo  'npx --no-install commitlint --edit "$1"' > .husky/commit-msg
  1. 修改package.json
  "lint-staged": {
    "**/src/*.{js,jsx,ts,tsx}": [
      "eslint --fix"
    ],
    "*.{js,jsx,,md,json}": [
      "prettier --write"
    ],
    "*.ts?(x)": [
      "prettier --parser=typescript --write"
    ],
    "*.css": [
      "stylelint --fix"
    ]
  },
  1. 新建commitlint.config.cjs
module.exports = {
  extends: ['@commitlint/config-conventional'],
};

提交格式:

git commit -m <type>[optional scope]: <description> # 注意冒号后面有空格
- type:提交的类型(如新增、修改、更新等)
- optional scope:涉及的模块,可选
- description:任务描述

type类型:

类别含义
feat新功能
fix修复 bug
style样式修改(UI校验)
docs文档更新
refactor重构代码(既没有新增功能,也没有修复 bug)
perf优化相关,比如提升性能、体验
test增加测试,包括单元测试、集成测试等
build构建系统或外部依赖项的更改
ci自动化流程配置或脚本修改
revert回退某个commit提交
chore杂项
  1. 示范(非规范提交,将提交失败)
git commit -m 'feat: 增加登录功能' # 提交成功
git commit -m '修复登录失败的bug' # 将会提交失败

10. 配置unocss、添加ahooks

unocss 原子化css,ahooks一个好用的react hooks库

  1. 安装依赖
pnpm add unocss -D
pnpm add ahooks
  1. 在vite.config.ts里面引入
import UnoCSS from 'unocss/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [UnoCSS()],
});
  1. 创建src/uno.config.ts
import {
  defineConfig,
  presetUno,
  presetIcons,
  presetAttributify,
  presetTypography,
  presetWebFonts,
  transformerDirectives,
  transformerVariantGroup,
} from 'unocss';
export default defineConfig({
  presets: [
    // 默认预设
    presetUno({ dark: 'class' }),
    // 支持attributify mode,简单说就是为了避免样式写太长难维护,能将py-2 px-2这种相关属性整合起来写成p="y-2 x-4"
    presetAttributify(),
    presetIcons(),
    presetTypography(),
    presetWebFonts(),
  ],
  transformers: [transformerDirectives(), transformerVariantGroup()],
  // 自定义规则
  rules: [
    ['size-full', { width: '100%', height: '100%' }],
    [
      'flex-center',
      { display: 'flex', 'align-items': 'center', 'justify-content': 'center' },
    ],
  ],
});
  1. src/main.tsx 引入 unocss
import React from 'react';

import ReactDOM from 'react-dom/client';

import 'virtual:uno.css';

import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
  1. src/pages/home/index.tsx使用unocssahooks
import { useCountDown } from 'ahooks';
import { Button } from 'antd';

import useCounterStore from '@/store/counter';

const Home = () => {
  // const counter = useCounterStore((state) => state.counter)
  // const increase = useCounterStore((state) => state.increase);
  const { counter, increase } = useCounterStore();
  const [, formattedRes] = useCountDown({
    targetDate: `${new Date().getFullYear()}-12-31 23:59:59`,
  });
  const { days, hours, minutes, seconds, milliseconds } = formattedRes;
  return (
    <div>
      <div className="text-blue">Home Page</div>
      <button onClick={() => increase(1)}>counter: {counter}</button>
      <Button type="primary">Primary</Button>
      <p>
        There are {days} days {hours} hours {minutes} minutes {seconds} seconds{' '}
        {milliseconds} milliseconds until {new Date().getFullYear()}-12-31
        23:59:59
      </p>
    </div>
  );
};

export default Home;

11. 配置css

使用lightningcss来解析打包css,使用原生css,嵌套使用postcss-nesting插件

pnpm add lightningcss -D
pnpm add browserslist -D
pnpm add postcss-nest -D

修改vite.config.ts

import browserslist from 'browserslist';
import { browserslistToTargets } from 'lightningcss';
import postcssNesting from 'postcss-nesting';
{
  css: {
    transformer: 'lightningcss',
    lightningcss: {
      targets: browserslistToTargets(browserslist('>= 0.25%')),
    },
    postcss: {
      plugins: [postcssNesting],
    },
  },
  build: {
    cssCodeSplit: true,
    cssMinify: 'lightningcss',
  }
}

12. 配置svg

  1. 安装依赖
pnpm add vite-plugin-svgr
  1. 配置vite.config.ts
import svgr from 'vite-plugin-svgr'; // 新增

export default defineConfig({
  plugins: [
    react(),
    svgr(), // 新增
  ],
  // ...
});
  1. 配置tsconfig.app.json
{
  "compilerOptions": {
    // ...
    "types": ["vite-plugin-svgr/client", "node"]
  }
}
  1. 引用

方式一:携带后缀 ?react

import GoogleSvg from '@/assets/svg/google.svg?react'; // !不要遗漏 ?react
const Login = () => {
  return (
    <div>
      <GoogleSvg />
    </div>
  );
};

export default Login;

方式二:如果你不想要每次编写后缀,你需要修改 vite.config.ts 配置,确保在使用 svgr 插件时指定了 include 字段。如下:

svgr({ include: 'src/assets/icons/*.svg' });

13. 打包配置

  1. 分包,修改vite.config.ts
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom', 'react-router-dom'],
          lib: ['zustand']
          antd: ['antd'],
        },
      },
    },
  },

  1. 生成.gz文件
  • 安装依赖
pnpm add vite-plugin-compression -D

  • 修改vite.config.ts 默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用,使用 apply 属性指明它们仅在 buildserve 模式时调用
import viteCompression from 'vite-plugin-compression'

  plugins: [
    //...
    {
      ...viteCompression(),
      apply: 'build',
    },
  ],

  1. jscss文件分离
 build: {
  rollupOptions: {
  
    output: {
      chunkFileNames: "static/js/[name]-[hash].js",
      entryFileNames: "static/js/[name]-[hash].js",
      assetFileNames: "static/[ext]/[name]-[hash].[ext]",
    },
  },
},
  1. 分析生成包的大小
  • 安装依赖
pnpm add rollup-plugin-visualizer -D
  • 修改vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

  plugins: [
    //...
    visualizer({ open: true }),
  ],

最后项目地址: github.com/fsj930210/r…