H5 页面嵌入 APP 的 web-view 时-调用原生能力(uniapp)

H5 与 APP 原生通信方案 - NativeBridge 实现

概述

在 uni-app 项目中,H5 页面嵌入 APP 的 web-view 时,需要调用原生能力(如微信分享)。本文档总结了一套基于 URL Scheme 拦截 的 NativeBridge 通信方案。

架构原理

javascript 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      APP 壳 (h5-full-webview)                │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  1. mounted 时注入 window.NativeBridge 到 H5 webview   │ │
│  │  2. overrideUrlLoading 拦截 bridge:// 协议             │ │
│  │  3. 解析 URL 参数,调用原生 API(如 plus.share)       │ │
│  └────────────────────────────────────────────────────────┘ │
│                          ↓ evalJS 注入                      │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                   H5 页面 (webview)                     │ │
│  │  1. 调用 window.NativeBridge.share(data)               │ │
│  │  2. Bridge 内部执行 window.location.href = 'bridge://...'│ │
│  │  3. APP 壳拦截并处理                                    │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

核心实现

1. APP 壳端(h5-full-webview/index.vue)

1.1 注入 NativeBridge
javascript 复制代码
mounted() {
  // #ifdef APP-PLUS
  this.$nextTick(() => {
    setTimeout(() => {
      const currentWebview = this.$scope.$getAppWebview();
      const wv = currentWebview.children()[0];
      if (!wv) return;

      // 监听 webview 加载完成
      wv.addEventListener('loaded', () => {
        this.injectNativeBridge();
      });
    }, 500);
  });
  // #endif
},

methods: {
  injectNativeBridge() {
    if (this.bridgeInjected) return; // 防止重复注入

    const wv = this.getWebview();
    if (!wv) return;

    // 通过 evalJS 注入 Bridge 对象
    const bridgeJs = `
      (function() {
        if (window.NativeBridge) return 'already';
        window.NativeBridge = {
          share: function(data) {
            var json = typeof data === 'string' ? data : JSON.stringify(data || {});
            window.location.href = 'bridge://share?data=' + encodeURIComponent(json);
          }
          // 可扩展其他方法:scan, getLocation, etc.
        };
        return 'ok';
      })();
    `;

    wv.evalJS(bridgeJs, (result) => {
      console.log('[NativeBridge] 注入结果:', result);
      this.bridgeInjected = true;
    });

    // 拦截 bridge:// 协议
    wv.overrideUrlLoading({ mode: 'reject' }, (e) => {
      const url = e.url;
      if (url && url.startsWith('bridge://share')) {
        this.handleShare(url);
      }
      // 其他 bridge:// 协议可在此扩展
    });
  },

  handleShare(url) {
    try {
      const queryStr = url.split('?')[1] || '';
      const params = new URLSearchParams(queryStr);
      const dataStr = params.get('data');
      const shareData = dataStr ? JSON.parse(decodeURIComponent(dataStr)) : null;
      
      if (shareData) {
        this.showSharePanel(shareData);
      }
    } catch (err) {
      console.error('[NativeBridge] 解析分享数据失败:', err);
    }
  },

  showSharePanel(shareData) {
    const scene = shareData.scene || 'WXSceneSession'; // 默认微信好友
    
    const share = {
      content: {
        type: 'web',
        href: shareData.url,
        title: shareData.title,
        summary: shareData.content,
        thumbs: [shareData.imageUrl],
        scene: scene, // WXSceneSession=好友, WXSceneTimeline=朋友圈
      },
    };

    plus.share.getServices((services) => {
      const wxService = services.find((s) => s.id === 'weixin');
      if (wxService) {
        wxService.send(share, () => {
          uni.showToast({ title: "分享成功", icon: "none" });
        }, (err) => {
          uni.showToast({ title: "分享失败", icon: "none" });
        });
      }
    }, (err) => {
      uni.showToast({ title: "分享功能不可用", icon: "none" });
    });
  }
}

2. H5 页面端(teacher-activity/index.vue)

2.1 调用 NativeBridge
javascript 复制代码
// 显示分享渠道选择弹框
share() {
  this.showSharePopup = true;
},

// 用户选择渠道后执行分享
doShare(scene) {
  this.showSharePopup = false;
  
  // #ifdef H5
  const shareData = {
    ...this.getShareData(),
    scene: scene // WXSceneSession 或 WXSceneTimeline
  };
  
  if (window.NativeBridge && window.NativeBridge.share) {
    window.NativeBridge.share(shareData);
  } else {
    uni.showToast({ title: "分享功能暂不可用", icon: "none" });
  }
  // #endif
},

getShareData() {
  return {
    url: typeof window !== 'undefined' ? window.location.href : '',
    title: "答题赢好礼",
    content: "通途控股9月投教主题活动,答题赢好礼~",
    imageUrl: "https://d.tongtuzhengxin.com/img/teacher-activity/circle.png",
  };
}
2.2 分享弹框 UI
vue 复制代码
<template>
  <!-- 分享渠道弹框 -->
  <view class="share-mask" v-if="showSharePopup" @click="showSharePopup = false">
    <view class="share-popup" @click.stop>
      <view class="share-popup-title">分享到</view>
      <view class="share-popup-options">
        <view class="share-option" @click="doShare('WXSceneSession')">
          <view class="share-icon share-icon-wechat">
            <text class="icon-text">微</text>
          </view>
          <text class="share-label">微信好友</text>
        </view>
        <view class="share-option" @click="doShare('WXSceneTimeline')">
          <view class="share-icon share-icon-moments">
            <text class="icon-text">朋</text>
          </view>
          <text class="share-label">朋友圈</text>
        </view>
      </view>
      <view class="share-popup-cancel" @click="showSharePopup = false">取消</view>
    </view>
  </view>
