☰
Spring WebFlux + antd pro v5 管理后台配置实战
2026/10/14 11:46:13 网站建设 项目流程

如果你是从这个系列一路跟过来的,到这里应该已经有点迫不及待了。前三篇我们把 Spring WebFlux 的后端骨架跑通了,React 的基础工程也立起来了,但说实话,那时候前端页面还停留在“能打开、能跳转”的阶段,离一个真正可用的后台管理系统还有不小的距离。这一篇要做的,就是把它真正“设置”到位——路由、菜单、请求层、登录态、主题布局,一步步调成能跟 WebFlux 后端顺畅协作的样子。文章适合已经装了脚手架但还没动手改配置的人,也适合在 antd pro v5 里翻过跟头、想弄清楚配置文件到底谁管谁的人。下面我就按实际动手的顺序,把这次改造里的关键设置和踩过的坑完整讲一遍。

1. 先搞清楚:antd pro v5 这套东西到底怎么组织配置

很多人第一次上手 antd pro v5,最大的困惑不是写页面,而是“配置到底该写在哪”。它不像传统 Spring Boot 项目那样有个application.yml一把梭,而是把配置拆成了三个层面,每个层面管的事完全不一样。理解这个分层,后面的所有设置才不会乱套。

第一层是编译期配置,集中在config/目录下。config/config.ts是入口,里面会引用config/routes.ts的路由表、config/defaultSettings.ts的默认布局主题。这些配置在 Umi 4 启动时被读取、编译,改动大部分需要重启 dev server 才能完整生效。第二层是运行期配置,核心是src/app.tsx。它导出的layout、request、getInitialState等约定式导出,会在运行时被框架消费,比如动态决定当前用户信息、统一处理请求拦截。这一层改了通常热更新就能看到效果。第三层才是业务代码里的配置,比如页面里ProTable的request、access.ts里的权限判断,这部分跟着业务走,灵活性最高。

这三层最容易被搞混的地方在于:有人想在运行期动态改菜单,结果菜单是路由表生成的;有人想在defaultSettings.ts里改某个 token 主题色,结果 antd 5 的主题 token 又偏向运行期控制。我自己的经验是,先按“编译期管结构和默认值,运行期管状态和拦截,业务期管权限和请求数据”这个口诀来定位,基本不会找错文件。

另外要提醒一点,mock/目录下的假数据接口只在 dev 环境生效,它不属于上述三层配置,但会在你调试后端联调时狠狠插一脚,这个坑我放到后面专门讲。整体文件结构大致是这样:

config/ config.ts # Umi 主配置:路由引用、代理、构建项 routes.ts # 路由表,菜单栏由它生成 defaultSettings.ts # 默认布局与主题 openapi.ts # 后端接口文档生成前端服务的配置 src/ app.tsx # 运行期入口:layout / request / getInitialState access.ts # 权限定义 pages/ # 页面组件 services/ # 根据 OpenAPI 自动生成的接口层 utils/ # 工具函数 mock/ # 开发期假数据

这套分层的设计思路,本质上是把“框架级配置”和“业务级配置”解耦。你在config.ts里改的是项目骨架,在app.tsx里改的是运行时行为,在页面里改的是业务逻辑。下面几节我就按这个顺序展开。

2. 初始化与开发环境:跑起来之前先解决三个坑

2.1 脚手架初始化与版本选型

第2篇我们已经把前端工程初始化过了,这里快速回顾一下,重点说版本。antd pro v5 的脚手架是基于 Umi 4 的,官方推荐用npm create umi选择 ant design pro 模板,或者直接拉取官方仓库的稳定分支再pnpm install。我的建议是:Node 用 18 LTS,包管理器用 pnpm。Node 16 也不是不行,但 Umi 4 的部分插件在新版本里已经不太照顾 16 了,碰到奇怪的编译报错浪费一小时不值得。pnpm 相比 npm 在依赖安装速度和磁盘占用上优势明显,但这个项目真正的问题不是安装慢,而是依赖树复杂,pnpm 的严格依赖管理反而能提前暴露一些幽灵依赖问题。

如果你用的是 Node 17 以上,跑npm run dev偶发报digital envelope routines::unsupported,那不是 Umi 4 的问题,是你历史项目里残留的 webpack 4 习惯性思维。Umi 4 默认 webpack 5,一般不触发这个 OpenSSL 报错。真遇到内存溢出,优先检查是不是开了太多进程,或者用NODE_OPTIONS=--max-old-space-size=4096临时顶一下,治标不治本,长期还是建议统一 Node 版本。

2.2 坑一:mock 数据挡在代理前面

脚手架自带的mock/目录里有一堆假接口,比如用户登录、当前用户信息。这个设计在纯前端调试时很爽,但当你后端 WebFlux 已经跑在 8080 端口、准备联调时,它就成了最大的拦路虎。现象是:你在config.ts里配好了代理指向http://localhost:8080,浏览器 Network 面板里看到的请求也打到了 8000 端口的/api/xxx,但返回的数据明显不是后端给的,而是 mock 的假数据。

这是因为 Umi 4 的 mock 机制在 dev server 内部优先匹配了/api前缀的路由。解决方式很简单,在config/config.ts里把 mock 关掉:

export default defineConfig({ // ... mock: false, });

或者只排除特定前缀:

mock: { exclude: ['/api/**'], },

我的建议是直接mock: false,除非你还需要用假数据做前端原型演示。联调阶段最大的敌人就是“不知道数据到底是哪来的”,关掉 mock,心里踏实。

2.3 坑二:代理路径的pathRewrite到底该不该写

config.ts里的代理配置是前后端联调的咽喉,我这次用的是这套:

proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' }, }, },

这里最关键的是changeOrigin: true。它会把请求头里的Host改写成 target 的域名,后端如果做了域名校验就不会误判。至于pathRewrite要不要去掉/api前缀,取决于你后端的接口路径。如果 WebFlux 的 controller 直接暴露/user/login、/dashboard/stats,那就把/api剥掉;如果后端自己加了server.servlet.context-path=/api,那就不用 rewrite。这个决策要跟后端约定一致,否则 404 找半天才发现是前缀重复了。

提一句,这套 proxy 只在umi dev时生效,umi build产物里是没有的。生产环境要么用 nginx 转发,要么让后端网关处理,这个放到第7节。

3. 路由与菜单设置:侧边栏不是写死的,是路由表生成的

3.1 路由表字段逐个说

ProLayout 的菜单不是你在侧边栏组件里手工渲染出来的,而是从config/routes.ts的路由表反射生成的。这个设计一开始可能不太适应,但想通了就觉得很合理:菜单项本质上就是一个能跳转的路由,路由表里声明了name和icon,菜单自然就有了文案和图标,顺序就是数组顺序。我改造后的路由表长这样:

export default [ { path: '/user', layout: false, routes: [ { name: '登录', path: '/user/login', component: './User/Login' }, ], }, { path: '/welcome', name: '工作台', icon: 'smile', component: './Welcome', }, { path: '/system', name: '系统管理', icon: 'setting', routes: [ { path: '/system/user', name: '用户管理', icon: 'team', component: './System/User' }, { path: '/system/role', name: '角色管理', icon: 'safety', component: './System/Role' }, { path: '/system/audit', name: '操作日志', icon: 'profile', component: './System/Audit' }, ], }, { path: '/', redirect: '/welcome' }, { component: './404' }, ];

有几个字段值得说透。layout: false表示这个路由不套 ProLayout 的外壳,登录页要用,否则登录页会出现侧边栏,视觉上很奇怪。redirect是默认跳转,component指向src/pages下的组件路径,不需要写全文件后缀。菜单的层级由嵌套routes决定,/system这一层有子路由,菜单就会展开成二级菜单。

3.2 隐藏菜单:详情页、编辑页不能都往侧边栏塞

后台系统里有一类页面是“能进但不想出现在菜单里”的,比如用户详情页/system/user/detail/:id、编辑页。处理方式是在路由项里加menu: false:

{ path: '/system/user/detail/:id', component: './System/UserDetail', menu: false, },

加了之后,菜单不显示,但你依然可以编程式跳转:history.push('/system/user/detail/123')。组件里用useParams()拿id。这个细节很多人忽略了,结果要么是把详情页硬塞进菜单,要么是写一堆 if 判断去过滤,其实一个字段就解决了。

3.3 菜单的可见性配合权限做裁剪

菜单和权限是天然绑定的。普通用户不该看到“管理员”菜单,但这不该只靠前端隐藏,前端只是“不可见”,真正的数据访问控制还必须由 WebFlux 后端做鉴权。前端这层用access字段绑定权限:

{ path: '/system', name: '系统管理', icon: 'setting', access: 'canAdmin', routes: [ ... ], },

access的值来自src/access.ts的返回值,这个文件我在第6节详细讲。ProLayout 在生成菜单时会自动读取当前用户的initialState结算出权限,没有权限的菜单直接隐藏。要注意的是,access只做展示层裁剪,如果用户手动输入 URL,前端路由守卫同样会拦截,但后端接口必须自己校验 token 和角色,千万别把前端隐藏当成安全手段。

4. 布局与主题设置:把默认后台改造成自己团队的样子

4.1 defaultSettings 里的每一项都要心里有数

config/defaultSettings.ts是 Pro 布局的默认值来源,我第一次看里面十几个字段是有点懵的,用久了才明白每个字段对应 ProLayout 的一个视觉开关。我列一张表说明:

配置项可选值作用
navThemelight/dark/realDark导航整体明暗风格
layoutside/top/mix菜单在侧边栏、顶部,还是混合布局
contentWidthFluid/Fixed内容区宽度自适应还是固定宽度
fixedHeadertrue/false顶栏是否固定,滚动不跟随
fixSiderbartrue/false侧边栏是否固定
colorPrimary色值antd 5 的主色 token
splitMenustrue/falsemix 布局下按层级拆分菜单渲染
title字符串浏览器标题与侧边栏标题
logo图片 URL侧边栏 logo
pwatrue/false是否启用 PWA 相关能力

我个人偏爱layout: 'mix'+navTheme: 'light'的组合:顶部是亮色、放一级菜单,侧边栏放子菜单,视觉上比较现代,也不会像全 dark 那样压抑。splitMenus: true在 mix 布局下会把一级菜单拆到顶栏、子菜单留在侧边栏,信息层级更清楚,代价是屏幕小的设备上顶栏会挤。

4.2 开发期神器:SettingsDrawer 设置抽屉

antd pro v5 模板默认在src/app.tsx里挂了一个SettingDrawer组件。运行项目后,右下角会浮出一个设置按钮,点开可以实时切换主题色、布局方式、内容宽度等,改动立刻生效。它的实现原理是:抽屉里改动的 settings 会通过setInitialState写进全局状态,ProLayout 消费后就重新渲染。模板里大概是这样的:

<SettingDrawer enableDarkTheme settings={initialState?.settings} onSettingChange={async (settings) => { await setInitialState((pre) => ({ ...pre, settings })); }} />

这个抽屉是联调时调 UI 的利器,但我建议上线前把它隐藏掉。可以按环境判断:

{ childrenRender: (children) => ( <> {children} {process.env.NODE_ENV === 'development' && <SettingDrawer ... />} </> ), }

理由很简单:生产环境露出一个能让普通用户随便改布局的入口,既没必要,又可能让验收时界面变得奇怪。开发期随便玩,上线前锁死。

4.3 antd 5 的 token 定制:ProLayout 也能按 token 微调

antd 5 全面拥抱了 CSS-in-JS 的 token 体系,colorPrimary只是最表层的主色。如果你想要更细的定制,比如侧边栏的背景色、菜单标题的颜色、顶栏底色,可以直接在defaultSettings.ts里配token子项:

token: { sider: { colorMenuBackground: '#f6f7f9', colorMenuTitle: '#1f2329', colorTextMenuSelected: '#1677ff', colorBgMenuItemSelected: '#e6f4ff', }, header: { colorBgHeader: '#ffffff', colorTextMenu: '#4a4a4a', }, },

这里有个认知升级:以前 antd 4 时代改主题色得调 less 变量,antd 5 全都收敛到 token 了,连 ProLayout 的侧边栏、顶栏这些组件也支持 token 覆盖。你不需要去翻它的样式文件,按token结构配置即可。如果记不住字段名,开发时在 SettingsDrawer 里改了之后,在浏览器控制台看生成的 CSS 变量名,再反推到配置文件,效率很高。

5. 请求层设置:与 Spring WebFlux 对接的格式和数据流

5.1 最大的坑:后端返回结构和 ProTable 期望的结构不一致

前后端联调最痛的不是网络不通,而是“通了但解析不对”。Spring WebFlux 里如果用 Spring Data 的PageImpl直接返回分页对象,JSON 结构默认是{ content: [...], totalElements: 100, totalPages: 10, ... }。而 antd pro 里的ProTable期望的是{ data: [...], total: 100, success: true }。字段对不上,表格就永远拿到空数据或者报错。

我在这个系列第1篇里就把后端响答应成了统一信封格式:

{ "code": 0, "message": "ok", "data": { "records": [], "total": 0 } }

code === 0表示成功,非 0 表示业务失败,message给前端弹提示用,data才是真正的业务数据。分页结构固定为records+total。前端要做的事,就是在请求层统一做一次转换,而不是在每个ProTable的request里手工 map。

5.2 在 app.tsx 里做全局拦截器

max 的request配置集中在src/app.tsx里。以下是我这次设置的完整形态:

export const request: RequestConfig = { timeout: 15000, requestInterceptors: [ (url, options) => { const token = getToken(); if (token) { options.headers = { ...options.headers, Authorization: `Bearer ${token}`, }; } return { url, options }; }, ], responseInterceptors: [ async (response) => { const { status } = response; if (status === 401) { clearToken(); message.error('登录已过期,请重新登录'); history.replace('/user/login'); return response; } const data = response.data; if (data && typeof data.code !== 'undefined') { if (data.code !== 0) { message.error(data.message || '请求失败'); throw new Error(data.message); } response.data = data.data; } return response; }, ], };

这段代码有几处值得说明。第一,请求拦截器统一加Authorization头,所有接口都不用自己处理 token,这是“设置一次、全局受益”的典型。第二,401 统一清 token 并跳登录页,页面里不用每处都写。第三,响应拦截器里把信封拆开,response.data直接变成业务数据,后面的login()、fetchUserPage()这些接口函数拿到的就是干净的数据结构。

有两点需要根据自己的 max 版本微调:拦截器的参数签名在不同版本里可能略有差异,以你项目里@umijs/max的 TypeScript 类型提示为准;另外如果后端用 200 状态码 + 业务错误码,拦截器里判断code !== 0就行,如果后端直接返回 500,要额外处理非 2xx 的情况。

5.3 ProTable 的 request 怎么接分页参数

后端接口的约定是前端传current和pageSize。ProTable 内部会解构出params.current、params.pageSize,我们把它转成后端认识的参数名:

<ProTable columns={columns} request={async (params) => { const res = await fetchUserPage({ pageNum: params.current, pageSize: params.pageSize, }); return { data: res.records ?? [], total: res.total ?? 0, success: true, }; }} />

如果你的所有表格接口都统一成records + total结构,其实可以把这个转换放进第5.2节的响应拦截器里,返回{ data: records, total, success },这样每个表格的request就更薄了。我的经验是:全局统一数据结构,比在每个页面里做适配节省的时间多得多,这也是这一节设置最有价值的地方。

5.4 绕不开的 CORS:什么时候才需要 WebFlux 配跨域

理论上 dev 环境有代理就不用担心 CORS,但实际工作中总有同事绕过代理直接访问后端,或者你本地起了一个不经过代理的原生调试环境,这时候跨域问题就暴露了。与其反复被问,不如在 WebFlux 后端直接配一个宽松的 CORS 过滤器兜底:

@Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:8000"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); }

等生产环境有了正式域名,把addAllowedOrigin换成具体域名就行。注意CorsWebFilter是 WebFlux 的类,不要在传统 Spring MVC 的WebMvcConfigurer里配,模块不一样容易闹乌龙。

另外提一个 WebFlux 特有的体验:如果后端有推送类接口,比如用Flux做实时通知、SSE,那这些接口不适合走 max 的request插件,建议直接用浏览器原生的EventSource或者fetch的 ReadableStream 处理。这不算 bug,是请求模型本身就不同,提前在前后端约定好,免得后面改架构。

6. 登录与权限设置:JWT 在 Pro 里的落地路径

6.1 initialState:登录状态的总闸

antd pro v5 的鉴权链路是围绕getInitialState建立的。应用启动时,max 会先执行src/app.tsx里导出的getInitialState,把返回值塞进全局状态。我们的做法是:本地有 token 就去后端拉当前用户信息,没有就直接空着。

export async function getInitialState() { const token = getToken(); if (!token) { return { currentUser: undefined, settings: defaultSettings }; } try { const data = await fetchCurrentUser(); return { currentUser: data, settings: defaultSettings }; } catch { return { currentUser: undefined, settings: defaultSettings }; } }

登录点在这个链路里扮演的角色,就是“拿到 token、写入本地、刷新 initialState”。登录页的LoginForm提交后做三件事:调用登录接口、保存 token、调用initialStateRef.current?.refresh()重新执行getInitialState。刷新完成后,整个布局和菜单就会根据新的用户信息重新渲染。

6.2 access.ts 与路由守卫联动

src/access.ts是整个前端权限的判官:

export default function access(initialState: { currentUser?: API.CurrentUser }) { const { currentUser } = initialState || {}; return { canUser: Boolean(currentUser), canAdmin: currentUser?.role === 'admin', }; }

函数名就是权限名,返回布尔值。路由表里access: 'canAdmin'的配置,ProLayout 会拿这里的结果决定菜单显隐;你还能用 max 的<Access>组件在页面级控制:

import { Access } from '@umijs/max'; <Access accessible={access.canAdmin} fallback={<div>无权访问</div>}> <UserForm /> </Access>

必须重复强调的是:前端权限只是用户体验的一部分,后端 WebFlux 的每个接口都要在WebFilter或 Spring Security 里校验 JWT 和角色。前端把按钮藏起来、把菜单过滤掉,不代表接口安全,这个分寸一定要拿捏好。

6.3 openapi 一键生成服务层:少写 80% 的接口代码

antd pro v5 模板自带 openapi 插件,可以从后端的 OpenAPI 文档直接生成src/services/下面的类型定义和请求函数。WebFlux 后端如果用 springdoc 暴露文档,这一步非常顺。具体是:

  • 后端加 springdoc 的 webflux starter(版本按 Spring Boot 3/2 选对应坐标),配置springdoc.api-docs.path=/v3/api-docs;
  • 前端在config/openapi.ts里把schemaPath指到后端文档地址:
export default { openAPI: { requestLibPath: "import { request } from '@umijs/max'", schemaPath: 'http://localhost:8080/v3/api-docs', projectName: 'backend', }, };
  • 然后执行模板自带的脚本:
npm run openapi

之后src/services/backend/下就有typings.d.ts和一堆接口函数。这样一来,后端 DTO 改字段,前端类型自动跟着变,再也不用对着 Swagger 文档手敲 interface。这里有一个小前提:npm run openapi需要后端服务正在运行,而且如果后端接口需要鉴权,可能要临时关闭文档的安全校验,或者把鉴权 token 通过请求参数带过去。我用下来觉得,这个设置是这类后台项目投入产出比最高的一项配置。

7. 构建与生产环境设置:上线前最后要确认的事

7.1 路由 base 与静态资源 publicPath

如果这个后台系统部署在域名根路径,那不用改。但很多公司后台是挂在子路径下的,比如https://example.com/admin/,这时候必须在config.ts里设置:

export default defineConfig({ base: '/admin/', publicPath: '/admin/', });

base影响路由的浏览器路径,publicPath影响 JS/CSS 等静态资源的加载路径。两个不一起改的话,会出现“页面打开空白、控制台报找不到 chunk 文件”的经典问题。这个坑我见过太多次了,上线前先确认部署路径,再决定要不要配。

7.2 生产环境的代理交给 nginx

前面说过proxy只在 dev 生效。生产环境最稳妥的做法是 nginx 统一入口:静态资源由 nginx 直接serve,/api请求转发给后端的 WebFlux 服务。一个最简配置:

server { listen 80; root /srv/admin/dist; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

注意location /api/里proxy_pass结尾带的斜杠,它表示把/api/前缀剥掉再转发。如果你的后端没有/api的 context-path,这种写法刚好把前缀去掉;如果后端自己带/api,结尾就别加斜杠。跟 dev 环境的pathRewrite保持同一套约定,前后端就不会出现“dev 好了、生产 404”的割裂。

7.3 上线前快速检查清单

根据这次改造的体会,我整理了一个上线前检查清单:

  • mock: false已经配置,确保不会把假数据当真实接口;
  • SettingsDrawer 只在开发环境渲染,生产环境不暴露;
  • 路由表里没有残留的测试路由,redirect指向正确的默认页;
  • 请求拦截器的 token 读取、401 跳转逻辑跑通,登录过期能自动回到登录页;
  • base/publicPath与实际部署路径一致;
  • 后端 CORS 允许的 Origin 列表收敛为生产域名,别再放*;
  • 构建产物dist/用 nginx 试跑一遍,重点验证刷新子路由页面不 404。

照着这个清单走一遍,基本能把最常见的翻车点提前排掉。

最后再分享一点实际操作中的体会

这次把 antd pro v5 的设置从头到尾理顺之后,我最深的感受是:这类工程化框架的配置,怕的不是选项多,而是不知道每个选项属于哪个生命周期。编译期的结构定义、运行期的状态拦截、业务期的权限数据,这三层脑子里有了数,遇到问题定位就很快。再碰到什么奇怪的报错,先别急着搜解决方案,问一句“这是哪个层的东西”,往往答案自己就出来了。如果你也正在做 WebFlux + React 的后台项目,建议按这篇文章的顺序把配置过一遍,尤其是请求层的数据结构统一和登录态链路,这两块弄顺了,后面写页面会轻松非常多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询