Skip to content

H5 发布流水线规划

平台级文档(不限于 vem)。规划"子应用前端 → 合并进主项目 → 构建 H5 → 部署 CDN,平台内全租户共用"的自动化构建。

状态:规划中,接口已落地一部分。创建于 2026-06-15,最近更新 2026-06-15。


1. 背景与目标

我们的产品是可独立交付的多租户 SaaS:同一套 hiapi-cloud 会卖给多个平台/运营商,每个平台一套独立部署(独立 hiapi-cloud-public、独立数据库、独立装了哪些子应用)。主前端是 uniapp/hiapi-cloud-uniapp(下称主项目),各业务子应用(vem / shop / store / public …)各自独立开发,产物发布到中心应用市场(appstore.cloud.adinz.com,跨所有平台共享)。

本期只做 H5。 小程序、APP 等项目多了之后再单独规划(见文末「后续」)。

H5 的发布模型(已与负责人确认):

  • 平台内一份产物,全租户共用。 同一平台下不同租户的差异(启用了哪些子应用、装修内容、主题、菜单)全是运行时配置,不进代码、不触发构建。
  • 构建是低频运营动作:某平台的运营者在「新增子应用 / 子应用升版」时触发一次,产出该平台自己的 H5。
  • 每个平台各自独立构建:用哪些子应用、各自什么版本,是平台级数据(各平台 CloudApplication 表),平台之间互不相同。

为什么不用阿里云云效,改用客户服务器上的 k8s 构建

  • 产品要交付给 N 个平台/客户,很多部署在客户自有环境(含内网)。中心化的云效流水线服务不了"每个客户独立、各自数据、各自产物"的形态。
  • 因此构建能力随产品一起部署到客户的 k8s 集群,在客户服务器上就近构建——读本平台的 CloudApplication、就近部署到本平台的 CDN/静态托管。
  • 构建本质仍是几条 shell(node 合并脚本 + pnpm build + 产物上传),用一个 k8s Job(可选常驻 build-controller)承载即可,不需要中心调度系统。

目标:把"拉主项目 → 读本平台子应用清单 → 下载并合并子应用代码 → pnpm build:h5 → 部署本平台 CDN"这条链路,在客户 k8s 集群内用 Job 自动化,运营点一下「发布」即可。


2. 两层构建模型(核心,务必区分)

整套体系有两层完全不同的构建,千万别混:

第 1 层:子应用制品层(已存在,无需改造)

每个子应用仓库(如 hiapi-cloud-vem/vem-web)用 lib 模式 pnpm build:h5,经 @hiapi/hiapi-cloud-components-uploadUploadPlugincloseBundle 阶段:

  1. src/components/<componentDir>(如 hiapi-vem)+ src/<pageDir>(如 subVem)拷进 dist/zip/,压成 zip.zip;
  2. 解析 src/components/index.tswidgetExportcomponents.json(组件模板);
  3. 连同 umd.js(装修预览用)、pages(对外页面列表)、menus(后台菜单)一起 POST 到中心应用市场https://appstore.cloud.adinz.com/api/v1/releases/frontend,按 project / version 归档。

最终在应用市场 OSS 上形成(参见 hiapi-cloud-nuwa service.goprefix):

hiapi-cloud/<project>/<version>/
    components.json          组件模板(入库,供装修选择组件)
    pages.json               页面路径(入库)
    <project>.umd.js         H5 组件预览(后台可视化装修用)
    zip.zip                  源代码:components/<componentDir>/ + <pageDir>/

这一层产出的 umd.js 只服务后台可视化装修预览;zip.zip真正可运行的源码,是第 2 层的输入。这两个用途不要搞混。

第 2 层:H5 运行时层(本文档要做的)

把第 1 层各子应用的 zip.zip 里的源码合并进主项目,做一次普通的 pnpm build:h5(注意:主项目是正常 app 构建,不是 lib 模式),产出真正给终端用户访问的 H5 站点,部署到本平台 CDN,平台内全租户共用。

