










最近遇到一个问题:APP内嵌的象棋游戏在切到后台后背景音乐仍在播放。排查后发现需要从原生端和H5端两侧配合才能彻底解决,记录一下完整方案。
我们的APP通过 plus.webview.create() 创建原生 WebView 来加载象棋游戏的H5页面。游戏使用 uni.createInnerAudioContext() 循环播放背景音乐。
问题很简单:把APP切到后台,游戏背景音乐还在响。
一开始以为只需要在原生端处理就行,结果发现没那么简单:
plus.webview.create() 创建的子 WebView,在 APP 切后台后并不会自动暂停 JS 执行和媒体播放evalJS 注入 JS 代码的时机可能已被系统限制,不一定能可靠执行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()
在 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 都能正常工作。
在游戏的 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'),如果用户在游戏设置里关了音乐,切回来也不会自动播放最初只用了 evalJS 注入 JS 来暂停 <audio> 元素,实测无效。原因是 APP 切后台后 WebView 的 JS 执行已被系统限制,evalJS 可能无法可靠执行。
解决:加上 WebView.onPause() 这个 Android 原生 API,从系统层面暂停 WebView。
WebView.onPause() 虽然能暂停 JS 执行,但不同厂商 ROM 的行为不一致,有些只是降低了 WebView 优先级而非真正暂停。而且 onResume() 后音频也不一定能自动恢复。
解决:H5 端自己监听 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如果你的 APP 也内嵌了其他 H5 游戏/页面遇到类似问题,这套方案可以直接复用。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。