uniapp中App端实现WebView 与内嵌H5(uniapp)网页的双向通讯

uniapp中App端实现WebView 与内嵌H5(uniapp)网页的双向通讯

​​​​主要功能说明:

WebView集成:内嵌Web页面并实现双向通信设备信息获取:自动适配Android不同版本的设备标识获取方案权限动态申请处理H5消息处理:指纹认证流程(支持检测→录入检查→执行认证)二维码扫描功能特殊交互处理:物理返回键拦截(优先WebView返回)进度条样式定制通信机制:使用evalJS调用H5全局函数结构化消息协议(type/data模式)

基本使用

App端

指纹识别和扫码需要先配置模块

关键方法

向 H5 发送消息:sendMessageToH5(data)
JSON 序列化URL 编码通过 evalJS 调用 H5 全局方法
设备信息获取:getDeviceIMEI()

功能:获取设备唯一标识

智能 IMEI 获取策略:Android 10+:使用 OAIDAndroid 10 以下:动态申请权限获取 IMEI备用方案:使用设备 ID自动适配不同 Android 版本权限申请封装
指纹认证模块:handleFingerprintAuth(userId, challenge)

功能:执行指纹认证流程

完整的指纹认证流程:设备支持性检测指纹录入检查执行指纹认证结果回调机制可以使用服务器生成的随机challenge进行防重验证,每次认证使用服务端生成的唯一challenge
扫码功能:scanCode()
调用原生相机扫码扫码结果回传 H5

返回键处理:onBackPress()

拦截返回按钮事件优先 WebView 内部返回防止意外退出

App → H5 消息格式

{ "type": "消息类型", "data": { ... } // 类型相关数据 }
 // 发送消息给H5 sendMessageToH5(data) { if (!this.currentWebview) return; // 确保WebView存在 const jsonString = JSON.stringify(data); // 序列化数据 const safeString = encodeURIComponent(jsonString); // URL编码 // 通过evalJS调用H5全局方法 this.currentWebview.evalJS( `getAppMsg(decodeURIComponent('${safeString}'))` ); console.log("已发送", jsonString); // 日志记录 }, // 发送认证结果给H5 sendAuthResult(userId, challenge, success, message, resultJSON = null) { const resultData = { type: "fingerprintAuthResult", // 消息类型 data: { userId: userId, challenge: challenge, success: success, // 认证结果 message: message, // 结果描述 resultJSON: resultJSON, // 原始认证数据 timestamp: new Date().getTime(), // 时间戳 }, }; // 发送消息 this.sendMessageToH5(resultData); },

监听并处理来自H5的消息

 // 处理来自H5的消息 fromWebviewMessage(e) { console.log("收到 webview 页面发回的消息", e.detail.data); const msg = e.detail.data[0]; // 提取消息数据 // 根据action类型处理不同操作 if (msg.action === "startFingerprintAuth") { this.handleFingerprintAuth(msg.userId, msg.challenge); // 处理指纹认证 } if (msg.action === "startCameraScan") { this.scanCode(); // 处理扫码操作 } },

完整代码

<!-- 定义模板 --> <template> <!-- 根视图 --> <view> <!-- WebView组件 <web-view ref="myWebview" // 组件引用标识 :webview-styles="webviewStyles" // 绑定WebView样式对象 :src="webviewSrc" // 绑定WebView加载的URL @message="fromWebviewMessage" // 监听H5发送的消息事件 /> --> <web-view ref="myWebview" :webview-styles="webviewStyles" :src="webviewSrc" @message="fromWebviewMessage" /> </view> </template> <script> // 导出Vue组件 export default { data() { return { // WebView加载的URL,添加时间戳防止缓存 webviewSrc: `http://x.x.x.x:xxxx?t=${Date.now()}`, // 当前WebView对象 currentWebview: null, // WebView样式配置 webviewStyles: { progress: { // 进度条样式 color: "#aaaaff", }, top: "45px", // 顶部位置 bottom: "0", // 底部位置 }, }; }, mounted() { // DOM更新后执行 this.$nextTick(() => { // 获取WebView对象 this.currentWebview = this.$scope.$getAppWebview().children()[0]; // 向H5发送初始化消息,方便H5端判断当前访问形式 this.sendMessageToH5({ type: "isAPP", }); }); // 添加返回按钮拦截器 uni.addInterceptor("navigateBack", { invoke: this.onBackPress, // 拦截返回按钮事件 }); // 获取设备IMEI this.getDeviceIMEI().then((res) => { console.log("设备IMEI", res); }); }, beforeDestroy() { // 组件销毁前移除拦截器 uni.removeInterceptor("navigateBack"); }, methods: { // 获取设备IMEI号 getDeviceIMEI() { return new Promise((resolve, reject) => { // 获取系统信息 let sysInfo = uni.getSystemInfoSync(); // 判断是否为 Android 且版本 >= 10 if ( sysInfo.osName.toLowerCase() === "android" && sysInfo.osVersion >= 10 ) { // 获取 OAID (Android 10+的替代标识) plus.device.getOAID({ success: ({ oaid }) => resolve({ imei: oaid, // 成功返回OAID }), fail: () => resolve({ imei: sysInfo.deviceId, // 失败返回设备ID }), }); } else { // Android 10 以下,申请权限后获取 IMEI // 请求READ_PHONE_STATE权限 this.requestPermissions(["android.permission.READ_PHONE_STATE"]) .then(() => { // 获取设备信息 plus.device.getInfo({ success: ({ imei }) => resolve({ imei, // 成功返回IMEI }), fail: () => resolve({ imei: sysInfo.deviceId, // 失败返回设备ID }), }); }) .catch(() => resolve({ imei: sysInfo.deviceId, // 权限拒绝返回设备ID }) ); } }); }, // 请求指定权限 requestPermissions(permissions) { return new Promise((resolve, reject) => { // 调用原生权限请求 plus.android.requestPermissions( permissions, // 权限请求成功回调 function (e) { if (e.granted.length > 0) { resolve(); // 权限授予成功 } else { reject({ // 权限被拒绝 message: "权限被拒绝", details: e, }); } }, // 权限请求失败回调 function (error) { reject(error); // 请求过程出错 } ); }); }, // 处理来自H5的消息 fromWebviewMessage(e) { console.log("收到 webview 页面发回的消息", e.detail.data); const msg = e.detail.data[0]; // 提取消息数据 // 根据action类型处理不同操作 if (msg.action === "startFingerprintAuth") { this.handleFingerprintAuth(msg.userId, msg.challenge); // 处理指纹认证 } if (msg.action === "startCameraScan") { this.scanCode(); // 处理扫码操作 } }, // 发送消息给H5 sendMessageToH5(data) { if (!this.currentWebview) return; // 确保WebView存在 const jsonString = JSON.stringify(data); // 序列化数据 const safeString = encodeURIComponent(jsonString); // URL编码 // 通过evalJS调用H5全局方法 this.currentWebview.evalJS( `getAppMsg(decodeURIComponent('${safeString}'))` ); console.log("已发送", jsonString); // 日志记录 }, // 调用相机扫码 scanCode() { uni.scanCode({ success: (res) => { if (res.result) { // 显示扫码结果 uni.showToast({ title: res.result, icon: "none", }); // 构造扫码结果消息 const scanData = { type: "scanResult", data: res.result, }; // 发送给H5 this.sendMessageToH5(scanData); } else { // 扫码失败提示 uni.showToast({ title: "扫描失败", icon: "none", }); } }, fail: (err) => { // 调用相机失败提示 uni.showToast({ title: "调用相机失败", icon: "none", }); }, }); }, // 处理指纹认证流程 async handleFingerprintAuth(userId, challenge) { try { // 1. 检查是否支持指纹认证 const supportRes = await this.checkSupport(); if (!supportRes.supported) { this.sendAuthResult(userId, challenge, false, "设备不支持指纹认证"); return; } // 2. 检查是否已录入指纹 const enrolledRes = await this.checkEnrolled(); if (!enrolledRes.enrolled) { this.sendAuthResult(userId, challenge, false, "设备未录入指纹"); return; } // 3. 开始指纹认证 const authRes = await this.startFingerprintAuth(challenge); console.log("指纹认证信息:",authRes.resultJSON); // 发送认证结果 this.sendAuthResult( userId, challenge, authRes.success, authRes.success ? "指纹认证成功" : "指纹认证失败", authRes.resultJSON ); } catch (error) { // 异常处理 console.error("指纹认证流程异常:", error); this.sendAuthResult( userId, challenge, false, error.message || "指纹认证异常" ); } }, // 封装检查支持指纹的方法 checkSupport() { return new Promise((resolve) => { uni.checkIsSupportSoterAuthentication({ success: (res) => { resolve({ supported: true }); // 支持指纹 }, fail: () => resolve({ supported: false }), // 不支持指纹 }); }); }, // 封装检查是否录入指纹的方法 checkEnrolled() { return new Promise((resolve) => { uni.checkIsSoterEnrolledInDevice({ checkAuthMode: "fingerPrint", // 检查指纹模式 success: (res) => resolve({ enrolled: res.isEnrolled }), // 已录入 fail: () => resolve({ enrolled: false }), // 未录入 }); }); }, // 封装开始指纹认证的方法 startFingerprintAuth(challenge) { return new Promise((resolve) => { uni.startSoterAuthentication({ requestAuthModes: ["fingerPrint"], // 认证模式为指纹 challenge: challenge, // 挑战码 authContent: "请验证指纹", // 用户提示 success: (res) => // 认证成功 resolve({ success: res.errCode === 0, // 判断成功标志 resultJSON: res, // 完整结果 }), fail: () => // 认证失败 resolve({ success: false }), }); }); }, // 发送认证结果给H5 sendAuthResult(userId, challenge, success, message, resultJSON = null) { const resultData = { type: "fingerprintAuthResult", // 消息类型 data: { userId: userId, challenge: challenge, success: success, // 认证结果 message: message, // 结果描述 resultJSON: resultJSON, // 原始认证数据 timestamp: new Date().getTime(), // 时间戳 }, }; // 发送消息 this.sendMessageToH5(resultData); }, // 返回按钮处理 onBackPress() { // 如果WebView可以返回则执行返回 if ( this.currentWebview && typeof this.currentWebview.back === "function" ) { this.currentWebview.back(); return true; // 拦截默认返回行为 } return false; // 不拦截 }, }, }; </script> <!-- 样式部分 --> <style> /* WebView全屏样式 */ web-view { width: 100%; height: 100%; } </style>

H5 页面

准备工作

在static目录下创建webview.1.5.4.js文件(下面用webUni.webview.1.5.4命名),并在main.js中全局引入

webview.1.5.4.js

! function(e, n) { "object" == typeof exports && "undefined" != typeof module ? module.exports = n() : "function" == typeof define && define.amd ? define(n) : (e = e || self).uni = n() }(this, (function() { "use strict"; try { var e = {}; Object.defineProperty(e, "passive", { get: function() { !0 } }), window.addEventListener("test-passive", null, e) } catch (e) {} var n = Object.prototype.hasOwnProperty; function i(e, i) { return n.call(e, i) } var t = []; function r() { return window.__dcloud_weex_postMessage || window.__dcloud_weex_ } var o = function(e, n) { var i = { options: { timestamp: +new Date }, name: e, arg: n }; if (r()) { if ("postMessage" === e) { var o = { data: [n] }; return window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessage(o) : window .__dcloud_weex_.postMessage(JSON.stringify(o)) } var a = { type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } }; window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessageToService(a) : window .__dcloud_weex_.postMessageToService(JSON.stringify(a)) } if (!window.plus) return window.parent.postMessage({ type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" }, "*"); if (0 === t.length) { var d = plus.webview.currentWebview(); if (!d) throw new Error("plus.webview.currentWebview() is undefined"); var s = d.parent(),; w = s ? s.id : d.id, t.push(w) } if (plus.webview.getWebviewById("__uniapp__service")) plus.webview.postMessageToUniNView({ type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } }, "__uniapp__service"); else { var u = JSON.stringify(i); plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat( "WEB_INVOKE_APPSERVICE", '",').concat(u, ",").concat(JSON.stringify(t), ");")) } }, a = { navigateTo: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("navigateTo", { url: encodeURI(n) }) }, navigateBack: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.delta; o("navigateBack", { delta: parseInt(n) || 1 }) }, switchTab: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("switchTab", { url: encodeURI(n) }) }, reLaunch: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("reLaunch", { url: encodeURI(n) }) }, redirectTo: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("redirectTo", { url: encodeURI(n) }) }, getEnv: function(e) { r() ? e({ nvue: !0 }) : window.plus ? e({ plus: !0 }) : e({ h5: !0 }) }, postMessage: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; o("postMessage", e.data || {}) } }, d = /uni-app/i.test(navigator.userAgent), s = /Html5Plus/i.test(navigator.userAgent), w = /complete|loaded|interactive/; var u = window.my && navigator.userAgent.indexOf(["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"].reverse().join("")) > -1; var g = window.swan && window.swan.webView && /swan/i.test(navigator.userAgent); var v = window.qq && window.qq.miniProgram && /QQ/i.test(navigator.userAgent) && /miniProgram/i.test( navigator.userAgent); var c = window.tt && window.tt.miniProgram && /toutiaomicroapp/i.test(navigator.userAgent); var m = window.wx && window.wx.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i .test(navigator.userAgent); var p = window.qa && /quickapp/i.test(navigator.userAgent); var f = window.ks && window.ks.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i .test(navigator.userAgent); var l = window.tt && window.tt.miniProgram && /Lark|Feishu/i.test(navigator.userAgent); var _ = window.jd && window.jd.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i .test(navigator.userAgent); var E = window.xhs && window.xhs.miniProgram && /xhsminiapp/i.test(navigator.userAgent); for (var h, P = function() { window.UniAppJSBridge = !0, document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady", { bubbles: !0, cancelable: !0 })) }, b = [function(e) { if (d || s) return window.__dcloud_weex_postMessage || window.__dcloud_weex_ ? document .addEventListener("DOMContentLoaded", e) : window.plus && w.test(document .readyState) ? setTimeout(e, 0) : document.addEventListener("plusready", e), a }, function(e) { if (m) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("WeixinJSBridgeReady", e), window.wx.miniProgram }, function(e) { if (v) return window.QQJSBridge && window.QQJSBridge.invoke ? setTimeout(e, 0) : document .addEventListener("QQJSBridgeReady", e), window.qq.miniProgram }, function(e) { if (u) { document.addEventListener("DOMContentLoaded", e); var n = window.my; return { navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv } } }, function(e) { if (g) return document.addEventListener("DOMContentLoaded", e), window.swan.webView }, function(e) { if (c) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram }, function(e) { if (p) { window.QaJSBridge && window.QaJSBridge.invoke ? setTimeout(e, 0) : document .addEventListener("QaJSBridgeReady", e); var n = window.qa; return { navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv } } }, function(e) { if (f) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("WeixinJSBridgeReady", e), window.ks.miniProgram }, function(e) { if (l) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram }, function(e) { if (_) return window.JDJSBridgeReady && window.JDJSBridgeReady.invoke ? setTimeout(e, 0) : document.addEventListener("JDJSBridgeReady", e), window.jd.miniProgram }, function(e) { if (E) return window.xhs.miniProgram }, function(e) { return document.addEventListener("DOMContentLoaded", e), a }], y = 0; y < b.length && !(h = b[y](P)); y++); h || (h = {}); var B = "undefined" != typeof uni ? uni : {}; if (!B.navigateTo) for (var S in h) i(h, S) && (B[S] = h[S]); return B.webView = h, B }));

H5 → App 消息格式

{ "action": "操作类型", "userId": "用户ID", // 指纹认证时使用 "challenge": "随机码" // 指纹认证时使用 } 
H5 调用指纹认证
 // H5 页面中,向APP发送调用指纹认证消息 confirmFingerprint() { this.$myUni.webView.postMessage({ data: { action: "startFingerprintAuth", userId: 123456, challenge: "1234567890", }, }); },
H5 调用扫码
// H5 页面中,调用手机相机扫码 scanCode() { this.$myUni.webView.postMessage({ data: { action: "startCameraScan", challenge: new Date().getDate().toString(),//用时间戳来生成的唯一challenge }, }); },

接收并处理App端发来的消息

在window对象上定义一个全局方法getAppMsg,用于接收从原生App发送过来的消息。 当原生App通过WebView向H5页面发送消息时,会调用window.getAppMsg方法,并传递一个字符串格式的消息。
mounted() { // 创建全局消息接收方法 window.getAppMsg = (msg) => { try { // 1. 解析消息数据 const AppMsgData = JSON.parse(msg); // 2. 处理扫码结果 if (AppMsgData.type === "scanResult") { console.log(AppMsgData.data); } if (AppMsgData.type === "fingerprintAuthResult") { if (AppMsgData.data.success) { console.log(AppMsgData.data.message); } else { console.error(AppMsgData.data.message); } } } catch (e) { // 3. 错误处理 console.error("JSON解析失败:", e); } }; },
Could not load content