Skip to content

<web-view> 组件接入 ​

接入方式 ​

在 uni-app 页面模板中声明 <web-view>,由组件创建、挂载并管理 WebView。手动创建容器的方案保留在 HTML5+ WebView 接入。两篇使用相同的 H5 业务入口和事件格式,区别在于容器创建方式和消息传输层。

本文面向 Android / iOS App-vue 的 .vue 页面:

方向H5 侧App 侧
H5 调 ApphtmlToFlutterEvent.postMessage,通过注入脚本适配为 uni.webView.postMessage组件的 @message 接收,再通过子 WebView 的 evalJS 回传结果
App 调 H5flutterToHtmlEvent 接收对象并返回结果子 WebView 的 evalJS 调用,再通过组件消息带回执行结果

App-vue 中可从页面的 $getAppWebview().children() 取得组件创建的子 WebView。App-nvue 使用 @onPostMessage 和组件 ref 上的 evalJS;App-vue 使用本篇的 @message 和子 WebView 实例。组件消息的数据位于 event.detail.data 数组中,App 端支持实时投递。DCloud 组件文档

一、本地桥接脚本示例 ​

在接入方 uni-app 项目的 static 目录准备三个文件:

文件来源与用途
uni.webview.1.5.8.js从 DCloud 官方 SDK 地址 下载实际 JavaScript 文件,提供网页调用 uni 接口的能力
bridgeRuntime.js下载 公共适配脚本,补齐 Promise、请求配对及可选 callback
uniComponentBridge.js使用下面的代码,将公共入口适配到组件消息

uni.webView.postMessage 本身不返回本次业务的 Promise 结果,因此还需通过 evalJS 将响应送回公共适配脚本。业务 H5 继续调用原有的 htmlToFlutterEvent.postMessage。

uniComponentBridge.js:

javascript
(function () {
  if (window.UniComponentBridge) return;
  var documentId = 'doc_' + Date.now() + '_' + Math.random().toString(36).slice(2);
  var installed = false;

  function send(message) {
    message.source = 'live-component';
    message.documentId = documentId;
    window.uni.webView.postMessage({ data: message });
  }

  function install() {
    if (installed || !window.UniAppJSBridge || !window.uni ||
        !window.uni.webView || typeof window.uni.webView.postMessage !== 'function') return;
    installed = true;
    window.UniComponentBridge = {
      documentId: documentId,
      sendResult: function (id, result) {
        send({ kind: 'h5Result', id: id, result: result });
      }
    };
    window.AppBridgeAdapter.install(function (id, payload) {
      send({ kind: 'request', id: id, payload: payload });
    });
    send({ kind: 'ready' });
  }

  document.addEventListener('UniAppJSBridgeReady', install);
  install(); // SDK 已经就绪时直接安装,兼容脚本晚于就绪事件执行。
})();

source、documentId、kind、id 用于适配层的消息与文档匹配,不作为直播间业务参数传入。SDK 就绪后才安装通道,App 收到 ready 后才主动调用 H5。

二、在页面直接声明组件示例 ​

以下是一份 App-vue 页面示例。页面只包含一个 <web-view>,因此可以使用 children()[0];存在多个子 WebView 时,需要自行按目标实例定位。

先让组件加载 about:blank,取得实例、安装脚本和监听后,再更新 src 加载观看 URL。这样首次实际观看请求前就已设置 UA。账号打通和观看 URL 的获取见 接入总览。

vue
<template>
  <view>
    <web-view
      v-if="showWebview"
      :src="src"
      :update-title="false"
      @message="onWebMessage"
    ></web-view>
  </view>
</template>

<script>
export default {
  data: function () {
    return { showWebview: false, src: 'about:blank' };
  },
  onLoad: function (query) {
    this.watchUrl = decodeURIComponent(query.url || '');
    this.nativeView = null;
    this.documentId = null;
    this.h5Pending = new Map();
    this.sequence = 0;
    this.closed = false;
    this.closing = false;
    // #ifdef APP-PLUS
    if (!/^https:\/\/live\.example\.com(?::443)?\//.test(this.watchUrl)) return;
    this.platform = uni.getSystemInfoSync().platform === 'ios' ? 'iOS' : 'Android';
    this.previousUA = plus.navigator.getUserAgent();
    plus.navigator.setUserAgent(this.previousUA + ' channel/APP' + this.platform +
      ' version/1.0.0 package/com.example.live' +
      ' wxappid/example_wechat_appid scheme/examplelive');
    this.showWebview = true;
    // #endif
  },
  onReady: function () {
    // #ifdef APP-PLUS
    if (!this.showWebview) return;
    var self = this;
    var attempts = 0;
    var parent = this.$scope.$getAppWebview();
    function attach() {
      if (self.closed) return;
      var children = parent.children();
      if (children.length !== 1) {
        if (++attempts < 40) self.attachTimer = setTimeout(attach, 50);
        else uni.showToast({ title: '未找到观看组件', icon: 'none' });
        return;
      }
      var child = children[0];
      self.nativeView = child;
      self.loadingListener = function () {
        self.documentId = null;
        self.cancelH5Requests('H5 已导航');
      };
      child.addEventListener('loading', self.loadingListener);
      child.overrideUrlLoading({
        mode: 'reject',
        match: '^(?!https://live\\.example\\.com(?::443)?(?:/|$)).*'
      }, function () {});
      child.appendJsFile('_www/static/uni.webview.1.5.8.js');
      child.appendJsFile('_www/static/bridgeRuntime.js');
      child.appendJsFile('_www/static/uniComponentBridge.js');
      self.src = self.watchUrl; // 由组件加载地址,不手动 create、append 或 loadURL。
    }
    attach();
    this.appLifecycleListener = function (state) {
      self.callH5('appLifecycle', state).catch(function () {});
    };
    uni.$on('live-app-lifecycle', this.appLifecycleListener);
    // #endif
  },
  onBackPress: function (options) {
    // closePage 中的 uni.navigateBack 也会触发此钩子,必须放行。
    if (options && options.from === 'navigateBack') return false;
    if (this.closing) return true;
    if (!this.nativeView) return false;
    var self = this;
    this.nativeView.canBack(function (event) {
      if (self.closed) return;
      if (event.canBack) self.nativeView.back();
      else self.closePage();
    });
    return true;
  },
  onUnload: function () {
    this.closed = true;
    clearTimeout(this.attachTimer);
    this.cancelH5Requests('页面已关闭');
    if (this.appLifecycleListener) uni.$off('live-app-lifecycle', this.appLifecycleListener);
    if (this.nativeView && this.loadingListener) {
      this.nativeView.removeEventListener('loading', this.loadingListener);
    }
    this.nativeView = null; // 容器由组件随页面销毁,不重复 close。
    // #ifdef APP-PLUS
    if (this.previousUA) plus.navigator.setUserAgent(this.previousUA);
    // #endif
  },
  methods: {
    onWebMessage: function (event) {
      if (this.closed) return;
      var self = this;
      var messages = event.detail && event.detail.data;
      if (!Array.isArray(messages)) return;
      messages.forEach(function (message) {
        if (!message || message.source !== 'live-component') return;
        if (message.kind === 'ready') {
          self.documentId = message.documentId;
          return;
        }
        if (!self.documentId || message.documentId !== self.documentId) return;
        if (message.kind === 'h5Result') {
          var pending = self.h5Pending.get(message.id);
          if (!pending) return;
          clearTimeout(pending.timer);
          self.h5Pending.delete(message.id);
          pending.resolve(message.result);
        } else if (message.kind === 'request') {
          var child = self.nativeView;
          var doc = message.documentId;
          function reply(result) {
            if (self.closed || self.nativeView !== child || self.documentId !== doc) return;
            child.evalJS('if(window.UniComponentBridge && window.UniComponentBridge.documentId===' +
              JSON.stringify(doc) + '){window.AppBridgeAdapter.receive(' + JSON.stringify(message.id) +
              ',' + JSON.stringify(JSON.stringify(result)) + ');}');
          }
          self.handleNativeRequest(message.payload).then(reply, function () {
            reply({ code: 500, message: 'App 执行失败', data: null });
          });
        }
      });
    },
    cancelH5Requests: function (reason) {
      this.h5Pending.forEach(function (pending) {
        clearTimeout(pending.timer);
        pending.reject(new Error(reason));
      });
      this.h5Pending.clear();
    },
    closePage: async function () {
      if (this.closing) return;
      this.closing = true;
      try { await this.callH5('pageLifecycle', 'dispose'); } catch (error) {}
      uni.navigateBack();
    },
    // 将下文 handleNativeRequest、callH5 方法加入此处,方法之间用逗号分隔。
  }
};
</script>

App-vue 的组件 ref 不直接提供本篇使用的 evalJS;这里操作的是组件已经创建的原生实例。appendJsFile 会按添加顺序加载本地文件,并在导航到新文档时再次执行。HTML5+ WebView API

示例域名、应用版本、包名、微信 AppID 和 scheme 需要替换为自己的配置。setUserAgent 会影响之后创建的 WebView,不能据此更新已有子 WebView;因此示例在修改 UA 后才通过 v-if 创建组件。多容器项目应统一管理 UA,关闭时的恢复操作不代表已创建实例也已恢复。UA API 说明

Android / iOS App-vue 使用组件创建的子 WebView 执行脚本;uni.createWebviewContext 的平台支持范围见 上下文 API 兼容说明。

三、H5 调 App示例:请求、接收和结果回传 ​

H5 发起调用示例 ​

业务调用与其他技术栈相同,区别由第一节的注入脚本处理:

javascript
async function onGetAppInfoClick() {
  if (!window.htmlToFlutterEvent) throw new Error('App 通道尚未就绪');
  var responseText = await window.htmlToFlutterEvent.postMessage(JSON.stringify({
    event: 'getAppInfo', data: {}
  }));
  var result = JSON.parse(responseText);
  if (Number(result.code) !== 200) throw new Error(result.message);
  console.log(result.data.appName, result.data.platform);
}

async function onOpenNativePageClick() {
  if (!window.htmlToFlutterEvent) throw new Error('App 通道尚未就绪');
  var responseText = await window.htmlToFlutterEvent.postMessage(JSON.stringify({
    event: 'openNativePage',
    data: { page: 'orderDetail', params: { orderId: 'order_12345' } }
  }));
  var result = JSON.parse(responseText);
  if (Number(result.code) !== 200) throw new Error(result.message);
  return result.data;
}

App 接收并回传示例 ​

将该方法加入页面 methods。@message 对应的 onWebMessage 会调用此方法,再用 evalJS 执行 AppBridgeAdapter.receive,完成 H5 等待的 Promise。

javascript
handleNativeRequest: async function (payload) {
  var request;
  try {
    request = JSON.parse(payload);
    if (!request || typeof request.event !== 'string') throw new Error('Invalid event');
  } catch (error) {
    return { code: 400, message: '消息格式错误', data: null };
  }
  var result = {
    event: request.event, timestamp: new Date().toISOString(),
    code: 200, message: '', data: null
  };
  if (request.event === 'getAppInfo') {
    result.data = {
      appName: '示例应用', packageName: 'com.example.live', version: '1.0.0',
      buildNumber: '1', platform: this.platform, scheme: 'examplelive://'
    };
  } else if (request.event === 'openNativePage') {
    var data = request.data || {};
    var params = data.params || {};
    if (data.page !== 'orderDetail' || typeof params.orderId !== 'string' || !params.orderId.trim()) {
      result.code = 400;
      result.message = '页面或订单参数错误';
    } else {
      try {
        // 此路由需在接入项目的 pages.json 中注册。
        await new Promise(function (resolve, reject) {
          uni.navigateTo({
            url: '/pages/order/detail?orderId=' + encodeURIComponent(params.orderId),
            success: resolve,
            fail: reject
          });
        });
        result.data = { page: 'orderDetail' };
      } catch (error) {
        result.code = 500;
        result.message = '无法打开 App 页面';
      }
    }
  } else {
    result.code = 400;
    result.message = '未知事件';
  }
  return result;
}

可选 callback 由公共适配脚本处理,App 不再重复调用。getUserInfo、分享等其他事件仍需在 App 分发方法中按实际业务实现。

openNativePage 是自定义事件示例,使用统一的 data.page、data.params,并将 orderDetail 映射到 /pages/order/detail。接入项目应实现 pages/order/detail.vue、在 pages.json 中注册它,并在目标页的 onLoad(query) 中读取 query.orderId。200 表示已发起路由跳转,参数缺失返回 400,路由失败返回 500。完整字段见 共用协议。

handleNativeRequest 与 closePage、callH5 放在同一个 methods 对象内,彼此同级。

四、App 调 H5示例:执行并读取返回值 ​

App 发起调用示例 ​

将该方法加入同一个 methods。evalJS 不直接返回业务值,因此由注入层的 sendResult 通过组件消息送回,并按请求编号完成 App 的 Promise。

javascript
callH5: function (event, data) {
  var self = this;
  if (!this.nativeView || !this.documentId || this.closed) {
    return Promise.reject(new Error('H5 通道尚未就绪'));
  }
  var id = 'native_' + (++this.sequence);
  var doc = this.documentId;
  var message = { event: event, timestamp: new Date().toISOString(), data: data };
  return new Promise(function (resolve, reject) {
    var timer = setTimeout(function () {
      self.h5Pending.delete(id);
      reject(new Error('H5 返回结果超时'));
    }, 3000);
    self.h5Pending.set(id, { resolve: resolve, reject: reject, timer: timer });
    var source = '(function(id,doc,message){' +
      'var bridge=window.UniComponentBridge;' +
      'if(!bridge || bridge.documentId!==doc)return;' +
      'function fail(){bridge.sendResult(id,{code:500,message:"H5 执行失败",data:null});}' +
      'try{' +
        'var result=typeof window.flutterToHtmlEvent==="function"' +
          '?window.flutterToHtmlEvent(message)' +
          ':{code:400,message:"H5 处理函数不存在",data:null};' +
        'Promise.resolve(result).then(function(value){bridge.sendResult(id,value);},fail);' +
      '}catch(error){fail();}' +
    '})(' + JSON.stringify(id) + ',' + JSON.stringify(doc) + ',' + JSON.stringify(message) + ');';
    self.nativeView.evalJS(source);
  });
}

在收到 ready 后,通过页面业务方法或导航栏按钮调用:

javascript
// 放入页面 methods。
queryVideoSize: async function () {
  var result = await this.callH5('getVideoInfo', {});
  if (Number(result.code) !== 200) throw new Error(result.message);
  console.log(result.data.width, result.data.height);
}

H5 接收并返回示例 ​

真实直播间已有 flutterToHtmlEvent,App 直接调用,不覆盖现有实现。以下用于自建测试 H5,接收函数与其他方案相同:

javascript
window.flutterToHtmlEvent = function (message) {
  var result = { event: message.event, timestamp: Date.now(), code: 200, message: '', data: null };
  if (message.event === 'getVideoInfo') {
    var video = document.querySelector('video');
    if (video) result.data = { width: video.videoWidth, height: video.videoHeight };
    else { result.code = 401; result.message = '播放器尚未就绪'; }
  } else if (message.event === 'appLifecycle' || message.event === 'pageLifecycle') {
    console.log(message.event, message.data);
  } else {
    result.code = 400;
    result.message = '未知事件';
  }
  return result;
};

五、前后台、返回和组件销毁示例 ​

App 的 App.vue 发出真实的前后台事件,上面的页面监听后调用 H5:

javascript
export default {
  onShow: function () { uni.$emit('live-app-lifecycle', 'resumed'); },
  onHide: function () { uni.$emit('live-app-lifecycle', 'paused'); }
};

返回键优先处理网页历史,无历史时先通知 pageLifecycle / dispose 再退出路由。完整导航清理旧文档的查询,页面卸载移除监听;组件随页面释放原生容器。示例超时按实际业务调整,分享等需要用户操作的事件不能简单套用短超时。

uni.navigateBack() 触发的 onBackPress 必须放行,否则关闭流程可能被自身拦截。iOS 侧滑返回不会触发该钩子;需要在关闭前通知 H5 时,应另行管理手势并统一关闭入口。官方返回逻辑说明、页面生命周期说明

联调检查 ​

  1. Android / iOS App 中确认页面由 <web-view> 加载,未额外创建第二个容器。
  2. 验证首次观看请求的 UA 和参数,确认 SDK、本地脚本按顺序执行并收到 ready。
  3. H5 查询 getAppInfo,确认 @message 实时收到请求,H5 Promise 获得结果;调用 openNativePage,验证订单参数、跳转响应及路由失败处理。
  4. App 查询 getVideoInfo,确认 H5 返回尺寸,多个并发请求正确配对。
  5. 刷新、完整导航、返回关闭及切换前后台后,确认无旧请求回传或残留监听。