中心应用市场(OSS)          主项目 hiapi-cloud-uniapp            平台 CDN
┌──────────────┐          ┌───────────────────────────┐      ┌─────────┐
│ vem/0.0.2/   │          │ src/components/hiapi-vem/  │      │         │
│   zip.zip    │──解压──▶ │ src/subVem/                │      │ 平台内  │
│ shop/x/      │──合并──▶ │ src/components/hiapi-shop/ │─build│ 一份 H5 │
│   zip.zip    │          │ src/subShop/               │──────▶ 全租户  │
│ public/...   │          │ (vite 自动接线,无需改配置)│      │ 共用    │
└──────────────┘          └───────────────────────────┘      └─────────┘
        ▲ 下载哪些 / 哪个版本,由本平台 build-manifest 接口(§6)决定

3. 合并契约(Merge Contract)

主项目 vite.config.ts 已做好自动接线,合并因此非常简单——只是放文件,不改配置:

机制配置作用
分包自动注册subPackages: fg(['src/sub*'])任何 src/sub* 目录(如 subVem)自动成为分包
组件自动导入UniHelperComponents({ dirs:['src/components'], directoryAsNamespace:true })src/components/hiapi-vem/** 自动 easycom,命名空间隔离
动态渲染器dynamicRendererPlugin({ dirs:'src/components/**/*.vue' })扫描所有组件生成 DynamicRenderer.vue,按装修 schema 的 item.component 渲染
页面路由UniHelperPages 读各页面 <route> 块自动生成 pages.json合并页面后路由自动注册,无需手动改 pages.json

现成样板:主项目已存在 src/components/hiapi-public(public 子应用合并进来的),vem 照此放 src/components/hiapi-vem + src/subVem 即可。

合并落点(每个子应用)

解压 zip.zip 后,zip 内结构为:

zip/
  components/<componentDir>/   →  主项目 src/components/<componentDir>/
  components/index.json        →  组件模板(入库用,运行时不需要,合并时丢弃或单独收集)
  <pageDir>/                   →  主项目 src/<pageDir>/

合并动作:

src/components/<componentDir>/   ← zip/components/<componentDir>/      (整目录覆盖)
src/<pageDir>/                   ← zip/<pageDir>/                       (整目录覆盖)

注意:子应用源码里的 property.vue(装修属性面板)只在装修后台用,运行时不需要。可在合并时按需剔除(**/property.vue)以减小包体;不剔除也不影响运行,只是多打一点。

强约束(必须在子应用开发期遵守,否则合并构建会炸)

  1. 依赖白名单:子应用只能使用主项目 package.json 已有的依赖 + 共享库(@hiapi/hiapi-cloud-web-basicwot-design-uni 等)。绝不在合并期 merge 子应用自己的 package.json。 这条要在子应用仓库用 CI / lint 卡死,否则生产构建会因缺依赖随机失败,极难排查。
  2. 命名空间:组件 hiapi-<project>-<widget>、页面路径 /sub<Project>/pages/...、组件目录 hiapi-<project>、页面目录 sub<Project>。靠前缀天然防撞,继续强制。
  3. 不碰主项目公共文件:子应用不得改 main.ts、根 pages.jsonApp.vue 等;一切扩展通过"放自己的目录"完成。

4. 运行时多租户机制(平台内一份产物如何服务所有租户)

H5 在一个平台内是一份产物,租户差异全靠运行时:

  1. 识别租户:H5 启动时按访问域名(*.tenant-domain 泛域名)或 URL 上的 tenantId 确定当前租户。
  2. 拉租户配置:向后端请求该租户的配置 —— 启用了哪些子应用、各页面/首页的装修 schema(JSON)、主题色、菜单(tabBar/导航)。
  3. 渲染:装修页面把 schema 交给 DynamicRenderer.vue,按 item.component 渲染对应组件(组件代码已在包里)。
  4. 功能开关:未启用的子应用,其菜单/入口运行时隐藏;即使包里有代码也访问不到。

因此:装修内容变化、租户启停子应用,都不需要重新构建,改后端配置即生效。只有"平台新增/升级子应用的代码"才需要跑本流水线。


5. 子应用构建清单接口(build-manifest)

决定"本平台这次构建合并哪些子应用、各自什么版本"。来源是本平台CloudApplication 表(本平台装了哪些应用、各自已安装的前端版本),再对接中心应用市场补齐 project / componentDir / pageDir(这些目录信息平台本地不存)。

接口

