uni-app 打包发布与热更新实战:云打包、证书管理、审核与 wgt 热更

uni-app 打包发布与热更新实战:云打包、证书管理、审核与 wgt 热更

admin
2026-07-28 / 0 评论 / 5 阅读

开发完成只是半程,把 uni-app 项目发布到微信、H5、App 三个渠道,每条路都有独立的流程和坑:证书管理、各平台审核规则、App 热更新。本文讲透发布链路的完整实操。

一、发布全景

渠道产物发布方式更新机制
微信小程序wxapkg微信后台上传 + 提审冷启动异步更新
H5静态文件部署到服务器/CDN刷新即最新
App(Android)apk / aab应用市场/官网分发整包更新 + wgt 热更新
App(iOS)ipaApp Store + TestFlight整包更新 + wgt(受限)

二、微信小程序发布

发布流程

# 1. HBuilderX 发行 → 小程序-微信,生成编译产物
# 产物在 unpackage/dist/build/mp-weixin

# 2. 用微信开发者工具打开该目录

# 3. 工具栏「上传」→ 填版本号和备注 → 上传为开发版本

# 4. 微信公众平台 → 版本管理 → 提交审核 → 审核通过 → 全量发布

也可以用 miniprogram-ci 做命令行自动化上传:

// scripts/upload-mp.js
const ci = require('miniprogram-ci')

async function upload() {
  const project = new ci.Project({
    appid: 'wx_xxxxxxxx',
    type: 'miniProgram',
    projectPath: './unpackage/dist/build/mp-weixin',
    privateKeyPath: './private.key' // 微信后台下载的上传密钥
  })

  await ci.upload({
    project,
    version: require('../package.json').version,
    desc: 'release: ' + new Date().toLocaleString(),
    setting: { es6: true, minify: true }
  })
}

配到 package.json scripts 里,CI/CD 流水线一键发布。

审核避坑清单

微信审核被拒的高频原因,发布前自查:

  1. 类目与功能不符:有交易功能但类目没选电商;有社区内容但没选社交类目
  2. 诱导分享:分享得奖励、强制分享解锁功能的文案全部要清掉
  3. 测试账号:提审备注里必须给一个可登录的测试账号(有登录功能时)
  4. 隐私接口声明:用了 getLocation、chooseAddress、相机等隐私接口,必须在 app.jsonrequiredPrivateInfos 声明并申请权限
  5. 虚拟支付:iOS 端虚拟商品(会员、课程)不能直接用微信支付,要走 IAP 或隐藏入口
  6. 完整可体验:每个 tab 每个入口都要能点,"敬请期待"的空页面是高频被拒项

小程序的静默更新机制

小程序新版本发布后,已打开的用户用的仍是旧版,下次冷启动时检查更新。强制立即更新的方案:

// App.vue
const updateManager = uni.getUpdateManager()

updateManager.onCheckForUpdate((res) => {
  console.log('是否有新版本:', res.hasUpdate)
})

updateManager.onUpdateReady(() => {
  uni.showModal({
    title: '更新提示',
    content: '新版本已准备好,是否重启应用?',
    success(res) {
      if (res.confirm) {
        updateManager.applyUpdate() // 立即应用新版本并重启
      }
    }
  })
})

updateManager.onUpdateFailed(() => {
  uni.showToast({ title: '更新失败,请删除小程序重新打开', icon: 'none' })
})

三、H5 发布

构建与部署

# HBuilderX 发行 → 网站-H5,或 CLI
npx uni build
# 产物 unpackage/dist/build/h5

部署要点:

# Nginx 配置:history 路由需要 fallback
server {
  listen 443 ssl;
  server_name h5.example.com;
  root /var/www/h5;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }

  # 静态资源长缓存(文件名带 hash)
  location /static/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}

跨域处理:H5 调后端接口要么后端开 CORS,要么 Nginx 加反向代理把 /api 转发到服务端,生产环境强烈推荐代理方案,前端请求同源路径,省掉 CORS 一切麻烦。

微信 H5 特殊处理

在微信内打开的 H5 要做微信 JS-SDK 授权(自定义分享卡片、微信支付):

  1. 公众号后台配 JS 接口安全域名
  2. 后端实现签名接口(access_token → jsapi_ticket → signature)
  3. 前端 wx.config 注入配置后才能调分享等 API

四、App 打包:云打包与证书

证书体系认知

Android

  • 正式签名证书(keystore):自己用 keytool 生成,务必自己保管,丢了就无法给老用户覆盖升级
  • 云打包时可以用 DCloud 公共测试证书(仅测试),正式包必须用自己的证书
# 生成 Android 证书
keytool -genkey -alias myapp -keyalg RSA -keysize 2048 -validity 36500 \
  -keystore myapp.keystore

iOS

  • 需要 Apple 开发者账号(个人版 $99/年)
  • 证书(p12)+ 描述文件(mobileprovision)+ Bundle ID 三件套
  • 发布 App Store 需要 distribution 证书;真机调试需要 development 证书

云打包实操

HBuilderX → 发行 → 原生App-云打包
  → 选择平台(Android/iOS)
  → 填证书信息(Android: keystore + 密码 + 别名)
  → 勾选原生插件(推送、地图、支付等模块)
  → 打包

云打包排队可能要几分钟到半小时。打完的产物:

  • Android:apk 直接可装;上架 Google Play 需要转 aab
  • iOS:ipa 需要 TestFlight 或 XCode 上传(Windows 用户的经典痛点,需要 Mac 或云打包的"越狱包/测试包"过渡)

