uni-app Vue3 中使用 nvue 嵌套 web-view 的相互通信

524 阅读3分钟

笔者在使用腾讯云直播推流和拉流的 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 方法,就能够实现相互通信了。如果需要进一步判断传递是否建立,那么可以让一方先发送一条状态消息,另一方接受后进行通知,后续只需要执行回传就能判断基本状态了。

参考阅读: