APP与H5通信:JSBridge实战指南

更新日期: 2025-12-16 阅读: 43 标签: H5

在现代移动应用开发中,原生APP与H5页面的混合开发模式非常普遍。要实现两者间的顺畅通信,JSBridge技术至关重要。本文将详细介绍JSBridge的原理、实现方法和实际应用。


一、通信原理与方式

1. 通信基础原理

JSBridge是原生APP与H5页面之间的桥梁,通过这个桥梁,两者可以相互调用方法和传递数据

核心原理

H5页面 ⇄ JSBridge ⇄ 原生APP

2. 两种主要通信方式

H5调用APP

  • 注入api方式:APP将原生方法注入到WebView中

  • URL Scheme拦截:H5通过特定格式的URL触发APP功能

APP调用H5

  • 执行JavaScript:APP在WebView中执行JavaScript代码

  • 事件触发:通过事件机制通知H5执行特定操作


二、H5端JSBridge封装

1. 基础JSBridge类实现

/**
 * JSBridge - H5与原生APP通信桥梁
 */
class JSBridge {
  constructor() {
    this.bridgeReady = false;
    this.queue = []; // 待执行队列
    this.callbacks = {}; // 回调函数存储
    this.callbackId = 0;
    
    // 环境检测
    this.isAndroid = /android|adr/i.test(navigator.userAgent);
    this.isIOS = /iPhone|iPad|iPod|iOS/i.test(navigator.userAgent);
    
    this._init();
  }

  /**
   * 初始化JSBridge
   */
  _init() {
    if (this.isAndroid) {
      this._initAndroid();
    } else if (this.isIOS) {
      this._initIOS();
    } else {
      // Web环境,用于开发和测试
      console.log('当前不是APP环境');
      this.bridgeReady = true;
      this._executeQueue();
    }
  }

  /**
   * Android平台初始化
   */
  _initAndroid() {
    if (window.AndroidBridge) {
      this.bridgeReady = true;
      this._executeQueue();
    } else {
      // 轮询等待Android Bridge初始化
      const checkInterval = setInterval(() => {
        if (window.AndroidBridge) {
          clearInterval(checkInterval);
          this.bridgeReady = true;
          this._executeQueue();
        }
      }, 100);

      // 超时处理
      setTimeout(() => {
        clearInterval(checkInterval);
        console.error('Android JSBridge初始化超时');
      }, 5000);
    }
  }

  /**
   * iOS平台初始化
   */
  _initIOS() {
    if (window.WebViewJavascriptBridge) {
      this._connectIOSBridge(window.WebViewJavascriptBridge);
    } else {
      // 监听Bridge就绪事件
      document.addEventListener('WebViewJavascriptBridgeReady', 
        (e) => {
          this._connectIOSBridge(e.bridge || window.WebViewJavascriptBridge);
        }, false
      );
    }
  }

  /**
   * 连接iOS Bridge
   */
  _connectIOSBridge(bridge) {
    bridge.init((message, responseCallback) => {
      console.log('收到iOS消息:', message);
      if (responseCallback) {
        responseCallback({ response: '消息已接收' });
      }
    });

    this.bridgeReady = true;
    this.iosBridge = bridge;
    this._executeQueue();
  }

  /**
   * 执行队列中的消息
   */
  _executeQueue() {
    while (this.queue.length > 0) {
      const { method, params, callback } = this.queue.shift();
      this._callNative(method, params, callback);
    }
  }

  /**
   * 注册H5方法供原生调用
   */
  registerHandler(name, callback) {
    if (!name || typeof callback !== 'function') {
      console.error('registerHandler: 需要方法名和回调函数');
      return;
    }

    if (this.isAndroid) {
      // Android: 将方法挂载到window对象
      window[`_h5_${name}`] = callback;
    } else if (this.isIOS && this.iosBridge) {
      // iOS: 通过Bridge注册
      this.iosBridge.registerHandler(name, (data, responseCallback) => {
        try {
          const result = callback(data);
          if (responseCallback) {
            responseCallback(result);
          }
        } catch (error) {
          console.error(`方法 ${name} 执行错误:`, error);
          if (responseCallback) {
            responseCallback({ error: error.message });
          }
        }
      });
    }
  }

  /**
   * 调用原生方法
   */
  callHandler(method, params = {}, callback) {
    if (!method) {
      console.error('callHandler: 需要方法名');
      return;
    }

    // Bridge未就绪时放入队列
    if (!this.bridgeReady) {
      this.queue.push({ method, params, callback });
      return;
    }

    this._callNative(method, params, callback);
  }

  /**
   * 实际调用原生方法
   */
  _callNative(method, params, callback) {
    try {
      if (this.isAndroid && window.AndroidBridge) {
        // Android调用
        const callbackId = callback ? this._generateCallbackId(callback) : null;
        const data = {
          method: method,
          params: params,
          callbackId: callbackId
        };
        window.AndroidBridge.call(method, JSON.stringify(data));
      } else if (this.isIOS && this.iosBridge) {
        // iOS调用
        this.iosBridge.callHandler(method, params, callback);
      } else {
        // Web环境模拟
        if (callback) {
          setTimeout(() => {
            callback({ mock: true, method, params });
          }, 100);
        }
      }
    } catch (error) {
      console.error(`调用原生方法 ${method} 失败:`, error);
      if (callback) {
        callback({ error: error.message });
      }
    }
  }

  /**
   * 生成回调ID
   */
  _generateCallbackId(callback) {
    const id = `cb_${Date.now()}_${this.callbackId++}`;
    this.callbacks[id] = callback;
    return id;
  }

  /**
   * 执行回调(由原生调用)
   */
  dispatchCallback(callbackId, data) {
    const callback = this.callbacks[callbackId];
    if (callback) {
      try {
        callback(data);
      } catch (error) {
        console.error(`回调 ${callbackId} 执行错误:`, error);
      }
      delete this.callbacks[callbackId]; // 清理
    }
  }

  /**
   * 检查是否在APP环境
   */
  isAppEnvironment() {
    return this.isAndroid || this.isIOS;
  }

  /**
   * 获取平台信息
   */
  getPlatform() {
    if (this.isAndroid) return 'android';
    if (this.isIOS) return 'ios';
    return 'web';
  }
}

// 创建单例实例
const jsBridge = new JSBridge();
export default jsBridge;

2. 常用API封装

/**
 * 常用原生方法封装
 */
import jsBridge from './jsBridge';

const bridgeAPI = {
  /**
   * 获取用户信息
   */
  getUserInfo() {
    return new Promise((resolve, reject) => {
      jsBridge.callHandler('getUserInfo', {}, (result) => {
        if (result && !result.error) {
          resolve(result);
        } else {
          reject(result?.error || '获取用户信息失败');
        }
      });
    });
  },

  /**
   * 显示Toast提示
   */
  showToast(message, duration = 2000) {
    jsBridge.callHandler('showToast', { message, duration });
  },

  /**
   * 页面跳转
   */
  navigateTo(url, params = {}, type = 'push') {
    jsBridge.callHandler('navigateTo', { url, params, type });
  },

  /**
   * 分享功能
   */
  share(shareData) {
    return new Promise((resolve, reject) => {
      jsBridge.callHandler('share', shareData, (result) => {
        if (result && result.success) {
          resolve(result);
        } else {
          reject(result?.error || '分享失败');
        }
      });
    });
  },

  /**
   * 选择图片
   */
  chooseImage(options = {}) {
    const defaultOptions = {
      count: 1,
      quality: 0.8,
      sourceType: ['album', 'camera']
    };
    
    return new Promise((resolve, reject) => {
      jsBridge.callHandler('chooseImage', 
        { ...defaultOptions, ...options }, 
        (result) => {
          if (result && result.images) {
            resolve(result);
          } else {
            reject(result?.error || '选择图片失败');
          }
        }
      );
    });
  },

  /**
   * 支付功能
   */
  pay(payParams) {
    return new Promise((resolve, reject) => {
      jsBridge.callHandler('pay', payParams, (result) => {
        if (result && result.success) {
          resolve(result);
        } else {
          reject(result?.error || '支付失败');
        }
      });
    });
  }
};

export default bridgeAPI;


三、vue项目集成

1. Vue 2.x 集成

// main.js
import Vue from 'vue'
import App from './App.vue'
import jsBridge from './utils/jsBridge'
import bridgeAPI from './utils/bridgeAPI'

// 全局注册
Vue.prototype.$bridge = jsBridge
Vue.prototype.$native = bridgeAPI

// 创建Bridge插件
const bridgePlugin = {
  install(Vue) {
    Vue.prototype.$bridge = jsBridge
    Vue.prototype.$native = bridgeAPI
    
    // 全局混入
    Vue.mixin({
      created() {
        // 注册页面事件
        if (this.$options.bridgeEvents) {
          this._bridgeHandlers = {}
          Object.keys(this.$options.bridgeEvents).forEach(eventName => {
            const handler = this.$options.bridgeEvents[eventName]
            this._bridgeHandlers[eventName] = handler.bind(this)
            jsBridge.registerHandler(eventName, this._bridgeHandlers[eventName])
          })
        }
      },
      beforeDestroy() {
        // 清理
        if (this._bridgeHandlers) {
          this._bridgeHandlers = null
        }
      }
    })
  }
}

Vue.use(bridgePlugin)

new Vue({
  render: h => h(App)
}).$mount('#app')

2. Vue 3.x 集成

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import jsBridge from './utils/jsBridge'
import bridgeAPI from './utils/bridgeAPI'

const app = createApp(App)

// 全局属性
app.config.globalProperties.$bridge = jsBridge
app.config.globalProperties.$native = bridgeAPI

// 依赖注入
app.provide('bridge', jsBridge)
app.provide('native', bridgeAPI)

// 自定义指令
app.directive('bridge', {
  mounted(el, binding) {
    const { arg: eventName, value: handler } = binding
    if (eventName && typeof handler === 'function') {
      el._bridgeHandler = handler
      jsBridge.registerHandler(eventName, handler)
    }
  },
  beforeUnmount(el, binding) {
    const { arg: eventName } = binding
    if (eventName && el._bridgeHandler) {
      // 清理逻辑
      delete el._bridgeHandler
    }
  }
})

app.mount('#app')

3. Composition API Hook封装

// hooks/useBridge.js
import { ref, onUnmounted } from 'vue'
import jsBridge from '../utils/jsBridge'
import bridgeAPI from '../utils/bridgeAPI'

export function useBridge() {
  const isConnected = ref(jsBridge.bridgeReady)
  const platform = ref(jsBridge.getPlatform())
  const handlers = ref(new Map())

  // 注册处理器
  const registerHandler = (name, handler) => {
    if (!name || typeof handler !== 'function') return
    
    handlers.value.set(name, handler)
    jsBridge.registerHandler(name, handler)
    
    return () => unregisterHandler(name)
  }

  // 注销处理器
  const unregisterHandler = (name) => {
    handlers.value.delete(name)
  }

  // 调用原生方法
  const callHandler = (method, params, callback) => {
    return jsBridge.callHandler(method, params, callback)
  }

  // 清理
  const cleanupHandlers = () => {
    handlers.value.clear()
  }

  onUnmounted(() => {
    cleanupHandlers()
  })

  return {
    isConnected,
    platform,
    registerHandler,
    unregisterHandler,
    callHandler,
    bridgeAPI,
    jsBridge
  }
}


四、使用案例

1. Vue 2.x 组件示例

