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); } }; },

Read more

Java Web 在线考试系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】

Java Web 在线考试系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】

系统架构设计### 摘要 随着信息技术的快速发展,传统的线下考试模式逐渐暴露出效率低下、资源浪费和安全性不足等问题。在线考试系统因其高效、灵活和可扩展的特点,成为教育领域的重要研究方向。特别是在后疫情时代,远程学习和在线评估的需求激增,推动了在线考试系统的广泛应用。然而,现有的许多系统在性能、安全性和用户体验方面仍有不足,尤其是在高并发场景下的稳定性和数据安全性方面亟待优化。本研究旨在设计并实现一个基于SpringBoot2和Vue3的在线考试系统,通过现代化的技术栈解决上述问题,为教育机构提供高效、安全的在线考试解决方案。关键词:在线考试系统、SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0。 本研究采用前后端分离的架构,后端基于SpringBoot2框架,结合MyBatis-Plus实现高效的数据持久化操作,前端使用Vue3构建响应式用户界面。系统实现了用户管理、试题库管理、在线考试、自动阅卷和成绩分析等核心功能。通过JWT实现安全的用户认证与授权,Redis缓存技术提升系统响应速度,MySQL8.0确保数据的高效存储与查询。系统支持多种题型(如单选

前端实战:手把手教你接入腾讯云 ASR 实时语音识别(避坑指南)

前端实战:手把手教你接入腾讯云 ASR 实时语音识别(避坑指南)

在数字人交互、智能客服或语音助手的 Web 开发中,实时语音识别(ASR) 是最基础也是最核心的入口。市面上方案众多,今天我们基于一个真实的测试文件 test-asr.html,深入剖析如何在前端(H5/Web)直接接入腾讯云的一句话识别 SDK。 这篇文章不讲废话,只讲代码里的“魔鬼细节”和真实调试经验。 1. 为什么选择纯前端接入? 通常 ASR 接入有两种模式: 1. 后端代理:前端录音传给后端,后端调用腾讯云 API。安全,但延迟高。 2. 前端直连:浏览器直接录音并通过 WebSocket 直连腾讯云。速度最快,交互体验最好。 我们手中的 test-asr.html 采用的就是前端直连方案。这种方案最大的挑战在于:如何在前端安全且正确地生成鉴权签名,以及如何处理复杂的音频流事件。 2. 核心依赖与准备 代码中引入了两个关键文件: <

PyCharm激活码永久破解不可取,但GLM-4.6V-Flash-WEB完全开源免费

PyCharm激活码永久破解不可取,但GLM-4.6V-Flash-WEB完全开源免费 在智能应用日益普及的今天,开发者们正面临两个看似无关却本质相通的抉择:一边是功能强大但需要付费的专业开发工具,另一边是性能卓越但部署成本高昂的人工智能模型。许多人为了“节省成本”,选择在网络上寻找PyCharm专业版的“永久激活码”——这种做法不仅违反软件许可协议,还可能引入恶意代码、后门程序,甚至导致项目数据泄露。 更讽刺的是,就在人们为了一款IDE绞尽脑汁破解时,真正能改变生产力的技术反而被忽略了:一个完全开源、免费、高性能的多模态大模型 GLM-4.6V-Flash-WEB 已经悄然上线,并且支持本地一键部署。 这不仅仅是一个技术产品的发布,更是一种开发哲学的回归——不靠破解获取权限,而是通过开放赢得自由。 为什么我们需要真正的“可落地”多模态模型? 当前,视觉语言模型(VLM)已在内容审核、图像问答、自动化文档处理等领域展现出巨大潜力。然而,现实中的落地困境依然突出: * 闭源API贵得离谱:像GPT-4V这样的模型,一次图像推理调用动辄几毛到一块钱,企业高频使用月账单轻松突

前端状态管理方案选型指南:从 Redux 到 Zustand 再到 Pinia

深度对比主流状态管理方案,帮你找到最适合项目的那把"钥匙" 📋 前言 在前端开发中,状态管理一直是绕不开的核心话题。从早期的全局变量,到 Redux 的单向数据流,再到如今 Zustand、Pinia 等轻量级方案的崛起,状态管理工具经历了多次迭代。 但问题来了:2026 年了,到底该选哪个? 本文将从 学习成本、性能表现、生态支持、适用场景 四个维度,深度剖析当前主流状态管理方案,帮你做出最适合的选择。 🎯 一、主流状态管理方案概览 方案框架体积学习曲线适用场景Redux ToolkitReact11KB+⭐⭐⭐大型复杂应用ZustandReact1.1KB⭐⭐中小型应用、快速开发Jotai / RecoilReact3-7KB⭐⭐⭐原子化状态管理PiniaVue1.5KB⭐⭐Vue3 官方推荐VuexVue2KB⭐⭐⭐Vue2 历史项目MobXReact/Vue16KB+⭐⭐响应式编程爱好者 🔴 二、Redux Toolkit:企业级应用的首选