跨平台应用开发进阶(十七) :uni-app 内嵌 H5 应用并实现双端通信

一、前言

uni-app应用开发过程中,需要应用内嵌H5页面,H5实现分为htmlVue两种。

现如今,各大APP平台都拥有属于自己的小程序体系,各种各样的应用都可直接内嵌在APP中实现一站式体验。使用uniapp开发的APP如何实现这样的功能呢?答案就是内嵌web-view

自 HBuilderX v1.1.0 起,在 5+App 平台下 web-view 支持加载应用内的 HTML 资源。 本地的 HTML 资源,必须存放在规定的目录下,即 uni-app 项目->hybrid->html 目录。

java 复制代码
├─common      
├─components        
├─hybrid  
│  └─html  
│          test.html        
├─pages              
├─static  
│  App.vue  
│  main.js  
│  manifest.json  
│  pages.json

html 文件相关的 cssjs 等本地资源,同样放在这个 hybrid->html 目录下。

这个hybrid目录不会被编译器编译,所以这里的不能放vue文件,而其他目录也不能放本地HTML文件。

未来hybrid目录还会支持其他语言在uni-app的中的混合使用。

二、注意事项

📢 注意事项: APP中有vue页面及nvue页面,两种页面均可内嵌web-view,但两种页面的表现不一:

  • 每个vue页面,其实都是一个webview,而vue页面里的web-view组件,其实是webview里的一个子webview。这个子webviewappend到父webview上。
  • vue页面会自动铺满整个页面,接收web-view页面通信使用的是@message
  • nvue页面则需要指定页面宽高,接收web-view页面通信使用的是@onPostMessage
  • app-vueweb-view组件不支持自定义样式,而v-show的本质是改变组件的样式。即组件支持v-if而不是支持v-show
  • <web-view> 组件在 vue页面默认铺满全屏并且层级高于前端组件。App端想调节大小或在其上覆盖内容需使用plus规范或通过subNvue实现
  • H5端的web-view其实是被转为iframe运行,使用的是当前的浏览器;
  • App端,iOS,是分为UIWebviewWKWebview的,2.2.5+起默认为WKWebview
  • nvue web-view 必须指定样式宽高;
  • App 网页向应用 postMessage 为实时消息;
  • app-nvue web-view 默认没有大小,可以通过样式设置大小,如果想充满整个窗口,设置 flex: 1 即可,标题栏不会自动显示 web-view 页面中的 title。如果想充满整个窗口且想要显示标题,推荐使用 vue 页面的 web-view(默认充满屏幕不可控制大小), 想自定义 web-view 大小使用 nvue web-view

注意⚠️:使用uni-app发布为H5项目时,若存在页面嵌套,可使用web-view,但是消息通信不支持通过@message实现,可从官方文档的平台差异说明处获悉。可通过 window.postMessage 实现双端通信。

有关 window.postMessage ,详参博文:

APP通知web-view页面,无论是vue页面还是nvue页面,只有evalJS方法,但调用姿势不一致。

三、uni-app && H5(html)双端通信

vue页面调用:

javascript 复制代码
<template>
	<view class="center">
		<!-- 视频直播 -->
		<view class="vedioLive-view">
			<!-- 网页链接后添加时间戳解决webview缓存问题 -->
			<web-view :src="webviewSrc" :webview-styles="webviewStyles" @message='message'></web-view>
		</view>
	</view>
</template>

<script>
	import { BASEURL } from "@/common/h5address.js"
	var wv;//计划创建的webview
	export default {
		data() {
			return {
				webviewSrc: '',
				webviewStyles:{
					progress: {
						color: '#CCA152'
					}
				}
			}
		},
		onReady() {

		},
		onLoad() {
			this.webviewSrc = BASEURL + 'H5/vedioLive/hybrid/html/vedioLive.html' + '?timestamp=' + new Date().getTime();
		},
		methods: {
			message(arg) {
				console.log('-------------------message-------------------:', JSON.stringify(arg))
				this.sendMsgToWebview();
			},
			// uni-app向内嵌网页发消息
			sendMsgToWebview() {
				const
				  _funName = 'msgFromUniapp',
				  _data = {
					mode: 1,
					...
				  };
				 //此对象相当于html5plus里的plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效
				const currentWebview = this.$scope.$getAppWebview().children()[0];
				console.log('------------------currentWebview------------------:', currentWebview)
				currentWebview.evalJS(`${_funName}(${JSON.stringify(_data)})`);
				console.log('------------------currentWebview2------------------:', currentWebview)
			}
		}
	}
