前端发版真的有那么麻烦吗?
真的很麻烦:
- 改了一个 UI 样式或修复了一个小 Bug
- 重新打包、提审、等审核
- 安卓还好,iOS 动辄等几天
- 用户还不一定马上更新
如果是一个小 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 └──────────────┘
流程说明:
- App 冷启动 (
App.vue的onLaunch)时,读取当前版本号 - 调用后端接口,传入当前版本
- 后端比对数据库中的最新版本,返回是否需要更新、WGT 下载地址等
- 客户端下载 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.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 中调用:
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?
以下场景 必须走整包更新:
- 原生 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。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流,一起踩坑、一起填坑。