原来不用发版也可以做到版本更新

前端发版真的有那么麻烦吗?

真的很麻烦:

  1. 改了一个 UI 样式或修复了一个小 Bug
  2. 重新打包、提审、等审核
  3. 安卓还好,iOS 动辄等几天
  4. 用户还不一定马上更新

如果是一个小 bug 却影响了大的功能,改了几行前端代码还能回退版本重新发版,却要完整走一遍应用商店发版流程,你自己想想...

以下只适合于 uniapp 的开发直接使用复制,其他方向的在最后有做一些说明,可提供借鉴。

其实这个能力应该是很久之前就有了,不过利用起来使用的还是相对较少的,一部分是现在用于开发小程序的比较多,小程序得审核所以用不到,另一部分觉得发都发了直接发整版比较稳妥,可能发版也不是很经常。

很多人都不喜欢 uniapp,觉得这不行那不行,但是国内很多都在用,邪笑。。。来,说出你的想法。


一、WGT 热更新是什么?

WGT(Widget Package)是 uni-app 的 App 资源升级包 ,本质上打包的是 manifest.json、页面、JS、CSS、图片等前端资源,不包含原生引擎和原生插件的变更

对比项 WGT 热更新 整包更新(APK/IPA)
更新内容 前端页面、样式、逻辑 原生引擎、SDK、插件
是否需要上架
包体大小 通常几 MB 几十 MB 起
用户感知 下载后重启即可 需重新安装
适用场景 Bug 修复、UI 调整、业务逻辑变更 新增原生模块、升级 SDK

二、整体架构:后台存数据,冷启动做判断

核心思路非常简单:

复制代码
┌─────────────┐     冷启动请求      ┌──────────────┐
│   App 客户端  │ ────────────────▶ │  版本检查接口  │
│  (uni-app)   │ ◀──────────────── │  (后端 API)   │
└─────────────┘   返回版本信息       └──────┬───────┘
       │                                    │
       │ 有新版本                            │ 读取
       ▼                                    ▼
  下载 .wgt 包                         ┌──────────────┐
       │                               │ 数据库 / 配置  │
       ▼                               │ version      │
  plus.runtime.install                 │ wgtUrl       │
       │                               │ pkgUrl       │
       ▼                               │ forceUpdate  │
  plus.runtime.restart                 └──────────────┘

流程说明:

  1. App 冷启动App.vueonLaunch)时,读取当前版本号
  2. 调用后端接口,传入当前版本
  3. 后端比对数据库中的最新版本,返回是否需要更新、WGT 下载地址等
  4. 客户端下载 WGT → 安装 → 重启,新版本生效

这套方案不依赖特定后台框架,数据库、Redis、JSON 配置文件、CMS 后台 都可以,只要接口能返回约定字段即可。


三、后台设计:数据库里放什么?

3.1 推荐的数据表结构

以 MySQL 为例,一张 app_version 表就够起步:

sql 复制代码
CREATE TABLE app_version (
  id            INT PRIMARY KEY AUTO_INCREMENT,
  app_id        VARCHAR(64)  NOT NULL COMMENT '应用标识,如 __UNI__XXXXXX',
  platform      VARCHAR(16)  NOT NULL DEFAULT 'all' COMMENT 'android / ios / all',
  version_name  VARCHAR(32)  NOT NULL COMMENT '版本名,如 1.0.1',
  version_code  INT          NOT NULL COMMENT '版本号,递增整数',
  wgt_url       VARCHAR(512) DEFAULT NULL COMMENT 'WGT 包下载地址',
  pkg_url       VARCHAR(512) DEFAULT NULL COMMENT '整包下载地址(可选)',
  update_type   TINYINT      NOT NULL DEFAULT 1 COMMENT '1=热更新 2=整包更新 3=强制整包',
  force_update  TINYINT      NOT NULL DEFAULT 0 COMMENT '是否强制更新',
  update_log    TEXT         COMMENT '更新说明',
  status        TINYINT      NOT NULL DEFAULT 1 COMMENT '1=启用 0=禁用',
  created_at    DATETIME     DEFAULT CURRENT_TIMESTAMP
);

