笔者在使用腾讯云直播推流和拉流的 web sdk 时,项目需要同时对 APP 和 H5 端进行适配,腾讯云的 Web sdk 使用的是原生 HTML 的 video 标签以及 JS 对象完成推流控制的。但是,这在 APP 上并不支持,因此转而考虑使用 webview 引入 HTML 文件的方式进行适配。
前置问题须知
在官方文档中提到,vue 使用实际上也是一个 webview,所以在大部分情况下,单纯使用 vue 文件就可以兼容大部分的 APP 界面。
在 App 端,如果使用 vue 页面,则使用 webview 渲染;如果使用 nvue 页面(native vue 的缩写),则使用原生渲染。一个 App 中可以同时使用两种页面
但是,如果 vue 使用了内置的 web-view 组件,就无法调整这个组件的宽高,因为它是默认填充整个布局的,这相当于在 webview 中覆盖了一个新的 webview。
web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面,而在 nvue 中 可以手动调整宽高
而且 web-view 在界面的层级很高,这也会导致类似于弹窗的布局无法覆盖在 web-view 的上层的问题, 相关解决方式可参考 web-view组件的层级问题解决。
所以,为了让布局和按钮位于 web-view 的上层,我们就需要通过 nvue 文件对 web-view 组件进行样式调整。
nvue 和 web-view 通信
为了更好的通过 nvue 对 web-view 进行控制,我们就需要在 nvue 和 webview 之间通过通信来传递数据。
nvue -> webview 通信(父传子)
nvue 与 webview 通信的方式主要通过两种方法
- URL 的查询参数传递
- evalJS 调用 webview 内部函数
URL 查询参数传递
这个方法只需要直接在 web-view 的 src 参数中拼接查询参数,但是当 URL 变动会时会刷新 web-view 界面,导致状态丢失。同时,也无法及时传递消息,nvue 中无法判断传递何时完成。
<template>
<!-- #ifndef H5 -->
<list class="webview-page">
<cell class="webview-cell">
<web-view
:src="`/static/html/webview.html?data=$${encodeURIComponent(
JSON.stringify(curData)
)}&time=${new Date().getTime()}`"
/>
</cell>
</list>
<!-- #endif -->
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
type Resp = {
data: string;
};
const curData = ref<Resp>(null);
onMounted(() => {
curData.value = { data: "hello world" };
});
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webview demo</title>
</head>
<body>
<div>webview demo</div>
</body>
<script>
const urlParams = new URLSearchParams(window.location.search);
const data = urlParams.get("data");
const decodedData = decodeURIComponent(data);
const curData = JSON.parse(decodedData);
alert(curData?.data);
</script>
</html>
evalJS 调用 webview 内部函数
这个方法主要通过 ref 获取 web-view 组件引用的 webview,然后执行 webview 对象的 evalJS 方法,调用内部函数传入数据。
<template>
<!-- #ifndef H5 -->
<list class="webview-page">
<cell class="webview-cell">
<web-view src="/static/html/webview.html" ref="webview" />
</cell>
</list>
<!-- #endif -->
<button class="eval-btn" @click="evalJs">evalJs</button>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const webview = ref<any>(null);
const curData = ref<Resp>(null);
type Resp = {
data: string;
};
onMounted(() => {
curData.value = { data: "hello world" };
});
function evalJS() {
const data = JSON.stringify(curData.value);
//模板字符串中的函数括号内需要保留引号,不然会导致函数调用失败
webview.value.evalJS(`callback('${data}')`);
}
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webview demo</title>
</head>
<body>
<div>webview demo</div>
</body>
<script>
function callback(data){
const curData = JSON.parse(data)
alert(curData?.data);
}
</script>
</html>
web-view -> nvue 通信(子传父)
web-view 通信需要先在界面中引入 uni.webview.js 这个 sdk,然后执行 uni.postMessage 完成消息发送,回传数据给布局函数。然后在 nvue 中为 web-view 添加 @onPostMessage 事件。
<template>
<!-- #ifndef H5 -->
<list class="webview-page">
<cell class="webview-cell">
<web-view
src="/static/html/webview.html"
ref="webview"
@onPostMessage="getMessage"
/>
</cell>
</list>
<!-- #endif -->
<button class="eval-btn" @click="evalJs">evalJs</button>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const webview = ref<any>(null);
const curData = ref<Resp>(null);
type Resp = {
data: string;
};
onMounted(() => {
curData.value = { data: "hello world" };
});
function evalJS() {
const data = JSON.stringify(curData.value);
//模板字符串中的函数括号内需要保留引号,不然会导致函数调用失败
webview.value.evalJS(`callback('${data}')`);
}
function getMessage(data: any) {
const respData = JSON.parse(data);
console.log("来自子页面的数据=", respData);
}
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webview demo</title>
</head>
<body>
<div id="app">webview demo</div>
</body>
<!-- 建议后续把 uni.webview.1.5.6.js 下载到自己的服务器 -->
<script
type="text/javascript"
src="https://gitcode.net/dcloud/uni-app/-/raw/dev/dist/uni.webview.1.5.6.js"
></script>
<script>
const respData = { data: "success" };
//当 webview SDK 载入成功, 且 uniapp 和 webview 之间的桥成功建立时执行
document.addEventListener("UniAppJSBridgeReady", function () {
uni.postMessage({
data: {
action: JSON.stringify(respData),
},
});
});
</script>
</html>
结语
最后,我们只要结合 uni.postMessage 方法和 evalJS 方法,就能够实现相互通信了。如果需要进一步判断传递是否建立,那么可以让一方先发送一条状态消息,另一方接受后进行通知,后续只需要执行回传就能判断基本状态了。
参考阅读: