资讯动态

React中后台脚手架完整复盘:从权限设计到性能优化

发布时间:2026/10/21 5:42:08 来源:尧图企业网站定制
1. rea 是什么一个内部中后台脚手架项目的完整复盘项目代号 rea全称是我随手缩写的 React Enterprise Application。起因很简单团队里前后端加起来不到二十人后台管理系统一年要交付七八个每次接到新项目都是从老项目里复制一份代码把登录、权限、菜单、表格那套东西原样搬过去再改改页面就上线。表面看效率挺高实际上埋了一堆雷老代码里过期的依赖没人敢动权限判断散落在各个页面组件里换了需求就要改好几个地方最要命的是一个人写的东西只有他自己看得懂。rea 就是为了终结这种复制粘贴式开发而做的内部框架。这个框架解决的核心问题一句话就能说清楚把后台系统里大量重复的基础设施抽出来变成约定让业务开发只写真正的业务逻辑。具体包括三块基于路由和按钮的权限控制、统一封装并进行异常处理的请求层、以及只需配置列配置就能生成表格页面的通用组件。三年的实际使用下来新项目从初始化到第一个可演示页面基本上能控制在两个小时以内。这篇文章写给两类人一类是刚入行一两年、正在被后台系统各种重复劳动折磨的前端看完你能理解一套脚手架背后的设计思路而不是只会用别人封装好的模板另一类是团队里还没有统一前端规范、每次开工都要从零搭一次的人我这边踩过的坑、做过的取舍、以及可以直接抄走的代码都在后面。需要提前说明的是rea 只是我们团队内部的项目代号不是什么开源项目的名字文中所有方案都是基于真实项目场景的常见实践整理出来的。2. 架构设计为什么我偏偏选这套技术栈2.1 第一版是从抄开始的但抄完必须消化做 rea 之前我先后用过好几个现成的中后台模板包括社区里很流行的那几个 React 后台项目。不可否认它们功能齐全但用起来总有一种受制于人的感觉模板作者对权限的设计、对目录的划分、对组件的抽象有自己的偏好你不一定认可一旦模板发版升级本地改过的代码很容易冲突更麻烦的是模板里很多功能你可能根本用不到却必须带着它们一起构建首屏体积动不动就几百 KB。所以做 rea 的时候我的原则是借鉴思路不照搬代码。权限怎么做、路由怎么动态加载、请求层怎么拦截这些问题先梳理成文档再自己动手一行行写。这样做的最大收益是框架里的每一处逻辑我都能讲清楚为什么存在团队里任何人接手都能顺着代码思路走。代价当然也有初期开发周期比直接套模板长了不少但从后续维护和二次开发的角度看这笔账是划算的。2.2 技术栈选型每个选择背后都有明确理由最终定下来的技术栈是 Vite React 18 TypeScript React Router v6 Zustand TanStack Query Ant Design。逐项说一下理由。构建工具选 Vite 而不是 Webpack主要是开发体验差距太明显Vite 基于 ESM 的按需编译冷启动基本在秒级热更新也是毫秒级反馈对于需要频繁调表格、调样式的后台开发场景等待时间直接决定心情。框架选 React 18 而不是 Vue没有特别的立场纯粹是因为团队现有代码存量以 React 为主换 Vue 意味着全员重新学习成本。状态管理选了 Zustand 而不是 Redux Toolkit核心原因是 Redux 的样板代码太多了。后台系统里真正需要全局共享的状态本来就不多无非是用户信息、权限列表、菜单数据、几个全局开关用 Zustand 只需要几行代码就能完成 store 的定义和订阅而且它的getState()可以在 React 组件外直接调用这对请求拦截器里读取 token 来说极其方便。服务端数据缓存选了 TanStack Query这个选择直接影响后面表格组件的设计稍后会专门展开。组件库用 Ant Design是因为它在中后台场景下覆盖度最高表格、表单、弹窗、上传这些高频组件都比较成熟加上 v5 以后改用 CSS-in-JS 方案按需样式不再需要额外的 babel 插件配置成本低很多。2.3 目录结构按类型分还是按业务分目录结构我纠结了很久。按类型分就是常见的components/、hooks/、utils/、pages/优点是结构清晰、新人容易理解缺点是业务相关代码被拆得七零八落改一个用户管理功能要同时在四五个目录里找文件。按业务分就是features/user/、features/order/这种模式优点是内聚缺点是目录一多容易重复造轮子。最终 rea 采用的是混合方案全项目共用的基础组件和工具函数走类型分目录业务功能走特性分目录。具体结构大致是src/ components/ # 通用组件ProTable、Auth、PageContainer 等 directives/ # 权限相关封装按钮级、路由级 hooks/ # 通用 hooksuseTable、usePermission 等 layouts/ # 布局组件侧边栏、顶部栏、多页签 pages/ # 页面级组件按业务模块再分子目录 router/ # 路由配置、路由守卫、动态路由生成 services/ # 请求层封装、接口定义 stores/ # Zustand store utils/ # 工具函数鉴权、格式化、常量这套结构实践下来比较顺手。共用组件放在最顶层所有人都能发现它避免重复造轮子业务页面各自成块改动影响范围可控删掉一个模块直接移除对应目录就行。2.4 服务端状态和客户端状态必须分开管这是 rea 架构设计里我认为最关键的一个认知。很多后台项目的通病是把接口请求回来的数据一股脑塞进全局 store然后在组件里手动触发重新请求。问题是这些数据本质上是服务端状态的缓存它有自己的生命周期什么时候过期、什么时候失效、多个组件共同依赖同一份数据时如何同步。这些逻辑如果揉进组件代码里很快就会被各种if (xxx null)的判断淹死。TanStack Query 解决的就是这个问题。每个接口调用通过useQuery({ queryKey, queryFn })声明queryKey 是缓存的唯一标识组件重新挂载时直接读缓存数据失效通过queryClient.invalidateQueries({ queryKey })主动触发多组件共享同一份数据的时候只要 queryKey 相同就能天然同步。而 Zustand 只负责真正的客户端状态比如 token、当前用户信息、菜单折叠状态、页面全局 loading。这个分工明确之后代码的可维护性提升了一个台阶这也是 rea 如今能支撑多个业务线同时开发的基础。3. 权限、请求、表格三大核心模块的落地细节3.1 权限体系路由权限和按钮权限分开做权限是后台系统里最容易做乱的部分。rea 把权限拆成两层路由权限控制的是你能不能访问这个页面按钮权限控制的是这个页面里的某个操作你能不能看到/点击。两者分开实现逻辑才清晰。路由权限的实现思路是用户登录后后端返回它的角色标识和权限码列表前端根据权限码动态过滤出可访问的路由表再通过 React Router v6 的createBrowserRouter注册进去。未登录或 token 失效时路由守卫统一拦截跳转登录页。这里用到了useUserStore的非组件调用能力在路由守卫里直接读 store 状态// src/router/guard.tsx import { createBrowserRouter, Navigate, useRouteError } from react-router-dom import { useUserStore } from /stores/user const WHITE_LIST [/login, /error/404] function AuthGuard({ children }: { children: React.ReactNode }) { const token useUserStore((s) s.token) const userInfo useUserStore((s) s.userInfo) const location useLocation() if (!token) { return Navigate to/login replace state{{ from: location.pathname }} / } if (!userInfo) { return PageLoading / } return {children}/ }这里注意一个细节if (!userInfo)时我渲染的是全局 loading而不是直接跳转因为这个阶段正在异步拉取用户信息和权限数据没有任何跳转动作。很多项目在这里处理不当刷新页面时会看到权限菜单先一闪而过然后再跳走体验很差。rea 的做法是在应用入口处先阻塞渲染等数据拉完再挂载路由组件后面实践部分会给出完整代码。按钮权限在 React 里没有 Vue 那种自定义指令语法但我们可以封装一个等价的能力。我的做法是一个Auth组件传入需要的权限码内部判断当前用户是否拥有// src/components/Auth.tsx import { useUserStore } from /stores/user export function Auth({ code, children }: { code: string; children: React.ReactNode }) { const hasPermission useUserStore((s) s.hasPermission(code)) if (!hasPermission) return null return {children}/ } // 用法 // Auth codeuser:createButton typeprimary新增用户/Button/Auth至于权限码本身我强烈建议用模块:操作的冒号格式比如user:create、order:export比单纯的数字 ID 可读性强得多。权限码的来源要唯一后端接口返回的权限码列表就是唯一事实来源前端永远不要写死权限判断。这样做的好处是权限调整只需要改后端配置前端代码完全不用动。3.2 请求层拦截器里处理的远远不止 token后台系统的请求层是另一个容易乱的地方。很多人只是简单封装一个 axios 实例然后把 token 塞到 header 里就完事了。rea 的请求层做了更多事情统一的 baseURL 和超时设置、请求头自动带上 token、响应统一解包、401 自动登出、错误码统一处理、请求超时和服务端异常单独提示。核心实现大致如下// src/services/http.ts import axios from axios import { message } from antd import { useUserStore } from /stores/user const http axios.create({ baseURL: /api/v1, timeout: 15000, }) // 请求拦截器注入 token http.interceptors.request.use((config) { const token useUserStore.getState().token if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一解包 http.interceptors.response.use( (response) { const { code, data, message: msg } response.data if (code 200) { return data } if (code 401) { useUserStore.getState().logout() window.location.href /login return Promise.reject(new Error(登录已过期)) } message.error(msg || 请求失败) return Promise.reject(new Error(msg || 请求失败)) }, (error) { if (error.code ECONNABORTED) { message.error(请求超时请重试) } else if (error.response?.status 500) { message.error(服务异常请稍后重试) } else if (error.response?.status 403) { message.error(没有权限执行此操作) } else { message.error(error.message || 网络异常) } return Promise.reject(error) } )这里特别提醒一下响应拦截器的返回值和 axios 默认行为不一样我们直接返回了response.data.data也就是说业务层拿到的已经是真正的业务数据不需要在每个接口调用里再解一层res.data.data。这个约定一旦定下来全项目都要遵守否则新人很容易写混。另外一个感触是401 的处理必须放在拦截器里而不是业务代码里否则每个请求失败回调都要写一遍跳登录、清空本地状态迟早会漏。3.3 配置化表格页面写业务页不再需要复制粘贴后台系统百分之八十的页面本质上是一个查询表单 一张表格 分页 操作列它们的逻辑几乎完全一样区别只是字段配置和接口地址。rea 把这块抽象成了 ProTable 组件业务侧只需要提供列配置和请求函数分页、加载、查询、重置、刷新这些交互全部收敛在组件内部。一个典型业务页面的完整代码是这样的// src/pages/user/UserList.tsx import { ProTable } from /components/ProTable import { fetchUserList } from /services/user const columns: ProColumn[] [ { title: 用户名, dataIndex: name, width: 160 }, { title: 角色, dataIndex: roleName, width: 120 }, { title: 状态, dataIndex: status, width: 100, render: (value: number) (value 1 ? 启用 : 停用), }, { title: 最近登录, dataIndex: lastLoginAt, valueType: dateTime }, ] export default function UserList() { return ProTable columns{columns} request{fetchUserList} search{{ keyword: 用户名 }} / }ProTable 内部的结构长这样关键点在于用 TanStack Query 管理查询参数// src/components/ProTable/index.tsx export function ProTable({ columns, request, search }: ProTableProps) { const [query, setQuery] useStateRecordstring, unknown({ page: 1, size: 10 }) const { data, isLoading, refetch } useQuery({ queryKey: [pro-table, query], queryFn: () request(query), }) // 内部处理排序、分页、列宽度自适应等 // 搜索表单提交时 setQuery({ page: 1, size: 10, ...values }) // 重置时 setQuery({ page: 1, size: 10 }) }用 TanStack Query 管理表格数据最大的优势是天然带了防抖、缓存和并发去重。同一个页面来回切换不会反复请求接口切回来直接命中缓存响应是即时的。要刷新数据时调用refetch或者invalidateQueries都行。这套抽象在 rea 内部叫配置化页面后来连表单的查询区也做成了配置式一个searchFields数组就能生成带默认值、带校验的查询表单业务开发基本不用碰 Ant Design 的 Form API。3.4 动态路由菜单由后端返回之后怎么落地权限模块里最难的一环是动态路由。很多后台项目菜单是前端写死的但真实场景中不同角色能看到的菜单不一样所以 rea 的做法是登录成功后后端返回一个菜单树每个节点包含路径、组件路径、标题、图标等元信息前端把它映射成 React Router 的路由表。组件映射是这里最核心的难点。后端返回的只是一个字符串比如sys/user/UserList前端怎么把它变成真正的组件我用的方案是import.meta.glob全量注册页面组件再按字符串找组件// src/router/dynamic.ts const modules import.meta.glob(/src/pages/**/*.tsx) function lazyLoad(componentPath: string) { const loader modules[/src/pages/${componentPath}.tsx] if (!loader) { return Navigate to/error/404 replace / } const Component lazy(loader as () Promise{ default: React.ComponentType }) return ( Suspense fallback{PageLoading /} Component / /Suspense ) } export function generateRoutes(menus: MenuItem[]): RouteObject[] { return menus.map((menu) ({ path: menu.path, element: lazyLoad(menu.component), children: menu.children?.length ? generateRoutes(menu.children) : undefined, })) }用import.meta.glob的好处是构建时 Vite 会做静态分析自动把所有页面组件打进单独的 chunk配合路由级懒加载首屏只加载当前用到的页面。这里的隐患是动态路由拼接发生在运行时后端返回的组件路径一旦和目录结构对不上就会白屏所以我们前端约定了一个硬规则后端菜单里的component字段必须和src/pages下的相对路径完全一致比如sys/user/UserList对应src/pages/sys/user/UserList.tsx。4. 从零搭到上线完整实操记录与关键代码4.1 初始化工程pnpm Vite 的环境准备搭一个 rea 这样的项目并不神秘关键步骤就那么多难的是每一步背后的参数选择。先说工程初始化这一步我推荐直接用 pnpm而不是 npm 或 yarn。pnpm 的硬链接 符号链接机制能显著减少磁盘占用多个项目共用一套依赖也不会冲突更重要的是它天生具备幽灵依赖检测能力package.json 里没声明却能 import 到的第三方包会直接报错这在做框架时尤其有用逼着每个人显式声明依赖层级清晰很多。初始化命令很简单mkdir rea cd rea pnpm create vite . --template react-ts pnpm install接下来安装核心依赖。注意版本号我没有写死实际使用时建议以安装时的最新稳定版为准锁定版本交给 lock 文件# 运行时依赖 pnpm add react-router-dom tanstack/react-query zustand axios antd dayjs # 开发依赖 pnpm add -D types/node vite-plugin-compression装完后第一件事是配置路径别名不然../../../../这种引用能把人看疯。在vite.config.ts里加resolve.alias再在tsconfig.json里加上对应的baseUrl和paths两边必须保持一致// vite.config.ts import { defineConfig } from vite import react from vitejs/plugin-react import path from path export default defineConfig({ plugins: [react()], resolve: { alias: { : path.resolve(__dirname, src), }, }, server: { proxy: { // 本地开发时把 /api 开头的请求代理到后端 /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (p) p.replace(/^\/api/, ), }, }, }, })这里代理配置值得多说一句。开发环境走代理生产环境由 nginx 或网关统一转发这是前后端分离项目的标准姿势。如果你把http://localhost:8080直接写死在 axios 的 baseURL 里上线后要么 ajax 请求头带着本机地址被拦截要么还得再改代码不划算。统一走相对路径/api环境切换只动配置不动代码。4.2 核心代码路由守卫、权限组件、请求封装工程搭好后按照依赖关系先写 stores、再写 services、再写 router顺序不能乱因为后面每个模块都要依赖前面的状态。用户 store 是最底层的模块它负责 token 的持久化和用户信息的异步加载// src/stores/user.ts import { create } from zustand import { fetchUserInfo } from /services/user interface UserState { token: string userInfo: null | { id: number; name: string; perms: string[]; menus: MenuItem[] } login: (token: string) void fetchUserInfo: () Promisevoid hasPermission: (code: string) boolean logout: () void } export const useUserStore createUserState((set, get) ({ token: localStorage.getItem(rea_token) || , userInfo: null, login: (token) { localStorage.setItem(rea_token, token) set({ token }) }, fetchUserInfo: async () { const userInfo await fetchUserInfo() set({ userInfo }) }, hasPermission: (code) Boolean(userInfo?.perms.includes(code)), logout: () { localStorage.removeItem(rea_token) set({ token: , userInfo: null }) }, }))注意这里我把hasPermission定义在 store 上而不是单独的工具函数里这样权限判断逻辑集中在一处避免在组件里出现userStore.userInfo?.perms.includes(...)这种散落代码。login方法里同时写了localStorage和set两者一定同步比如 token 要持久化就都持久化不持久化就都不要否则会出现内存里有 token、刷新后却跳走的情况。路由守卫的落地我选择在应用入口处用一个封装组件统一处理。要理解这个思路得先想明白 React Router v6 的守卫逻辑和 Vue 路由守卫不同React 没有beforeEach钩子而是用组件包裹的方式实现。rea 的做法是定义一个RootProvider在路由渲染之前检查登录状态和数据加载状态// src/router/RootProvider.tsx import { Navigate, useLocation } from react-router-dom import { useUserStore } from /stores/user export function RootProvider({ children }: { children: React.ReactNode }) { const location useLocation() const token useUserStore((s) s.token) const userInfo useUserStore((s) s.userInfo) const fetchUserInfo useUserStore((s) s.fetchUserInfo) // 还在白名单页面直接放行 if (location.pathname /login || location.pathname /error/404) { return {children}/ } // 没有 token跳登录页并把来源路径带上 if (!token) { return Navigate to/login replace state{{ from: location.pathname }} / } // 有 token 但用户信息没加载则先加载再渲染 children if (!userInfo) { return ( Suspense fallback{PageLoading /} AsyncInit onInit{fetchUserInfo}{children}/AsyncInit /Suspense ) } return {children}/ }这个AsyncInit组件内部一个useEffect触发fetchUserInfo加载中显示全局 loading完成后渲染 children。这样能保证菜单数据和权限数据一定在页面组件渲染前就绪不会出现刷新页面时菜单先闪一下再重新生成的尴尬。我见过太多后台系统在这个细节上翻车必须把它当成硬性要求来做。请求封装在 3.2 节已经给出这里不再重复代码但要补充一个实操心得接口定义最好集中放在services目录并且所有接口返回类型都用 TypeScript 泛型标注。比如fetchUserList(params): PromisePageResultUser这样业务层拿到的数据天然有类型提示写错字段名在编译期就会报错而不是等到运行时才发现。4.3 动态菜单和路由注册的完整串联等到用户信息拉取完了我们就可以根据返回的菜单数据生成路由了。这里的顺序是先通过generateRoutes(menus)生成业务路由表再把它和固定的登录页、404 页、布局组件拼成完整的路由对象。// src/router/index.tsx import { createBrowserRouter } from react-router-dom import { useUserStore } from /stores/user import { generateRoutes } from ./dynamic export function buildRouter() { const menus useUserStore.getState().userInfo?.menus ?? [] const dynamicRoutes generateRoutes(menus) return createBrowserRouter([ { path: /login, element: LoginPage /, }, { path: /, element: RootProviderMainLayout //RootProvider, children: dynamicRoutes, }, { path: *, element: NotFoundPage /, }, ]) }关于createBrowserRouter一个非常容易踩的坑是 history 路由模式刷新 404。BrowserRouter 依赖前端路由 history fallback如果部署的服务器没有把未知路径重写到index.html用户一刷新就直接请求了这个不存在的路径返回 404。这个问题和前端代码无关是服务器配置问题后面排查章节再说具体解法。开发环境下 Vite 内置了 fallback所以这个坑往往要等到测试环境才暴露。4.4 构建优化从首屏 800KB 压到 200KB 以内rea 第一个正式项目上线后我做的第一件事就是开 DevTools 看请求体积结果吓了一跳光入口 JS 就有 800 多 KB这还是没有 gzip 的原始体积。首屏加载慢得让人怀疑人生尤其后台系统面对的往往是内网弱网环境体验非常糟糕。第一刀砍在路由懒加载上。React Router v6 支持通过React.lazySuspense实现路由级代码分割我把所有业务页面改成了懒加载。这个改动立竿见影入口 chunk 从 800KB 直接降到 300KB 左右但还不够。第二刀是手动分包把体积庞大的第三方库拆成独立 chunk。放在vite.config.ts的build.rollupOptions.output.manualChunks里// vite.config.ts build: { sourcemap: false, rollupOptions: { output: { manualChunks: { react-vendor: [react, react-dom, react-router-dom], antd-vendor: [antd, ant-design/icons], query-vendor: [tanstack/react-query], }, }, }, },这样设置的目的是充分利用浏览器缓存项目代码频繁编译上线但 react、antd 这些大依赖版本稳定独立 chunk 的 hash 不变的话用户访问时可以直接命中缓存不用反复下载。实际优化后首屏只要加载index、react-vendor、antd-vendor、axios-vendor四个 chunk再配合 nginx 开启 gzip首屏传输量压到 200KB 以内白屏时间从原来的 3 秒以上降到 1 秒左右。第三刀是压缩产物。本地装一个vite-plugin-compression构建时自动生成 .gz 文件前提是服务器能正确处理.gz的 Content-Encoding。如果服务器不支持预压缩产物也可以直接用 gzip 在线压缩效果一样只是稍微增加点 CPU 开销。这三个优化是性价比最高的组合强烈建议所有做后台项目的都配上。5. 常见问题排查实录刷新 404、请求缓存、样式错乱5.1 刷新页面 404却以为是前端路由写错了这是 BrowserRouter 最经典的坑。现象是在页面上点击路由跳转正常但地址栏手动刷新或者直接输入 URL 就 404。第一反应往往是在前端找原因检查路由配置、检查 basename、检查有没有拼错路径折腾半天发现代码完全没问题。问题的根源在服务器。所有前端路由都是 History API 在浏览器端模拟的服务器上的真实文件里并不存在/user/list这个路径刷新时请求发到服务器服务器找不到对应资源就返回 404。解决方案是要在服务器层配置 fallback所有未知路径都重写到index.html让前端路由接管。以 nginx 为例这是标准配置location / { try_files $uri $uri/ /index.html; }排查这类问题的思路我后来总结成一个口诀先分端再分级。分端是看请求到底打到哪一端F12 网络面板里那个红色 404 请求的 URL如果是完整的前端地址说明请求已经发出去了问题多半在服务器如果根本没发出请求那才是前端路由的问题。分级是看 404 是在页面层面还是资源层面页面 404 通常是路由 fallback 没配好图片、CSS、JS 404 则多半是静态资源路径前缀配错了比如部署到子路径但没有在 Vite 里配置base。5.2 表格的查询条件好像被缓存了切换参数不生效用上 TanStack Query 之后团队里出现了一个让我印象深刻的 bug某个表格第一次按关键词查询结果正常换一个关键词再查数据还是旧的必须手动刷新页面才更新。看代码觉得逻辑完全正确setQuery调用触发了重新渲染新的关键词也传进去了但useQuery返回的数据就是不动。原因其实不复杂TanStack Query 的缓存机制依赖queryKey只有queryKey变了才会重新请求我一开始只用了[pro-table]作为 key没有把查询参数放进去导致每次都是同一个 key命中缓存直接返回旧数据。修正后const { data, isLoading } useQuery({ queryKey: [pro-table, JSON.stringify(query)], queryFn: () request(query), })这次踩坑给我一个重要教训用任何带缓存能力的数据请求库第一件事就是搞清楚它的缓存键是怎么生成的键的设计直接决定数据更新的正确性。后来我们做了一个约定所有接口的查询参数都要序列化进 queryKey并且保证参数的顺序一致。这样既不会出现脏数据也方便在 DevTools 里精确看出当前页面是基于什么参数请求的。5.3 开发环境下接口请求了两次差点改成请求去重这个现象几乎每个用 React 18 的开发者都会遇到页面一加载同一个接口在 Network 面板里出现两次请求而且长得一模一样。我当时的第一反应是请求层重复触发了准备加一个全局请求去重逻辑后来仔细排查才发现这根本是 React StrictMode 在开发环境下的故意行为。React 18 的 StrictMode 会在开发模式下对组件的挂载、卸载、再挂载做一次额外的模拟目的是提前暴露组件内部不安全的副作用。如果你在组件里直接写了useEffect(() { fetchData() }, [])这个副作用会被执行两次这是 React 官方有意为之的提醒不是 bug。生产环境的构建里去掉了 StrictMode 的额外行为请求只会发一次。正确的应对有两种。一是接受这个行为只要确保接口是幂等的GET、查询类接口天然幂等开发时多一次请求并不影响正确性。二是把严格模式去掉但我不建议这么做因为 StrictMode 对发现内存泄漏、不规范副作用很有价值。如果你的场景实在对重复请求敏感比如本地开发联调时后端告警也可以在useEffect里加一个标识变量控制但千万别为了这个去全局加请求去重那会掩盖真正的问题。5.4 打包后样式错乱的真正原因不是 CSS 冲突还有一个印象很深的坑本地开发一切正常打出来的测试环境包Ant Design 的某些弹窗和抽屉组件样式完全是乱的像是少了 CSS。一开始以为是样式顺序问题找了半天 CSS 都没有后来才意识到这可能和 antd v5 的 CSS-in-JS 机制有关。Ant Design v5 用的是 CSS-in-JS样式是通过运行时注入到style标签里的。如果页面数据是动态渲染的且组件的样式 hash 生成时机不对可能会出现样式丢失。排查步骤是看浏览器控制台有没有类似 find style 的警告再检查ConfigProvider的配置。我在 rea 里最终使用的方案是给ConfigProvider设置theme{{ hashed: process.env.NODE_ENV ! production }}在动态样式场景下关闭 hash本质上让样式选择器更简单避免动态渲染时的样式匹配问题。这类问题的排查思路是本地正常、生产异常优先怀疑构建配置和运行环境的差异不要一上来怀疑第三方库。把问题逐层剥离先看是不是资源加载失败再看是不是样式注入时机不对再看是不是配置项有环境差异基本都能找到根因。5.5 常见问题速查表现象根因解决方案刷新页面 404服务器未配置 history fallbacknginx 加try_files $uri $uri/ /index.html表格查询条件不刷新queryKey 没有包含查询参数将查询参数序列化后放入 queryKey开发环境接口请求两次React StrictMode 的刻意行为确认接口幂等即可无需修改打包后弹窗样式错乱antd v5 动态样式 hash 问题按环境关闭 theme hashed动态路由刷新白屏组件路径与后端菜单配置不一致约定菜单 component 字段与 pages 路径严格对应点击按钮没反应、控制台无错误按钮被 Auth 组件拦截渲染成了 null检查权限码是否正确、用户权限列表是否已加载构建产物极大缺少路由懒加载和手动分包配合 React.lazy 和 manualChunks 优化6. 后续想做的事与实际操作中沉淀的体会rea 做到现在这个程度满足日常业务开发已经问题不大但我心里清楚它还有很长的路要走。团队里已经有人提出希望把 ProTable 进一步扩展成支持拖拽排序、列固定、导出 Excel 这些高级能力还有人建议做一个可视化表单设计器让业务同学自己配置查询表单。这些方向我都认同但每次动手前我会提醒自己一句话框架的每一行代码都会有团队里的人读宁可慢一点也要保证设计的一致性。实际维护这几年来我最大的体会反而是技术之外的东西一套内部框架能不能存活不取决于它的代码有多优雅而取决于用它的团队是不是真的理解它的约束。很多人拿到框架后第一反应是这个权限组件不好用我要改那个 ProTable 不灵活我要自己写一个结果项目里很快就出现了两套半标准的写法。所以我在团队里定了两个规矩一是所有公共能力的变更必须先在框架仓库里做业务项目只允许引用框架而不是修改框架二是新人入职第一周不看业务代码先花时间通读 rea 的目录结构和核心模块理解每个约定背后的原因。最后分享一个小的实操技巧也是我在框架里保留至今的一个习惯所有业务页面的接口请求建议统一放在services目录下通过http.get、http.post这些封装方法调用不要在页面组件里直接用 axios 或者裸 fetch。这样做的第一个好处是接口地址集中管理后端改了路径只动一处第二个好处是后续如果需要给某个模块统一加埋点或者加缓存只需要在services层做一层包装业务代码完全无感。就凭这一条后面避免的返工就远超我当初封装那点代码的时间成本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