也可以不用数据库,直接在后台管理系统里维护一个 JSON 配置,原理一样:

json 复制代码
{
  "version": "1.0.2",
  "versionCode": 102,
  "update": true,
  "wgtUrl": "https://cdn.example.com/app/__UNI__XXXXXX.wgt",
  "pkgUrl": "",
  "forceUpdate": false,
  "description": "修复若干已知问题,优化首页加载速度"
}

3.2 版本检查接口约定

请求参数:

参数名 类型 说明
name String 应用名称
version String 客户端当前版本号
platform String 可选,android / ios

响应参数:

参数名 类型 说明
update Boolean 是否有更新
wgtUrl String WGT 包下载地址
pkgUrl String 整包下载地址(大版本升级时使用)
forceUpdate Boolean 是否强制更新
description String 更新说明

Node.js 示例(Express):

javascript 复制代码
router.get('/api/app/check-update', async (req, res) => {
  const { name, version, platform = 'android' } = req.query;

  // 从数据库查询最新启用版本
  const latest = await db.query(
    'SELECT * FROM app_version WHERE app_id = ? AND status = 1 ORDER BY version_code DESC LIMIT 1',
    [name]
  );

  if (!latest) {
    return res.json({ update: false });
  }

  const hasUpdate = compareVersion(version, latest.version_name) < 0;

  res.json({
    update: hasUpdate,
    wgtUrl: hasUpdate ? latest.wgt_url : '',
    pkgUrl: latest.pkg_url || '',
    forceUpdate: !!latest.force_update,
    description: latest.update_log || ''
  });
});

// 简单版本号比较:1.0.1 vs 1.0.2
function compareVersion(v1, v2) {
  const a = v1.split('.').map(Number);
  const b = v2.split('.').map(Number);
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
    const diff = (a[i] || 0) - (b[i] || 0);
    if (diff !== 0) return diff;
  }
  return 0;
}

提示: 版本比对逻辑可按业务自定义,有的团队用 versionCode 整数比较更稳妥。


四、制作 WGT 包:HBuilderX 两步搞定

4.1 修改版本号

打开 manifest.json,递增版本信息:

  • 应用版本名称 (versionName):如 1.0.11.0.2
  • 应用版本号 (versionCode):如 101102

⚠️ 关键: 新 WGT 包的版本号必须 严格大于 当前 App 已安装的版本,否则安装会报错:

WGT安装包中manifest.json文件的version版本不匹配

4.2 发行 WGT 包

HBuilderX 菜单:

复制代码
发行 → App-制作应用wgt包

生成完成后,控制台会输出 .wgt 文件路径,文件名通常为 {appid}.wgt,例如 __UNI__832D722.wgt

4.3 上传到 CDN / 服务器

将 WGT 文件上传到可公网访问的 HTTPS 地址,例如:

复制代码
https://cdn.example.com/app/__UNI__832D722.wgt

然后把该 URL 写入数据库或后台配置,供接口返回。


五、客户端实现:冷启动检查 + 下载安装

5.1 为什么必须用 plus.runtime.getProperty

这是最容易踩的坑之一:

API 读取来源 WGT 更新后是否变化
plus.runtime.version 原生 APK/IPA 版本 ❌ 不变
plus.runtime.getProperty() manifest.json 资源版本 ✅ 会变

结论:版本检测必须用 plus.runtime.getProperty,不能用 plus.runtime.version

否则 WGT 更新成功后,客户端读到的仍是原生包版本,会 无限提示更新

5.2 示例(这里没有放灰度、兜底回退等代码逻辑,需要的可自行修改添加)

建议封装为独立模块,在 App.vueonLaunch 中调用:

javascript 复制代码
// utils/appUpdate.js

const CHECK_UPDATE_URL = 'https://api.example.com/api/app/check-update';

/**
 * 获取当前 App 资源版本信息
 */
