React18 + Vite + TS + Recoil + RouterV6 |管理端项目实践 🐱‍🏍🐱‍🏍🐱‍🏍

React18 + Vite + TS + Recoil + RouterV6 |管理端项目实践 🐱‍🏍🐱‍🏍🐱‍🏍

前言

这是一个基于React18并使用Vite4.0构建的管理端项目。它实现了动态路由,标准的RBAC权限模型和多角色多岗位权限模型,包括登录鉴权,菜单管理等功能。

项目末尾附有源码地址和项目预览图(本项目仅供学习React相关知识使用)。

一、项目介绍

  1. 项目使用最新的Vite进行构建。经过以往项目的实践,我们发现Vite的打包和项目启动速度远高于Webpack(具体实践过程将在后续文章中讲述)。因此,本项目也是对Vite的一次学习。
  2. 项目使用React18。目前,hooks已经成为了React的主流,本项目完全拥抱函数化组件,不再使用Class来实现组件。
  3. 项目使用Recoil,这是下一代的React官方状态管理工具(后续考虑使用hox)。我们放弃了使用臃肿庞大的redux,转而使用原子化的状态管理工具。
  4. 项目使用react-router-dom V6。V6版本的路由相比之前有较大升级,包括路由的跳转,路由表配置,路由鉴权等等。
  5. 项目使用TS。在多人协作的项目中,TS能避免很多语法和类型判断上的错误(例如数据处理与展示时的空字符串与0与null的问题),因此,本项目也是对TS的一次实践。
  6. 项目中也配置了ESlinthusky。由于每个人都有自己的编码风格,在项目迭代过程中更换开发人员可能会导致项目越来越臃肿。使用这两个工具能在一定程度上改善这种情况。

二、搭建基础项目

  1. 直接使用Vite创建一个基础模板项目 Vite链接☞ 开始 | Vite 官方中文文档 (vitejs.dev)
lua 复制代码
pnpm create vite react-admin --template react-ts
  1. 进入项目在项目根目录安装依赖并启动项目(推荐使用pnpm)
css 复制代码
pnpm i
pnpm dev
  1. 控制台输出如下则证明启动成功

    arduino 复制代码
      VITE v4.5.0  ready in 332 ms
    ​
      ➜  Local:   http://127.0.0.1:5173/
      ➜  Network: use --host to expose
      ➜  press h to show help
  2. 手动打开链接即可看到效果,到此一个基础的React项目就创建完成

  3. 简单配置vite.config.ts

    php 复制代码
    import react from '@vitejs/plugin-react'
    import path, { resolve } from 'path'  // 需要安装@types/node@18.14.2才能识别node模块
    import { defineConfig } from 'vite'
    export default defineConfig({
      base: '',
      plugins: [
        react()
      ],
      // 配置css加载器  需要下载less@4.1.3  less-loader@11.1.0
      css: {
        preprocessorOptions: {
          less: {
            javascriptEnabled: true
          }
        },
      },
      resolve: {
        //设置路径别名
        alias: {
          '@': resolve(__dirname, 'src/'),
          'views/*': resolve(__dirname, 'src/views'),
          '@icons': resolve(__dirname, 'src/assets/svg/'),
          'components/*': resolve(__dirname, 'src/components'),
          '*': resolve('')
        }
      },
      server: {
        port: 1102, // 自定义端口
        host: '0.0.0.0',
        open: true, // 自动打开浏览器
        proxy: { // 设置代理
          '/api': {
            target: 'http://127.0.0.1:1103/', // easymock
            changeOrigin: true,
            rewrite: (path) => path.replace(/^/api/, '')
          }
        }
      },
      build: { // 打包设置
        manifest: true,
        rollupOptions: {
          output: {
            sourcemap: false
          }
        }
      }
    })

三、使用RouterV6实现动态路由及鉴权

  1. 安装react-router-dom@^6.3.0 V6版本官网 ☞ Docs Home v6.3.0 | React Router

    css 复制代码
    pnpm i react-router-dom@^6.3.0
  2. 新建404,login,home页面以及router配置,命名规则组件文件夹名称大写组件以index.tsx形式存在方便后面路由匹配

    css 复制代码
    ├── src
        ├── router
        |    └── index.tsx
        ├── views
             ├── 404
             |    └── index.tsx
             ├── Home
             |    └── index.tsx
             └── Login
                  └── index.tsx
    javascript 复制代码
    // 组件内容如下
    function Login() {
      return (
        <>
          <h1>Login</h1>
        </>
      )
    }
    export default Login
    ​
  3. 编辑router下面的index.tsx配置路由表信息下面是我们最常用的引入方式(这样会引入所有的路由无论是否用到)

    javascript 复制代码
    import Error from '@/view/404';
    import Home from '@/view/Home';
    import Login from '@/view/Login';
    import { Navigate, useRoutes } from 'react-router-dom';
    ​
    // 定义默认的路由表
    export const defRouter: Array<Router> = [
      // 需要在路由最前面添加 优先匹配 重定向
      {
        path: '/',
        name: '',
        isShow: false,
        element: <Navigate to={'/home'} />
      },
      {
        path: '/login',
        name: '登录',
        isShow: false,
        element: <Login></Login>
      },
      {
        path: '*',
        name: '404',
        isShow: false,
        element: <Error></Error>
      },
      // 这里是后面的异步路由,通过异步导入就是这样
      // {
      //   path: '/home',
      //   name: '主页',
      //   isShow: true,
      //   element: lazyLoad("Home")
      // }
    ]
  4. 我们可以使用懒加载来引入组件。lazyreact提供的组件懒加载工具,我们可以使用vite来动态导入所有的组件,并创建一个方法,传入菜单名称自动懒加载对应组件。需要注意的是,lazy需要配合Suspense组件一起使用。由于组件是通过懒加载加载进来的,所以渲染页面的时候可能会有延迟,但使用了Suspense之后,可以优化交互。我们可以继续编辑index.tsx,懒加载Home等可配置的菜单组件。

    typescript 复制代码
    import { lazy, useState } from 'react';
    ​
    const Mod: any = import.meta.glob('../views/**/*.tsx') // 在vite中必须这样动态引入所有组件地址
    ​
    // 快速导入工具函数
    const lazyLoad = (moduleName: string) => {
      const Module = lazy(Mod[`../views/${moduleName}/index.tsx`])
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <Module></Module>
        </Suspense>
      )
    }
  5. 工具后台返回菜单实现动态懒加载路由 (这里模拟后台返回的菜单),并将异步路由和默认的路由结合生成最终的路由表,继续编辑index.tsx如下

    javascript 复制代码
    // 假设后台给我们返回了一个菜单
    const menu = [
        {
            "id": 8,
            "name": "主页",
            "icon": "icon-aliens-fill",
            "path": "/home",
            "component": "Home", // 上面说到了组件文件夹名大写并在子文件index.tsx中实现
            "sort_num": 1,
            "redirectTo": "",
            "parent_id": null,
            "isShow": true,
            "create_time": "2023-03-17T08:06:21.283Z"
        }
    ]
    ​
    // 根据菜单筛选路由
    const filterAsyncRouter = (menus: Array<Router> = []) => {
      const addRouter: Array<Router> = []
      menus.forEach((item: Router) => {
        const route: Router = {
          name: item.name,
          path: item.path,
          isShow: item.isShow,
          element: ''
        }
    ​
        route.element = lazyLoad(item.component)  // 懒加载路由
    ​
        if (item.children) {
          route.children = filterAsyncRouter(item.children) // 如有有嵌套路由则递归加载
        }
    ​
        addRouter.push(route)
      })
      return addRouter  // 返回动态路由表
    }
    ​
    // 合并路由
    const marRouter = (arr) => {
      return [
        ...defRouter,  // 上面我们配置了默认路由
        ...arr// 将异步路由合并到
      ]
    }
    ​
  6. 最后实现路由组件

    scss 复制代码
    const RouterCom = () => {
      const [routerList, setrouterList] = useState<Router[]>([])
    ​
      useEffect(() => {
        const asyncArr = filterAsyncRouter(menu)
        setrouterList(marRouter(asyncArr)) // 合并异步路由
      }, [menu])
    ​
      const routes = useRoutes(routerList) // 生成路由表结构
      return routes
    }
    ​
    export default RouterCom // 返回给APP.tsx使用
  7. 返回App.tsx使用路由

    javascript 复制代码
    import Router from '@/router'
    function App() {
      console.log('APP 渲染')
      return <Router></Router>
    }
    ​
    export default App
  8. main.tsx中使用HashRouter包裹组件

    javascript 复制代码
    import ReactDOM from 'react-dom/client'
    import { HashRouter } from 'react-router-dom'
    import App from './App'
    import './index.css'
    ​
    ReactDOM.createRoot(document.getElementById('root')!).render(
      <HashRouter>
        <App />
      </HashRouter>,
    )
  9. 路由鉴权,在src目录下新建Auth文件夹,添加子文件index.tsx如下,通过判断tokenRec不存在重定向到login

    typescript 复制代码
    import { Navigate, useLocation } from 'react-router-dom'
    ​
    const Auth = (props: any) => {
      const { children } = props
      const { pathname } = useLocation()
      const tokenRec = ''
      return <>{tokenRec || pathname === '/login' ? children : <Navigate to="/login" replace />}</>
    }
    ​
    export default Auth
  10. App.tsx中使用,以上就实现了简单动态路由及路由鉴权,后续更新使用recoil将路由保存在本地并生成菜单实现点击跳转

    javascript 复制代码
    import Auth from '@/Auth'
    import Router from '@/router'
    function App() {
      console.log('APP 渲染')
      return (
        <Auth>
          <Router></Router>
        </Auth>
      )
    }
    ​
    export default App

