ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RuoYiApp页面开发实战:从目录结构到部署避坑指南

RuoYiApp页面开发实战:从目录结构到部署避坑指南 1. 先搞懂RuoYiApp是什么页面层的前世今生做后台管理系统绕不开若依RuoYi这个框架在国内Java开发圈里几乎是“默认选项”一样的存在。早期大家在PC端用若依做管理后台后来移动端需求多了就出现了RuoYiApp这个分支。简单说RuoYiApp是基于uni-app开发的移动端应用把若依后台的能力搬到手机上——审批、工单、巡检、报表查看这类场景用得上。很多人第一次拿到RuoYiApp代码会有点懵因为它的页面结构和PC端Vue版本完全是两套东西。PC端用的是Vue2或Vue3加Element UIRuoYiApp这边技术栈是uni-app一套代码编译到App、H5、微信小程序Vue3最新版本已经切到Vue3了Vite构建工具Pinia状态管理uview-plusUI组件库uView的Vue3版本SCSS样式预处理器如果你平时只写PC端管理后台第一次打开RuoYiApp工程会发现很多不习惯的地方。最典型的就是页面文件后缀是.vue但写法上有很多uni-app特有的API比如uni.request、uni.navigateTo、uni.getStorageSync。这套东西不是若依独有的但若依把请求做了二次封装所以你看页面代码时经常能看到this.$tab、this.$modal这类自定义挂载方法。RuoYiApp适合谁来学我认为三类人最需要已经在用若依做后端但需要快速给系统加一个移动端入口的Java开发。做小程序或App外包项目甲方要求“界面风格和管理后台统一”直接拿RuoYiApp做二开能省不少事。想研究uni-app工程化实践的开发者。RuoYiApp在请求封装、权限路由、多端适配这几个维度的代码写得比较规整比网上很多uni-app模板要规范。这篇文章我就围绕RuoYiApp的“页面”这条线展开讲清楚页面目录怎么组织、页面如何跟后端交互、怎么从零写一个页面、以及每一层会遇到什么坑。文章里所有代码基于RuoYiApp官方仓库的Vue3版本。2. 读懂RuoYiApp页面层的根基2.1 页面目录与路由之间的对应关系RuoYiApp的页面文件集中在src/pages目录下。跟PC端Vue-admin-template那套路由文件驱动页面不一样uni-app的页面路由是“文件即路由”你在pages.json里注册哪个路径哪个页面就能被访问。打开RuoYiApp的pages.json你会发现它把页面分成了几类pages/login/index登录页pages/index/index首页登录后默认落地页pages/user/index个人中心pages/...业务模块页面按模块目录存放有一个细节要注意uni-app的pages.json里第一个page字段对应的页面就是应用启动后的默认首页。RuoYiApp里通常把pages/index/index放在第一位而登录页反而是独立注册的。这意味着什么意味着如果你直接改pages.json把登录页放第一位启动就会先进登录页但你会丢掉uni-app框架自动处理token过期跳转的逻辑。页面之间的跳转用的是uni.navigateTo这个是栈式跳转页面会一层层压栈。RuoYiApp在工具类里封装了tab切换和redirectTo的逻辑比如this.$tab.switchTab用于底部tab切换this.$tab.navigateTo用于普通带参跳转。2.2 底层请求封装页面里的this.$http是怎么回事RuoYiApp页面里最常见的写法是const res await this.$http.post(/system/user/list, params);这个$http不是uni-app自带的是若依在src/utils/request.js里基于uni.request封装的Promise化请求工具。为什么要再包一层因为要统一处理几件麻烦事每次请求自动带上Authorization请求头token从storage里读取。响应回来后统一判断HTTP状态码和后端业务状态码。401状态码自动清理本地登录态并跳转登录页。网络异常时弹出统一报错提示。这个思路和PC端若依的request.js如出一辙。看代码你会发现核心逻辑是拦截器模式——请求发出前在拦截器里做身份注入收到响应后在拦截器里做业务码分发。RuoYiApp的request.js里有一个关键设计它用config.header[Authorization] Bearer token的形式加token这个Bearer前缀和后端Spring Security配置是对应的。很多新手二开时遇到“明明登录了但接口还是401”的问题十有八九是token前缀对不上或者本地存token的key被改掉了。2.3 登录页为何值得细读几乎所有RuoYiApp项目二次开发都要动登录页。默认登录页是账号密码加验证码但很多企业项目要接短信登录、企业微信扫码登录或者统一身份认证。登录页的核心逻辑在src/pages/login/index.vue里。它的流程是这样的页面加载时先请求验证码接口因为若依后端默认开启了验证码开关。用户输入账号密码点登录按钮。前端把账号密码用AES加密传给后端RuoYiApp默认用了crypto-js做加密密钥在src/utils/auth.js或环境配置文件里。成功后后端返回token前端存到uni.setStorageSync然后uni.switchTab跳转首页。AES加密这一段是很多初次接触若依App端代码的人容易忽略的。如果你只是把登录表单改成手机号验证码登录务必保留这个加密逻辑否则后端SecurityUtils会直接报“用户名或密码错误”。密钥不对也是个常见坑PC端和App端的AES密钥如果配置不一致同一个账号在两边登录会出现一个能成一个失败。3. 页面实战从零开发一个带后端联调的“今日待办”页3.1 需求场景与页面设计思路假设现在有个需求——在RuoYiApp里加一个“今日待办”页面展示当前登录用户的待办任务列表每条任务包含标题、提交人、发起时间、紧急程度、状态点进去可以查看详情。这个需求很典型几乎每个OA类二次开发都会遇到。开发前先把逻辑掰开揉碎页面数据来源后端现成的/todo/list接口RuoYiApp里POST请求用request.data传参。列表形态待办建议用卡片列表每条卡片展示任务标题、提交人、时间、紧急程度标签。紧急程度用不同颜色区分这个用uview-plus的u-tag组件可以快速实现。交互点卡片跳转详情页需要携带任务ID。详情页先写一个静态框架后续接后端详情接口。分页列表数据如果量大必须分页。RuoYiApp的习惯是每页10条或者20条用loadmore组件配合触底加载。3.2 编写页面结构template与script先看src/pages/todo/list.vue的完整结构这是按RuoYiApp规范写的template view classtodo-page u-list :scrollabletrue loadmoreloadMore :loadmore-statusloadStatus u-list-item v-foritem in list :keyitem.id view classtodo-card tapgoDetail(item.id) view classtodo-header text classtodo-title{{ item.title }}/text u-tag :texturgencyText(item.urgency) :typeurgencyType(item.urgency) sizemini / /view view classtodo-body text classtodo-sub提交人{{ item.submitter }}/text text classtodo-sub发起时间{{ item.createTime }}/text /view /view /u-list-item /u-list u-empty v-if!loading list.length 0 text暂无待办 modelist / /view /templateu-list和u-list-item是uview-plus的列表组件支持触底加载事件。注意loadmore只有在u-list滚动到底部时才触发所以需要配合loadStatus字段控制加载状态加载中/加载完成/没有更多了。u-empty是空数据占位这个组件很多人不知道默认uview-plus的组件库是按需引入的如果pages.json的easycom配置开了组件直接用就行不需要手动import。RuoYiApp官方工程默认开启了easycom所以你在template里写u-tag、u-empty这类标签编译时自动按需加载对应组件。3.3 编写页面逻辑请求封装与状态管理script setup import { ref } from vue; import { onShow } from dcloudio/uni-app; import { getTodoList } from /api/todo; const list ref([]); const queryParams ref({ pageNum: 1, pageSize: 10 }); const loadStatus ref(loadmore); const loading ref(false); // 请求列表数据 const fetchList async () { if (loading.value) return; loading.value true; try { const res await getTodoList(queryParams.value); list.value list.value.concat(res.rows || []); if (list.value.length res.total) { loadStatus.value nomore; } } finally { loading.value false; } }; // 触底加载 const loadMore () { if (loadStatus.value nomore) return; queryParams.value.pageNum 1; fetchList(); }; // 下拉刷新 const onPullDownRefresh async () { queryParams.value.pageNum 1; list.value []; loadStatus.value loadmore; await fetchList(); uni.stopPullDownRefresh(); }; // 跳转详情 const goDetail (id) { uni.navigateTo({ url: /pages/todo/detail?id${id} }); }; /script这段代码有几个RuoYiApp特色方法要注意onShow是从dcloudio/uni-app导入的生命周期。uni-app页面默认有onLoad、onShow、onHide在script setup写法中不能直接用选项式onShow必须从dcloudio/uni-app里导入API。fetchList里拼接分页数据用concatRuoYiApp接口返回的格式是{ rows: [], total: 100 }这个和若依PC端的分页响应结构完全一致因为后端都是PageHelper插件统一处理的。触底加载的判断逻辑很实用判断当前已加载数据量是否达到总数达到就置为nomore避免重复请求。这里的getTodoList建议单独放一个src/api/todo.js文件里。RuoYiApp的接口文件规范是每个业务模块一个JS文件统一导出该模块的API方法import request from /utils/request; // 查询待办列表 export function getTodoList(query) { return request({ url: /todo/list, method: post, data: query }); } // 查询待办详情 export function getTodoDetail(id) { return request({ url: /todo/detail, method: get, params: { id } }); }3.4 注册页面路由与底部tab页面文件写好后一定要在pages.json里注册否则编译后找不到这个页面。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页 } }, { path: pages/todo/list, style: { navigationBarTitleText: 今日待办 } } ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/todo/list, text: 待办 } ] } }如果你需要把待办页加入底部tab直接加到tabBar.list数组里就行。但有个细节想用uni.switchTab跳转tab页面时url不能带参数比如uni.switchTab({ url: /pages/todo/list })是允许的但url: /pages/todo/list?id1会被忽略掉。如果tab页需要参数只能靠本地存储或全局状态传递。3.5 页面权限控制不是每个页面都能直接访问RuoYiApp做了一个页面访问拦截它在src/permission.js或main.js里通过uni.addInterceptor拦截路由跳转检查目标页面是否有权限。RuoYiApp的权限点设计跟PC端一致后端权限码格式类似todo:list:query。前端拿到用户信息后会把权限点列表存到Pinia的user模块里。页面跳转前拦截器提取当前路由路径去权限点集合里匹配匹配不上就提示“无权限访问”。所以你在二开新增页面时光写页面还不行还要做两件事后端菜单管理里给对应角色分配页面路径的权限码。前端在src/permission.js里检查meta.roles或动态路由是否包含当前页面。如果只是本地调试不想走权限拦截可以临时在拦截器里把校验逻辑注释掉。但上线前一定要恢复否则接口虽然能调通页面权限却形同虚设。4. 页面联调避坑从本地调试到H5部署4.1 接口地址配置与“请求失败”问题RuoYiApp在本地开发时默认请求地址指向http://localhost:8080后端启动端口。但这个配置藏得比较深不在config/index.js里而是在项目根目录的.env.development文件里。# .env.development VITE_APP_BASE_API /dev-api VITE_APP_ENV development/dev-api是Vite的代理前缀。开发模式下前端工程通过Vite的server.proxy配置把/dev-api代理到后端真实地址。RuoYiApp的vite.config.js里这么写server: { port: 9000, proxy: { /dev-api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/dev-api: } } } }很多新手打开项目发现H5页面能打开但接口全部404多半是后端端口不是8080或者后端部署在远程服务器上而target还指向本地。改成远程地址后还要注意跨域问题。如果后端没有配CORS前端代理又没生效浏览器就会报跨域错。我的建议是本地调试阶段不要绕开代理直接用Vite的proxy解决。只有后端服务器已经配置好CORS时才考虑把VITE_APP_BASE_API改成完整地址。4.2 H5发布到Nginx的路径陷阱如果你用H5方式发布RuoYiApp也就是把编译后的dist/build/h5目录丢到Nginx下一定会遇到“刷新页面404”的问题。原因很简单uni-app的H5路由默认是history模式页面路径是/pages/todo/list这种Nginx默认配置找不到这个路径对应的静态文件返回404。解决办法是给Nginx配置try_files让所有路径都回退到index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这一段也是RuoYiApp二开后上线最容易被卡住的地方。工程里的manifest.json可以配置H5路由模式但默认是history如果你不想配置Nginx的try_files可以改成hash模式{ h5: { router: { mode: hash } } }改hash模式省事但URL会带#号不美观也不利于分享。我的习惯是保持history用Nginx的try_files解决一劳永逸。4.3 页面里经常用到的缓存与状态同步RuoYiApp里很多业务页面的数据是从首页带过来的比如用户信息、组织架构。这些数据如果每次进页面都调一次后端体感会很差。RuoYiApp用Pinia做全局状态常见做法是在src/store/modules/user.js里维护userInfo页面通过storeToRefs取出来直接用。但有一个页面生命周期的问题App端切后台再回到前台页面onShow会重新触发但Pinia里的数据还在不会重新请求。如果你需要每次进页面都拿最新数据必须在onShow里主动调接口刷新。有个土办法我一直在用在onShow里判断数据时间戳超过一定时间才重新请求避免每次触发页面显示都会加载用户切几个tab回来就一直在转圈。5. 高频问题实录二开踩过的坑与排查思路5.1 vue3 ts报错问题用Vue3版本RuoYiApp时很多用TypeScript写业务的同学会遇到类型报错。最常见的一类是Property xxx does not exist on type never这个大概率是ref初始化时没给泛型。RuoYiApp官方的页面大多是JS写的你接入TS后定义变量时应该写成const list refTodoItem[]([]);如果懒得分类型可以直接用refany([])先跑通但团队项目不建议这么做。另一个高频TS报错是引入组件时找不到声明文件。RuoYiApp的jsx和ts混用比较常见如果报找不到dcloudio/uni-app的类型声明需要在tsconfig.json里把types配置好或者安装dcloudio/types这个依赖包。有些人直接把报错行前面加// ts-ignore短期能压住但类型检查形同虚设不建议这么干。5.2 idea导入工程时“error adding module to project: null”这个问题在Gitee上把若依分支拉下来用IDEA打开时经常遇到报错信息看起来像是IDEA无法添加模块。实际原因一般是工程根目录下没有.gitignore正确过滤IDE文件或者子模块的.iml文件被提交上来了导致IDEA模块解析冲突。处理办法比较简单先把工程目录下的*.iml、.idea目录删掉。File - Invalidate Caches / Restart清一下IDEA缓存。重新导入工程时别选“Open as Project”而是用Import Project选择根目录下的pom.xml让Maven重新导入。5.3 mvn命令无法识别或clean install失败搜索词里有一条很典型“mvn : 无法将“mvn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这就是Maven没配环境变量。Windows下解决办法# 临时设置当前终端有效 $env:Path ;D:\apache-maven-3.8.6\bin更稳妥的做法是把Maven的bin目录加到系统环境变量Path里然后重新打开终端。如果mvn clean install过程中报依赖下载失败优先检查阿里云镜像仓库有没有在settings.xml里配置。RuoYiApp的Vue3版本依赖安装建议用npm install而不是cnpm install。cnpm在一些依赖的postinstall脚本执行上不够稳容易生成残缺的node_modules导致页面编译时各种奇奇怪怪的报错。我遇到过几次uni-app相关插件在cnpm下装完直接跑不起来换成npm install后一次通过。5.4 ruoyi导入表失败与黑马版本有同学在基于黑马程序员的若依教程学习时导入SQL文件报错。这和RuoYiApp页面本身关系不大但会影响页面联调。ry_*.sql文件导入失败最常见的原因有两个MySQL版本太高SQL文件里的注释语法在MySQL 8.0以上被解析出错。解决办法是手动删除文件头的注释块再导。数据库字符集不对报Unknown collation这类错。建议创建数据库时显式指定字符集CREATE DATABASE ruoyi DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;有的教程给的SQL是老版本5.7导出的字符集是utf8在MySQL 8.0里容易报错。还有的SQL文件没有使用USE语句你导入的时候必须选中对应数据库再导入否则表建到了别的库下面页面接口自然查不到数据。5.5 iframe嵌入页面刷新与父页面交互搜索词里有一条“iframe关闭jquery并刷新父页面js”这其实是PC端页面集成场景在RuoYiApp里也会遇到。有些企业系统需要把移动端页面嵌到PC门户的iframe里或者反过来在移动端App里用web-view加载PC页面。这里有个必须注意的点uni-app的H5页面被iframe嵌入后uni.navigateTo的跳转不会影响父页面URL但页面内的登录态和token会单独存在iframe的localStorage里。如果父页面和iframe页面是同域的你可以通过postMessage同步登录态如果是跨域必须走后端OAuth授权或者短token绑定不能简单用localStorage共享。刷新父页面这个操作被嵌的页面如果是在PC门户里可以这样触发window.parent.location.reload();但如果你这个移动端页面未来要上线到微信小程序window.parent这行代码不能出现在公共模块里否则小程序编译会直接报错。建议把这类H5专属调用放到#ifdef H5条件编译里// #ifdef H5 if (window.parent ! window) { window.parent.location.reload(); } // #endif这种写法我在多个项目里都在用稳妥不报错。5.6 后台任务不执行引起的页面空数据有个搜索词是“ruoyi 任务不执行什么原因”。如果待办页面数据是由定时任务生成的任务不执行页面就会一直空。若依的任务不执行常见原因有三个Scheduled注解所在的类没有被Spring扫描到通常是因为启动类扫描包范围没覆盖到。任务的cron表达式写错了比如只执行一次后不再触发。可以在若依的“定时任务”菜单里手动执行一次看任务日志有没有报错。服务有多个实例任务被多个实例重复调度导致数据异常或锁冲突。这时需要在任务方法里加分布式锁或者用若依自带的Scheduled配合数据库锁表实现单实例执行。页面端排查技巧是先用Postman直接调后端任务生成的接口如果接口有数据说明任务本身OK问题出在页面参数或权限码如果接口都没数据就往服务端日志查。6. 页面升级迁移单节点部署环境怎么平稳过渡6.1 为何页面代码不动换环境还会出问题搜索词里有一条比较典型的完整场景——单节点K8s上的若依微服务整套环境要迁到云服务器迁移后还要做高并发压测。页面代码本身没变但换了环境后RuoYiApp页面会遇到几个新问题token失效变频繁因为迁移后各服务的时钟不同步JWT过期校验有时会误判。页面加载变慢因为静态资源图片、PDF模板没有一并迁移到对象存储。接口网关地址变了但前端环境变量还指向旧地址。微服务场景下文件上传、短信验证码这类依赖Redis的服务Redis地址没更新导致页面登录和上传功能异常。迁移之前我通常是先把前端的环境配置和后端网关地址梳理成一张清单逐项确认。RuoYiApp页面本身不用改但要保证VITE_APP_BASE_API、VITE_APP_ENV指向新环境正确。微服务版本里还要检查网关的/prod-api路由前缀是否一致否则登录能过业务接口全404。6.2 不停服迁移页面时缓存与灰度策略迁移期间如果用户正在用App前端页面构建版本和旧版本混着跑很容易出现“接口通了但页面样式错乱”或者“js报错页面白屏”的情况。这几招比较实用静态资源强缓存要谨慎。index.html设置为no-cacheJS/CSS资源带hash发布后用户拿到的是新版本。如果index.html也被缓存了用户就会一直白屏。Nginx做灰度发布旧版本先保留一个目录新版本通过Cookie或Header灰度放量。若依的PC端管理后台和RuoYiApp页面可以共用一套网关但静态资源分开部署方便回滚。后端接口要保证新旧版本兼容尤其是接口返回结构不能在一夜之间改掉。RuoYiApp的request.js对响应体的解析是强依赖code字段的如果后端升级后code字段语义变了前端所有页面都会走异常分支。6.3 高并发压测时页面端的表现迁移到云服务器后用Jmeter做高并发测试注意页面端的瓶颈往往不在前端本身而在几个容易被忽略的点登录接口的验证码在压测时会被流量打爆因为验证码的生成和校验都在后端并发一高Redis连接就吃紧。压测前建议临时关掉验证码开关。页面静态资源如果直接走网关压测时网关压力会掺杂静态资源流量。建议静态资源走CDN或独立Nginx别让网关扛。uni-app的H5包如果体积过大首屏加载时间会拖垮体验。压测时观察DOMContentLoaded时间如果超过3秒优先优化页面首屏把非关键组件改成异步加载。RuoYiApp页面里有些组件是全量引入的比如uview-plus的组件库页面一多打包体积就上去了。优化思路是开启easycom按需加载并且在pages.json里避免用globalStyle里塞太多自定义组件。7. 写在最后的实践经验RuoYiApp的页面开发核心其实就是三件事理解uni-app的文件即路由机制、读透request.js封装的请求与响应规范、掌握uview-plus组件的用法。第二件事是最容易翻车的因为很多二开的人一上来就写页面调接口结果接口报错、401跳转、token失效这些问题反反复复到最后才发现是对请求封装的理解有偏差。我自己在实际项目中踩过最深的坑是部署到线上后用户反馈“昨天还能用今天打开白屏”。排查了大半天发现是静态资源被CDN缓存了旧的index.html而工程里新版本的JS文件又已经清理掉边缘节点还在引用旧路径。从那以后我凡是发布H5版本都会在Nginx配置里把index.html设成no-cache同时让JS和CSS文件带上内容哈希。这个小习惯后来帮我省了很多麻烦。还有一个心得在RuoYiApp里做页面不要急着把后端接口返回的数据结构硬编码到前端。若依后端统一返回{ code, msg, rows, total }但实际业务接口有可能在data里嵌套了其他对象也有可能分页参数名从pageNum变成pageNo。和Java后端约定好返回结构再动手写页面能少走很多弯路。RuoYiApp页面这块内容其实很深光是一个pages.json的配置就能延展出小程序分包、App原生组件混写、H5路由模式切换这些话题。如果你正在做二开建议从一个小而完整的模块入手——比如本文的“今日待办”页面——跑通页面、接口、权限、部署这一整套链路之后再往深了挖微服务、容器化和集群部署的部分也不迟。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进