export function getCurrentVersion() {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS
    plus.runtime.getProperty(plus.runtime.appid, (info) => {
      resolve({
        name: info.name,
        version: info.version,
        versionCode: info.versionCode
      });
    }, reject);
    // #endif

    // #ifndef APP-PLUS
    reject(new Error('非 App 环境'));
    // #endif
  });
}

/**
 * 检查并执行更新
 */
export async function checkAppUpdate(options = {}) {
  const { silent = false } = options;

  try {
    const current = await getCurrentVersion();

    const res = await uni.request({
      url: CHECK_UPDATE_URL,
      method: 'GET',
      data: {
        name: current.name,
        version: current.version,
        platform: uni.getSystemInfoSync().platform
      }
    });

    const data = res[1]?.data || res.data;
    if (!data?.update) return;

    // 整包更新(大版本 / 原生变更)
    if (data.pkgUrl && !data.wgtUrl) {
      handlePkgUpdate(data);
      return;
    }

    // WGT 热更新
    if (data.wgtUrl) {
      if (data.forceUpdate) {
        await downloadAndInstallWgt(data.wgtUrl);
      } else {
        const confirmed = await showUpdateDialog(data.description);
        if (confirmed) {
          await downloadAndInstallWgt(data.wgtUrl);
        }
      }
    }
  } catch (err) {
    if (!silent) {
      console.error('[AppUpdate] 检查更新失败', err);
    }
  }
}

/** 弹出更新提示 */
function showUpdateDialog(description) {
  return new Promise((resolve) => {
    uni.showModal({
      title: '发现新版本',
      content: description || '是否立即更新?',
      confirmText: '立即更新',
      cancelText: '稍后再说',
      success: (res) => resolve(res.confirm)
    });
  });
}

/** 下载并安装 WGT */
function downloadAndInstallWgt(wgtUrl) {
  return new Promise((resolve, reject) => {
    uni.showLoading({ title: '下载更新中...', mask: true });

    const downloadTask = uni.downloadFile({
      url: wgtUrl,
      success: (downloadResult) => {
        if (downloadResult.statusCode !== 200) {
          uni.hideLoading();
          uni.showToast({ title: '下载失败', icon: 'none' });
          return reject(new Error('下载失败'));
        }

        uni.showLoading({ title: '安装中...', mask: true });

        plus.runtime.install(
          downloadResult.tempFilePath,
          { force: true },
          () => {
            uni.hideLoading();
            uni.showModal({
              title: '更新完成',
              content: '应用将重启以生效',
              showCancel: false,
              success: () => {
                plus.runtime.restart();
                resolve();
              }
            });
          },
          (err) => {
            uni.hideLoading();
            uni.showToast({
              title: '安装失败: ' + (err.message || '未知错误'),
              icon: 'none'
            });
            reject(err);
          }
        );
      },
      fail: (err) => {
        uni.hideLoading();
        uni.showToast({ title: '下载失败', icon: 'none' });
        reject(err);
      }
    });

    // 监听下载进度(可选)
    downloadTask.onProgressUpdate((res) => {
      uni.showLoading({
        title: `下载中 ${res.progress}%`,
        mask: true
      });
    });
  });
}

/** 整包更新:跳转浏览器或应用市场 */
function handlePkgUpdate(data) {
  uni.showModal({
    title: '发现新版本',
    content: data.description || '请下载安装新版本',
    showCancel: !data.forceUpdate,
    confirmText: '去下载',
    success: (res) => {
      if (res.confirm && data.pkgUrl) {
        plus.runtime.openURL(data.pkgUrl);
      }
    }
  });
}

App.vue 中调用:

javascript 复制代码
// App.vue
import { checkAppUpdate } from '@/utils/appUpdate.js';

export default {
  onLaunch() {
    // 冷启动时静默检查更新
    // #ifdef APP-PLUS
    checkAppUpdate({ silent: true });
    // #endif
  }
};

5.3 核心 API 说明

官方文档:plus.runtime.install

javascript 复制代码
plus.runtime.install(filePath, options, successCallback, errorCallback)
  • filePath :WGT 本地路径(需先用 uni.downloadFile 下载到本地)
  • options.force:是否强制安装(版本不匹配时强制覆盖)
  • 安装成功后 必须 调用 plus.runtime.restart(),新资源才会生效

