一、更新通知方式
1.1 应用内推送(主要方式)
启动时检查更新
// App.vue / main.js
export default {
onLaunch() {
// 延迟检查,避免影响启动速度
setTimeout(() => {
this.checkAppUpdate()
}, 2000)
},
methods: {
async checkAppUpdate() {
try {
const res = await uni.request({
url: 'https://api.xxx.com/app/version/check',
data: {
platform: uni.getSystemInfoSync().platform,
currentVersion: this.getCurrentVersion(),
deviceId: this.getDeviceId(),
userId: this.getUserId()
}
})
const {
hasUpdate,
versionName,
versionCode,
downloadUrl,
updateType, // force/recommended/silent
updateInfo,
fileSize
} = res.data
if (hasUpdate) {
switch(updateType) {
case 'force':
this.showForceUpdateDialog(versionName, downloadUrl, updateInfo)
break
case 'recommended':
this.showRecommendedUpdateDialog(versionName, downloadUrl, updateInfo)
break
case 'silent':
this.silentDownload(downloadUrl)
break
}
}
} catch (error) {
console.error('检查更新失败:', error)
}
},
showForceUpdateDialog(version, url, info) {
uni.showModal({
title: '版本更新',
content: `发现新版本 ${version}\n\n${info}\n\n必须更新才能继续使用`,
showCancel: false,
confirmText: '立即更新',
success: () => {
this.downloadAndInstall(url)
}
})
},
showRecommendedUpdateDialog(version, url, info) {
uni.showModal({
title: '版本更新',
content: `发现新版本 ${version}\n\n${info}`,
confirmText: '立即更新',
cancelText: '稍后提醒',
success: (res) => {
if (res.confirm) {
this.downloadAndInstall(url)
} else {
// 记录跳过次数
this.recordSkipUpdate()
}
}
})
},
recordSkipUpdate() {
let skipCount = uni.getStorageSync('update_skip_count') || 0
skipCount++
uni.setStorageSync('update_skip_count', skipCount)
// 跳过3次后,下次变成强制更新
if (skipCount >= 3) {
uni.setStorageSync('force_update_next_time', true)
}
},
getCurrentVersion() {
// #ifdef APP-PLUS
return plus.runtime.version
// #endif
return '1.0.0'
}
}
}
后台激活时检查
// App.vue
export default {
onShow() {
// 从后台恢复到前台时
const lastCheckTime = uni.getStorageSync('last_check_update_time') || 0
const now = Date.now()
// 距离上次检查超过1小时才再次检查
if (now - lastCheckTime > 60 * 60 * 1000) {
this.checkAppUpdate()
uni.setStorageSync('last_check_update_time', now)
}
}
}
手动检查入口
<!-- pages/settings/index.vue -->
<template>
<view class="setting-item" @click="checkUpdate">
<text>检查更新</text>
<text class="version">当前版本 v{{ version }}</text>
<text class="arrow">></text>
</view>
</template>
<script>
export default {
data() {
return {
version: '1.0.0'
}
},
onLoad() {
// #ifdef APP-PLUS
this.version = plus.runtime.version
// #endif
},
methods: {
async checkUpdate() {
uni.showLoading({ title: '检查中...' })
try {
const res = await uni.request({
url: 'https://api.xxx.com/app/version/check',
data: {
platform: uni.getSystemInfoSync().platform,
currentVersion: this.version
}
})
uni.hideLoading()
if (res.data.hasUpdate) {
// 显示更新弹窗
} else {
uni.showToast({
title: '已是最新版本',
icon: 'success'
})
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: '检查失败',
icon: 'none'
})
}
}
}
}
</script>
1.2 系统推送通知
配置推送服务
// 使用个推、极光推送等第三方服务
// utils/push.js
class PushService {
init() {
// #ifdef APP-PLUS
const main = plus.android.runtimeMainActivity()
const context = main.getApplicationContext()
// 初始化推送SDK
const PushManager = plus.android.importClass('com.igexin.sdk.PushManager')
PushManager.getInstance().initialize(context)
// 监听推送消息
plus.push.addEventListener('click', (msg) => {
this.handlePushClick(msg)
})
// #endif
}
handlePushClick(msg) {
const { payload } = msg
if (payload.type === 'app_update') {
// 跳转到更新页面
uni.navigateTo({
url: '/pages/update/index?version=' + payload.version
})
}
}
// 获取推送token
getClientId() {
return new Promise((resolve) => {
// #ifdef APP-PLUS
plus.push.getClientInfo((info) => {
resolve(info.clientid)
})
// #endif
})
}
}
export default new PushService()
服务端推送
// server/push-notification.js
const request = require('request')
// 个推服务端推送
function sendUpdateNotification(userTokens, versionInfo) {
const data = {
appkey: 'your_app_key',
master_secret: 'your_master_secret',
audience: {
cid: userTokens // 用户设备token列表
},
notification: {
title: '版本更新',
content: `${versionInfo.name} 版本已发布,点击查看详情`,
click_type: 'startapp',
intent: 'intent://app.update#Intent;scheme=myapp;launchFlags=0x4000000;end'
},
push_info: {
payload: {
type: 'app_update',
version: versionInfo.name,
url: versionInfo.downloadUrl
}
}
}
request({
url: 'https://restapi.getui.com/v2/push/single/cid',
method: 'POST',
json: true,
body: data
}, (error, response, body) => {
if (error) {
console.error('推送失败:', error)
} else {
console.log('推送成功:', body)
}
})
}
// 批量推送给所有用户
async function pushToAllUsers(versionInfo) {
// 从数据库获取所有用户的推送token
const users = await User.find({ pushToken: { $exists: true } })
// 分批推送,每批1000个
const batchSize = 1000
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize)
const tokens = batch.map(u => u.pushToken)
await sendUpdateNotification(tokens, versionInfo)
// 延迟避免频率限制
await sleep(1000)
}
}
1.3 短信通知(重要更新)
// server/sms-notification.js
const tencentcloud = require('tencentcloud-sdk-nodejs')
async function sendUpdateSMS(phoneNumbers, versionInfo) {
const SmsClient = tencentcloud.sms.v20210111.Client
const client = new SmsClient({
credential: {
secretId: 'your_secret_id',
secretKey: 'your_secret_key'
},
region: 'ap-guangzhou'
})
const params = {
PhoneNumberSet: phoneNumbers,
SmsSdkAppId: 'your_app_id',
SignName: '您的应用名',
TemplateId: 'template_id',
TemplateParamSet: [
versionInfo.name,
versionInfo.updateInfo
]
}
try {
const response = await client.SendSms(params)
console.log('短信发送成功:', response)
} catch (error) {
console.error('短信发送失败:', error)
}
}
// 短信模板示例:
// 【应用名】尊敬的用户,{1}版本已发布,{2}。请及时更新体验新功能。
1.4 邮件通知
// server/email-notification.js
const nodemailer = require('nodemailer')
async function sendUpdateEmail(emails, versionInfo) {
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: {
user: 'your@email.com',
pass: 'your_password'
}
})
const mailOptions = {
from: '"应用名" <your@email.com>',
to: emails.join(','),
subject: `${versionInfo.name} 版本更新通知`,
html: `
<h2>版本更新</h2>
<p>尊敬的用户,${versionInfo.name} 版本已发布!</p>
<h3>更新内容:</h3>
<p>${versionInfo.updateInfo}</p>
<p>
<a href="${versionInfo.downloadUrl}" style="
display: inline-block;
padding: 10px 20px;
background: #007aff;
color: white;
text-decoration: none;
border-radius: 5px;
">立即更新</a>
</p>
<p>或在应用内检查更新</p>
`
}
try {
await transporter.sendMail(mailOptions)
console.log('邮件发送成功')
} catch (error) {
console.error('邮件发送失败:', error)
}
}
1.5 应用内消息中心
<!-- pages/message/index.vue -->
<template>
<view class="message-list">
<view class="message-item"
v-for="msg in messages"
:key="msg.id"
@click="handleMessageClick(msg)">
<image :src="msg.icon" class="icon" />
<view class="content">
<text class="title">{{ msg.title }}</text>
<text class="desc">{{ msg.content }}</text>
<text class="time">{{ formatTime(msg.time) }}</text>
</view>
<view class="badge" v-if="!msg.read"></view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
messages: []
}
},
onLoad() {
this.loadMessages()
},
methods: {
async loadMessages() {
const res = await uni.request({
url: 'https://api.xxx.com/messages',
data: {
userId: this.getUserId()
}
})
this.messages = res.data.list
},
handleMessageClick(msg) {
if (msg.type === 'app_update') {
// 标记为已读
this.markAsRead(msg.id)
// 跳转更新
uni.navigateTo({
url: '/pages/update/index?version=' + msg.version
})
}
}
}
}
</script>
二、更新策略分类
2.1 强制更新(Force Update)
使用场景
- 严重安全漏洞
- 接口不兼容(旧版本无法正常使用)
- 重大bug影响核心功能
- 监管要求必须更新
实现方案
// utils/force-update.js
class ForceUpdateManager {
check(serverVersion, currentVersion) {
// 服务端配置强制更新的最低版本
const minRequiredVersion = serverVersion.minRequired
if (this.compareVersion(currentVersion, minRequiredVersion) < 0) {
return {
needUpdate: true,
force: true,
reason: '当前版本过低,必须更新才能继续使用'
}
}
return { needUpdate: false }
}
showForceUpdateDialog(versionInfo) {
uni.showModal({
title: '必须更新',
content: `当前版本过低,必须更新到 ${versionInfo.name} 才能继续使用\n\n更新内容:\n${versionInfo.updateInfo}`,
showCancel: false,
confirmText: '立即更新',
success: () => {
this.startDownload(versionInfo.downloadUrl)
}
})
}
startDownload(url) {
// 禁用返回键
// #ifdef APP-PLUS
plus.key.addEventListener('backbutton', () => {
// 阻止返回
return false
})
// #endif
uni.showLoading({
title: '下载中 0%',
mask: true
})
const downloadTask = uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
uni.hideLoading()
this.installApp(res.tempFilePath)
}
}
})
downloadTask.onProgressUpdate((res) => {
uni.showLoading({
title: `下载中 ${res.progress}%`,
mask: true
})
})
}
installApp(filePath) {
// #ifdef APP-PLUS
plus.runtime.install(filePath, {
force: true
}, () => {
// 安装成功后重启
plus.runtime.restart()
}, (error) => {
uni.showModal({
title: '安装失败',
content: '请手动安装更新包',
showCancel: false
})
})
// #endif
}
compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number)
const arr2 = v2.split('.').map(Number)
for (let i = 0; i < 3; i++) {
if (arr1[i] > arr2[i]) return 1
if (arr1[i] < arr2[i]) return -1
}
return 0
}
}
export default new ForceUpdateManager()
服务端配置
// server/version-config.js
const versionConfig = {
latest: {
versionName: '2.0.0',
versionCode: 200,
downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk',
updateInfo: '1. 新增功能\n2. 修复bug',
releaseTime: '2024-01-01 10:00:00'
},
// 强制更新配置
minRequired: {
versionName: '1.5.0',
versionCode: 150,
reason: '旧版本存在严全漏洞',
forceTime: '2024-01-05 00:00:00' // 该时间后强制更新
},
// 根据时间逐步强制
forceUpdateSchedule: {
'2024-01-01': 10, // 1月1日:10%用户强制
'2024-01-03': 50, // 1月3日:50%用户强制
'2024-01-05': 100 // 1月5日:100%用户强制
}
}
2.2 推荐更新(Recommended Update)
使用场景
实现方案
// utils/recommended-update.js
class RecommendedUpdateManager {
showUpdateDialog(versionInfo) {
// 检查用户是否多次跳过
const skipCount = uni.getStorageSync('update_skip_count') || 0
// 跳过3次后,下次弹窗更明显
const isUrgent = skipCount >= 3
uni.showModal({
title: isUrgent ? '强烈建议更新' : '发现新版本',
content: `版本 ${versionInfo.name}\n\n${versionInfo.updateInfo}\n\n文件大小:${this.formatSize(versionInfo.fileSize)}`,
confirmText: '立即更新',
cancelText: isUrgent ? '忽略' : '稍后',
confirmColor: isUrgent ? '#FF0000' : '#007aff',
success: (res) => {
if (res.confirm) {
this.startUpdate(versionInfo)
// 清空跳过次数
uni.removeStorageSync('update_skip_count')
} else {
this.handleSkip()
}
}
})
}
handleSkip() {
let skipCount = uni.getStorageSync('update_skip_count') || 0
skipCount++
uni.setStorageSync('update_skip_count', skipCount)
// 设置下次提醒时间
let remindDelay
if (skipCount === 1) {
remindDelay = 24 * 60 * 60 * 1000 // 24小时后
} else if (skipCount === 2) {
remindDelay = 12 * 60 * 60 * 1000 // 12小时后
} else {
remindDelay = 6 * 60 * 60 * 1000 // 6小时后
}
const nextRemindTime = Date.now() + remindDelay
uni.setStorageSync('next_remind_time', nextRemindTime)
}
shouldShowDialog() {
const nextRemindTime = uni.getStorageSync('next_remind_time') || 0
return Date.now() >= nextRemindTime
}
formatSize(bytes) {
if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + ' KB'
}
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
}
export default new RecommendedUpdateManager()
2.3 静默更新(Silent Update)
使用场景
- 小版本更新(bug修复)
- 资源文件更新
- 配置更新
- 不影响用户使用的更新
实现方案
// utils/silent-update.js
class SilentUpdateManager {
async checkAndDownload() {
try {
const res = await uni.request({
url: 'https://api.xxx.com/app/version/check',
data: {
currentVersion: this.getCurrentVersion()
}
})
if (res.data.hasUpdate && res.data.updateType === 'silent') {
// 静默下载
this.downloadInBackground(res.data.downloadUrl)
}
} catch (error) {
console.error('检查更新失败:', error)
}
}
downloadInBackground(url) {
// 只在 WiFi 下自动下载
const networkType = uni.getSystemInfoSync().networkType
if (networkType !== 'wifi') {
console.log('非WiFi环境,跳过静默更新')
return
}
console.log('开始静默下载')
const downloadTask = uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// 下载完成,保存文件路径
uni.setStorageSync('update_package_path', res.tempFilePath)
// 显示小提示(不打扰)
this.showUpdateTip()
}
},
fail: (error) => {
console.error('静默下载失败:', error)
}
})
// 不显示进度,完全后台下载
downloadTask.onProgressUpdate((res) => {
console.log('下载进度:', res.progress)
})
}
showUpdateTip() {
// 显示一个不打扰的提示
uni.showToast({
title: '新版本已准备就绪',
icon: 'none',
duration: 2000
})
// 设置角标或红点
uni.setTabBarBadge({
index: 3, // 设置页的索引
text: '1'
})
}
// 用户下次启动时安装
installOnNextLaunch() {
const packagePath = uni.getStorageSync('update_package_path')
if (packagePath) {
uni.showModal({
title: '更新提示',
content: '检测到新版本已下载完成,是否立即安装?',
success: (res) => {
if (res.confirm) {
this.installUpdate(packagePath)
}
}
})
}
}
installUpdate(filePath) {
// #ifdef APP-PLUS
plus.runtime.install(filePath, {}, () => {
// 清除缓存
uni.removeStorageSync('update_package_path')
// 重启应用
plus.runtime.restart()
})
// #endif
}
}
export default new SilentUpdateManager()
2.4 热更新(Hot Update)
使用场景
- 紧急bug修复
- H5页面更新
- 业务逻辑调整
- 不需要审核的更新
uni-app wgt包热更新
// utils/hot-update.js
class HotUpdateManager {
async check() {
try {
const res = await uni.request({
url: 'https://api.xxx.com/wgt/version',
data: {
platform: plus.os.name,
currentVersion: plus.runtime.version
}
})
if (res.data.hasUpdate) {
this.downloadWgt(res.data.wgtUrl, res.data.version)
}
} catch (error) {
console.error('检查热更新失败:', error)
}
}
downloadWgt(url, version) {
// 后台静默下载
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// 安装wgt包
plus.runtime.install(res.tempFilePath, {
force: false
}, () => {
console.log('wgt安装成功')
// 提示用户重启
uni.showModal({
title: '更新完成',
content: '应用需要重启以完成更新',
showCancel: false,
success: () => {
plus.runtime.restart()
}
})
}, (error) => {
console.error('wgt安装失败:', error)
})
}
}
})
}
}
export default new HotUpdateManager()
三、灰度发布策略
3.1 按用户百分比灰度
// server/gray-release.js
class GrayReleaseManager {
checkUserInGray(userId, grayPercentage) {
// 使用userId的hash值决定是否在灰度范围
const hash = this.hashCode(userId)
const bucket = hash % 100
return bucket < grayPercentage
}
hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i)
hash = hash & hash
}
return Math.abs(hash)
}
getUpdateInfo(userId, currentVersion) {
const config = {
// 灰度配置
grayRelease: {
enabled: true,
percentage: 20, // 20%用户
startTime: '2024-01-01 00:00:00',
endTime: '2024-01-03 23:59:59'
},
// 最新版本
latestVersion: {
versionName: '2.0.0',
versionCode: 200,
downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk'
},
// 稳定版本
stableVersion: {
versionName: '1.9.0',
versionCode: 190,
downloadUrl: 'https://cdn.xxx.com/app-1.9.0.apk'
}
}
// 灰度期间
if (config.grayRelease.enabled) {
const now = Date.now()
const start = new Date(config.grayRelease.startTime).getTime()
const end = new Date(config.grayRelease.endTime).getTime()
if (now >= start && now <= end) {
// 判断用户是否在灰度范围
if (this.checkUserInGray(userId, config.grayRelease.percentage)) {
return config.latestVersion
} else {
return config.stableVersion
}
}
}
// 灰度结束,全量推送
return config.latestVersion
}
}
3.2 按地区灰度
class RegionGrayRelease {
getUpdateInfo(userId, region) {
const config = {
grayRegions: ['beijing', 'shanghai', 'guangzhou'],
latestVersion: { /* ... */ },
stableVersion: { /* ... */ }
}
if (config.grayRegions.includes(region)) {
return config.latestVersion
} else {
return config.stableVersion
}
}
}
3.3 按设备灰度
class DeviceGrayRelease {
getUpdateInfo(deviceInfo) {
const config = {
// Android 设备灰度
android: {
brands: ['xiaomi', 'huawei', 'oppo'],
minVersion: '8.0'
},
// iOS 设备灰度
ios: {
minVersion: '13.0'
}
}
if (deviceInfo.platform === 'android') {
const brandMatch = config.android.brands.includes(deviceInfo.brand)
const versionMatch = this.compareVersion(
deviceInfo.osVersion,
config.android.minVersion
) >= 0
if (brandMatch && versionMatch) {
return this.getLatestVersion()
}
}
return this.getStableVersion()
}
}
3.4 白名单机制
class WhitelistManager {
constructor() {
this.whitelist = [
'user_test_001', // 测试账号
'user_test_002',
'user_vip_001' // VIP用户
]
this.blacklist = [
'user_problem_001' // 问题用户
]
}
checkAccess(userId) {
// 黑名单直接拒绝
if (this.blacklist.includes(userId)) {
return {
allow: false,
reason: 'blacklist'
}
}
// 白名单直接通过
if (this.whitelist.includes(userId)) {
return {
allow: true,
priority: 'high'
}
}
return { allow: true }
}
getUpdateInfo(userId) {
const access = this.checkAccess(userId)
if (!access.allow) {
return this.getStableVersion()
}
if (access.priority === 'high') {
return this.getLatestVersion()
}
// 普通用户走灰度逻辑
return this.getGrayVersion(userId)
}
}
四、更新通知时机
4.1 启动时通知(最常用)
// App.vue
export default {
onLaunch() {
// 延迟2秒检查,避免影响启动体验
setTimeout(() => {
this.checkUpdate()
}, 2000)
}
}
优点
- 覆盖率高,每次启动都能检查
- 用户体验好,不打断使用
缺点
4.2 定时检查
class ScheduledUpdateChecker {
constructor() {
this.checkInterval = 24 * 60 * 60 * 1000 // 24小时
this.timer = null
}
start() {
this.check()
this.timer = setInterval(() => {
this.check()
}, this.checkInterval)
}
stop() {
if (this.timer) {
clearInterval(this.timer)
}
}
async check() {
const lastCheckTime = uni.getStorageSync('last_check_time') || 0
const now = Date.now()
// 距离上次检查超过24小时才检查
if (now - lastCheckTime > this.checkInterval) {
await this.checkUpdate()
uni.setStorageSync('last_check_time', now)
}
}
}
4.3 关键操作时提示
// 用户在关键操作时提示更新
class CriticalOperationUpdateChecker {
checkBeforeOperation(operationType) {
const needUpdate = this.shouldUpdate()
if (needUpdate) {
uni.showModal({
title: '建议更新',
content: `检测到新版本,建议更新后再${operationType}`,
confirmText: '立即更新',
cancelText: '继续操作',
success: (res) => {
if (res.confirm) {
this.startUpdate()
} else {
this.continueOperation()
}
}
})
} else {
this.continueOperation()
}
}
}
// 使用示例
// 用户要发起支付时
onPayment() {
updateChecker.checkBeforeOperation('支付')
}
// 用户要上传文件时
onUpload() {
updateChecker.checkBeforeOperation('上传文件')
}
4.4 空闲时检查
class IdleUpdateChecker {
constructor() {
this.idleTime = 5 * 60 * 1000 // 5分钟空闲
this.lastActivityTime = Date.now()
this.timer = null
}
start() {
// 监听用户活动
this.addActivityListeners()
// 定时检查是否空闲
this.timer = setInterval(() => {
this.checkIdle()
}, 60 * 1000) // 每分钟检查一次
}
addActivityListeners() {
// 监听页面切换
uni.$on('page-changed', () => {
this.updateActivityTime()
})
// 监听触摸事件(需要在各页面添加)
// onTouchStart() { this.updateActivityTime() }
}
updateActivityTime() {
this.lastActivityTime = Date.now()
}
checkIdle() {
const idleDuration = Date.now() - this.lastActivityTime
if (idleDuration >= this.idleTime) {
// 用户空闲,检查更新
this.checkUpdate()
}
}
}
五、业界最佳实践
5.1 微信更新策略
特点
- 启动时静默检查更新
- 有更新时显示红点提示
- 用户手动进入才弹窗
- 下载在后台进行
- 安装需要用户确认
借鉴点
5.2 抖音更新策略
特点
- 启动时检查更新
- WiFi下自动下载
- 下载完成后角标提示
- 下次启动时提示安装
借鉴点
- 静默下载不打扰
- 角标提示很温和
- 延迟到下次启动安装
5.3 淘宝更新策略
特点
- 强制更新很少用
- 推荐更新可跳过多次
- 跳过3次后加强提示
- 关键功能前再次提示
借鉴点
- 给用户多次选择机会
- 逐步加强提示力度
- 在关键节点提醒
5.4 推荐策略组合
// 综合最佳实践
class BestPracticeUpdateManager {
async checkUpdate() {
const updateInfo = await this.getUpdateInfo()
if (!updateInfo.hasUpdate) {
return
}
// 1. 强制更新:直接弹窗,无法跳过
if (updateInfo.updateType === 'force') {
this.showForceUpdate(updateInfo)
return
}
// 2. 推荐更新:根据用户跳过次数决定策略
const skipCount = this.getSkipCount()
if (skipCount === 0) {
// 首次提示:温和
this.showGentleReminder(updateInfo)
} else if (skipCount < 3) {
// 2-3次:稍微强化
this.showNormalReminder(updateInfo)
} else {
// 3次以上:加强提示
this.showStrongReminder(updateInfo)
}
// 3. 静默更新:WiFi下后台下载
if (updateInfo.updateType === 'silent') {
this.silentDownload(updateInfo)
}
}
showGentleReminder(info) {
// 只在设置页显示红点
uni.setTabBarBadge({ index: 3, text: '1' })
// 不弹窗,用户主动进入设置才看到
}
showNormalReminder(info) {
// 启动时弹窗,可跳过
setTimeout(() => {
uni.showModal({
title: '发现新版本',
content: info.updateInfo,
confirmText: '更新',
cancelText: '稍后'
})
}, 2000)
}
showStrongReminder(info) {
// 立即弹窗,突出显示
uni.showModal({
title: '强烈建议更新',
content: `${info.updateInfo}\n\n已连续跳过${this.getSkipCount()}次`,
confirmText: '立即更新',
cancelText: '忽略',
confirmColor: '#FF0000'
})
}
}
六、服务端实现
6.1 版本管理接口
// server/routes/version.js
const express = require('express')
const router = express.Router()
// 检查更新
router.get('/check', async (req, res) => {
const {
platform, // android/ios
currentVersion,
userId,
deviceId,
region,
channel
} = req.query
try {
// 获取版本配置
const versionConfig = await getVersionConfig(platform)
// 判断是否需要更新
const needUpdate = compareVersion(
currentVersion,
versionConfig.latest.versionCode
) < 0
if (!needUpdate) {
return res.json({ hasUpdate: false })
}
// 判断更新类型
const updateType = determineUpdateType(
currentVersion,
userId,
versionConfig
)
// 灰度判断
const targetVersion = checkGrayRelease(
userId,
region,
versionConfig
)
res.json({
hasUpdate: true,
versionName: targetVersion.versionName,
versionCode: targetVersion.versionCode,
downloadUrl: targetVersion.downloadUrl,
updateType, // force/recommended/silent
updateInfo: targetVersion.updateInfo,
fileSize: targetVersion.fileSize,
md5: targetVersion.md5
})
// 记录检查日志
await logUpdateCheck({
userId,
deviceId,
currentVersion,
targetVersion: targetVersion.versionName,
updateType
})
} catch (error) {
res.status(500).json({ error: error.message })
}
})
// 记录更新行为
router.post('/action', async (req, res) => {
const {
userId,
action, // skip/download/install
version
} = req.body
await logUpdateAction({
userId,
action,
version,
timestamp: Date.now()
})
res.json({ success: true })
})
// 获取更新统计
router.get('/stats', async (req, res) => {
const { version } = req.query
const stats = await UpdateStats.findOne({ version })
res.json({
version,
totalUsers: stats.totalUsers,
updatedUsers: stats.updatedUsers,
updateRate: (stats.updatedUsers / stats.totalUsers * 100).toFixed(2) + '%',
skipCount: stats.skipCount,
downloadCount: stats.downloadCount,
installCount: stats.installCount
})
})
module.exports = router
6.2 版本配置管理
// server/models/version-config.js
const versionConfig = {
android: {
latest: {
versionName: '2.0.0',
versionCode: 200,
downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk',
updateInfo: '1. 新增视频功能\n2. 优化性能\n3. 修复已知问题',
fileSize: 50 * 1024 * 1024, // 50MB
md5: 'abc123...',
releaseTime: '2024-01-01 10:00:00',
// 更新策略
updateStrategy: {
// 强制更新最低版本
forceUpdateMin: '1.5.0',
// 推荐更新
recommendedUpdate: true,
// 静默更新(小版本)
silentUpdate: false
}
},
// 灰度配置
grayRelease: {
enabled: true,
percentage: 20,
whitelist: ['user_001', 'user_002'],
blacklist: ['user_999'],
regions: ['beijing', 'shanghai'],
startTime: '2024-01-01 00:00:00',
endTime: '2024-01-03 23:59:59'
},
// 渠道配置
channels: {
huawei: {
downloadUrl: 'https://cdn.xxx.com/app-huawei-2.0.0.apk'
},
xiaomi: {
downloadUrl: 'https://cdn.xxx.com/app-xiaomi-2.0.0.apk'
}
}
},
ios: {
latest: {
versionName: '2.0.0',
versionCode: 200,
appStoreUrl: 'https://apps.apple.com/app/idxxxxx',
updateInfo: '1. 新增视频功能\n2. 优化性能\n3. 修复已知问题',
releaseTime: '2024-01-01 10:00:00'
}
}
}
6.3 推送通知管理
// server/services/push-notification.js
class PushNotificationService {
async sendUpdateNotification(userIds, versionInfo) {
// 批量推送
const batchSize = 1000
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize)
await this.pushToBatch(batch, {
title: '版本更新',
content: `${versionInfo.name} 已发布,快来体验新功能!`,
payload: {
type: 'app_update',
version: versionInfo.name,
url: versionInfo.downloadUrl
}
})
// 避免频率限制
await this.sleep(1000)
}
}
async pushToBatch(userIds, notification) {
// 获取用户的推送token
const users = await User.find({
_id: { $in: userIds },
pushToken: { $exists: true }
})
const tokens = users.map(u => u.pushToken)
// 调用推送服务
await this.callPushAPI(tokens, notification)
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
七、数据监控和分析
7.1 更新漏斗分析
// 监控更新的每个环节
const updateFunnel = {
// 1. 检查更新
checkUpdate: 100000, // 10万用户检查更新
// 2. 发现更新
hasUpdate: 80000, // 8万用户有更新
// 3. 看到弹窗
showDialog: 70000, // 7万用户看到弹窗
// 4. 点击更新
clickUpdate: 50000, // 5万用户点击更新
// 5. 开始下载
startDownload: 48000, // 4.8万开始下载
// 6. 下载完成
downloadComplete: 45000, // 4.5万下载完成
// 7. 开始安装
startInstall: 43000, // 4.3万开始安装
// 8. 安装完成
installComplete: 40000, // 4万安装完成
// 转化率
conversionRate: {
updateRate: '40%', // 最终更新率
downloadRate: '96%', // 下载完成率
installRate: '93%' // 安装完成率
}
}
7.2 数据埋点
// utils/analytics.js
class UpdateAnalytics {
// 检查更新
trackCheckUpdate(currentVersion) {
this.report('update_check', {
current_version: currentVersion,
timestamp: Date.now()
})
}
// 显示弹窗
trackShowDialog(version, updateType) {
this.report('update_dialog_show', {
version,
update_type: updateType
})
}
// 点击更新
trackClickUpdate(version) {
this.report('update_click', {
version
})
}
// 跳过更新
trackSkipUpdate(version, skipCount) {
this.report('update_skip', {
version,
skip_count: skipCount
})
}
// 开始下载
trackStartDownload(version) {
this.report('update_download_start', {
version,
network_type: uni.getSystemInfoSync().networkType
})
}
// 下载进度
trackDownloadProgress(version, progress) {
// 每10%上报一次
if (progress % 10 === 0) {
this.report('update_download_progress', {
version,
progress
})
}
}
// 下载完成
trackDownloadComplete(version, duration) {
this.report('update_download_complete', {
version,
duration
})
}
// 安装完成
trackInstallComplete(version) {
this.report('update_install_complete', {
version
})
}
// 更新失败
trackUpdateError(version, error, step) {
this.report('update_error', {
version,
error: error.message,
step // download/install
})
}
report(eventName, data) {
uni.request({
url: 'https://analytics.xxx.com/track',
method: 'POST',
data: {
event: eventName,
properties: {
...data,
user_id: this.getUserId(),
device_id: this.getDeviceId(),
platform: uni.getSystemInfoSync().platform
}
}
})
}
}
export default new UpdateAnalytics()
八、总结和建议
推荐策略组合
10万用户的更新方案
- 第1天:灰度10%(1万用户)
- 方式:应用内启动检查
- 类型:推荐更新
- 监控:密切观察崩溃率、反馈
- 第3天:灰度50%(5万用户)
- 方式:应用内启动检查 + 推送通知
- 类型:推荐更新
- 监控:持续观察数据
- 第5天:全量100%(10万用户)
- 方式:应用内启动检查 + 推送通知 + 应用内消息
- 类型:推荐更新
- 逐步加强:跳过3次后变强提示
- 第7天:加强提示
- 对未更新用户加强提示
- 关键功能前再次提醒
- 考虑短信/邮件通知
- 第10天:考虑强制
- 如果重要更新,开始强制
- 给用户最后期限
- 旧版本逐步不可用
不同场景选择
| 更新类型 |
通知方式 |
更新策略 |
适用场景 |
| 严重bug |
强制更新 |
立即强制 |
安全漏洞、核心功能不可用 |
| 重要功能 |
推送+应用内 |
推荐更新 |
新功能上线 |
| 性能优化 |
应用内 |
推荐更新 |
性能提升 |
| 小bug修复 |
静默更新 |
后台下载 |
不影响使用 |
| 资源更新 |
热更新 |
wgt包 |
H5页面、配置 |
关键数据指标
const updateMetrics = {
// 覆盖率
coverageRate: '检查更新用户数 / 总用户数',
// 更新率
updateRate: '完成更新用户数 / 检查更新用户数',
// 转化率
conversionRate: '点击更新用户数 / 看到弹窗用户数',
// 下载成功率
downloadSuccessRate: '下载完成数 / 开始下载数',
// 安装成功率
installSuccessRate: '安装完成数 / 开始安装数',
// 平均更新时长
averageUpdateTime: '总更新时长 / 更新用户数',
// 跳过率
skipRate: '跳过更新次数 / 弹窗显示次数'
}
// 健康指标参考
const healthMetrics = {
coverageRate: '> 90%', // 覆盖率要高
updateRate: '> 60%', // 更新率要达标
downloadSuccessRate: '> 95%', // 下载成功率要高
installSuccessRate: '> 90%', // 安装成功率要高
skipRate: '< 30%' // 跳过率要低
}