</script>

nvue页面调用:

javascript 复制代码
<template>
	<web-view ref="webview" :src="url" @message="message"></web-view>
</template>
...
<script>
export default {
	xxx,
	onReady() {
		this.currentWebview = this.$refs.webview;
		this.currentWebview.evalJS('xxx');
	}
}
</script>

html 内容如下:

html 复制代码
<!DOCTYPE html>
<html style="height: 100%" lang="zh-CN">
  <head>
    <title>双向通信Demo</title>
	<meta charset="utf-8" content="text/html">
    <meta name="viewport" http-equiv="Content-Type" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover">		
    </head>
  <body style='
        font-size: 10px; margin: 0; background-color: #080B12;
        height: 100%; overflow: hidden;'>
	<script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js"></script>
  </body>
  <script>
	// 等待sdk加载,待触发 `UniAppJSBridgeReady` 事件后,即可调用 uni 的 API。
	document.addEventListener('UniAppJSBridgeReady', function() {
		console.log('-----------UniAppJSBridgeReady------------')
		// 向应用发送消息
		uni.postMessage({
			data: {
				order: 'playRecord'
			}
		});
		uni.getEnv(function(res) {
			console.log('当前环境:' + JSON.stringify(res));
		});
	});
	window.msgFromUniapp= function(arg) {
		console.log('<<<<<<<<<<<<<arg>>>>>>>>>>>:', arg);
		console.log('<<<<<<<<<<<<<JSON.stringify(arg)>>>>>>>>>>>:', JSON.stringify(arg));
	}
  </script>
</html>

或者uni-app中如下方式传递参数:

javascript 复制代码
onLoad(){
  const data = {
    "name":"张三",
    "age":18
  }
  plus.storage.setItem('data',""+JSON.stringify(data));
},

h5通过如下方式获取参数:

javascript 复制代码
<script>
  // 接收webView 传递的信息
  const time = setInterval(()=>{
    if(window.plus){
      clearInterval(time)
      const data = JSON.parse(plus.storage.getItem('data')); 
      console.log(data.name,data.age);
    }
  },10)
</script>

或者在uni-app中通过如下方式传递参数:

javascript 复制代码
data() {
	return {
		url:'/hybrid/html/local.html?data='
	};
},
onLoad(data) {
  //这里对要传入到webview中的参数进行encodeURIComponent编码否则中文乱码
   this.url+=encodeURIComponent(data.data)
},

h5中通过如下方式接收参数:

javascript 复制代码
console.log(getQuery('data'));  //获取 uni-app 传来的值
//取url中的参数值
function getQuery(name) {
    // 正则:[找寻'&' + 'url参数名字' = '值' + '&']('&'可以不存在)
    let reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
    let r = window.location.search.substr(1).match(reg);
    console.log(r);
    if(r != null) {
        // 对参数值进行解码
        return decodeURIComponent(r[2]);
    }
    return null;
}

以上三种方式便实现了uni-app通过webview形式配合H5(html)引用uni.webview.1.5.4.js SDK实现了双端通信。

注意⚠️:nvue页面中使用的web-view页面是无法调用plus API的,vue页面是可以控制外部web-view页面是否可用plus API,其他事项具体参考web-view | uni-app官网

四、uni-app && H5(Vue)双端通信

html形式的H5页面中通过动态引入uni.webview.1.5.4.js SDK实现了与uni-app的双端通信。在vue实现的H5项目中,如何去引入相应的SDK呢?答案是通过离线SDK引入uni.webview.1.5.4.js 的方式实现双端通信。

下载好uni.webview.1.5.4.js SDK后,首先在H5项目的main.js文件中引入该SDK:

javascript 复制代码
import * as uni from './js_sdk/uni.webview.1.5.4.js'  

document.addEventListener("UniAppJSBridgeReady", function() {  
    Vue.prototype.myUni = uni  
});

然后,在需要应用的地方使用即可:

javascript 复制代码
this.myUni.webView.postMessage({  
	data: {  
		action: 'czcp'
	},  
}); 

注意⚠️:

  • HBuilderX 1.0.0 版本开始,uni-app 支持在 web-view 中调用 uni-app 的 API。例如:

    javascript 复制代码
    uni.webView.navigateBack({  
    	delta: 1,  
    })
  • 每次执行 postMessage 后,传递的消息会以数组的形式存放。因此,在 web-viewmessage 事件回调中,接收到的 event.detail.data 的值是一个数组。