</template>

关键要点

1. 为什么用 URL Scheme 而不是 evalJS 回调?

  • evalJS 回调在某些环境下不触发(HBuilder 调试模式、特定 Android 版本)
  • URL Scheme 是单向通信,更可靠,不依赖回调机制
  • 数据通过 URL 参数传递,解析简单直观

2. overrideUrlLoading 的 mode: 'reject'

javascript 复制代码
wv.overrideUrlLoading({ mode: 'reject' }, callback)
  • mode: 'reject':阻止 webview 实际导航到该 URL,只触发回调
  • 这样可以拦截 bridge:// 而不让页面跳转

3. 防止重复注入

javascript 复制代码
if (this.bridgeInjected) return;
  • HMR(热更新)会导致 loaded 事件多次触发
  • 用标志位防止重复注入 NativeBridge

4. 防重复触发

H5 端和 APP 端都加了节流:

javascript 复制代码
// H5 端
if (this._sharePending) return;
this._sharePending = true;
setTimeout(() => { this._sharePending = false; }, 3000);

// APP 端
if (this.sharePending) return;
this.sharePending = true;
setTimeout(() => { this.sharePending = false; }, 3000);

扩展其他原生能力

injectNativeBridge 中添加新方法:

javascript 复制代码
const bridgeJs = `
  (function() {
    if (window.NativeBridge) return;
    window.NativeBridge = {
      // 分享
      share: function(data) {
        var json = typeof data === 'string' ? data : JSON.stringify(data || {});
        window.location.href = 'bridge://share?data=' + encodeURIComponent(json);
      },
      
      // 扫码(示例)
      scan: function() {
        window.location.href = 'bridge://scan';
      },
      
      // 获取位置(示例)
      getLocation: function() {
        window.location.href = 'bridge://getLocation';
      }
    };
  })();
`;

overrideUrlLoading 中处理:

javascript 复制代码
wv.overrideUrlLoading({ mode: 'reject' }, (e) => {
  const url = e.url;
  if (url.startsWith('bridge://share')) {
    this.handleShare(url);
  } else if (url.startsWith('bridge://scan')) {
    this.handleScan();
  } else if (url.startsWith('bridge://getLocation')) {
    this.handleGetLocation();
  }
});

调试技巧

1. 查看注入是否成功

H5 控制台:

javascript 复制代码
console.log(window.NativeBridge); // 应该输出 {share: ƒ}

2. 查看 URL 拦截

APP 壳日志:

perl 复制代码
[NativeBridge] 拦截到 URL: bridge://share?data=%7B%22url%22%3A...

3. 常见问题排查

问题 原因 解决
NativeBridge 不存在 注入失败或时机不对 检查 loaded 事件是否触发,增加延时
evalJS 回调不执行 调试环境兼容性问题 改用 URL Scheme 方式(本方案)
分享面板不弹出 标准基座无微信 SDK 打自定义基座(包含微信分享模块)
重复触发 用户快速点击或 HMR 加节流标志位

总结

这套 NativeBridge 方案的核心思想:

  1. APP 壳注入 JS 对象 到 H5 webview
  2. H5 调用对象方法 → 触发 window.location.href = 'bridge://...'
  3. APP 壳拦截 URL → 解析参数 → 调用原生 API

优点:

  • 不依赖 evalJS 回调,兼容性好
  • 数据通过 URL 传递,简单直观
  • 易于扩展其他原生能力

适用场景:

  • uni-app APP-PLUS 环境
  • H5 页面嵌入 APP web-view
  • 需要调用原生能力(分享、扫码、定位等)
相关推荐
ITmaster07312 小时前
Vibe Coding 时代:Vue 消失了还是 React 太强?
前端·vue.js·react.js
万敏3 小时前
Vue3 全栈实战第七周:错误处理与请求层封装实战记录
vue.js·node.js·全栈
请你吃div4 小时前
Electron 从零开始的新手开发教程
vue.js·windows·electron
咏方舟【长江支流】6 小时前
【前端1】单据编辑 -EasyUI/Vue/React/Bootstrap 主流框架实现订单单据主子表显示和编辑比较,哪个你最易入门?
前端·vue.js·easyui·咏方舟-长江支流·userbaodatagrid
m0_579146656 小时前
Element UI 表格合并单元格完全指南:从原理到实战
前端·vue.js·elementui
雪芽蓝域zzs6 小时前
Prettier (代码格式化)配置详解
前端·vue.js
运维全栈笔记10 小时前
Vue + Spring Boot 前后端分离项目部署笔记(若依 RuoYi-Vue 3.9.2)
运维·服务器·vue.js·spring boot·笔记·开源·开源软件
Simon_He21 小时前
一个渲染内核,五个框架包:我的流式 Markdown 渲染库是怎么长成“全家桶”的
前端·vue.js·markdown
夏雪coding21 小时前
JeecgBoot 动态路由踩坑(Vue2):刷新白屏、permissionList[0] 报 undefined,以及我最后为什么全删了
javascript·vue.js