GET /cloud-api/public/app-store/build-manifest
鉴权:无(公开接口,放在 /public/* 路径,不被网关拦截鉴权)

实现在 hiapi-cloud-publiccn.hiapi.system.basic.api.pub.AppBuildController(@RequestMapping("/public/app-store"),无 @Secured)。 构建流水线跑在客户集群内、无登录态,故必须走公开路径。内部调中心市场批量接口 GET https://appstore.cloud.adinz.com/api/v1/market/apps/batch?appIds=...(公开,只回 ONLINE 应用)补齐前端目录信息。 数据是平台级(本平台 CloudApplication),不含租户维度、不暴露敏感字段(无 appSecret),可安全公开。

实现逻辑

  1. 取本平台 CloudApplicationinstall == 2(安装成功) 的应用。
  2. 用这些 appId 调中心市场批量接口,拿到每个应用的 project / componentDir / pageDir
  3. 逐条组装:
    • project / componentDir / pageDir 任一为空 ⇒ 视为无前端产物(纯后端应用 / 主宿主应用),跳过;
    • 构建用版本 = 本平台已安装的前端版本 CloudApplication.frontendVersion(为空回退 version)——保证构建结果与本平台当前安装状态一致、可复现,不取市场最新版;
    • zipPath = hiapi-cloud/<project>/<version>/zip.zip(应用市场 OSS 相对路径,与 nuwa service.goprefix 约定一致)。

响应

jsonc
{
  "code": 200,
  "data": [
    {
      "appId": "A-11001",
      "name": "无人售卖机",
      "project": "hiapi-cloud-vem",
      "frontendVersion": "0.0.2",
      "componentDir": "hiapi-vem",
      "pageDir": "subVem",
      "zipPath": "hiapi-cloud/hiapi-cloud-vem/0.0.2/zip.zip"
    }
  ]
}
  • 流水线拿 zipPath,用自身配置的 应用市场 OSS base(环境变量 APPSTORE_OSS_BASE)拼成完整下载地址。当前 base = https://hiapi-cloud.oss-cn-shenzhen.aliyuncs.com,拼出完整地址如 https://hiapi-cloud.oss-cn-shenzhen.aliyuncs.com/hiapi-cloud/hiapi-cloud-vem/0.0.2/zip.zip。把 base 留给流水线而非写死在接口,便于不同环境/镜像源切换。
  • 范围是"本平台已装的所有有前端的子应用",不带租户维度——H5 平台内一份产物,租户启停是运行时的事。

6. k8s 构建设计(随产品部署在客户集群)

租户后台点「发布 H5」
   → hiapi-cloud-public 写一条发布记录(pending) + 创建一个 k8s Job
       (由 Job 模板渲染;或写入队列交给常驻 build-controller)
   → Job 容器执行:
        ① 读 build-manifest 接口(集群内直连 service)
        ② 逐个下载 zipPath 的 zip(APPSTORE_OSS_BASE + zipPath)
        ③ node scripts/merge-subapps.mjs   解压、合并到主项目 src/(见 §7)
        ④ pnpm install --frozen-lockfile
        ⑤ pnpm build:h5:production         产物在 dist/build/h5
        ⑥ 部署到本平台静态托管(nginx 挂载卷 / 平台 OSS / CDN)
        ⑦ 回调 hiapi-cloud-public 更新发布记录(success/fail + 构建记录)

要点(都是为了"在客户服务器、低频、要稳"):

  • 构建工具链镜像 hiapi-cloud-h5-builder:<版本>:只装 node22 + pnpm9.9 + git + unzip/rsync/curl 和一个通用 bootstrap(deploy/builder/),不打进 app 源码——主项目仓库变动频繁,代码由 Job 运行时 git clone 最新拉取(GIT_REPO/GIT_BRANCH,私有仓库用 GIT_TOKEN)。镜像因此很稳定,只在 node/pnpm/工具版本变更时才重打。
  • install 提速:没源码就没法预烤 node_modules,改用挂载的 pnpm store 卷(PNPM_STORE_DIR,内容寻址、跨构建复用)做热缓存,首次后 install 几乎不联网。
  • 触发与并发:低频、单平台,一次发布一个 Job;加平台级锁防并发构建。Job 失败保留日志供后台查看。
  • 产物落点可配:每个平台 CDN/静态托管不同,作为环境变量注入 Job。

后端怎么"指挥" k8s 建 Job(A 路线,已实现)

k8s API server 本身就是个 HTTPS 接口,"建一个构建任务" = 往它 POST 一个 Job 的 JSON。后端跑在集群内时,Pod 自动挂好 ServiceAccount 凭证:

/var/run/secrets/kubernetes.io/serviceaccount/token   身份(JWT,放 Authorization: Bearer)
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt  校验 API server 证书
API server: https://kubernetes.default.svc            (用 DNS 名以匹配证书 SAN)
动作k8s API
建构建任务POST /apis/batch/v1/namespaces/<ns>/jobs + Job JSON
查状态GET .../jobs/hiapi-h5-build-<buildNo>.status.succeeded/.failed(本方案不轮询,靠 Job 回调)
看日志GET .../pods/<pod>/log
清理Job 的 ttlSecondsAfterFinished 自动回收

实现:K8sJobClient(复用框架已有的 okhttp,用集群 CA 单建一个 client,不动全局 HttpService)+ CloudH5BuildService.launch() 程序化拼 Job JSON 并 POST。前提是给后端 ServiceAccount 配 RBAC(在 deploy/h5-prereqs.hiapi-cloud.yaml 内:建/查/删 Job、读 Pod 日志)。非集群环境(本地无 token)自动跳过,记录保持 PENDING,便于联调。

  • 环境变量(后端 hiapi-cloud-public):H5_BUILDER_IMAGE(构建镜像 tag,跟产品版本)、H5_BUILD_NAMESPACE(Job 命名空间,缺省读 ServiceAccount 挂载)、K8S_API_BASE(API server 地址,缺省 https://kubernetes.default.svc)。
  • 环境变量(Job 注入,走 deploy 的 ConfigMap/Secret):APPSTORE_OSS_BASE(市场 OSS 基址)、BUILD_MANIFEST_URL(本平台接口地址)、PLATFORM_TOKEN(调接口鉴权)、DEPLOY_TARGET(产物上传目标)、CALLBACK_URL(回调发布记录,= /public/app-store/build-callback)、BUILD_ID(= 发布记录 buildNo,由后端按 buildNo 注入)。

M3 已落地的后端接口(hiapi-cloud-public)

接口路径鉴权作用
触发发布POST /platform/h5-build/triggerPlatform平台级并发锁 → 建发布记录(PENDING)→ 直接调 k8s API 创建 Job(转 BUILDING)
发布记录分页POST /platform/h5-build/queryPlatform后台列表(TablePage),按 status/时间筛选,created 倒序
发布记录详情GET /platform/h5-build/{id}Platform单条详情(含 record 溯源、message)
构建回调POST /public/app-store/build-callback无(公开)Job 完成回调:按 buildId(=buildNo)更新状态 success/fail + 写 record
  • 发布记录实体 CloudH5Build(表 hiapi_core_h5_build,平台级、不分租户):buildNo(唯一,= Job 名后缀 / BUILD_ID / 回调键)、status(PENDING/BUILDING/SUCCESS/FAILED)、operatorFidimagerecord(build-record.json 原文)、message
  • 并发锁:存在 PENDING/BUILDING 记录时拒绝再次触发("已有 H5 构建任务进行中")。

7. 合并脚本设计(scripts/merge-subapps.mjs)

放在主项目仓库(随构建镜像一起),纯 Node,无额外重依赖(用内置 + 已有的 fast-glob/解压库)。

输入:build-manifest 接口返回的清单。流程:

manifest = GET $BUILD_MANIFEST_URL              // 本平台子应用清单
for app of manifest:
    url = `${APPSTORE_OSS_BASE}/${app.zipPath}`
    download url -> tmp/${app.project}.zip
    校验(大小/可解压;可加 sha 校验)
    unzip -> tmp/${app.project}/
    assert exists tmp/.../components/${app.componentDir}     // 缺目录即失败,fail fast
    assert exists tmp/.../${app.pageDir}
    cpSync components/${app.componentDir}  ->  src/components/${app.componentDir}   (先清后拷)
    cpSync ${app.pageDir}                  ->  src/${app.pageDir}                   (先清后拷)
    (可选) rm src/${app.pageDir}/**/property.vue, src/components/${app.componentDir}/**/property.vue
    收集 app 版本信息
打印合并报告(项目/版本/文件数),写 dist/build-record.json(本份产物含哪些子应用@版本)

要点:

  • fail fast:任何子应用下载失败 / 目录缺失 → 整个构建失败,绝不"带病构建"。
  • 幂等:合并前先清理上次合并落点(src/components/hiapi-* 中清单内的、src/sub* 中清单内的),避免残留旧版本。
  • 不动主项目自带的、清单之外的公共组件。
  • 可复现:build-record.json 记录本次锁定的子应用与版本,随产物发布,便于追溯"线上这份 H5 是用哪些版本拼的"。

8. 需要配合准备的事项(后端 / 运营 / 运维)

  • [x] build-manifest 接口:hiapi-cloud-public 公开端 AppBuildController.buildManifest(/public/app-store/build-manifest,无鉴权),读 CloudApplication + 中心市场批量接口,已实现。
  • [x] 合并脚本:主项目 scripts/merge-subapps.mjs(读清单 → 下载/解压 → 合并 src/ → 写 build-record.json),已本地端到端跑通。
  • [x] 构建入口 + 镜像 + Job 模板:scripts/build-h5.shdeploy/builder/(工具链镜像 + git clone bootstrap)、deploy/h5-build-job.yaml(参考 Job),已落地。
  • [ ] 应用市场 OSS 下载可达:确认 APPSTORE_OSS_BASE + hiapi-cloud/<project>/<version>/zip.zip 在构建环境可下载。
  • [x] 发布记录 + 触发 + 回调接口:CloudH5Build 记录 + H5BuildController(触发/分页/详情,平台端)+ AppBuildController.buildCallback(公开回调)+ 平台级并发锁,已实现。
  • [x] 后端直建 k8s Job:K8sJobClient(集群 CA + ServiceAccount token,POST Job 到 API server)+ CloudH5BuildService.launch() 程序化拼 Job,已实现。
  • [ ] 预置资源 apply:apply deploy/h5-prereqs.hiapi-cloud.yaml(SA/Role/RoleBinding + ConfigMap/Secret/PVC,一把梭),并把 hiapi-cloud-public 的 Deployment 绑到 serviceAccountName: hiapi-cloud-public
  • [ ] 集群内验证:打镜像 + 后端触发,跑通一次发布(建 Job → 构建 → 产物落 CDN → 回调)。
  • [ ] 后台「发布 H5」页面:接 POST /platform/h5-build/trigger + TablePage/platform/h5-build/query 展示发布历史。
  • [ ] 依赖白名单校验:在各子应用仓库 CI 增加检查(子应用引用的依赖必须 ⊆ 主项目依赖 + 共享库)。
  • [ ] 后端租户配置接口:返回某租户的「启用子应用列表 + 各页面装修 schema + 主题 + 菜单」,供 H5 运行时拉取(运行时多租户的基础)。
  • [ ] 泛域名 / CDN:规划 *.<域名>path/tenantId 方案,让前端能按访问入口识别租户;各平台 CDN/静态托管配置。

9. 缓存与性能

手段收益
工具链镜像(node+pnpm+git,不含源码)镜像稳定、与频繁变动的 app 仓库解耦;代码每次 git clone 最新
挂载 pnpm store 卷热缓存install 几乎不联网,单次构建降到分钟内
--frozen-lockfile保证依赖确定性、可复现
产物版本目录 + 软链切换秒级回滚

构建频率本身很低(只在子应用增/升版时),性能不是瓶颈,但预烤镜像能让单次发布从"几分钟"降到"一两分钟"。


10. 风险与约束

风险说明对策
依赖冲突子应用引入主项目没有的依赖 → 构建炸依赖白名单 + 子应用 CI 校验(§3 强约束 1)
命名/路由碰撞子应用间组件名/页面路径重复强制 hiapi-<project>-* / sub<Project> 命名空间
带病构建某子应用 zip 损坏仍继续打包合并脚本 fail fast
包体积膨胀H5 含本平台所有子应用代码H5 无硬上限 + 分包懒加载,可接受;真大了再按需精简
版本漂移不知道线上这份产物含哪些版本构建用已安装版本(非市场最新)+ 产物写 build-record.json
客户集群资源构建占用客户 k8s 资源低频、单 Job、可限资源配额;构建完即回收

11. 落地里程碑

  1. M0 接口:build-manifest 接口(✅ 已实现)。
  2. M1 合并脚本:主项目 scripts/merge-subapps.mjs(✅ 已实现并本地跑通;pnpm build:h5 在镜像内由 build-h5.sh 串联)。
  3. M2 构建镜像 + Job:工具链镜像 deploy/builder/(Dockerfile + entrypoint,git clone 拉源码)+ scripts/build-h5.sh 入口 + deploy/h5-build-job.yaml + deploy/h5-prereqs.hiapi-cloud.yaml(✅ 已实现;待客户集群内 apply 跑通一次)。
  4. M3 后端触发 + 发布记录:发布记录表 CloudH5Build + 触发/分页/详情/回调接口 + 平台级并发锁 + 后端直接调 k8s API 建 Job(K8sJobClient,A 路线)(✅ 已实现;待 apply RBAC/预置资源、集群验证、后台页面)。
  5. M4 运行时多租户:后端租户配置接口 + 前端按域名/tenantId 拉配置 + DynamicRenderer 渲染,打通"平台内一份产物多租户"。
  6. M5 子应用 CI 校验:依赖白名单 + 命名规范检查,防止脏制品流入。

12. 后续(本期不做)

  • 小程序:必须构建期打包(微信禁远程代码),且需"微信开发者工具 CLI"做 headless 构建/上传——届时用一台装了开发者工具的自有构建机接进同样的 k8s 构建体系。建议按"启用子应用组合"构建,租户下载精简工程后用自己的 AppID 自助上传提审(平台不碰上传密钥)。
  • APP:图标/名称/包名/证书因租户而异,需按租户打包;优先"原生壳构建一次 + wgt 热更新"减少重打。

变更记录

日期变更备注
2026-06-15创建文档,确定 H5 两层模型、合并契约、清单契约规划,待实现
2026-06-15架构改为多平台 + 客户服务器 k8s 构建(弃用云效);清单来源改为 hiapi-cloud-publicbuild-manifest 接口(读 CloudApplication + 中心市场补 componentDir/pageDir);接口已实现规划,接口已落地
2026-06-15build-manifest 改为公开接口 /public/app-store/build-manifest(AppBuildController,无鉴权),构建流水线无登录态需走公开路径接口已落地
2026-06-15落地流水线脚本与镜像:主项目 scripts/merge-subapps.mjs(合并,已本地跑通)、scripts/build-h5.sh(构建入口:合并→install→build:h5→部署→回调)、Dockerfile.builder(预烘焙镜像)、deploy/h5-build-job.yaml(k8s Job + ConfigMap + Secret 模板)M1/M2 已落地,待集群验证
2026-06-15M3 后端落地:发布记录实体 CloudH5Build + JPA + CloudH5BuildService(触发/并发锁/回调)、平台端 H5BuildController(trigger/query/详情)、公开 AppBuildController.buildCallbackM3 接口已落地
2026-06-15确定 A 路线并落地:后端直接调 k8s API 建 Job(K8sJobClient 用集群 CA + ServiceAccount token POST Job),弃用 build-controller;新增 deploy/h5-builder-rbac.yaml,deploy/h5-build-job.yaml 改为预置资源 + 参考清单A 路线已落地,待集群验证
2026-06-16镜像不再打进 app 源码:改为工具链镜像(deploy/builder/ Dockerfile + entrypoint)+ Job 运行时 git clone 最新代码;install 改用挂载的 pnpm store 卷热缓存(PNPM_STORE_DIR)。Job 增 pnpm-store PVC,去掉 SKIP_INSTALL;ConfigMap 增 GIT_REPO/GIT_BRANCH/PNPM_STORE_DIR,Secret 增 GIT_TOKEN已落地,待集群验证
2026-06-16收敛 deploy/ 冗余:删 h5-builder-rbac.yaml(已并入 prereqs),h5-build-job.yaml 瘦成纯「参考 Job」;h5-prereqs.hiapi-cloud.yaml 成唯一需 apply 的预置资源文件,GIT_REPO 填 gitee文档/部署整理