uni-app 热更新
章节介绍
热更新是移动应用开发中的一项重要功能,它允许应用在不经过应用商店审核的情况下,快速更新应用内容和修复 bug。uni-app 提供了完善的热更新机制,使开发者能够为用户提供更加及时和流畅的更新体验。本章节将详细介绍 uni-app 热更新的原理、实现方法和最佳实践。
核心知识点
1. 热更新的概念
热更新(Hot Update)是指应用在运行过程中,无需重新安装即可更新应用代码和资源的技术。在 uni-app 中,热更新主要用于更新以下内容:
- 页面代码(.vue 文件)
- 静态资源(图片、音频、视频等)
- 样式文件(.css 文件)
- 配置文件(manifest.json 等)
需要注意的是,热更新不能更新原生插件和原生代码,这些内容的更新仍然需要通过应用商店发布新版本。
2. 热更新的原理
uni-app 热更新的基本原理如下:
- 生成更新包:开发者将需要更新的代码和资源打包成一个更新包(通常是 .wgt 文件)
- 上传更新包:将更新包上传到服务器,并记录版本信息
- 检测更新:应用启动时或定期检查是否有新版本的更新包
- 下载更新包:如果有新版本,下载更新包到本地
- 安装更新包:解压并安装更新包,替换旧的代码和资源
- 重启应用:重启应用以应用新的代码和资源
3. 热更新的优势
- 快速发布:绕过应用商店审核,快速发布更新
- 即时修复:及时修复应用中的 bug 和安全问题
- 用户体验:无需用户手动更新,提供无缝的更新体验
- 成本降低:减少应用商店版本发布的频率和成本
- 灵活迭代:支持小范围灰度发布和 A/B 测试
4. 热更新的限制
平台限制:
- iOS:Apple 官方限制热更新,可能导致应用被拒
- Android:支持热更新,但部分厂商可能有额外限制
- 小程序:不支持热更新,必须通过平台审核发布
内容限制:不能更新原生插件和原生代码
安全限制:需要确保热更新包的安全性,防止被篡改
5. 热更新的实现方式
uni-app 提供了两种热更新实现方式:
- uniCloud 热更新:基于 uniCloud 服务的热更新方案,简单易用
- 自定义热更新:开发者自行实现热更新逻辑,更加灵活可控
实用案例分析
案例一:使用 uniCloud 热更新
功能说明
使用 uniCloud 提供的热更新服务,实现应用的自动检测、下载和安装更新。
实现步骤
开通 uniCloud 服务
在 HBuilderX 中,点击「uniCloud」->「开通服务空间」,按照提示完成开通。
创建热更新云函数
在 uniCloud 服务空间中,创建一个名为
check-update的云函数:// check-update/index.js 'use strict'; exports.main = async (event, context) => { // 获取客户端版本号 const clientVersion = event.version; // 最新版本信息 const latestVersion = { version: '1.0.1',
description: '\n1. 修复了登录失败的问题\n2. 优化了首页加载速度\n3. 新增了个人中心页面',
downloadUrl: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-uni-app/1.0.1/1.0.1.wgt'
};
// 比较版本号
if (compareVersion(clientVersion, latestVersion.version) < 0) {
return {
hasUpdate: true,
version: latestVersion.version,description: latestVersion.description,
downloadUrl: latestVersion.downloadUrl
};
} else {
return {
hasUpdate: false
};
}
};
// 版本号比较函数
function compareVersion(version1, version2) {
const arr1 = version1.split('.');
const arr2 = version2.split('.');
const length1 = arr1.length;
const length2 = arr2.length;
const minlength = Math.min(length1, length2);
let i = 0;
for (; i < minlength; i++) {
const a = parseInt(arr1[i]);
const b = parseInt(arr2[i]);
if (a > b) {
return 1;
} else if (a < b) {
return -1;
}
}
if (length1 > length2) {
for (let j = i; j < length1; j++) {
if (parseInt(arr1[j]) != 0) {
return 1;
}
}
return 0;
} else if (length1 < length2) {
for (let j = i; j < length2; j++) {
if (parseInt(arr2[j]) != 0) {
return -1;
}
}
return 0;
}
return 0;
}
3. **制作更新包**
在 HBuilderX 中,选择「发行」->「原生 APP-制作应用wgt包」,按照提示完成更新包的制作。
4. **上传更新包**
将生成的 .wgt 文件上传到服务器或云存储,并记录下载地址。
5. **实现热更新检测**
```javascript
// app.vue
export default {
onLaunch() {
// 检测热更新
this.checkUpdate();
},
methods: {
async checkUpdate() {
try {
// 获取当前应用版本号
const appInfo = uni.getAppInfo();
const version = appInfo.version;
// 调用云函数检查更新
const res = await uniCloud.callFunction({
name: 'check-update',
data: {
version: version
}
});
if (res.result.hasUpdate) {
// 显示更新提示
uni.showModal({
title: '发现新版本',
content: res.result.description,
showCancel: true,
confirmText: '立即更新',
cancelText: '稍后更新',
success: (modalRes) => {
if (modalRes.confirm) {
// 下载更新包
this.downloadUpdate(res.result.downloadUrl);
}
}
});
}
} catch (error) {
console.log('检查更新失败', error);
}
},
downloadUpdate(downloadUrl) {
// 显示下载进度
uni.showLoading({
title: '正在下载更新...'
});
// 下载更新包
uni.downloadFile({
url: downloadUrl,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
// 安装更新包
this.installUpdate(downloadResult.tempFilePath);
} else {
uni.hideLoading();
uni.showToast({
title: '下载更新包失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '下载更新包失败',
icon: 'none'
});
console.log('下载更新包失败', err);
}
});
},
installUpdate(tempFilePath) {
// 安装更新包
uni.updateAppPlus({
apkPath: tempFilePath,
success: (res) => {
uni.hideLoading();
uni.showModal({
title: '更新完成',
content: '应用将重启以应用更新',
showCancel: false,
confirmText: '确定',
success: () => {
// 重启应用
plus.runtime.restart();
}
});
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '安装更新包失败',
icon: 'none'
});
console.log('安装更新包失败', err);
}
});
}
}
}案例二:实现自定义热更新
功能说明
实现一个自定义的热更新方案,包括版本检测、更新包下载、安装和回滚机制。
实现步骤
创建版本管理接口
在服务器端创建一个版本管理接口,用于返回最新版本信息:
// server/api/update.js const express = require('express'); const router = express.Router(); // 版本信息 const versions = [ { version: '1.0.0', minVersion: '1.0.0',
description: '初始版本',
downloadUrl: 'https://example.com/updates/1.0.0.wgt',
forceUpdate: false
},
{
version: '1.0.1',
minVersion: '1.0.0',
description: '修复了登录失败的问题',
downloadUrl: 'https://example.com/updates/1.0.1.wgt',
forceUpdate: false
},
{
version: '1.0.2',
minVersion: '1.0.0',
description: '修复了严重的安全漏洞',
downloadUrl: 'https://example.com/updates/1.0.2.wgt',
forceUpdate: true
}
];
// 检查更新接口
router.get('/check', (req, res) => {
const clientVersion = req.query.version;
const platform = req.query.platform;
// 获取最新版本
const latestVersion = versions[versions.length - 1];
// 比较版本号
if (compareVersion(clientVersion, latestVersion.version) < 0) {
// 检查是否需要强制更新
if (compareVersion(clientVersion, latestVersion.minVersion) < 0) {
latestVersion.forceUpdate = true;
}
res.json({
code: 0,
data: {
hasUpdate: true,
version: latestVersion.version,description: latestVersion.description,
downloadUrl: latestVersion.downloadUrl,
forceUpdate: latestVersion.forceUpdate
}
});
} else {
res.json({
code: 0,
data: {
hasUpdate: false
}
});
}
});
// 版本号比较函数
function compareVersion(version1, version2) {
const arr1 = version1.split('.');
const arr2 = version2.split('.');
const length1 = arr1.length;
const length2 = arr2.length;
const minlength = Math.min(length1, length2);
let i = 0;
for (; i < minlength; i++) {
const a = parseInt(arr1[i]);
const b = parseInt(arr2[i]);
if (a > b) {
return 1;
} else if (a < b) {
return -1;
}
}
if (length1 > length2) {
for (let j = i; j < length1; j++) {
if (parseInt(arr1[j]) != 0) {
return 1;
}
}
return 0;
} else if (length1 < length2) {
for (let j = i; j < length2; j++) {
if (parseInt(arr2[j]) != 0) {
return -1;
}
}
return 0;
}
return 0;
}
module.exports = router;
2. **实现热更新管理器**
```javascript
// utils/hotUpdateManager.js
class HotUpdateManager {
constructor() {
this.updateInfo = null;
this.downloadTask = null;
}
// 检查更新
async checkUpdate() {
try {
// 获取当前应用版本号
const appInfo = uni.getAppInfo();
const version = appInfo.version;
// 获取平台信息
const systemInfo = uni.getSystemInfoSync();
const platform = systemInfo.platform;
// 调用服务器接口检查更新
const res = await uni.request({
url: 'https://example.com/api/update/check',
method: 'GET',
data: {
version: version,
platform: platform
}
});
if (res.statusCode === 200 && res.data.code === 0) {
return res.data.data;
} else {
throw new Error('检查更新失败');
}
} catch (error) {
console.log('检查更新失败', error);
return { hasUpdate: false };
}
}
// 下载更新包
downloadUpdate(downloadUrl, progressCallback) {
return new Promise((resolve, reject) => {
// 显示下载进度
uni.showLoading({
title: '正在下载更新...'
});
// 下载更新包
this.downloadTask = uni.downloadFile({
url: downloadUrl,
success: (downloadResult) => {
uni.hideLoading();
if (downloadResult.statusCode === 200) {
resolve(downloadResult.tempFilePath);
} else {
reject(new Error('下载更新包失败'));
}
},
fail: (err) => {
uni.hideLoading();
reject(err);
},
progress: (progress) => {
if (progressCallback) {
progressCallback(progress.progress);
}
}
});
});
}
// 安装更新包
installUpdate(tempFilePath) {
return new Promise((resolve, reject) => {
// 安装更新包
uni.updateAppPlus({
apkPath: tempFilePath,
success: (res) => {
console.log('安装更新包成功', res);
// 保存当前版本信息
this.saveVersionInfo();
resolve(res);
},
fail: (err) => {
console.log('安装更新包失败', err);
reject(err);
}
});
});
}
// 保存版本信息
saveVersionInfo() {
try {
const appInfo = uni.getAppInfo();
uni.setStorageSync('lastVersion', appInfo.version);
uni.setStorageSync('updateTime', new Date().getTime());
} catch (error) {
console.log('保存版本信息失败', error);
}
}
// 回滚到上一个版本
rollback() {
try {
// 清除本地版本信息
uni.removeStorageSync('lastVersion');
uni.removeStorageSync('updateTime');
// 重启应用
plus.runtime.restart();
} catch (error) {
console.log('回滚失败', error);
}
}
// 取消下载
cancelDownload() {
if (this.downloadTask) {
this.downloadTask.abort();
this.downloadTask = null;
}
}
}
export default new HotUpdateManager();使用热更新管理器
// app.vue import hotUpdateManager from '@/utils/hotUpdateManager'; export default { onLaunch() { // 检测热更新 this.checkUpdate(); }, methods: { async checkUpdate() { try { // 检查更新 const updateInfo = await hotUpdateManager.checkUpdate(); if (updateInfo.hasUpdate) { // 显示更新提示 this.showUpdateDialog(updateInfo); } } catch (error) { console.log('检查更新失败', error); } }, showUpdateDialog(updateInfo) { uni.showModal({ title: '发现新版本', content: updateInfo.description, showCancel: !updateInfo.forceUpdate, confirmText: '立即更新', cancelText: '稍后更新', success: async (modalRes) => { if (modalRes.confirm) { try { // 下载更新包 const tempFilePath = await hotUpdateManager.downloadUpdate( updateInfo.downloadUrl, (progress) => { console.log('下载进度', progress); } ); // 安装更新包 await hotUpdateManager.installUpdate(tempFilePath); // 显示安装成功提示 uni.showModal({ title: '更新完成', content: '应用将重启以应用更新', showCancel: false, success: () => { // 重启应用 plus.runtime.restart(); } }); } catch (error) { uni.showToast({ title: '更新失败,请稍后重试', icon: 'none' }); } } } }); } } }
案例三:实现灰度发布和 A/B 测试
功能说明
实现一个支持灰度发布和 A/B 测试的热更新方案,能够根据用户分组推送不同的更新内容。
实现步骤
创建灰度发布接口
// server/api/gray-update.js const express = require('express'); const router = express.Router(); // 灰度规则 const grayRules = { // 按用户 ID 分组 userId: { groupA: ['user1', 'user2', 'user3'], groupB: ['user4', 'user5', 'user6'] }, // 按百分比分组 percentage: { groupA: 30, // 30% groupB: 70 // 70% } }; // 版本信息 const versions = { groupA: { version: '1.0.1-A',
description: '灰度版本 A:新增了深色模式',
downloadUrl: 'https://example.com/updates/1.0.1-A.wgt',
forceUpdate: false
},
groupB: {
version: '1.0.1-B',
description: '灰度版本 B:新增了浅色模式',
downloadUrl: 'https://example.com/updates/1.0.1-B.wgt',
forceUpdate: false
}
};
// 检查更新接口
router.get('/check', (req, res) => {
const clientVersion = req.query.version;
const userId = req.query.userId;
const deviceId = req.query.deviceId;
// 确定用户分组
let userGroup = 'groupA';
// 按用户 ID 分组
if (grayRules.userId.groupA.includes(userId)) {
userGroup = 'groupA';
} else if (grayRules.userId.groupB.includes(userId)) {
userGroup = 'groupB';
} else {
// 按百分比分组
const random = Math.random() * 100;
if (random <= grayRules.percentage.groupA) {
userGroup = 'groupA';
} else {
userGroup = 'groupB';
}
}
// 获取对应分组的版本信息
const versionInfo = versions[userGroup];
// 比较版本号
if (compareVersion(clientVersion, versionInfo.version) < 0) {
res.json({
code: 0,
data: {
hasUpdate: true,
version: versionInfo.version,description: versionInfo.description,
downloadUrl: versionInfo.downloadUrl,
forceUpdate: versionInfo.forceUpdate,
userGroup: userGroup
}
});
} else {
res.json({
code: 0,
data: {
hasUpdate: false,
userGroup: userGroup
}
});
}
});
// 版本号比较函数
function compareVersion(version1, version2) {
// 实现版本号比较逻辑
// ...
}
module.exports = router;
2. **实现灰度发布客户端**
```javascript
// utils/grayUpdateManager.js
class GrayUpdateManager {
constructor() {
this.updateManager = require('./hotUpdateManager').default;
}
// 检查灰度更新
async checkGrayUpdate() {
try {
// 获取用户信息
const userId = uni.getStorageSync('userId') || 'anonymous';
const deviceId = uni.getStorageSync('deviceId') || this.getDeviceId();
// 获取当前应用版本号
const appInfo = uni.getAppInfo();
const version = appInfo.version;
// 调用灰度发布接口
const res = await uni.request({
url: 'https://example.com/api/gray-update/check',
method: 'GET',
data: {
version: version,
userId: userId,
deviceId: deviceId
}
});
if (res.statusCode === 200 && res.data.code === 0) {
// 保存用户分组信息
if (res.data.data.userGroup) {
uni.setStorageSync('userGroup', res.data.data.userGroup);
}
return res.data.data;
} else {
throw new Error('检查灰度更新失败');
}
} catch (error) {
console.log('检查灰度更新失败', error);
return { hasUpdate: false };
}
}
// 获取设备 ID
getDeviceId() {
try {
const systemInfo = uni.getSystemInfoSync();
const deviceId = systemInfo.deviceId || systemInfo.platform + '_' + Date.now();
uni.setStorageSync('deviceId', deviceId);
return deviceId;
} catch (error) {
console.log('获取设备 ID 失败', error);
return 'unknown_' + Date.now();
}
}
// 执行灰度更新
async performGrayUpdate() {
try {
// 检查灰度更新
const updateInfo = await this.checkGrayUpdate();
if (updateInfo.hasUpdate) {
// 执行更新
// ...
}
} catch (error) {
console.log('执行灰度更新失败', error);
}
}
}
export default new GrayUpdateManager();使用灰度发布
// app.vue import grayUpdateManager from '@/utils/grayUpdateManager'; export default { onLaunch() { // 执行灰度更新 grayUpdateManager.performGrayUpdate(); } };
技术要点总结
热更新的原理:了解热更新的基本原理和工作流程,包括版本检测、更新包下载和安装。
平台差异:不同平台对热更新的支持程度不同,需要:
- iOS:注意 Apple 官方的限制,避免应用被拒
- Android:处理不同厂商系统的差异
- 小程序:了解平台的更新机制
安全性:
- 对更新包进行签名验证,防止被篡改
- 使用 HTTPS 协议传输更新包
- 对敏感操作进行权限控制
用户体验:
- 提供清晰的更新提示和进度反馈
- 支持后台下载和静默更新
- 实现强制更新和可选更新
稳定性:
- 实现更新失败的回滚机制
- 对更新包进行完整性校验
- 处理各种异常情况
版本管理:
- 建立完善的版本号管理机制
- 支持增量更新和全量更新
- 实现版本回滚功能
学习目标
- 了解 uni-app 热更新的基本原理和工作流程
- 掌握 uni-app 热更新的实现方法
- 学会使用 uniCloud 热更新服务
- 理解如何实现自定义热更新方案
- 掌握灰度发布和 A/B 测试的实现方法
- 了解热更新的安全性和稳定性保障措施
章节小结
本章节详细介绍了 uni-app 应用的热更新功能,包括热更新的原理、实现方法和最佳实践。通过三个实用案例,展示了如何使用 uniCloud 热更新、实现自定义热更新和灰度发布功能。
在实现热更新功能时,开发者需要注意以下几点:
- 了解并遵守各平台对热更新的限制和要求
- 确保热更新过程的安全性和稳定性
- 提供良好的用户体验,包括清晰的更新提示和进度反馈
- 实现完善的版本管理和回滚机制
- 定期测试热更新功能,确保其正常工作
通过合理使用热更新功能,开发者可以快速发布更新、及时修复 bug、优化用户体验,同时减少应用商店审核的等待时间和成本。然而,开发者也应该谨慎使用热更新,避免滥用导致应用被平台下架或用户体验下降。
热更新是一把双刃剑,只有在合适的场景下合理使用,才能发挥其最大的价值。