Android 各应用市场分发

国内市场(华为、小米、OPPO、vivo、应用宝)每个都要单独注册开发者、上传软著或承诺函、单独提审。实际操作建议:

  1. 官网直接放 apk 下载(最快触达存量用户)
  2. 主流市场覆盖上架(获取新用户的主要渠道)
  3. 各市场对隐私政策、权限用途说明的审查口径不同,逐个过

隐私合规(上架必过项)

国内 App 上架的硬性要求:

  • 首次启动弹《隐私政策》,用户同意前不得初始化任何采集 SDK(uni-app 里注意 App.vue onLaunch 的时机,把需要合规前置的 SDK init 放到同意回调之后)
  • 申请权限(定位、相册、相机)必须先弹用途说明
  • 在应用市场后台填写隐私采集清单

五、wgt 热更新:不发整包的升级

App 发版要走市场审核,周期长。纯前端代码(页面、JS、样式)的修改可以打 wgt 包热更新,只有原生层变更(新增原生插件、修改 SDK 配置)才必须整包升级

服务端版本接口

// GET /app/version?platform=android&version=1.2.0
{
  "code": 0,
  "data": {
    "forceUpdate": false,
    "wgtUrl": "https://cdn.example.com/app/1.2.1.wgt",
    "apkUrl": "https://cdn.example.com/app/app-1.3.0.apk",
    "updateLog": "1. 修复已知问题\n2. 优化性能"
  }
}

wgtUrl 有值走热更,只有 apkUrl 走整包(判断逻辑:版本号第一位变化 → 整包,否则 wgt)。

客户端热更新实现

// App.vue
export default {
  onLaunch() {
    this.checkUpdate()
  },
  methods: {
    async checkUpdate() {
      // #ifdef APP-PLUS
      const { version, platform } = await this.getAppInfo()

      try {
        const res = await request.get('/app/version', {
          platform: platform === 'android' ? 'android' : 'ios',
          version
        })

        if (this.compareVersion(res.wgtVersion, version) > 0) {
          this.doWgtUpdate(res)
        } else if (this.compareVersion(res.version, version) > 0) {
          this.doApkUpdate(res) // 大版本整包
        }
      } catch (e) {
        // 静默失败,不影响启动
      }
      // #endif
    },

    getAppInfo() {
      return new Promise((resolve) => {
        plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
          resolve({
            version: widgetInfo.version,          // wgt 版本
            platform: uni.getSystemInfoSync().platform
          })
        })
      })
    },

    compareVersion(v1, v2) {
      const a = v1.split('.').map(Number)
      const b = v2.split('.').map(Number)
      for (let i = 0; i < 3; i++) {
        if (a[i] > b[i]) return 1
        if (a[i] < b[i]) return -1
      }
      return 0
    },

    doWgtUpdate({ wgtUrl, updateLog, forceUpdate }) {
      uni.showModal({
        title: `发现新版本`,
        content: updateLog,
        showCancel: !forceUpdate,
        success: (res) => {
          if (!res.confirm) return
          uni.showLoading({ title: '更新中...' })

          uni.downloadFile({
            url: wgtUrl,
            success: (downloadRes) => {
              if (downloadRes.statusCode !== 200) return

              plus.runtime.install(
                downloadRes.tempFilePath,
                { force: false },
                () => {
                  uni.hideLoading()
                  uni.showModal({
                    title: '更新完成',
                    content: '重启应用后生效',
                    showCancel: false,
                    success: () => plus.runtime.restart()
                  })
                },
                (e) => {
                  uni.hideLoading()
                  uni.showToast({ title: '安装失败', icon: 'none' })
                }
              )
            },
            fail: () => {
              uni.hideLoading()
              uni.showToast({ title: '下载失败', icon: 'none' })
            }
          })
        }
      })
    }
  }
}

wgt 包的生成与发布

HBuilderX → 发行 → 原生App-制作应用wgt包

生成 .wgt 文件上传到 CDN,更新服务端版本接口的 wgtVersion 字段即可。

wgt 热更的纪律

  1. 原生层改动绝不发 wgt:新增了原生插件、改了 manifest 的 SDK 配置,wgt 升级后必崩
  2. wgt 版本号独立递增:和应用整包版本分开管理,plus.runtime.getProperty 拿到的是 wgt 版本
  3. iOS 慎用:Apple 对热更新口径收紧,wgt 仅限 JS 资源修复,且不保证过审安全;能走 App Store 就走 App Store

六、版本管理建议

  • package.json 的 version 作为唯一真实源,小程序、App 版本号统一从这读取
  • 云打包产物按 app-android-v1.2.1.apk 命名归档到对象存储,出问题可回滚
  • 服务端保留接口灰度能力:wgtUrl 按用户 ID 取模灰度放量,热更出问题影响面可控
  • 小程序保留"版本回退"操作权限:微信后台可在 24 小时内回退到上一版

总结

  • 小程序发布链路:上传 → 提审 → 全量,miniprogram-ci 可自动化;静默更新用 UpdateManager 主动提示
  • Android 证书自己生成自己保管,丢了等于丢掉全部老用户;iOS 三件套缺一不可
  • 合规是上架硬门槛:隐私弹窗前不初始化任何采集 SDK
  • wgt 热更新只适用于前端层变更,原生变更必须整包
  • 版本号统一管理 + CDN 归档 + 灰度放量,是热更安全的最后防线

发布链路琐碎且各平台规则持续变动,本文流程可作为 checklist,每次发版对照走一遍。

0

评论 (0)

取消
0:00