乐于分享
好东西不私藏

PC 端 Chrome 调试 Android 页面嵌套的 h5 页面

PC 端 Chrome 调试 Android 页面嵌套的 h5 页面

由于在这块是个小白,这里我直接借助 ai 的帮助,最终完成的页面的调试。

  1. 首先需要安卓 app 的同事开启调试的权限,增加下面截图的代码即可。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true)
}
  1. 然后是手机端配置及打开Chrome调试页面:chrome://inspect/#devices
  1. 最好在电脑上部署一个 adb 环境,方便验证手机是否已连接上电脑。下载工具包的地址为(包含Windows、Linux、Mac OS 工具包):https://developer.android.com/tools/releases/platform-tools?hl=zh-cn
  1. 若是手机开启开发者调试后,然后弹不出来授权弹窗,可以采用下面的方式进行尝试,我是部署adb环境后,然后在命令行(或者 Windows Terminal)输入: adb devices ,然后授权就出来了。
  1. 具体调试方法,可参考 Chrome 官方提供的教程,非常详细,这里不再赘述了,官方教程就是简体中文的,很好理解。这里放上链接:https://developer.chrome.com/docs/devtools/remote-debugging?hl=zh-cn

  2. 安卓端的同事在嵌套 h5 页面的 Activity 中增加如下代码:

import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

classWebActivity : AppCompatActivity() {

privatevar webView: WebView? = null

// 1. 缓存当前最新的生命周期状态
@Volatile
privatevar currentLifecycleState: String = "onCreate"

override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
// ... 初始化 view ...

  currentLifecycleState = "onCreate"
  setupWebView()

if (savedInstanceState != null) {
   webView?.restoreState(savedInstanceState)
  } else {
   webView?.loadUrl("https://your-h5-url.com")
  }
 }

/**
  * 2. 封装统一的通信方法
  */

private fun sendLifecycleToH5(state: String){
  currentLifecycleState = state

// 确保在主线程且 WebView 未销毁时执行
  runOnUiThread {
if (webView == nullreturn@runOnUiThread

   val jsCode = "javascript:if(window.onAndroidLifecycle){window.onAndroidLifecycle('$state');}"
   webView?.evaluateJavascript(jsCode, null)
  }
 }

/**
  * 3. WebView 配置
  */

private fun setupWebView(){
 webView?.apply {
   settings.javaScriptEnabled = true// 必须开启

// 【核心优化】:注册一个原生接口,供 H5 初始化完成后“主动拉取”当前状态
   addJavascriptInterface(AndroidLifecycleInterface(), "AndroidLifecycle")

   webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?){
super.onPageFinished(view, url)
// 传统 H5 补发兜底
     sendLifecycleToH5(currentLifecycleState)
    }
   }
  }
 }

// ==========================================
// 4. 重写原生生命周期(注意 onPause 和 onDestroy 的顺序)
// ==========================================

override fun onStart(){
super.onStart()
  sendLifecycleToH5("onStart")
 }