<template>
  <div class="user-profile">
    <h2>用户信息</h2>
    
    <div v-if="loading" class="loading">加载中...</div>
    
    <div v-else-if="userInfo" class="user-info">
      <img :src="userInfo.avatar" alt="头像" class="avatar">
      <p>昵称:{{ userInfo.nickname }}</p>
      <p>ID:{{ userInfo.userId }}</p>
    </div>
    
    <div class="actions">
      <button @click="getUserInfo">获取用户信息</button>
      <button @click="shareContent">分享</button>
      <button @click="selectImage">选择图片</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userInfo: null,
      loading: false
    }
  },

  // 注册原生调用的事件
  bridgeEvents: {
    refreshPage() {
      this.getUserInfo()
    },
    updateUserInfo(data) {
      this.userInfo = data
      this.$native.showToast('用户信息已更新')
    }
  },

  mounted() {
    // 设置页面标题
    this.$native.setTitle('用户中心')
  },

  methods: {
    async getUserInfo() {
      try {
        this.loading = true
        const userInfo = await this.$native.getUserInfo()
        this.userInfo = userInfo
      } catch (error) {
        console.error('获取用户信息失败:', error)
        this.$native.showToast('获取失败')
      } finally {
        this.loading = false
      }
    },

    async shareContent() {
      try {
        const shareData = {
          title: '分享标题',
          description: '分享描述',
          link: window.location.href
        }
        
        await this.$native.share(shareData)
        this.$native.showToast('分享成功')
      } catch (error) {
        console.error('分享失败:', error)
      }
    },

    async selectImage() {
      try {
        const result = await this.$native.chooseImage({
          count: 3
        })
        console.log('选择的图片:', result.images)
        this.$native.showToast(`选择了${result.images.length}张图片`)
      } catch (error) {
        console.error('选择图片失败:', error)
      }
    }
  }
}
</script>

<style scoped>
.user-profile {
  padding: 20px;
}

.loading {
  text-align: center;
  padding: 20px;
  color: #666;
}

.avatar {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-bottom: 10px;
}

.actions {
  margin-top: 20px;
}

button {
  margin: 5px;
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background: #0056b3;
}
</style>

2. Vue 3.x 组件示例

<template>
  <div class="product-detail">
    <h2>商品详情</h2>
    
    <div v-if="loading" class="loading">加载中...</div>
    
    <div v-else-if="product" class="product-info">
      <img :src="product.image" alt="商品图片" class="product-image">
      <h3>{{ product.name }}</h3>
      <p class="price">¥{{ product.price }}</p>
      <p class="description">{{ product.description }}</p>
    </div>
    
    <div class="actions">
      <button @click="addToCart" class="btn-primary">加入购物车</button>
      <button @click="buyNow" class="btn-secondary">立即购买</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useBridge } from '../hooks/useBridge'

const { callHandler, bridgeAPI } = useBridge()
const product = ref(null)
const loading = ref(false)

// 加载商品信息
const loadProduct = async () => {
  try {
    loading.value = true
    // 这里可以是API请求
    await new Promise(resolve => setTimeout(resolve, 500))
    
    product.value = {
      id: '123',
      name: '示例商品',
      price: 99.99,
      description: '商品详细描述',
      image: 'https://example.com/product.jpg'
    }
    
    // 设置页面标题
    bridgeAPI.setTitle(product.value.name)
  } catch (error) {
    console.error('加载失败:', error)
    bridgeAPI.showToast('加载失败')
  } finally {
    loading.value = false
  }
}

// 加入购物车
const addToCart = async () => {
  if (!product.value) return
  
  try {
    await callHandler('addToCart', {
      productId: product.value.id,
      quantity: 1
    })
    bridgeAPI.showToast('已加入购物车')
  } catch (error) {
    console.error('加入购物车失败:', error)
  }
}

// 立即购买
const buyNow = async () => {
  if (!product.value) return
  
  try {
    const payParams = {
      orderId: `ORDER_${Date.now()}`,
      amount: product.value.price
    }
    
    const result = await bridgeAPI.pay(payParams)
    if (result.success) {
      bridgeAPI.showToast('支付成功')
    }
  } catch (error) {
    console.error('支付失败:', error)
  }
}

onMounted(() => {
  loadProduct()
})
</script>

<style scoped>
.product-detail {
  padding: 20px;
}

.loading {
  text-align: center;
  padding: 40px;
  color: #666;
}

.product-image {
  width: 100%;
  max-width: 300px;
  margin-bottom: 20px;
}

.price {
  font-size: 24px;
  color: #e4393c;
  font-weight: bold;
  margin: 10px 0;
}

.description {
  color: #666;
  line-height: 1.6;
}

.actions {
  margin-top: 30px;
}

.btn-primary, .btn-secondary {
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  margin-right: 10px;
}

.btn-primary {
  background: #e4393c;
  color: white;
}

.btn-secondary {
  background: #f0f0f0;
  color: #333;
}
</style>


五、原生端配置

1. Android端实现

// AndroidBridge.java
public class AndroidBridge {
    private WebView webView;
    private Context context;
    
    public AndroidBridge(WebView webView, Context context) {
        this.webView = webView;
        this.context = context;
    }
    
    @JavascriptInterface
    public void call(String data) {
        try {
            JSONObject json = new JSONObject(data);
            String method = json.getString("method");
            JSONObject params = json.getJSONObject("params");
            String callbackId = json.optString("callbackId");
            
            switch (method) {
                case "getUserInfo":
                    handleGetUserInfo(callbackId);
                    break;
                case "showToast":
                    handleShowToast(params);
                    break;
                // 其他方法处理...
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
    
    private void handleGetUserInfo(String callbackId) {
        JSONObject userInfo = new JSONObject();
        try {
            userInfo.put("userId", "123456");
            userInfo.put("nickname", "测试用户");
            userInfo.put("avatar", "https://example.com/avatar.jpg");
            
            if (!TextUtils.isEmpty(callbackId)) {
                String js = String.format(
                    "javascript:window.jsBridge.dispatchCallback('%s', %s)",
                    callbackId, userInfo.toString()
                );
                webView.post(() -> webView.evaluateJavascript(js, null));
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
    
    private void handleShowToast(JSONObject params) {
        try {
            String message = params.getString("message");
            int duration = params.optInt("duration", 2000);
            
            // 显示Toast
            Toast.makeText(context, message, 
                duration > 2000 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT
            ).show();
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}

2. iOS端实现

// iOSBridge.swift
import WebKit
import WebViewJavascriptBridge

class iOSBridge {
    var bridge: WebViewJavascriptBridge?
    var webView: WKWebView
    
    init(webView: WKWebView) {
        self.webView = webView
        setupBridge()
    }
    
    private func setupBridge() {
        bridge = WebViewJavascriptBridge(webView: webView)
        
        // 注册原生方法
        bridge?.registerHandler("getUserInfo") { [weak self] data, responseCallback in
            let userInfo: [String: Any] = [
                "userId": "123456",
                "nickname": "测试用户",
                "avatar": "https://example.com/avatar.jpg"
            ]
            responseCallback?(userInfo)
        }
        
        bridge?.registerHandler("showToast") { [weak self] data, responseCallback in
            guard let params = data as? [String: Any],
                  let message = params["message"] as? String else { return }
            
            // 显示Toast(这里需要自定义Toast实现)
            self?.showToast(message: message)
            responseCallback?(["success": true])
        }
    }
    
    private func showToast(message: String) {
        // Toast显示逻辑
        print("显示Toast: \(message)")
    }
    
    // 调用H5方法
    func callH5Method(method: String, data: Any? = nil) {
        bridge?.callHandler(method, data: data)
    }
}


六、常见问题与解决方案

问题可能原因解决方案
Android调用失败H5方法未注册或时机不对确保在页面加载完成后注册方法
iOS Bridge初始化失败库引入问题或初始化代码错误检查库引入,确保在WebView加载前初始化
回调函数不执行回调ID不匹配或未正确调用检查回调ID生成和传递逻辑
数据格式错误JSON序列化失败确保数据是有效的JSON格式
Web环境报错未正确处理非APP环境使用isAppEnvironment()检测并提供降级方案

七、最佳实践建议

  1. 统一通信协议:定义清晰的通信协议格式

  2. 错误处理:完善的错误捕获和处理机制

  3. 类型安全:使用TypeScript提高代码质量

  4. 性能优化:避免频繁通信,合理使用缓存

  5. 安全性:验证通信来源,防止恶意调用

  6. 兼容性:考虑不同版本WebView的兼容性

  7. 调试支持:提供完善的调试工具和日志


八、总结

JSBridge是混合开发中的核心技术,合理的设计和实现能显著提升开发效率和用户体验。本文提供的方案经过实践验证,可以作为实际项目的参考基础。在实际应用中,还需要根据具体业务需求进行调整和优化。

记住,良好的通信机制不仅需要技术实现,更需要清晰的协议定义和团队协作规范。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13295

H5/web混合开发,js获取android,ios原生应用返回数据的实例方法

在很多应用都采用同H5页面混合开发模式,这篇主要讲解JS中如何获取原生应用返回给js的数据方法,包括android和ios

h5中新属性 data-属性名的获取与设置,详解原生js操作h5的属性data-*

h5中新功能用来描述自定义的数据属性,也就是 data-* 自定义属性。在h5中我们可以使用以 data- 为前缀来设置我们需要的自定义属性,来进行一些数据的存放 ,在实际项目中使用比较多。这篇文章就总结如何通过原生js对data-*属性的操作。

H5和原生的职责划分

减少工作量(一套代码,多个平台),以及快速的更新迭代(譬如线上更新),而且还需要考虑Native端的高性能以及系统API调用能力,混合页面导航栏组件由原生实现,一些重要的业务页面、带有复杂动画或交互的页面以及一些固定页面由原生实现

常见的H5移动端Web页面Bug问题解决方案总汇

移动web的兼容性bug的解决方法:远程接口的跨域问题,背景图片会模糊问题,图片加载,拨号功能等

h5手机摇一摇功能实现:基于html5重力感应DeviceMotionEvent事件监听手机摇晃

DeviceMotionEven是html5提供的一个用来获取设备物理方向及运动的信息(比如陀螺仪、罗盘及加速计)的Dom事件,利用devicemotion实现手机h5页面摇一摇功能

h5通过连接打开本地app_以及常见应用的URL Scheme

在做h5活动页面的时候,有这样的一个需求:点击页面的一个按钮就打开本地的一个app应用,如果该应用未下载,则跳转到app的下载页。这个操作是通过连接跳转的形式来实现的,这篇文章就简单讲解下如何操作的?

h5实现名片扫描识功能

点击名片识别按钮,将名片上的个人信息扫描并解析出来显示。需要调出手机摄像头和相册,让用户进行选择;获取照片或者图片的base64数据;调取第三方的orc接口进行图片解析,得到名片上的个人信息,并显示。

h5通过css实现禁止ios端长按复制选中文字的方法

在ios端默认的长按选择,可以对文字进行复制粘贴。但是在实际开发中,针对一些按钮一般要避免长按时弹出选中文字,或者一些罩层要避免弹出。 这篇文章通过css3实现禁止ios端长按复制选中文字的方法

H5与企业微信jssdk集成

注册企业微信,在应用与小程序栏目中,设置可信域名,配置公众号菜单。可信域名不得不说下,在最初开发时,认为设置并验证后,微信认证接口会实现跨域请求,其实并没有。所以全在H5端还得配合服务端完成票据获取等操作。

H5页面中唤起APP的方法

需要在从APP分享出去的H5页面中,带有一个立即打开的按钮,如果本地安装了app,那么就直接唤起本地的app,如果没有安装,则跳转到下载。这是一个很正常的推广和导流量的策略。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!