五、双端数据传递

APP中调用evalJS方法,调用到的是内嵌应用index.html中定义的方法,怎么将APP传递过来的信息传递到内嵌应用的vue页面去呢?

5.1 路由传参

一种方式是可以通过App通过路由传参的形式传递参数至H5(Vue)端,

javascript 复制代码
uni.navigateTo({
	url: '/pages/queryNewsDetailMessage?detailData=' +
		encodeURIComponent(JSON.stringify(e))
});

H5(Vue)端在onLoad中获取接收到的参数。

javascript 复制代码
onLoad(options) {
	console.log(options)
	this.resourceId = options.id;
	this.getInfo();
},

5.1.1 vue -> html 参数传递

通过vue页面传参值html页面,可直接将参数附于url中,例如如下形式:

javascript 复制代码
this.shareHref = 'https://blog.csdn.net/sunhuaqiang1/category_5641389.html?urlData=' + encodeURIComponent(JSON.stringify(this.urlData));

其中,urlData为json格式的参数体。

注意⚠️:url有长度限制,太长的字符串会传递失败,可改用窗体通信全局变量,另外参数中出现空格等特殊字符时需要使用encodeURIComponent对参数进行编码。

html端接收到参数时,进行如下解析:

javascript 复制代码
const params = JSON.parse(decodeURIComponent(window.location.href.split('?')[1]).split('=')[1].split('&')[0]);

解析后得到Json对象,便可以获取所需要的对象属性值了。

5.1.1 window.location.href 与 window.locaiton.hash

locationjavascript中管理地址栏的内置对象。

  • window.location.href

    window.location.href 表示重定向 ,获得和使用的是完整的url。比如window.location.href="https://blog.csdn.net/sunhuaqiang1"表示的是重新定向,页面跳转到新的页面。也可以通过window.location.href得到a标签的完整的href,比如<a href="#book">。如果使用href,那么可以得到完整的链接(url)。

  • window.location.hash

    得到的是锚链接 ,设置或获取 href 属性中在"#"后面的分段。相比于href,通过window.location.hash并不会跳转到新的链接,只会在当前链接里面改变锚链。并且如果有<a href="#book">,通过window.location.hash得不到完整的链接(URL),仅仅得到#book

window.location.hash这个属性能够对URL中的#参数进行修改,基于这个原理,可实现在不重载页面的前提下记录每一天新的访问记录。

通过下面的测试会发现区别,将代码放到HTML中,然后用浏览器打开:

点击"超链接",你会发现在地址栏URL发生了变化,URL后面多了一个"#foo"。

点击"href",你会发现弹出的是地址栏的URL地址。

点击"hash",你会发现弹出的是#foo。

html 复制代码
<a href="#foo">超链接</a>
<br />
<a href="javascript:alert(window.location.href)">href</a>
<a href="javascript:alert(window.location.hash)">hash</a>

通过window.location.hash=hash这个语句来调整地址栏的地址,使得浏览器里边的"前进"、"后退"按钮能正常使用(实质上欺骗了浏览器)。然后再根据hash值的不同来显示不同的面板(用户可以收藏对应的面板了)

比如常见的就是点击切换tab,显示不同tab对应的内容,页面刷新再次进来自动定位到上次停留的tab对应的内容。

vuetemplate中:

html 复制代码
<ul :class="['bar-box',tableBg]"  >
      <li @click="changeBG('1')"></li>
      <li @click="changeBG('2')"></li>
      <li @click="changeBG('3')"></li>
</ul>

对应的tab下面的内容:

html 复制代码
<div class="tab1-box"  v-if="current===1"></div>
<div class="tab1-box"  v-if="current===2"></div>
<div class="tab1-box"  v-if="current===3"></div>

mounted的时候判断一下上次浏览的tab内容并显示:

javascript 复制代码
if(window.location.href.indexOf("#tab") > -1 ) {
 	this.current = Number(window.location.href.split("#tab")[1]);
 	window.location.hash = "tab" + this.current
} else {
	this.current = 1;
 	window.location.hash = "tab" + this.current;
}

methods对应的btn点击事件:

javascript 复制代码
// tab切换
changeBG(x) {
 
    this.current = Number(x);
 
    window.location.hash = "tab" + this.current;
 
    this.tableBg = "bar-box" + this.current;
 
},

5.2 插件实现

还有就是可以通过插件实现,例如插件mitt

APP中调用:

javascript 复制代码
export default {
	onReady() {
		this.currentWebview = this.$mp.page.$getAppWebview();
		this.currentWebview.children()[0].evalJS("showTip('提示测试')");
	}
}

内嵌应用的index.html中,引用下载到本地的mitt.umd.js

html 复制代码
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="renderer" content="webkit" />
		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
		<!-- uni 的 SDK,必须引用。 -->
		<script src="<%= BASE_URL %>libs/uni-app/uni.app.webview.1.5.2.js"></script>
		<!--html页面与vue页面通讯js-->
		<script src="<%= BASE_URL %>libs/mitt/mitt.umd.js"></script>
		<title>xxx</title>
	</head>

	<body>
		<noscript>
			<strong>当前浏览器版本太低,请升级浏览器或使用其他浏览器</strong>
		</noscript>
		<div id="app"></div>
		<!-- built files will be auto injected -->
		<script>
			window.bus = window.mitt();
			const showTip = (tip) => {
				window.bus.emit('showTip', tip);
			};
		</script>
	</body>
</html>

内嵌应用vue页面中:

javascript 复制代码
export default {
	activated() {
		// 挂载
		window.bus.on('showTip', (tip) => {
			alert(tip);
		});
	}
	deactivated() {
		// 离开时记得销毁
		window.bus.off('showTip');
	}
}

六、适配问题剖析

Android、iOS平台顶部导航栏的表现不一:因内嵌web-view应用需要全屏显示,标题栏也交由内嵌应用自定义,故在pages.json中定义页面的时候,须将页面的titleNView设置为false。但即便如此,其表现仍然不一致,表现为Android端的页面起始位置是手机屏幕的最顶部(含状态栏在内 ),iOS端则是从状态栏之下开始渲染页面,为了抹平差异,可使用webviewsetStyle统一样式。

APP端设置:

javascript 复制代码
export default {
	onReady() {
		this.currentWebview = this.$mp.page.$getAppWebview();
		// 提前计算好屏幕比率(screenRatio),底部安全距离(safeAreaInsetsBtm),区分平台(platform)
		let { screenRatio, safeAreaInsetsBtm, platform } = this.$store.state;
		// 由于iOS端会自动定位top到状态栏底下,故这里需要判断手机系统
		// 因调节Android端的top值能看出头部变化,故转换思路调ios端的top值
		let top = 0;
		if (platform === 'ios') {
			let info = uni.getSystemInfoSync();
			top = -info.statusBarHeight;
		}
		let bottom = safeAreaInsetsBtm / screenRatio;
		this.currentWebview.children()[0].setStyle({ top, bottom: parseInt(bottom) });
	}
}

统一两端的top至手机屏幕最顶部,bottom至手机屏幕安全距离之上。

内嵌应用中设置:

javascript 复制代码
// 内嵌应用中,配置一个全局变量管理顶部样式,比如可在vuex的action中写个setPage方法
const actions = {
	setPage(context) {
		let statusbarHeight = plus.navigator.getStatusbarHeight();
		let pageStyle = { paddingTop: `calc(0.2rem + ${statusbarHeight}px)` };
		context.commit('pageStyle', pageStyle);
	}
};

当顶部操作栏为uni-app实现,内容区域采用web-view实现时,需要避免顶部操作栏出现遮挡问题。

javascript 复制代码
// #ifdef APP-PLUS
var height = 0; //定义动态的高度变量,如高度为定值,可以直接写
uni.getSystemInfo({
	//成功获取的回调函数,返回值为系统信息
	success: (sysinfo) => {
		height = sysinfo.windowHeight; //自行修改,自己需要的高度 此处如底部有其他内容,可以直接---(-50)这种
	},
	complete: () => {}
});
var currentWebview = this.$scope.$getAppWebview(); //获取当前web-view
setTimeout(function() {
	var wv = currentWebview.children()[0];
	wv.setStyle({ //设置web-view距离顶部的距离以及自己的高度,单位为px
		top:40 , //此处是距离顶部的高度,应该是你页面的头部
		height:  height , //webview的高度
		scalable: false, //webview的页面是否可以缩放,双指放大缩小,
	})
}, 500); //如页面初始化调用需要写延迟
// #endif

七、拓展阅读

相关推荐
摸鱼的春哥13 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
念念不忘 必有回响17 分钟前
viepress:vue组件展示和源码功能
前端·javascript·vue.js
C澒22 分钟前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅24 分钟前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘25 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭2 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端