
做 uni-app 项目的人大概都经历过这个阶段后端在 IntelliJ IDEA 里写 Java前端在 HBuilderX 里跑 uni-app两个 IDE 同时开着各自占一个屏幕。本地调试的时候一切顺畅等到要打包发布问题就集中爆发了——IDEA 那边mvn package出来的 jar 丢到服务器上起不来HBuilderX 这边发行的 H5 传到 nginx 上白屏App 打完包发现原生插件调用失败。我在好几个项目里都完整走过这条链路这篇就把 IDEA 和 HBuilderX 从启动到打包的完整过程拆开讲一遍重点放在那些文档里不写、但每次都会绊你一脚的地方。不管你是刚接手一个前后端分离的 uni-app 项目还是准备把本地跑通的东西交付出去这套流程都能直接照着走。后面涉及的每个参数、每个命令行我都会说明为什么这么设而不是只丢一个结论给你。1. 先理清 IDEA 和 HBuilderX 各自的活干到哪里1.1 一个常见误解两个 IDE 不是替代关系刚接触这套组合的人最容易问的一个问题是既然 HBuilderX 能写代码为什么还要开 IDEA或者反过来IDEA 装了 Vue 插件为什么不能直接跑 uni-app。这个问题的答案是——它们服务的是工程里两个完全不同的部分硬凑到一起只会让自己难受。IDEA 负责的是服务端那一半Java 源码、Maven 依赖树、Spring 容器启动、数据库连接、接口定义。它擅长的事情是编译期检查、依赖解析和 JVM 调试你可以在一个断点上停住整个请求链路看变量、看调用栈。HBuilderX 负责的是客户端那一半pages.json 路由、manifest.json 平台配置、页面生命周期、条件编译它的核心竞争力在于内置的编译器和真机运行基座能一键把代码推到手机上跑。把它们混在一起的代价很具体。我在早期试过在 IDEA 里直接开 uni-app 目录结果是node_modules被索引了十几万文件IDE 卡到打字都延迟同时 HBuilderX 的编辑器对 Java 支持几乎为零导入一个BigDecimal都提示不出来。后来就彻底分开了IDEA 只打开后端仓库HBuilderX 只打开前端仓库两边各自一个根目录互不干扰。1.2 我给别人讲这套分工时用的那张表如果你需要跟团队新人解释下面这张对照表比讲半小时都管用维度IntelliJ IDEAHBuilderX负责层服务端Java/Spring Boot、网关、定时任务客户端uni-app 页面、组件、静态资源主要产物可执行 jar、Docker 镜像H5 静态包、小程序包、App 安装包启动方式Run Configuration 跑主类或 mvn spring-boot:run运行到浏览器 / 模拟器 / 真机 / 开发者工具调试手段断点、条件断点、Evaluate Expressionconsole.log、真机调试、微信开发者工具调试面板打包入口Maven 生命周期 / Dockerfile菜单「发行」下的各平台选项常见坑端口占用、profile 不生效、依赖冲突基座版本、静态资源路径、跨域这张表里最关键的一行是「主要产物」。很多人打包出问题本质上是没搞清楚自己要交付的是哪一层的东西。后端交付的是进程前端交付的是静态资源或者安装包两类产物的验证方式完全不同。1.3 工程目录怎么放决定了后面启动顺不顺目录结构看起来是个小事但它直接决定了你启动和打包时会不会踩路径的坑。我现在的习惯是这样project-root/ ├── server/ # 后端IDEA 打开这一层 │ ├── pom.xml │ ├── src/main/java │ └── src/main/resources ├── client/ # 前端HBuilderX 打开这一层 │ ├── pages.json │ ├── manifest.json │ ├── App.vue │ └── pages/ └── deploy/ ├── Dockerfile └── nginx.conf为什么建议client作为独立的一层给 HBuilderX 打开而不是让 HBuilderX 打开project-root因为 HBuilderX 会把项目根当作编译根pages.json和manifest.json必须在项目根目录下才能被识别。如果你直接把整个大仓库丢给它编译根就错位了运行时会报找不到入口。同理IDEA 打开server而不是仓库根目录是为了让 Maven 的项目模型能正确识别pom.xml。我有一次给 IDEA 打开了仓库根结果它把根目录当成一个聚合工程mvn package直接报 no POM in this directory排查了十几分钟才反应过来是自己开错层了。提示如果因为历史原因前后端必须放在同一个仓库至少用 IDEA 的「Attach」或者把 client 目录标记为 Excluded避免索引node_modules。2. IDEA 侧把后端跑起来从 JDK 到端口占用的完整链路2.1 JDK、Maven、编码三件套的检查顺序后端启动失败八成以上不是代码问题而是环境问题。我现在的排查顺序固定是三步JDK 版本、Maven 配置、文件编码一个都不能跳。第一步看 JDK。IDEA 里有三个地方都跟 JDK 有关而且它们可以不一致Project Structure → Project SDK、Project Structure → Modules → Language level、以及Settings → Build Tools → Maven → Runner里的 JRE。这三个如果版本打架症状是编译能过但启动报UnsupportedClassVersionError。Spring Boot 2.x 一般跑 JDK 8 或 11Spring Boot 3.x 起步就是 JDK 17这个对应关系必须先确认。你可以用mvn -v看 Maven 实际用的 JDK再和 IDEA 里的设置对一遍。第二步看 Maven。Settings → Build Tools → Maven里三行要填对Maven home path 指向你本地的 Maven 安装目录而不是 IDEA 自带的 BundledUser settings file 指向你自己的settings.xmlLocal repository 会自动跟着 settings 走。用自己的 settings.xml 是为了走内网镜像否则拉依赖慢到你怀疑网络坏了。第三步是编码。Settings → Editor → File Encodings里Global Encoding、Project Encoding、Default encoding for properties files 全部设成 UTF-8并且勾上「Transparent native-to-ascii conversion」。这个勾选框的作用是让.properties文件里的中文在 IDE 里显示成中文、在文件里存成\uXXXX避免打包后读出来乱码。2.2 Run Configuration 里几个容易被忽略的开关主类上右键 Run 一次IDEA 会自动生成一个 Spring Boot 的运行配置。能跑起来不代表配置对了有几个地方我每次都会手动改一遍。VM options 里建议加上-Dfile.encodingUTF-8尤其是 Windows 环境下不加的话控制台日志里的中文经常是乱码。需要指定环境的时候加-Dspring.profiles.activedev比在配置文件里改来得干净切换环境直接改运行配置就行不用动代码。还有一组配置是Settings → Build Tools → Maven → Runner下面的「Build and run using」和「Run tests using」。默认是 Maven改成 IntelliJ IDEA 之后编译速度会快不少。但这里有个副作用如果你的项目用了 Lombok 或者 MapStruct 这类注解处理器必须同时确认Settings → Build Tools → Compiler → Annotation Processors里勾了 Enable annotation processing否则 IDEA 自己编译时不生成 getter/setter一堆红线报错。我遇到过一次cannot find symbol: method getXxx()代码明明没问题加到注解处理器开关上才解决。热部署是另一个高频需求。加了spring-boot-devtools之后改完代码需要触发一次编译才能生效。IDEA 里要在Settings → Advanced Settings里勾上「Allow auto-make to start even if developed application is currently running」否则改完代码它不会自动重编译。实测下来这个组合在开发阶段能省掉大量重启等待的时间但要注意它只重启类加载器静态资源和部分配置不一定会重新加载。2.3 端口被占用与启动卡死的排查路径端口冲突是启动阶段最高频的问题而且报错信息长得很像别的故障容易带偏。下面这张表是我踩坑之后整理的对照症状大概率原因处理方式Port 8080 was already in use上一个进程没退出查占用进程结束它或换端口启动后卡住不动无日志输出数据库连接超时检查连接串、网络、库是否可连日志停在一半特别慢循环依赖或 Bean 初始化阻塞开 debug 日志看 Bean 创建顺序启动成功但接口 404上下文路径或扫描包不对检查server.servlet.context-path和启动类包位置反复重启devtools 监控到了输出目录变动排除日志目录、上传目录查端口占用的命令Windows 上是netstat -ano | findstr :8080拿到 PID再taskkill /F /PID 具体PIDmacOS 和 Linux 上更简单lsof -i:8080直接能看到进程。注意 IDEA 自己有个「Stop」按钮不一定会真的杀掉进程尤其是你之前用命令行java -jar起过一个实例的时候。还有一类更隐蔽的情况启动不报错但服务对外不可访问。这种一般是绑定地址的问题默认只绑127.0.0.1的话同一台机器上的浏览器能访问局域网里另外一台设备就访问不到。真机联调的时候这个必须改配置里写server.address0.0.0.0或者启动参数里加--server.address0.0.0.0。3. HBuilderX 侧的启动姿势标准基座、自定义基座与端口3.1 首次运行前必须确认的三个文件uni-app 项目里HBuilderX 能不能正常跑起来几乎全押在三个文件上pages.json、manifest.json、App.vue。这三个文件里任何一个配置错症状都是白屏或者编译直接失败。pages.json管的是路由表和窗口样式。第一项就是默认首页很多人改了首页路径但忘记同步pages数组的顺序结果运行时还停在老页面上。tabBar的pagePath必须和pages里注册的路径完全一致少一个斜杠都不行。manifest.json管的是平台配置是这三个文件里最关键也最容易出问题的。appid是 DCloud 分配的应用标识没有它就用不了云打包app-plus下面配启动图、权限、模块h5下面配路由模式mp-weixin下面配微信小程序的 appid。这里有个经验这个文件不要手写 JSON用 HBuilderX 的「源码视图」和「可视化视图」切换着改可视化视图会自动帮你补全很多字段手写容易漏。App.vue管的是应用生命周期和全局样式。onLaunch里通常会放登录态检查、全局配置拉取。这里有个坑onLaunch是异步的但页面onLoad可能先执行如果页面的第一个请求依赖onLaunch里拿到的 token就会出现偶发的 401。稳妥的做法是把初始化逻辑包成 Promise在页面的请求前 await 一下。3.2 标准基座与自定义基座什么时候必须换这是 uni-app 里最容易让人困惑的概念之一。简单说标准基座就是 HBuilderX 自带的一个通用 App 外壳里面预置了官方的基础模块。你用「运行到手机或模拟器 → 运行到 Android App 基座」装到手机上的就是它。优点是快改完代码一刷新就能看到效果。但标准基座有个硬限制里面没有第三方原生插件。只要你的manifest.json里配了原生插件比如某个厂商的推送、某个扫码 SDK标准基座里调用就会直接报方法不存在。这时候必须用自定义基座——把当前项目的原生插件和配置一起打成调试用的安装包再装到手机上跑。具体流程是在 HBuilderX 里选「发行 → 原生App-云打包」打包时勾选「制作自定义调试基座」等云端出包下载安装到手机。之后在「运行到手机或模拟器」的菜单里选择「运行到自定义基座」代码就会注入到这个基座里跑。这个流程每次改了原生插件配置都得重来一遍所以要养成习惯凡是涉及原生能力的改动先确认基座是不是最新的。注意自定义基座和正式包是两个东西别把调试基座发出去给用户装包名后缀和签名都不一样。3.3 调试端口被占、真机连不上的处理HBuilderX 运行到浏览器时会起一个内置的开发服务器端口可以在「工具 → 设置 → 运行配置」里看到和修改。这个端口的默认值有时候会和你的后端冲突尤其是后端也在 8080 的时候表现是两个服务抢一个端口前端页面能打开但请求全跑到后端去了。我的习惯是统一错开后端用 8080HBuilderX 内置服务用 8090微信开发者工具用自己的端口这样三边互不影响。真机连不上是另一个高频问题排查顺序是这样手机开发者选项里的 USB 调试是否打开有些国产机还需要单独打开「USB 安装」。数据线是不是只能充电的线换成原装线试一次。命令行执行adb devices看设备列表里有没有你的机器。空的说明驱动或者授权没过。手机弹出「允许 USB 调试吗」的对话框必须点允许而且要勾「一律允许」。adb kill-server然后adb start-server重启一遍服务能解决相当一部分玄学问题。如果adb devices里显示的是unauthorized说明授权没通过去手机的开发者选项里撤销 USB 调试授权重新插一次。显示offline的话通常是 adb 版本和手机系统不匹配HBuilderX 安装目录下自带了 adb可以用它替代系统的。4. 两端联调时的接口错位与跨域4.1 baseUrl 的三种配置方式与各自适用场景前端要调后端第一件事是确定请求的 baseUrl。这个值在 uni-app 里怎么配直接决定了你打包的时候要改多少地方。常见的三种做法各有利弊。第一种是硬编码在请求封装里改环境靠手动注释。适合一个人做的小项目团队协作必挂。第二种是用条件编译在#ifdef H5和#ifdef APP-PLUS里写不同的地址。这个方式的好处是各端地址天然隔离H5 走代理路径App 走真实域名缺点是改一次要动好几处。第三种是抽一个config.js用环境变量或者 Node 的process.env区分打包环境构建时注入。这是我目前主要用的方式因为 HBuilderX 的打包流程支持在vue.config.js里读环境变量做替换。我的实际组合是这样的开发阶段 baseUrl 写相对路径/api由 H5 端的 devServer 代理转发到后端打包 H5 时改成 nginx 同域下的/api打包 App 时改成完整域名https://api.example.com。三套值放在一个配置表里通过构建变量切换避免发布前满项目找地址。4.2 跨域H5 端靠代理App 端靠什么跨域这个问题本质上是浏览器的同源策略所以只有 H5 端会遇到。App 端是原生请求不存在跨域限制小程序端则是在微信开发者工具里可以关掉校验、真机上必须服务端配好。H5 端的处理方式有两种。开发阶段用 devServer 代理最省事在vue.config.js里配proxy把/api前缀的请求转发到后端地址。这样浏览器看到的始终是同源的跨域问题不存在。但要注意changeOrigin: true一定要加否则后端拿到的 Host 还是前端的某些框架的校验会挂。生产阶段如果前后端同域部署nginx 里加一段location /api/ { proxy_pass http://后端地址; }就够了同样没有跨域。只有前后端不同域的时候才需要服务端配 CORS 头。这时候要注意三个头都得对Access-Control-Allow-Origin不能是通配符加凭证的组合Access-Control-Allow-Headers要包含你实际用到的自定义头预检请求OPTIONS必须返回 200 而不是 401。我踩过一次特别典型的坑接口本身通了但带 token 的请求全部失败。原因是Access-Control-Allow-Headers里没写Authorization预检被拒。还有一次是拦截器把OPTIONS请求拦下来做了鉴权导致预检永远返回 401。这两个问题的排查方向完全不同但表现都是「接口调不通」。4.3 mock 数据与真实后端的切换后端还没写好接口的时候前端不可能干等。这时候要么用 mock 数据要么用后端先返回假数据。我倾向于在请求封装层做开关而不是在各个页面里写 if 判断。做法是在config.js里放一个useMock标志请求方法里判断一下开启时走本地 mock 文件关闭时走真实请求。这样切换只需要改一个值页面代码完全不用动。mock 数据结构要和真实接口保持一致包括字段名、嵌套层级、分页字段否则切到真实接口时又是一轮改动。5. 打包这一步后端 jar/镜像与前端各端产物的产出逻辑5.1 后端mvn package 之后为什么还要验证 jar 能单独跑后端打包最基础的一条命令是mvn clean package -DskipTests。加-DskipTests是因为 CI 和本地打包的目的不同本地打包主要是为了快速验证产物测试放在流水线里跑更合适。如果你想连测试代码都不编译用-Dmaven.test.skiptrue这两者的区别是前者编译测试代码但不执行后者连编译都跳过。命令跑完target目录下通常会有两个 jar一个原始的xxx.jar.original一个被spring-boot-maven-plugin重新打包过的xxx.jar。能直接java -jar跑的是后面这个因为它把依赖和启动器都塞进去了。这里我强烈建议一个动作拿到 jar 之后别急着丢服务器先在本地用命令行起一次。因为你一直在 IDEA 里开发IDEA 的 classpath 和真实 jar 的 classpath 是两回事。我遇到过好几次 IDEA 里跑得好好的jar 起来报ClassNotFoundException原因是依赖的 scope 写成了providedIDEA 里能借到打包时不带。如果你的项目用了外置配置文件打包后要注意一个细节Spring Boot 默认会优先读 jar 同级目录下的config/目录然后是同级目录最后才是 jar 内部的。这个优先级顺序决定了你能不能在不重新打包的情况下改配置。生产环境我一般把配置文件放在 jar 同级的config目录下改配置只改文件重启不用重新发版。5.2 Docker 打包镜像的分层写法现在大部分后端交付都是镜像形式Dockerfile怎么写直接影响构建速度和镜像体积。一个能用的最小写法大概长这样FROM eclipse-temurin:17-jre WORKDIR /app COPY target/app.jar app.jar ENV TZAsia/Shanghai EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]这个写法能跑但每次改一行代码都要重新拷贝整个 jar构建缓存全废。优化的做法是把依赖层和应用层拆开先用mvn dependency:copy-dependencies把依赖拷到一个目录Dockerfile 里先 COPY 依赖目录再 COPY 应用代码。这样只要pom.xml没变依赖层就能命中缓存构建时间从几分钟降到几秒。构建和运行就两条命令docker build -t myapp:1.0.0 . docker run -d --name myapp -p 8080:8080 -v /data/config:/app/config myapp:1.0.0-v把外置配置目录挂进去配合上面说的配置优先级就能做到配置和镜像分离。另外注意容器里的时区不加ENV TZ的话日志时间会是 UTC排查问题的时候很容易看错时间线。5.3 uni-app 打包 H5、小程序、App 三条路前端这边HBuilderX 菜单里的「发行」下面有三条主要路径产出的东西和验证方式完全不同平台菜单路径产物目录验证方式H5发行 → 网站-PC Web或手机H5unpackage/dist/build/web丢 nginx 或本地静态服务打开微信小程序发行 → 小程序-微信unpackage/dist/build/mp-weixin微信开发者工具导入并预览App发行 → 原生App-云打包云端返回安装包装到真机走一遍核心链路H5 打包有两个参数必须提前想清楚。一个是manifest.json里h5.router.base如果你的站点部署在子目录比如https://example.com/app/这里必须写/app/不写的话所有静态资源都会 404。另一个是路由模式history模式地址干净但需要服务端配合hash模式地址带#但部署最省事。选 history 就必须在 nginx 里加try_files $uri $uri/ /index.html;否则刷新页面直接 404。小程序打包相对简单出包之后用微信开发者工具打开产物目录检查一遍页面是否都能打开、appid是否正确然后上传。这里要注意小程序的appid是在manifest.json的mp-weixin节点里配和 DCloud 的appid不是一个东西很多人只配了一个上传时提示无权限。App 云打包需要填的东西最多DCloud 账号、应用标识、Android 证书keystore 文件和别名密码、iOS 的证书和描述文件。证书这块我建议提前一次性建好并归档Android 用keytool -genkey生成 jks密码和别名记在团队文档里。丢证书的后果是应用无法覆盖安装升级只能让用户卸载重装这个代价很大。6. 打包后暴露的问题白屏、布局错乱、接口 404 的排查链6.1 白屏与静态资源 404 的定位H5 打包后白屏几乎可以按固定的顺序排查不用瞎猜。第一步打开浏览器开发者工具的 Network 面板看主文档加载成功没有、JS 和 CSS 是不是 404。如果全是 404基本就是router.base配错了或者部署目录和配置的 base 对不上。如果资源都 200 但页面还是白的看 Console。常见的是Uncaught SyntaxError说明有代码没被转译到目标环境支持的语法。uni-app 的 H5 端默认会做转译但如果你在vue.config.js里 exclude 了某些依赖或者引入了外部的老库就可能漏掉。还有一种白屏是路由命中不了。history 模式下直接访问深层路由服务端不认识这个路径返回了 nginx 的 404 页面表现就是白屏。这时候看 Network 里主文档的响应内容如果是 nginx 的默认页那就确定是try_files没配。小程序端的白屏排查路径不一样主要看开发者工具的 Console 有没有报「页面不存在」。这一般是pages.json里路径写错或者分包配置有问题导致页面没被打进去。6.2 布局异常多数不是打包的锅搜索这个词的时候你会发现讨论很多我的经验是打包本身极少改变布局逻辑它改变的是运行环境的渲染宽度、字体基准和 CSS 处理策略。所以看到「打包后布局异常」先别怀疑打包按下面这几个方向对第一是适配单位。uni-app 里rpx是按 750 设计稿等比换算的H5 端换算基准是屏幕宽度App 端也有自己的规则。如果你混用了px和rpx在不同宽度的设备上比例就会乱。我的习惯是布局尺寸统一用rpx字体在某些端上用px防止跟随系统字体缩放。第二是 CSS 压缩带来的顺序变化。打包时会做压缩和合并如果样式依赖书写顺序来覆盖比如两个同权重选择器先后出现压缩后顺序变了表现就变了。正确做法是提高选择器权重或者用更明确的类名而不是指望顺序。第三是 flex 和 grid 在旧 webview 上的支持差异。App 端安卓低版本用的是系统 webviewflex 的某些简写属性支持不全。如果你在 App 上看到布局塌陷但 H5 正常基本就是这个原因。可以把flex: 1拆成flex-grow: 1; flex-shrink: 1; flex-basis: 0;写全。第四是安全区适配。全面屏手机底部有手势条不加env(safe-area-inset-bottom)的话固定在底部的按钮会被挡住一部分。这个在 H5 上不明显装到 App 上就很明显。6.3 线上环境和本地环境差异清单打包上线之后问题集中爆发很多时候是因为本地和线上有几处关键差异没有被列出来。我整理了一份清单发布前逐条对一遍能挡掉大部分事故接口地址本地是代理路径线上是真实域名或同域路径。路由模式本地可能是 hash线上为了美观改成 history服务端要同步配try_files。静态资源 base部署在子目录时必须改。环境变量NODE_ENV、后端 profile、数据库连接串全部要确认。时间与时区后端容器时区、数据库时区、前端展示格式三处要对齐。缓存静态资源的文件名是否带 hash不带的话用户会拿到旧版本。nginx 上 HTML 不缓存、JS 和 CSS 长缓存是常规做法。权限跨域头、接口鉴权、静态资源的访问控制。7. 我在两套工具之间来回切换的一些实际体会用了几年下来最大的感受是这套组合的效率瓶颈从来不在工具本身而在于两个环境之间的边界有没有划清楚。边界清清楚了IDEA 只管编译和打包后端HBuilderX 只管编译和打包前端中间通过接口契约对接问题就变成了可以单独定位的小问题。边界模糊的时候一个白屏你会在两个 IDE 之间来回折腾半小时还不知道该看哪边。另外分享一个小习惯每次发布前我会把改动的东西按「后端产物」「前端产物」「部署配置」三行写在一张便签上发布完逐条验证。因为实际事故里占比最高的不是某个复杂的技术问题而是「配置改了但没部署」或者「部署了但改的是另一个环境」这类低级错误多花两分钟写清楚就能挡掉。证书、密钥这类资料一定要在项目立项的时候就建好归档位置别拖到发布前才想起来生成。我见过不止一个项目因为找不到当初的签名文件被迫换包名重新上架那个成本比什么都高。如果你现在正在做 uni-app 加 Java 后端的项目建议先花半天时间把两个 IDE 的环境按上面第二、三节的内容配一遍把自定义基座和证书这两件事提前办掉。这两件事都是「早做没成本、晚做很要命」的类型等到需求全做完再处理就变成卡发布的硬骨头了。