六、哪些情况不能只用 WGT?

以下场景 必须走整包更新

  1. 原生 SDK 变更:如新增 Maps 模块、升级推送 SDK
  2. 原生插件增改:新增或修改 uni 原生插件
  3. App 原生引擎升级
  4. 某些平台特殊限制:如从非 nvue 工程新增 nvue 且使用非自定义组件编译模式

遇到这些情况,接口应返回 pkgUrl 引导用户下载整包,而不是 wgtUrl


七、注意事项

7.1 开发调试

  • 真机运行期间读到的 appid、版本号是 HBuilder 基座信息,必须打自定义基座或正式包 才能正确测试热更新
  • 使用 #ifdef APP-PLUS 条件编译,避免在 H5 / 小程序环境调用 plus API

7.2 版本管理

  • WGT 包的 manifest.json 版本必须大于客户端当前版本
  • 版本检测用 plus.runtime.getProperty,不要用 plus.runtime.version
  • 建议在后台保留历史版本记录,方便回滚

7.3 安全与合规

  • WGT 下载地址 强烈建议使用 HTTPS,防止中间人篡改
  • iOS 上架审核期间 不要弹出热更新提示
  • 热更新内容需符合应用商店政策,不要通过热更新绕过虚拟支付等规则
  • 参考说明:热更新是否影响应用上架

7.4 兼容性

  • WGT 资源包与原生基座存在兼容关系,大跨度升级建议在 manifest 中配置忽略不兼容提示,或先充分测试
  • 详见:wgt 与原生基座兼容性说明

7.5 用户体验

  • 非强制更新建议弹窗让用户选择,不要每次冷启动都静默强制更新
  • 下载过程展示进度,避免用户以为 App 卡死
  • 安装成功后先 uni.hideLoading()plus.runtime.restart(),避免 loading 残留

八、官方升级中心:更省心的选择

如果使用 uniCloud,可以直接接入 DCloud 官方的 uni-upgrade-center,开箱支持:

  • WGT 热更新 + 整包更新
  • 后台可视化管理版本
  • 多应用、多平台统一管理

官方文档:uni-upgrade-center

对于已有自建后端的团队,本文的「数据库 + 接口 + 客户端」方案更灵活;对于 uniCloud 项目,官方方案能省不少轮子。


九、总结

步骤 操作
1 后台 / 数据库维护版本号、WGT 下载地址
2 提供版本检查接口,客户端冷启动时调用
3 HBuilderX 修改版本号 → 制作 WGT 包 → 上传 CDN
4 客户端 getProperty 读版本 → 下载 → installrestart

原来不用发版也可以做到版本更新------这句话成立的前提是:改的是前端资源,不是原生能力。在这个边界内,WGT 热更新能显著缩短从开发到用户手中的路径,特别适合高频迭代的业务型 App。


如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流,一起踩坑、一起填坑。

相关推荐
亦暖筑序2 小时前
AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步
java·前端·vue.js
北斗落凡尘2 小时前
Vue面试题
前端
程序员黑豆2 小时前
鸿蒙应用开发之父子组件传参:@Param、@Event、@Once 装饰器详解与实战
前端·harmonyos
ClouGence2 小时前
一个人录好的测试用例,团队怎么一起用?
前端·测试
无限压榨切图仔2 小时前
从 Claude Code 切到 Codex:我用 Agent、Skills、MCP 做完了一个内容运营工具
前端·后端
濮水大叔2 小时前
NestJS 与 CabloyJS 的 env/config 架构对比:从环境变量到实例级配置
前端·node.js·nestjs
成都渲染101云渲染66662 小时前
Blender渲染时,纯CPU渲染的设置教程
前端·javascript·blender
董员外2 小时前
RAG 系统进化论(一):纵览 RAG 的发展历程
前端·人工智能·后端
Gauss松鼠会3 小时前
【GaussDB】GaussDB锁阻塞源头查询
java·开发语言·前端·数据库·算法·gaussdb·经验总结