移动端导出
移动端导出
业务用途
平台支持把应用一键导出为 uni-app 移动端工程,覆盖三条目标平台:
- H5 移动 App(
mobilePlatform=h5):纯静态 H5,可直接丢 Nginx / 对象存储部署,也可继续用 HBuilderX 云打包成原生 iOS/Android 安装包。 - 微信小程序(
mobilePlatform=mp-weixin):构建后用微信开发者工具上传发布。 - 支付宝小程序(
mobilePlatform=mp-alipay):模板已预留条件编译支持。
移动端导出复用 的同一套 AppExportService,区别只是 mobilePlatform 字段不同,触发 renderMobileUi 渲染 templates/export/uni-app/ 模板。还支持 mobileOnly=true 仅导移动端、不导后端与 admin-ui。
涉及文件
| 角色 | 文件路径 |
|---|---|
| 导出控制器 | backend/src/main/java/com/lowcode/export/ApplicationExportController.java |
| 移动端渲染方法 | backend/src/main/java/com/lowcode/export/AppExportService.java(renderMobileUi) |
| 导出选项 | backend/src/main/java/com/lowcode/export/ExportOptions.java(mobilePlatform / mobileOnly / miniProgramAppId / mobileApiBase) |
| uni-app 模板根 | backend/src/main/resources/templates/export/uni-app/ |
| H5 打包脚本 | backend/src/main/resources/templates/export/uni-app/build/pack-app.sh.ftl |
| 小程序打包脚本 | backend/src/main/resources/templates/export/uni-app/build/pack-mp.sh.ftl |
| 前端 API | frontend/src/api/application.ts(exportMobileApp / exportMobileMiniProgram) |
后端实现(关键代码)
导出端点
| 方法 | 路径 | mobilePlatform | 说明 |
|---|---|---|---|
POST | /api/application/{appId}/export-mobile-app | h5 | 后端 + admin-ui + uni-app(H5) |
POST | /api/application/{appId}/export-mobile-mp | mp-weixin | 后端 + admin-ui + uni-app(小程序) |
两个端点都设 exportMode="single",再覆盖 mobilePlatform,然后走统一的 doExport。控制器代码:
// ApplicationExportController.java
@PostMapping("/{appId}/export-mobile-app")
public void exportMobileApp(@PathVariable Long appId,
@RequestBody(required = false) ExportOptions opts,
HttpServletResponse response) throws Exception {
if (opts == null) opts = new ExportOptions();
opts.setExportMode("single");
opts.setMobilePlatform("h5");
doExport(appId, opts, "-mobile-app.zip", response);
}
@PostMapping("/{appId}/export-mobile-mp")
public void exportMobileMiniProgram(@PathVariable Long appId,
@RequestBody(required = false) ExportOptions opts,
HttpServletResponse response) throws Exception {
if (opts == null) opts = new ExportOptions();
opts.setExportMode("single");
opts.setMobilePlatform("mp-weixin");
doExport(appId, opts, "-mobile-mp.zip", response);
}渲染触发条件
AppExportService.export 里的分支逻辑(简化):
if (Boolean.TRUE.equals(finalOpts.getMobileOnly())) {
// 仅导移动端:要求 mobilePlatform 非空
if (finalOpts.getMobilePlatform() == null || finalOpts.getMobilePlatform().isBlank()) {
throw new IllegalArgumentException("mobileOnly=true 时必须指定 mobilePlatform");
}
renderMobileUi(cfg, workDir.resolve("mobile"), baseCtx, finalOpts, entities);
} else if ("multiModule".equalsIgnoreCase(finalOpts.getExportMode())) {
renderMultiModuleBackend(...);
renderAdminUi(...);
if (finalOpts.getMobilePlatform() != null && !finalOpts.getMobilePlatform().isBlank()) {
renderMobileUi(cfg, workDir.resolve("mobile"), baseCtx, finalOpts, entities);
}
} else {
// 单体
renderSystemModule(...); renderBizModule(...); renderAdminUi(...);
if (finalOpts.getMobilePlatform() != null && !finalOpts.getMobilePlatform().isBlank()) {
renderMobileUi(cfg, workDir.resolve("mobile"), baseCtx, finalOpts, entities);
}
}即:只要 mobilePlatform 非空,就会额外渲染 mobile/ 目录;mobileOnly=true 时只渲染 mobile/。
renderMobileUi 实现
renderMobileUi 与 renderAdminUi 逻辑同构:用 PathMatchingResourcePatternResolver 扫 classpath*:templates/export/uni-app/**/*,逐个资源处理:
.ftl文件:用 FreeMarker 渲染,去.ftl后缀落到目标路径。- 非
.ftl文件(如static/logo.png、shims-uni.d.ts):原样拷贝。
private void renderMobileUi(Configuration cfg, Path mobileDir, Map<String, Object> baseCtx,
ExportOptions opts, List<EntityMeta> entities) throws Exception {
Resource[] all = rs.getResources("classpath*:templates/export/uni-app/**/*");
Map<String, Object> ctx = new HashMap<>(baseCtx);
ctx.put("entities", entities);
for (Resource res : all) {
String relative = ...; // 截掉模板前缀
Path target = mobileDir.resolve(relative.endsWith(".ftl") ? strip : relative);
if (relative.endsWith(".ftl")) {
cfg.getTemplate("uni-app/" + relative).process(ctx, writer);
} else {
Files.copy(in, target, REPLACE_EXISTING);
}
}
log.info("[AppExport] uni-app 移动端已渲染:platform={}", opts.getMobilePlatform());
}模板上下文 ctx 里除常规字段外,关键变量有:mobilePlatform(h5 / mp-weixin / mp-alipay)、miniProgramAppId、mobileApiBase、entities(用于生成 pages.json 路由与 common/entities.js)。
ExportOptions 移动端相关字段
| 字段 | 默认值 | 用途 |
|---|---|---|
mobilePlatform | 空 | h5 / mp-weixin / mp-alipay;空则不出移动端 |
mobileOnly | false | true 时仅导 uni-app,不导后端与 admin-ui |
miniProgramAppId | touristappid | 写入 manifest.json,发布前替换为真实 AppID |
mobileApiBase | /api | 移动端 H5 构建时后端 API 基础地址 |
uni-app 模板树
uni-app/
├─ package.json.ftl / pnpm-workspace.yaml.ftl / vite.config.js.ftl / index.html.ftl
├─ .gitignore.ftl / README.md.ftl
├─ shims-uni.d.ts
├─ build/
│ ├─ pack-app.sh.ftl # H5 一键打包脚本
│ └─ pack-mp.sh.ftl # 微信小程序一键打包脚本
└─ src/
├─ App.vue.ftl / main.js.ftl / uni.scss.ftl
├─ manifest.json.ftl # 含 miniProgramAppId、条件编译平台
├─ pages.json.ftl # 由 entities 动态生成 tabBar 与页面路由
├─ api/
│ ├─ request.js.ftl # uni.request 封装,baseURL=mobileApiBase
│ ├─ auth.js.ftl # 登录/JWT
│ └─ generic.js.ftl # 动态 CRUD 请求(与 admin-ui 思路一致)
├─ common/
│ ├─ entities.js.ftl # 由 EntityMeta 生成的前端实体描述
│ ├─ dict.js.ftl # 字典缓存
│ └─ format.js.ftl
├─ store/user.js.ftl
├─ static/
│ ├─ logo.png
│ └─ tabbar/ # home / menu / my 的普通态与 active 态图标
└─ pages/
├─ index/index.vue.ftl # 首页(数据看板)
├─ menu/menu.vue.ftl # 菜单(由 entities 动态生成)
├─ list/list.vue.ftl # 通用列表页
├─ detail/detail.vue.ftl # 通用详情页
├─ form/form.vue.ftl # 通用表单页
├─ login/login.vue.ftl
└─ my/my.vue.ftl # 我的
H5 打包脚本 pack-app.sh
build/pack-app.sh.ftl 渲染后的脚本四步:检查 Node/pnpm -> pnpm install -> pnpm run build:h5 -> 输出 dist/build/h5/。核心片段:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
echo "==> [3/4] 构建 H5 产物"
pnpm run build:h5
OUT="$ROOT/dist/build/h5"
echo " H5 打包完成!产物目录:$OUT"
echo " 部署:把 $OUT 拷到 Nginx 静态根,/api 反代到后端 ${serverPort?c} 端口"
echo " 打原生 App(可选):HBuilderX 打开本工程 -> 发行 -> 原生 App-云打包"想要原生 App
脚本不做云打包(需 DCloud 账号)。拿到 H5 工程后,用 HBuilderX 打开 ->「发行 -> 原生 App-云打包」即可生成 iOS/Android 安装包。
小程序打包脚本 pack-mp.sh
build/pack-mp.sh.ftl 同样四步,构建目标是 build:mp-weixin,产物在 dist/build/mp-weixin/,用微信开发者工具打开该目录上传发布:
echo "==> [3/4] 构建微信小程序产物"
pnpm run build:mp-weixin
OUT="$ROOT/dist/build/mp-weixin"
echo " 发布步骤:"
echo " 1) 微信开发者工具导入项目,目录选 $OUT"
echo " 2) AppID 用 manifest.json 里的 ${miniProgramAppId}(发布前替换真实 AppID)"
echo " 3) 后端域名需在小程序后台「开发设置 -> 服务器域名」加入 request 合法域名"前端实现
frontend/src/api/application.ts 封装两个导出接口,均用 responseType: 'blob' 接收 zip:
// application.ts
export function exportMobileApp(id: number, opts?: any) {
return request.post<any>(`/application/${id}/export-mobile-app`, opts || {}, { responseType: 'blob' })
}
export function exportMobileMiniProgram(id: number, opts?: any) {
return request.post<any>(`/application/${id}/export-mobile-mp`, opts || {}, { responseType: 'blob' })
}前端拿到 blob 后,用 URL.createObjectURL + <a download> 触发浏览器下载。
操作步骤
导出 H5 移动 App
- 确保应用下表与字段已保存。
- 调
POST /api/application/{appId}/export-mobile-app,请求体可空(走默认ExportOptions)。 - 浏览器下载
<artifactId>-mobile-app.zip,解压。 cd mobile(或解压后的移动端目录),bash build/pack-app.sh。- 脚本产出
dist/build/h5/,拷到 Nginx 静态根,/api反代到后端(默认端口 8080)。 - (可选)HBuilderX 打开工程 -> 发行 -> 原生 App-云打包,得 iOS/Android 包。
导出微信小程序
- 调
POST /api/application/{appId}/export-mobile-mp。 - 解压后
cd mobile && bash build/pack-mp.sh,产出dist/build/mp-weixin/。 - 打开「微信开发者工具」,导入项目,目录选
dist/build/mp-weixin。 - AppID 用
manifest.json里的miniProgramAppId(默认touristappid,发布前必须换成真实 AppID)。 - 调试时勾选「详情 -> 本地设置 -> 不校验合法域名」。
- 真机预览/上传 -> 微信公众平台「版本管理」提交审核。
- 后端域名需在小程序后台「开发设置 -> 服务器域名」加入 request 合法域名。
仅导出移动端(mobileOnly)
在请求体里传 {"mobileOnly": true, "mobilePlatform": "h5"}(或 mp-weixin),zip 里只有 mobile/,不含后端与 admin-ui。适合后端已独立部署、只想要移动端壳的场景。
mobileOnly 必须指定 platform
mobileOnly=true 时若 mobilePlatform 为空,后端抛 IllegalArgumentException("mobileOnly=true 时必须指定 mobilePlatform")。
常见问题
pack-app.sh / pack-mp.sh 跑不起来
脚本要求 Node.js >= 16 与 pnpm。command -v pnpm 检测不到时会尝试 npm install -g pnpm 自动装。若公司网络受限,手动装好 pnpm 再跑。node_modules 已存在时跳过安装,重装请删该目录。
小程序请求后端失败 / 不显示数据
微信小程序要求后端域名在「服务器域名」白名单里(且必须是 HTTPS)。开发期可在开发者工具勾「不校验合法域名」。正式发布必须配置合法域名。mobileApiBase 默认 /api,发布前改成线上绝对地址。
小程序 AppID 是 touristappid
ExportOptions.miniProgramAppId 默认 touristappid(DCloud 旅游 demo AppID)。导出后在 src/manifest.json 里替换为你的真实微信 AppID,或在导出请求体里传 {"miniProgramAppId": "wx你的appid"}。
移动端页面怎么和后端表对应?
pages.json.ftl 与 common/entities.js.ftl 都由 entities(List<EntityMeta>)驱动:每张业务表生成一个列表/详情/表单路由,菜单页 menu.vue 据此渲染入口。新增表后重新导出即可,不用手写页面。请求走 api/generic.js 封装的 /api/biz/<module>/list 等接口(与导出后端 admin Controller 对齐)。
想要支付宝小程序
ExportOptions.mobilePlatform 支持 mp-alipay(模板已预留条件编译)。但目前没有专用的导出端点(控制器只暴露 h5 与 mp-weixin),可调 POST /api/application/{appId}/export-springboot 并在请求体里传 {"mobilePlatform": "mp-alipay"},单体会触发 renderMobileUi。再用 HBuilderX 或 alipay 开发者工具打开产物。
