AIGC标识 uniapp打包的APP内嵌H5游戏切后台背景音乐停不下来的解决方案

最近遇到一个问题:APP内嵌的象棋游戏在切到后台后背景音乐仍在播放。排查后发现需要从原生端和H5端两侧配合才能彻底解决,记录一下完整方案。

问题现象

我们的APP通过 plus.webview.create() 创建原生 WebView 来加载象棋游戏的H5页面。游戏使用 uni.createInnerAudioContext() 循环播放背景音乐。

问题很简单:把APP切到后台,游戏背景音乐还在响

原因分析

一开始以为只需要在原生端处理就行,结果发现没那么简单:

  1. WebView 不会自动暂停plus.webview.create() 创建的子 WebView,在 APP 切后台后并不会自动暂停 JS 执行和媒体播放
  2. evalJS 不够可靠:APP 切后台后,通过 evalJS 注入 JS 代码的时机可能已被系统限制,不一定能可靠执行
  3. 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 之间缺少生命周期同步机制。解决方案的关键是:

  1. 原生端做"通知者":通过 onPause/onResume + evalJS 把 APP 的前后台状态传递给 H5
  2. H5 端做"执行者":收到信号后主动控制自己的音频实例
  3. 双策略互补:原生 API 保底 + JS 注入兜底,覆盖 Android 和 iOS

如果你的 APP 也内嵌了其他 H5 游戏/页面遇到类似问题,这套方案可以直接复用。

posted on 2026-08-28 16:06  码间留白  阅读(4)  评论(0)    收藏  举报

导航