override fun onResume(){
super.onResume()
  webView?.onResume() // 先恢复 WebView 功能
  sendLifecycleToH5("onResume"// 再发通知
 }

override fun onPause(){
// ⚠️ 核心修改:必须先通知 H5 暂停(比如 H5 要暂停视频播放、定时器)
  sendLifecycleToH5("onPause"
// ⚠️ 后挂起 WebView,否则 JS 代码将无法在后台顺利执行
  webView?.onPause() 
super.onPause()
 }

override fun onStop(){
  sendLifecycleToH5("onStop")
super.onStop()
 }

override fun onDestroy(){
// 尽力通知
  sendLifecycleToH5("onDestroy")

// 安全释放 WebView 防止内存泄漏
  webView?.let { wb ->
   (wb.parent as? android.view.ViewGroup)?.removeView(wb)
   wb.stopLoading()
   wb.webChromeClient = null
   wb.webViewClient = WebViewClient()
   wb.clearHistory()
   wb.removeAllViews()
   wb.destroy()
  }
  webView = null
super.onDestroy()
 }

/**
  * 5. 供 H5 主动调用的内部类
  */

 inner classAndroidLifecycleInterface{
@JavascriptInterface
fun getCurrentState(): String {
// 当 H5 页面内部的 Vue/React 加载完毕后,主动调用此方法获取最新状态
return currentLifecycleState
  }
 }
}
  1. 为了处理 app 返给我们的消息,这里我创建一个 hooks,代码如下:
import { ref, onMounted, onUnmounted } from"vue";

/**
 * 宿主 App 生命周期 Hook (全量闭环版)
 *
 * @param {Object} callbacks
 * @param {Function} [callbacks.onStateChange] - 任何状态改变时触发
 * --- 正向生命周期 ---
 * @param {Function} [callbacks.onCreate] - 页面创建
 * @param {Function} [callbacks.onStart] - 页面可见
 * @param {Function} [callbacks.onResume] - 页面激活 (⭐ 恢复渲染 / 刷新数据)
 * --- 逆向生命周期 ---
 * @param {Function} [callbacks.onPause] - 页面失去焦点/退到后台 (⭐ 暂停渲染)
 * @param {Function} [callbacks.onStop] - 页面完全不可见
 * @param {Function} [callbacks.onDestroy] - 页面即将销毁 (⚠️ JS 不一定来得及执行)
 * --- 统合兜底 ---
 * @param {Function} [callbacks.onAppExit] - 当退到后台(onPause)或浏览器/WebView原生卸载时触发 (⭐ 存数据最佳位置)
 */

exportfunctionuseAppLifecycle(callbacks = {}{
const {
    onStateChange,
    onCreate,
    onStart,
    onResume,
    onPause,
    onStop,
    onDestroy,
    onAppExit,
  } = callbacks;

const appState = ref("");

// 自定义事件名称
const EVENT_NAME = "app-lifecycle";

/**
   * =================================================================
   * 🛠️ 核心桥接胶水代码:将安卓的原生回调,转换为 Vue 能够监听的自定义事件
   * =================================================================
   */

const initAndroidBridge = () => {
if (typeofwindow !== "undefined") {
window.onAndroidLifecycle = function (state{
console.log("[Native -> H5] 收到安卓原生生命周期状态:", state);
if (!state) return;

// 派发自定义事件,通知所有使用该 Hook 的组件
const lifecycleEvent = new CustomEvent(EVENT_NAME, {
detail: { state: state },
        });
window.dispatchEvent(lifecycleEvent);
      };
    }
  };

/**
   * 处理来自安卓生命周期的自定义事件
   */

const handleLifecycleChange = (event) => {
const rawState = String(event.detail?.state || "").trim();

// 统一格式化:防止原生传过来的状态带不带 "on",统一转为小写处理
let state = rawState.toLowerCase();
if (!state.startsWith("on")) {
      state = "on" + state;
    }

    appState.value = state;

// 1. 触发通用状态流转回调
if (typeof onStateChange === "function") onStateChange(state);

// 2. 精确匹配对应的生命周期函数
switch (state) {
case"oncreate":
if (typeof onCreate === "function") onCreate();
break;
case"onstart":
if (typeof onStart === "function") onStart();
break;
case"onresume":
if (typeof onResume === "function") onResume();
break;
case"onpause":
if (typeof onPause === "function") onPause();
// 关键点:进入后台的瞬间是保存数据、记录埋点最稳妥的时机,作为 onAppExit 的触发源之一
if (typeof onAppExit === "function") onAppExit("onpause");
break;
case"onstop":
if (typeof onStop === "function") onStop();
break;
case"ondestroy":
if (typeof onDestroy === "function") onDestroy();
break;
    }
  };

/**
   * 处理 H5 浏览器/WebView 自身的页面卸载事件(兜底)
   */

const handlePageExit = (event) => {
if (typeof onAppExit === "function") {
      onAppExit(event.type); // 触发原因可能是 'pagehide' 或 'beforeunload'
    }
  };

  onMounted(() => {
// 1. 初始化挂载 window.onAndroidLifecycle
    initAndroidBridge();

// 2. 绑定事件监听
window.addEventListener(EVENT_NAME, handleLifecycleChange);
window.addEventListener("pagehide", handlePageExit);
window.addEventListener("beforeunload", handlePageExit);
  });

  onUnmounted(() => {
// 3. 解绑事件监听
window.removeEventListener(EVENT_NAME, handleLifecycleChange);
window.removeEventListener("pagehide", handlePageExit);
window.removeEventListener("beforeunload", handlePageExit);

// 注意:window.onAndroidLifecycle 作为全局桥梁不建议随意置空,
// 保持其单例挂载即可,避免多组件销毁时导致安卓调用报错。
  });

return {
    appState,
  };
}
  1. 然后我们在 main.js文件中引入:
window.onAndroidLifecycle = function (state{
if (!state) return;

// 创建一个自定义事件,把状态传递给你的 Hook
const lifecycleEvent = new CustomEvent("app-lifecycle", {
detail: { state: state },
  });

// 派发事件
window.dispatchEvent(lifecycleEvent);
};
  1. 最后在App.vue或者别的页面添加如下代码:
import { useAppLifecycle } from"@/hooks/useAppLifecycle.js";

useAppLifecycle({
// ==========================================
// 🟢 正向生命周期 (进入 / 可见 / 激活)
// ==========================================

onCreate() => {
console.log("📱 [onCreate] App 页面已创建");
  },

onStart() => {
console.log("📱 [onStart] App 页面已可见 (但可能无法交互)");
  },

onResume() => {
console.log("📱 [onResume] App 回到前台,完全激活!");
window.dispatchEvent(new CustomEvent("app-resume"));
  },

// ==========================================
// 🟡 逆向生命周期 (不可见 / 后台 / 销毁)
// ==========================================

onPause() => {
console.log("📱 [onPause] App 失去焦点 / 退到后台");
window.dispatchEvent(new CustomEvent("app-pause"));
  },

onStop() => {
console.log("📱 [onStop] App 彻底退到后台,完全不可见");
  },

onDestroy() => {
console.log("📱 [onDestroy] App WebView 即将被销毁");
  },

// ==========================================
// 🔴 统合与兜底监听
// ==========================================

onAppExit(reason) => {
console.log(`⚠️ [onAppExit] 页面进入后台或即将卸载,触发原因: ${reason}`);
  },

// ==========================================
// 🔵 状态流转监测 (常用于调试)
// ==========================================

onStateChange(state) => {
console.log(`🔄 [状态机流转] 当前最新状态 -> ${state}`);
  },
});
  1. 在 App.vue 中通过 window.dispatchEvent 分发自定义事件后,任何子组件(或者独立的数据管理模块,如 Pinia Store)都可以通过 原生 JS 的 window.addEventListener 来监听这些事件,从而做出相应的响应(比如暂停视频、恢复轮询、重新请求数据等)。

为了让子组件中的使用方式同样符合 Vue 3 的开发习惯,最优雅的做法是将子组件的监听逻辑也封装成轻量级的 Hooks,或者直接在子组件的 onMounted 中处理。

以下是具体的几种使用场景和代码示例:

  • 场景一:直接在子组件中使用 如果某个特定的子组件(如视频播放组件、地图轮询组件)需要感知前后台切换,可以直接这样写:
<template>
<divclass="video-player">
<videoref="videoRef"src="movie.mp4"controls></video>
</div>
</template>

<scriptsetup>
import { ref, onMounted, onUnmounted } from'vue';

const videoRef = ref(null);

// 1. 处理回到前台的逻辑
const handleAppResume = () => {
console.log('🎬 子组件感知到 App 回到前台,尝试恢复播放');
if (videoRef.value && videoRef.value.paused) {
    videoRef.value.play().catch(err =>console.log('自动播放受限:', err));
  }
};

// 2. 处理退到后台的逻辑
const handleAppPause = () => {
console.log('🎬 子组件感知到 App 退到后台,自动暂停视频');
if (videoRef.value && !videoRef.value.paused) {
    videoRef.value.pause();
  }
};

onMounted(() => {
// 3. 绑定监听 App.vue 发出的自定义事件
window.addEventListener('app-resume', handleAppResume);
window.addEventListener('app-pause', handleAppPause);
});

onUnmounted(() => {
// 4. 组件销毁时及时解绑,避免内存泄漏
window.removeEventListener('app-resume', handleAppResume);
window.removeEventListener('app-pause', handleAppPause);
});
</script>
  • 场景二:封装成专供子组件使用的轻量 Hooks(推荐) 如果有很多子组件都要用,每次都写 addEventListener 和 removeEventListener 太冗余了。你可以封装一个 useSubAppLifecycle.js 给子组件用:
import { onMounted, onUnmounted } from'vue';

/**
 * 供子组件快捷监听 App 前后台切换的 Hook
 * @param {Object} options 
 * @param {Function} [options.onResume] - App 回到前台回调
 * @param {Function} [options.onPause] - App 进到后台回调
 */

exportfunctionuseSubAppLifecycle({ onResume, onPause } = {}{
  onMounted(() => {
if (typeof onResume === 'function'window.addEventListener('app-resume', onResume);
if (typeof onPause === 'function'window.addEventListener('app-pause', onPause);
  });

  onUnmounted(() => {
if (typeof onResume === 'function'window.removeEventListener('app-resume', onResume);
if (typeof onPause === 'function'window.removeEventListener('app-pause', onPause);
  });
}

方便在子组件中使用:

import { useSubAppLifecycle } from'@/hooks/useSubAppLifecycle';

useSubAppLifecycle({
onResume() => {
console.log('🔄 轮询组件:回到前台,立即重启定时器并刷新列表');
    startPolling();
  },
onPause() => {
console.log('🛑 轮询组件:退到后台,暂停轮询以节省电量和流量');
    stopPolling();
  }
});

functionstartPolling() /* ... */ }
functionstopPolling() /* ... */ }
  • 场景三:在 Pinia / Vuex 全局状态管理中使用 有时候前后台切换不需要修改组件 UI,而是需要修改全局状态(例如:记录用户在线状态、清除全局未读消息定时器等)。你可以直接在 Pinia Store 中初始化监听:
import { defineStore } from'pinia';
import { ref } from'vue';

exportconst useUserStore = defineStore('user', () => {
const isAppActive = ref(true);

// 初始化全局监听(可以在 App.vue 挂载时触发 store 的初始化)
const initLifecycleListener = () => {
window.addEventListener('app-resume', () => {
      isAppActive.value = true;
console.log('Store 状态更新:App 已处于前台');
// 这里可以顺便去静默上报一次埋点或者同步用户信息
    });

window.addEventListener('app-pause', () => {
      isAppActive.value = false;
console.log('Store 状态更新:App 已进入后台');
    });
  };

return {
    isAppActive,
    initLifecycleListener
  };
});
  1. 注意事项如下:

    1. 注意解绑:子组件在监听 window 事件时,务必在 onUnmounted 中解绑。否则当子组件因为路由切换被销毁后,window 上依然挂载着对该组件方法的引用,会导致内存泄漏和逻辑报错(比如去操作一个已经被销毁的 Vue 实例/DOM)。
    2. 事件触发时机:因为 CustomEvent 是同步触发的,当你在 App.vue 触发 dispatchEvent 时,所有已挂载并监听了该事件的子组件会立刻顺次执行对应的回调函数。

调试的截图如下:

好了,以上就是本文的所有内容,我们下期见喽!