h5打开以查看
主要是针对前端发版
真的很麻烦:
- 改了一个 UI 样式或修复了一个小 Bug
- 重新打包、提审、等审核
- 安卓还好,iOS 动辄等几天
- 用户还不一定马上更新
如果是一个小 bug 却影响了大的功能,改了几行前端代码还能回退版本重新发版,却要完整走一遍应用商店发版流程,你自己想想...
以下只适合于 uniapp 的开发直接使用复制,整体的架构思想是结合现有的 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 └──────────────┘流程说明:
- App冷启动(
App.vue的onLaunch)时,读取当前版本号 - 调用后端接口,传入当前版本
- 后端比对数据库中的最新版本,返回是否需要更新、WGT 下载地址等
- 客户端下载 WGT → 安装 → 重启,新版本生效
这套方案不依赖特定后台框架,数据库、Redis、JSON 配置文件、CMS 后台都可以,只要接口能返回约定字段即可。
三、后台设计:数据库里放什么?
3.1 推荐的数据表结构
以 MySQL 为例,一张app_version表就够起步:
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 配置,原理一样:
{ "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):
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.1→1.0.2 - 应用版本号(versionCode):如
101→102
⚠️关键:新 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.vue的onLaunch中调用:
// 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中调用:
// App.vue import { checkAppUpdate } from '@/utils/appUpdate.js'; export default { onLaunch() { // 冷启动时静默检查更新 // #ifdef APP-PLUS checkAppUpdate({ silent: true }); // #endif } };5.3 核心 API 说明
官方文档:plus.runtime.install
plus.runtime.install(filePath, options, successCallback, errorCallback)- filePath:WGT 本地路径(需先用
uni.downloadFile下载到本地) - options.force:是否强制安装(版本不匹配时强制覆盖)
- 安装成功后必须调用
plus.runtime.restart(),新资源才会生效
六、哪些情况不能只用 WGT?
以下场景必须走整包更新:
- 原生 SDK 变更:如新增 Maps 模块、升级推送 SDK
- 原生插件增改:新增或修改 uni 原生插件
- App 原生引擎升级
- 某些平台特殊限制:如从非 nvue 工程新增 nvue 且使用非自定义组件编译模式
遇到这些情况,接口应返回pkgUrl引导用户下载整包,而不是wgtUrl。
七、注意事项
7.1 开发调试
- 真机运行期间读到的 appid、版本号是 HBuilder 基座信息,必须打自定义基座或正式包才能正确测试热更新
- 使用
#ifdef APP-PLUS条件编译,避免在 H5 / 小程序环境调用plusAPI
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读版本 → 下载 →install→restart |
原来不用发版也可以做到版本更新——这句话成立的前提是:改的是前端资源,不是原生能力。在这个边界内,WGT 热更新能显著缩短从开发到用户手中的路径,特别适合高频迭代的业务型 App。
h5打开以查看