uni-app 内嵌 H5 应用并实现双端通信

举报
SHQ5785 发表于 2023/10/10 08:46:04 2023/10/10
【摘要】 一、前言uni-app应用开发过程中,需要应用内嵌H5页面,H5实现分为html、Vue两种。现如今,各大APP平台都拥有属于自己的小程序体系,各种各样的应用都可直接内嵌在APP中实现一站式体验。使用uniapp开发的APP如何实现这样的功能呢?答案就是内嵌web-view。自 HBuilderX v1.1.0 起,在 5+App 平台下 web-view 支持加载应用内的 HTML 资源...

一、前言

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 目录。

├─common      
├─components        
├─hybrid  
│  └─html  
│          test.html        
├─pages              
├─staticApp.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页面调用:

<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页面调用:

<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 内容如下:

<!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中如下方式传递参数:

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

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

<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中通过如下方式传递参数:

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

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

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:

import * as uni from './js_sdk/uni.webview.1.5.4.js'  

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

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

this.myUni.webView.postMessage({  
	data: {  
		action: 'czcp'
	},  
}); 

注意⚠️:

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

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

五、双端数据传递

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

5.1 路由传参

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

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

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

onLoad(options) {
	console.log(options)
	this.resourceId = options.id;
	this.getInfo();
},

5.1.1 vue -> html 参数传递

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

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

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

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

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

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。

<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中:

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

对应的tab下面的内容:

<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内容并显示:

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点击事件:

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

5.2 插件实现

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

APP中调用:

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

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

<!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页面中:

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端设置:

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至手机屏幕安全距离之上。

内嵌应用中设置:

// 内嵌应用中,配置一个全局变量管理顶部样式,比如可在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实现时,需要避免顶部操作栏出现遮挡问题。

// #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

七、拓展阅读

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。