uniapp打包的APP内嵌H5游戏切后台背景音乐停不下来的解决方案
最近遇到一个问题:APP内嵌的象棋游戏在切到后台后背景音乐仍在播放。排查后发现需要从原生端和H5端两侧配合才能彻底解决,记录一下完整方案。
问题现象
我们的APP通过 plus.webview.create() 创建原生 WebView 来加载象棋游戏的H5页面。游戏使用 uni.createInnerAudioContext() 循环播放背景音乐。
问题很简单:把APP切到后台,游戏背景音乐还在响。
原因分析
一开始以为只需要在原生端处理就行,结果发现没那么简单:
- WebView 不会自动暂停:
plus.webview.create()创建的子 WebView,在 APP 切后台后并不会自动暂停 JS 执行和媒体播放 - evalJS 不够可靠:APP 切后台后,通过
evalJS注入 JS 代码的时机可能已被系统限制,不一定能可靠执行 - H5 端不感知 APP 生命周期:浏览器层面的
visibilitychange在原生 WebView 中不一定会自动触发
结论:必须原生端和 H5 游戏端双管齐下。
解决思路
核心思路是建立一个跨层通信机制,让原生端的 APP 生命周期事件能可靠地传递到 H5 端:
原生端(APP) H5游戏端
│ │
│ onHide(APP切后台) │
├─ WebView.onPause() │
├─ evalJS 注入标记 ──────────────────→ │
│ window.__APP_BACKGROUND__ = true │
│ dispatchEvent('visibilitychange') ─→ │
│ ├─ 监听到事件
│ └─ bgmCtx.pause()
│ │
│ onShow(APP回前台) │
├─ WebView.onResume() │
├─ evalJS 清除标记 ──────────────────→ │
│ window.__APP_BACKGROUND__ = false │
│ dispatchEvent('visibilitychange') ─→ │
│ ├─ 监听到事件
│ └─ bgmCtx.play()
原生端实现(APP)
在 GameWebview/index.vue 中监听页面生命周期,采用双策略控制音频:
import { onHide, onShow } from '@dcloudio/uni-app'
/**
* 暂停 WebView 音频
* 策略1: WebView.onPause() — 暂停整个 WebView 的 JS 和媒体(Android)
* 策略2: evalJS 注入 — 设置标记 + 触发 visibilitychange(全平台兜底)
*/
function pauseWebViewAudio() {
if (!webViewObj) return
// 策略1:Android WebView 原生暂停
// #ifdef APP-PLUS
try {
webViewObj.onPause()
} catch (e) {
console.warn('[GameWebview] onPause 调用失败:', e)
}
// #endif
// 策略2:evalJS 兜底
try {
webViewObj.evalJS(`
(function() {
window.__APP_BACKGROUND__ = true;
document.querySelectorAll('audio, video').forEach(function(el) {
if (!el.paused) {
el.dataset.wasPlaying = 'true';
el.pause();
}
});
try { document.dispatchEvent(new Event('visibilitychange')); } catch(e) {}
})();
`)
} catch (e) {
console.warn('[GameWebview] evalJS 暂停音频失败:', e)
}
}
/**
* 恢复 WebView 音频
*/
function resumeWebViewAudio() {
if (!webViewObj) return
// #ifdef APP-PLUS
try {
webViewObj.onResume()
} catch (e) {
console.warn('[GameWebview] onResume 调用失败:', e)
}
// #endif
try {
webViewObj.evalJS(`
(function() {
window.__APP_BACKGROUND__ = false;
document.querySelectorAll('[data-was-playing="true"]').forEach(function(el) {
el.play().catch(function() {});
delete el.dataset.wasPlaying;
});
try { document.dispatchEvent(new Event('visibilitychange')); } catch(e) {}
})();
`)
} catch (e) {
console.warn('[GameWebview] evalJS 恢复音频失败:', e)
}
}
// APP 切到后台时暂停音频
onHide(() => {
console.log('[GameWebview] App 进入后台,暂停游戏音频')
pauseWebViewAudio()
})
// APP 回到前台时恢复音频(延迟 300ms 确保 WebView 已就绪)
onShow(() => {
console.log('[GameWebview] App 进入前台,恢复游戏音频')
setTimeout(() => {
resumeWebViewAudio()
}, 300)
})
为什么要双策略?
| 策略 | 方法 | 优势 | 局限 |
|---|---|---|---|
| 策略1 | WebView.onPause()/onResume() |
Android 原生 API,最可靠,直接暂停 JS 执行 | 仅 Android 有效 |
| 策略2 | evalJS 注入 |
全平台通用,可传递自定义标记 | 后台时 JS 执行可能受限 |
两者互补,确保 Android 和 iOS 都能正常工作。
H5 游戏端实现
在游戏的 audio.ts 中新增 visibilitychange 监听,接收原生端发来的信号:
// 标记:是否因为 APP 切后台而暂停的 BGM
let bgmPausedByAppBackground = false
document.addEventListener('visibilitychange', () => {
// 判断是否在 APP WebView 环境中
// 原生端打开时会注入 code 参数和 isWebview 标记
const urlParams = new URLSearchParams(window.location.search)
const isInApp =
window.__APP_BACKGROUND__ !== undefined || // 原生端 evalJS 注入的标记
urlParams.get('isWebview') === 'true' ||
urlParams.has('code')
if (!isInApp) return // 纯浏览器访问不处理
if (document.hidden || window.__APP_BACKGROUND__) {
// APP 切到后台 → 暂停 BGM(不销毁实例,方便恢复)
if (bgmCtx && !bgmCtx.paused) {
bgmCtx.pause()
bgmPausedByAppBackground = true
}
} else {
// APP 回到前台 → 恢复 BGM(需音乐开关开启)
if (bgmPausedByAppBackground && isEnabled('musicEnabled')) {
bgmCtx?.play()
}
bgmPausedByAppBackground = false
}
})
几个设计细节值得注意:
bgmPausedByAppBackground标记:区分"被 APP 切后台暂停"和"用户自己暂停",避免用户关了音乐切回来又被自动打开- 只暂停不销毁:
pause()而不是stop() + destroy(),恢复时直接play(),无需重建音频实例,响应更快 - 尊重用户设置:恢复前检查
isEnabled('musicEnabled'),如果用户在游戏设置里关了音乐,切回来也不会自动播放 - 环境判断:通过 URL 参数判断是否在 APP 内,纯浏览器访问不会受影响
踩坑记录
坑1:只靠 evalJS 不行
最初只用了 evalJS 注入 JS 来暂停 <audio> 元素,实测无效。原因是 APP 切后台后 WebView 的 JS 执行已被系统限制,evalJS 可能无法可靠执行。
解决:加上 WebView.onPause() 这个 Android 原生 API,从系统层面暂停 WebView。
坑2:只靠原生端也不行
WebView.onPause() 虽然能暂停 JS 执行,但不同厂商 ROM 的行为不一致,有些只是降低了 WebView 优先级而非真正暂停。而且 onResume() 后音频也不一定能自动恢复。
解决:H5 端自己监听 visibilitychange,在事件回调中主动暂停/恢复,这是最可靠的一层保障。
坑3:visibilitychange 不会自动触发
在原生 WebView 中,APP 切后台不一定会触发浏览器的 visibilitychange 事件(不像普通浏览器切标签页那样可靠)。
解决:原生端通过 evalJS 手动 dispatchEvent(new Event('visibilitychange')),确保 H5 端能收到通知。
涉及文件
| 项目 | 文件 | 改动内容 |
|---|---|---|
| APP 端(new-sg-app) | src/pages/GameWebview/index.vue |
新增 onHide/onShow 监听 + pauseWebViewAudio/resumeWebViewAudio 方法 |
| H5 游戏端(qnl-xq-web) | src/utils/audio.ts |
在 #ifdef H5 块内新增 visibilitychange 监听器 |
测试清单
总结
这个问题的核心在于 原生 WebView 和 H5 之间缺少生命周期同步机制。解决方案的关键是:
- 原生端做"通知者":通过
onPause/onResume+evalJS把 APP 的前后台状态传递给 H5 - H5 端做"执行者":收到信号后主动控制自己的音频实例
- 双策略互补:原生 API 保底 + JS 注入兜底,覆盖 Android 和 iOS
如果你的 APP 也内嵌了其他 H5 游戏/页面遇到类似问题,这套方案可以直接复用。
浙公网安备 33010602011771号