四、配置烦人的Eslint和husky(根据个人或团队可跳过)

  1. 使用pnpmdev依赖中安装对应包

    json 复制代码
    "eslint": "^8.45.0",
    "eslint-config-prettier": "^8.6.0",
    "eslint-config-react-app": "^7.0.1",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "prettier": "2.8.4",
  2. 并在package.json中配置格式化脚本

    json 复制代码
    // 脚本如下
    "format": "prettier --write .",
    "lint": "eslint src --ext .ts,.tsx,.js,.jsx --fix --report-unused-disable-directives --max-warnings 0"
    ​
    // "--fix":这个选项会让 ESLint 自动修复可以被自动修复的问题。
    // "--report-unused-disable-directives":这个选项会让 ESLint 报告所有没有用到的 eslint-disable 注释。这可以帮助你发现并清理掉代码中不必要的 eslint-disable 注释。
    // "--max-warnings 0":这个选项会让 ESLint 在发现任何警告时返回一个非零的退出码,这将导致 npm 或 yarn 命令失败。这可以帮助确保所有的 ESLint 警告都被当作错误处理。
  3. 在项目根目录添加.eslintrc.cjs.prettierrc文件,内容如下

    java 复制代码
    /**
     * eslint: eslint的核心代码。
     * @typescript-eslint/parser: eslint的解析器,用于解析typescript,从而检查和规范Typescript代码。
     * @typescript-eslint/eslint-plugin: 这是一个eslint插件,包含了各类定义好的检测Typescript代码的规范。
     */
    module.exports = {
      parser: '@typescript-eslint/parser', // 定义eslint的解析器,在ts项目中必须指定解析器为@typescript-eslint/parser,才能正确的检测和规范TS代码。
      extends: [
        'eslint:recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:react-hooks/recommended',
        'plugin:prettier/recommended'
      ],
      plugins: ['react-refresh'],
      parserOptions: {
        ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features
        sourceType: 'module', // Allows for the use of imports
        ecmaFeatures: {
          jsx: true // Allows for the parsing of JSX
        }
      },
      settings: {
        react: {
          version: 'detect' // Tells eslint-plugin-react to automatically detect the version of React to use
        }
      },
      env: {
        es2021: true,
        // 指定代码的运行环境
        browser: true, // env环境变量配置,形如console属性只有在browser环境下才会存在,如果没有设置支持browser,那么可能报console is undefined的错误。
        node: true
      },
      rules: {
        quotes: ['error', 'single'],
        '@typescript-eslint/explicit-module-boundary-types': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/no-empty-function': 'off',
        '@typescript-eslint/no-this-alias': 'off',
        '@typescript-eslint/no-unused-vars': 'off',
        'react-hooks/exhaustive-deps': 'off',
        'react/prop-types': 'off',
        'react/display-name': 'off',
        'react/react-in-jsx-scope': 'off',
        'react/jsx-uses-react': 'off',
        '@typescript-eslint/no-var-requires': 'off'
      }
    }
    json 复制代码
    // .prettierrc 配置
    {
      "useTabs": false,
      "tabWidth": 2,
      "printWidth": 100,
      "singleQuote": true,
      "trailingComma": "none",
      "bracketSpacing": true,
      "jsxBracketSameLine": true,
      "semi": false
    }
  4. 以上的规则可自行添加或者删除,如遇到保存格式化代码时代码被格式化两次是因为Eslintprettierrc有冲突,上面我们已经配置了冲突时使用prettierrc规则来格式化代码,可以重启vscode让配置生效

  5. 配置husky提交代码时安装对应包"husky": "^8.0.0","lint-staged": "^15.0.2"

    json 复制代码
    "husky": "^8.0.0",
    "lint-staged": "^15.0.2",
  6. 安装完成后运行以下命令,

    csharp 复制代码
    pnpm dlx husky-init
  7. 执行如上命令后,项目里生成.husky文件夹,文件夹下有个pre-commit文件,最后一行的命令pnpm lint即为在commit之前插入执行的命令,lint命令我们已经配置了他会检查所有文件

    bash 复制代码
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"
    ​
    pnpm lint
  8. 一般我们会使用lint-stagedgit add到暂存区的文件进行lint检查,而不是直接对所有文件检查,配置如下

    json 复制代码
    {
      "scripts": {
        "prepare": "husky install",
        "lint-staged": "lint-staged"
      },
      "lint-staged": {
        "*.{js,ts,jsx,tsx}": [
          "prettier --write",
          "eslint --cache --fix",
          "git add"
        ]
      }
    }
    bash 复制代码
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"
    ​
    pnpm lint-staged

五、根据路由配置菜单(Recoil的使用)

  1. 上班期间时间实在有限😳,这个在下一篇Recoil的实践中讲到 文章末尾附源码(包含完整项目)

六、最新项目截图

最后

相关推荐
法欧特斯卡雷特6 分钟前
从 Kotlin 编译器 API 的变化开始: 2.2.2X -> 2.3.0-Beta1
后端·架构·开源
zjjuejin11 分钟前
Maven 现代开发流程的集成
java·后端·maven
牧羊人_myr12 分钟前
Ajax 技术详解
前端
浩男孩21 分钟前
🍀封装个 Button 组件,使用 vitest 来测试一下
前端
hrrrrb23 分钟前
【Spring Boot】Spring Boot 中常见的加密方案
java·spring boot·后端
蓝银草同学26 分钟前
阿里 Iconfont 项目丢失?手把手教你将已引用的 SVG 图标下载到本地
前端·icon
Lilian26 分钟前
Trae通过ssh远程访问服务器linux系统不兼容问题
后端·trae
123445235 分钟前
Spring Boot 启动流程全解析:从 SpringApplication.run() 到 Bean 初始化与自动配置
后端
布列瑟农的星空36 分钟前
重学React —— React事件机制 vs 浏览器事件机制
前端