实现WebView JSBridge

实现WebView JSBridge

引言

JSBridge是Hybrid应用开发中的核心技术,它架起了JavaScript与Native之间的通信桥梁。虽然市面上有许多开源的JSBridge库,但深入理解其实现原理并自己动手实现一个,不仅能让我们更好地掌握跨端通信的本质,也能根据业务需求进行灵活定制。本文将带你从零开始,完整实现一个功能完善、性能优良的JSBridge框架,涵盖JavaScript端、iOS端和Android端的全部代码。

一、整体架构设计

1.1 架构概览

一个完整的JSBridge系统由三部分组成:

graph TB subgraph JavaScript端 A[JSBridge.js] A1[方法注册] A2[消息队列] A3[回调管理] end subgraph Native桥接层 B[URL拦截器] C[消息解析器] D[方法路由] end subgraph Native功能层 E[相机模块] F[定位模块] G[支付模块] H[分享模块] end A --> A1 A --> A2 A --> A3 A1 --> B A2 --> B B --> C C --> D D --> E D --> F D --> G D --> H E -.回调.-> A3 F -.回调.-> A3 G -.回调.-> A3 H -.回调.-> A3 style A fill:#e3f2fd style B fill:#fff3e0 style D fill:#f3e5f5

1.2 通信协议设计

协议格式定义:

javascript 复制代码
// 标准协议格式
{
  "protocol": "jsbridge",           // 协议标识
  "version": "1.0",                  // 版本号
  "method": "methodName",            // 调用的方法名
  "params": {                        // 参数对象
    "key1": "value1",
    "key2": "value2"
  },
  "callbackId": "cb_1234567890",   // 回调ID
  "timestamp": 1640000000000        // 时间戳
}

URL Schema格式:

css 复制代码
jsbridge://methodName?data=encodeURIComponent(JSON.stringify(protocolData))

1.3 调用流程

sequenceDiagram participant JS as JavaScript participant Queue as 消息队列 participant Bridge as JSBridge participant Native as Native participant Module as 功能模块 JS->>JS: 调用bridge.invoke() JS->>Queue: 消息入队 Queue->>Bridge: 批量取出消息 Bridge->>Native: URL Schema触发 Native->>Native: 解析URL Native->>Module: 调用原生方法 Module->>Module: 执行功能 Module-->>Native: 返回结果 Native-->>Bridge: evaluateJavaScript Bridge-->>JS: 触发回调函数 JS->>JS: 执行业务逻辑

二、JavaScript端完整实现

2.1 核心类设计

JSBridge核心类的完整实现:

javascript 复制代码
class JSBridge {
  constructor(config = {}) {
    // 配置项
    this.config = {
      scheme: config.scheme || 'jsbridge',      // URL Schema
      timeout: config.timeout || 10000,         // 超时时间
      debug: config.debug || false,             // 调试模式
      useQueue: config.useQueue !== false       // 是否使用消息队列
    };

    // 回调函数存储
    this.callbacks = new Map();

    // 回调ID计数器
    this.callbackId = 0;

    // 消息队列
    this.messageQueue = [];

    // 队列处理定时器
    this.queueTimer = null;

    // Native注册的方法
    this.nativeMethods = new Set();

    // 初始化
    this._init();
  }

  /**
   * 初始化
   */
  _init() {
    // 挂载全局回调处理函数
    window._JSBridgeCallback = this._handleCallback.bind(this);

    // 挂载全局方法注册函数
    window._JSBridgeRegisterMethod = this._registerNativeMethod.bind(this);

    // 启动消息队列处理
    if (this.config.useQueue) {
      this._startQueueProcessor();
    }

    this._log('JSBridge initialized');
  }

  /**
   * 调用Native方法
   * @param {string} method - 方法名
   * @param {object} params - 参数
   * @param {function} callback - 回调函数
   * @returns {Promise} Promise对象
   */
  invoke(method, params = {}, callback = null) {
    return new Promise((resolve, reject) => {
      // 生成回调ID
      const callbackId = this._generateCallbackId();

      // 存储回调
      this.callbacks.set(callbackId, {
        resolve,
        reject,
        callback,
        timestamp: Date.now()
      });

      // 设置超时
      this._setCallbackTimeout(callbackId);

      // 构造消息
      const message = this._buildMessage(method, params, callbackId);

      // 发送消息
      this._sendMessage(message);

      this._log('Invoke:', method, params);
    });
  }

  /**
   * 注册JS方法供Native调用
   * @param {string} method - 方法名
   * @param {function} handler - 处理函数
   */
  registerHandler(method, handler) {
    if (typeof handler !== 'function') {
      throw new Error('Handler must be a function');
    }

    this[method] = handler;
    this._log('Register handler:', method);
  }

  /**
   * 批量注册方法
   * @param {object} handlers - 方法对象
   */
  registerHandlers(handlers) {
    Object.keys(handlers).forEach(method => {
      this.registerHandler(method, handlers[method]);
    });
  }

  /**
   * 生成回调ID
   * @returns {string} 回调ID
   */
  _generateCallbackId() {
    return `cb_${Date.now()}_${++this.callbackId}`;
  }

  /**
   * 构造消息对象
   * @param {string} method - 方法名
   * @param {object} params - 参数
   * @param {string} callbackId - 回调ID
   * @returns {object} 消息对象
   */
  _buildMessage(method, params, callbackId) {
    return {
      protocol: 'jsbridge',
      version: '1.0',
      method: method,
      params: params,
      callbackId: callbackId,
      timestamp: Date.now()
    };
  }

  /**
   * 发送消息
   * @param {object} message - 消息对象
   */
  _sendMessage(message) {
    if (this.config.useQueue) {
      // 加入队列
      this.messageQueue.push(message);
    } else {
      // 直接发送
      this._doSend(message);
    }
  }

  /**
   * 实际发送消息
   * @param {object} message - 消息对象
   */
  _doSend(message) {
    const url = this._buildURL(message);

    // 使用iframe触发
    const iframe = document.createElement('iframe');
    iframe.style.cssText = 'display:none;width:0;height:0;';
    iframe.src = url;
    document.documentElement.appendChild(iframe);

    setTimeout(() => {
      document.documentElement.removeChild(iframe);
    }, 100);
  }

  /**
   * 构造URL
   * @param {object} message - 消息对象
   * @returns {string} URL字符串
   */
  _buildURL(message) {
    const data = encodeURIComponent(JSON.stringify(message));
    return `${this.config.scheme}://${message.method}?data=${data}`;
  }

  /**
   * 启动队列处理器
   */
  _startQueueProcessor() {
    this.queueTimer = setInterval(() => {
      if (this.messageQueue.length > 0) {
        // 取出所有消息
        const messages = this.messageQueue.splice(0);

        // 批量发送
        messages.forEach(message => {
          this._doSend(message);
        });
      }
    }, 50); // 每50ms处理一次队列
  }

  /**
   * 停止队列处理器
   */
  _stopQueueProcessor() {
    if (this.queueTimer) {
      clearInterval(this.queueTimer);
      this.queueTimer = null;
    }
  }

  /**
   * 设置回调超时
   * @param {string} callbackId - 回调ID
   */
  _setCallbackTimeout(callbackId) {
    setTimeout(() => {
      const callbackInfo = this.callbacks.get(callbackId);

      if (callbackInfo) {
        this.callbacks.delete(callbackId);

        const error = new Error(`Timeout: ${this.config.timeout}ms`);
        callbackInfo.reject(error);

        this._log('Callback timeout:', callbackId);
      }
    }, this.config.timeout);
  }

  /**
   * 处理Native回调
   * @param {string} callbackId - 回调ID
   * @param {object} response - 响应数据
   */
  _handleCallback(callbackId, response) {
    const callbackInfo = this.callbacks.get(callbackId);

    if (!callbackInfo) {
      this._log('Callback not found:', callbackId);
      return;
    }

    this.callbacks.delete(callbackId);

    // 执行用户回调
    if (callbackInfo.callback) {
      callbackInfo.callback(response);
    }

    // 处理Promise
    if (response.code === 0 || response.success) {
      callbackInfo.resolve(response.data || response);
    } else {
      const error = new Error(response.message || 'Unknown error');
      error.code = response.code;
      callbackInfo.reject(error);
    }

    this._log('Callback executed:', callbackId, response);
  }

  /**
   * 注册Native方法
   * @param {string} method - 方法名
   */
  _registerNativeMethod(method) {
    this.nativeMethods.add(method);
    this._log('Native method registered:', method);
  }

  /**
   * 检查Native方法是否可用
   * @param {string} method - 方法名
   * @returns {boolean} 是否可用
   */
  hasNativeMethod(method) {
    return this.nativeMethods.has(method);
  }

  /**
   * 调试日志
   * @param {...any} args - 日志参数
   */
  _log(...args) {
    if (this.config.debug) {
      console.log('[JSBridge]', ...args);
    }
  }

  /**
   * 销毁实例
   */
  destroy() {
    this._stopQueueProcessor();
    this.callbacks.clear();
    this.messageQueue = [];
    this.nativeMethods.clear();

    delete window._JSBridgeCallback;
    delete window._JSBridgeRegisterMethod;

    this._log('JSBridge destroyed');
  }
}

// 导出单例
const bridge = new JSBridge({
  debug: true,
  useQueue: true
});

window.JSBridge = bridge;

export default bridge;

2.2 便捷方法封装

提供更友好的API接口:

javascript 复制代码
// 常用方法的快捷封装
class JSBridgeHelper {
  constructor(bridge) {
    this.bridge = bridge;
  }

  /**
   * 显示Toast提示
   * @param {string} message - 提示消息
   * @param {number} duration - 持续时间(ms)
   */
  async showToast(message, duration = 2000) {
    return this.bridge.invoke('showToast', { message, duration });
  }

  /**
   * 显示加载框
   * @param {string} message - 提示消息
   */
  async showLoading(message = '加载中...') {
    return this.bridge.invoke('showLoading', { message });
  }

  /**
   * 隐藏加载框
   */
  async hideLoading() {
    return this.bridge.invoke('hideLoading');
  }

  /**
   * 获取用户信息
   */
  async getUserInfo() {
    return this.bridge.invoke('getUserInfo');
  }

  /**
   * 获取位置信息
   * @param {string} type - 坐标类型 wgs84/gcj02
   */
  async getLocation(type = 'wgs84') {
    return this.bridge.invoke('getLocation', { type });
  }

  /**
   * 选择图片
   * @param {object} options - 配置选项
   */
  async chooseImage(options = {}) {
    const defaultOptions = {
      count: 9,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera']
    };

    return this.bridge.invoke('chooseImage', {
      ...defaultOptions,
      ...options
    });
  }

  /**
   * 分享内容
   * @param {object} data - 分享数据
   */
  async share(data) {
    return this.bridge.invoke('share', data);
  }

  /**
   * 调起支付
   * @param {object} payData - 支付数据
   */
  async pay(payData) {
    return this.bridge.invoke('pay', payData);
  }

  /**
   * 设置导航栏标题
   * @param {string} title - 标题文本
   */
  async setNavigationBarTitle(title) {
    return this.bridge.invoke('setNavigationBarTitle', { title });
  }

  /**
   * 关闭当前WebView
   */
  async closeWebView() {
    return this.bridge.invoke('closeWebView');
  }

  /**
   * 打开新的WebView
   * @param {string} url - 页面URL
   */
  async openWebView(url) {
    return this.bridge.invoke('openWebView', { url });
  }
}

// 导出便捷方法
const helper = new JSBridgeHelper(bridge);
export { helper };

2.3 使用示例

实际业务中的调用方式:

javascript 复制代码
import bridge, { helper } from './JSBridge';

// 示例1: 基础调用
async function example1() {
  try {
    const result = await bridge.invoke('getUserInfo');
    console.log('用户信息:', result);
  } catch (error) {
    console.error('获取失败:', error);
  }
}

// 示例2: 使用便捷方法
async function example2() {
  await helper.showLoading('获取位置中...');

  try {
    const location = await helper.getLocation();
    await helper.hideLoading();
    await helper.showToast(`位置: ${location.latitude}, ${location.longitude}`);
  } catch (error) {
    await helper.hideLoading();
    await helper.showToast('获取位置失败');
  }
}

// 示例3: 注册JS方法供Native调用
bridge.registerHandler('onUserLogin', (userData) => {
  console.log('用户登录:', userData);
  // 更新页面状态
  updateUserUI(userData);
});

bridge.registerHandler('onNetworkChange', (networkInfo) => {
  console.log('网络状态变化:', networkInfo);
  // 处理网络变化
  handleNetworkChange(networkInfo);
});

// 示例4: 批量注册方法
bridge.registerHandlers({
  onPageResume: () => {
    console.log('页面恢复');
    refreshPageData();
  },
  onPagePause: () => {
    console.log('页面暂停');
    savePageState();
  },
  onAppBackground: () => {
    console.log('应用进入后台');
  }
});

// 示例5: 图片选择和上传
async function chooseAndUploadImage() {
  try {
    // 选择图片
    const images = await helper.chooseImage({
      count: 1,
      sourceType: ['camera']
    });

    // 显示加载
    await helper.showLoading('上传中...');

    // 上传到服务器
    const uploadResult = await uploadToServer(images[0]);

    await helper.hideLoading();
    await helper.showToast('上传成功');

    return uploadResult;
  } catch (error) {
    await helper.hideLoading();
    await helper.showToast('操作失败');
    throw error;
  }
}

三、iOS端实现(WKWebView)

3.1 JSBridge桥接类

Objective-C实现(用JavaScript语法描述):

javascript 复制代码
// JSBridgeManager.h/m 伪代码

class JSBridgeManager {
  constructor(webView) {
    this.webView = webView;
    this.handlers = new Map();
    this._setupWebView();
  }

  /**
   * 配置WebView
   */
  _setupWebView() {
    // 设置导航代理
    this.webView.navigationDelegate = this;

    // 注入初始化脚本
    const initScript = this._getInitScript();
    const userScript = new WKUserScript(
      initScript,
      WKUserScriptInjectionTime.AtDocumentStart,
      false
    );
    this.webView.configuration.userContentController.addUserScript(userScript);

    // 注册消息处理器
    this.webView.configuration.userContentController.addScriptMessageHandler(
      this,
      'jsBridge'
    );
  }

  /**
   * 获取初始化脚本
   */
  _getInitScript() {
    return `
      (function() {
        // 注册所有可用的Native方法
        const nativeMethods = ${JSON.stringify(Array.from(this.handlers.keys()))};

        nativeMethods.forEach(method => {
          if (window._JSBridgeRegisterMethod) {
            window._JSBridgeRegisterMethod(method);
          }
        });

        console.log('[JSBridge] Native methods registered:', nativeMethods);
      })();
    `;
  }

  /**
   * WKNavigationDelegate - 拦截URL请求
   */
  decidePolicyForNavigationAction(webView, navigationAction, decisionHandler) {
    const url = navigationAction.request.URL.absoluteString;

    // 检查是否是JSBridge协议
    if (url.startsWith('jsbridge://')) {
      // 拦截并处理
      this._handleJSBridgeURL(url);

      // 取消导航
      decisionHandler(WKNavigationActionPolicy.Cancel);
    } else {
      // 允许导航
      decisionHandler(WKNavigationActionPolicy.Allow);
    }
  }

  /**
   * 处理JSBridge URL
   */
  _handleJSBridgeURL(urlString) {
    try {
      // 解析URL
      const url = new URL(urlString);
      const method = url.hostname;
      const dataParam = url.searchParams.get('data');

      if (!dataParam) {
        console.error('[JSBridge] No data parameter');
        return;
      }

      // 解析消息
      const message = JSON.parse(decodeURIComponent(dataParam));

      // 处理请求
      this._handleMessage(message);
    } catch (error) {
      console.error('[JSBridge] Parse URL error:', error);
    }
  }

  /**
   * WKScriptMessageHandler - 接收JS消息
   */
  userContentController(userContentController, didReceiveScriptMessage) {
    const message = didReceiveScriptMessage.body;
    this._handleMessage(message);
  }

  /**
   * 处理消息
   */
  async _handleMessage(message) {
    const { method, params, callbackId } = message;

    console.log('[JSBridge] Handle message:', method, params);

    // 查找处理器
    const handler = this.handlers.get(method);

    if (!handler) {
      this._callbackToJS(callbackId, {
        code: -1,
        message: `Method not found: ${method}`
      });
      return;
    }

    try {
      // 执行处理器
      const result = await handler(params);

      // 回调成功
      this._callbackToJS(callbackId, {
        code: 0,
        data: result
      });
    } catch (error) {
      // 回调失败
      this._callbackToJS(callbackId, {
        code: error.code || -1,
        message: error.message
      });
    }
  }

  /**
   * 回调给JS
   */
  _callbackToJS(callbackId, response) {
    if (!callbackId) return;

    const script = `
      window._JSBridgeCallback(
        '${callbackId}',
        ${JSON.stringify(response)}
      );
    `;

    this.webView.evaluateJavaScript(script, (result, error) => {
      if (error) {
        console.error('[JSBridge] Callback error:', error);
      }
    });
  }

  /**
   * 注册Native方法
   */
  registerHandler(method, handler) {
    this.handlers.set(method, handler);
    console.log('[JSBridge] Register handler:', method);
  }

  /**
   * JS调用Native方法
   */
  callHandler(method, params) {
    return new Promise((resolve, reject) => {
      const callbackId = `native_cb_${Date.now()}`;

      // 注册临时回调
      window[callbackId] = (response) => {
        delete window[callbackId];

        if (response.code === 0) {
          resolve(response.data);
        } else {
          reject(new Error(response.message));
        }
      };

      // 调用JS方法
      const script = `
        (function() {
          if (window.JSBridge && window.JSBridge['${method}']) {
            const result = window.JSBridge['${method}'](${JSON.stringify(params)});
            window['${callbackId}']({ code: 0, data: result });
          } else {
            window['${callbackId}']({
              code: -1,
              message: 'Method not found'
            });
          }
        })();
      `;

      this.webView.evaluateJavaScript(script, null);
    });
  }
}

// 使用示例
const bridgeManager = new JSBridgeManager(webView);

// 注册方法
bridgeManager.registerHandler('getUserInfo', async (params) => {
  // 获取用户信息
  const userInfo = await UserManager.getCurrentUser();
  return userInfo;
});

bridgeManager.registerHandler('getLocation', async (params) => {
  // 获取位置
  const location = await LocationManager.getCurrentLocation(params.type);
  return {
    latitude: location.coordinate.latitude,
    longitude: location.coordinate.longitude,
    accuracy: location.horizontalAccuracy
  };
});

bridgeManager.registerHandler('chooseImage', async (params) => {
  // 打开相册
  const images = await ImagePicker.pickImages(params);
  return images;
});

3.2 具体功能模块实现

以定位功能为例:

javascript 复制代码
// LocationHandler.swift 伪代码

class LocationHandler {
  constructor() {
    this.locationManager = new CLLocationManager();
    this.locationManager.delegate = this;
    this.resolver = null;
  }

  async getLocation(params) {
    return new Promise((resolve, reject) => {
      this.resolver = { resolve, reject };

      // 检查权限
      const authStatus = CLLocationManager.authorizationStatus();

      if (authStatus === 'notDetermined') {
        // 请求权限
        this.locationManager.requestWhenInUseAuthorization();
      } else if (authStatus === 'denied' || authStatus === 'restricted') {
        reject(new Error('定位权限被拒绝'));
        return;
      }

      // 配置定位管理器
      this.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
      this.locationManager.distanceFilter = 10;

      // 开始定位
      this.locationManager.startUpdatingLocation();

      // 设置超时
      setTimeout(() => {
        this.locationManager.stopUpdatingLocation();
        reject(new Error('获取位置超时'));
      }, 10000);
    });
  }

  // CLLocationManagerDelegate
  didUpdateLocations(manager, locations) {
    const location = locations[locations.length - 1];

    // 停止定位
    manager.stopUpdatingLocation();

    // 转换坐标系(如果需要)
    const coordinate = this._convertCoordinate(
      location.coordinate,
      this.params?.type
    );

    // 返回结果
    if (this.resolver) {
      this.resolver.resolve({
        latitude: coordinate.latitude,
        longitude: coordinate.longitude,
        accuracy: location.horizontalAccuracy,
        altitude: location.altitude,
        speed: location.speed
      });
      this.resolver = null;
    }
  }

  didFailWithError(manager, error) {
    manager.stopUpdatingLocation();

    if (this.resolver) {
      this.resolver.reject(error);
      this.resolver = null;
    }
  }

  _convertCoordinate(coord, type) {
    if (type === 'gcj02') {
      // WGS84转GCJ02(国测局坐标)
      return CoordinateConverter.wgs84ToGcj02(coord);
    }
    return coord;
  }
}

四、Android端实现(WebView)

4.1 JSBridge桥接类

Kotlin实现(用JavaScript语法描述):

javascript 复制代码
// JSBridgeManager.kt 伪代码

class JSBridgeManager {
  constructor(webView, context) {
    this.webView = webView;
    this.context = context;
    this.handlers = new Map();
    this._setupWebView();
  }

  /**
   * 配置WebView
   */
  _setupWebView() {
    const settings = this.webView.settings;

    // 启用JavaScript
    settings.javaScriptEnabled = true;

    // 允许文件访问
    settings.allowFileAccess = true;

    // 设置缓存模式
    settings.cacheMode = WebSettings.LOAD_DEFAULT;

    // 支持缩放
    settings.setSupportZoom(true);
    settings.builtInZoomControls = true;
    settings.displayZoomControls = false;

    // DOM Storage
    settings.domStorageEnabled = true;

    // 设置WebViewClient
    this.webView.webViewClient = new CustomWebViewClient(this);

    // 设置WebChromeClient
    this.webView.webChromeClient = new CustomWebChromeClient(this);

    // 添加JavaScript接口(可选,用于更快速的通信)
    this.webView.addJavascriptInterface(
      new AndroidJSInterface(this),
      'AndroidBridge'
    );
  }

  /**
   * 页面加载完成后注入初始化脚本
   */
  onPageFinished(url) {
    const initScript = `
      (function() {
        console.log('[JSBridge] Page loaded, initializing...');

        // 通知Native已经ready
        if (window.AndroidBridge) {
          window.AndroidBridge.onJSBridgeReady();
        }
      })();
    `;

    this._evaluateJavaScript(initScript);

    // 注册所有Native方法
    this._registerNativeMethods();
  }

  /**
   * 注册Native方法到JS
   */
  _registerNativeMethods() {
    const methods = Array.from(this.handlers.keys());
    const script = `
      (function() {
        const methods = ${JSON.stringify(methods)};
        methods.forEach(method => {
          if (window._JSBridgeRegisterMethod) {
            window._JSBridgeRegisterMethod(method);
          }
        });
      })();
    `;

    this._evaluateJavaScript(script);
  }

  /**
   * 自定义WebViewClient
   */
  CustomWebViewClient = class {
    constructor(manager) {
      this.manager = manager;
    }

    shouldOverrideUrlLoading(view, request) {
      const url = request.url.toString();

      // 拦截JSBridge协议
      if (url.startsWith('jsbridge://')) {
        this.manager._handleJSBridgeURL(url);
        return true;  // 拦截
      }

      return false;  // 正常加载
    }

    onPageFinished(view, url) {
      this.manager.onPageFinished(url);
    }
  }

  /**
   * 处理JSBridge URL
   */
  _handleJSBridgeURL(urlString) {
    // 在主线程处理
    this._runOnUiThread(() => {
      try {
        const uri = Uri.parse(urlString);
        const method = uri.host;
        const dataParam = uri.getQueryParameter('data');

        if (!dataParam) {
          console.error('[JSBridge] No data parameter');
          return;
        }

        // 解析消息
        const message = JSON.parse(decodeURIComponent(dataParam));

        // 处理消息
        this._handleMessage(message);
      } catch (error) {
        console.error('[JSBridge] Parse URL error:', error);
      }
    });
  }

  /**
   * Android JS接口类
   */
  AndroidJSInterface = class {
    constructor(manager) {
      this.manager = manager;
    }

    @JavascriptInterface
    postMessage(messageJson) {
      try {
        const message = JSON.parse(messageJson);
        this.manager._handleMessage(message);
      } catch (error) {
        console.error('[JSBridge] Parse message error:', error);
      }
    }

    @JavascriptInterface
    onJSBridgeReady() {
      console.log('[JSBridge] JS Bridge is ready');
      this.manager._registerNativeMethods();
    }
  }

  /**
   * 处理消息
   */
  async _handleMessage(message) {
    const { method, params, callbackId } = message;

    console.log('[JSBridge] Handle message:', method, params);

    // 查找处理器
    const handler = this.handlers.get(method);

    if (!handler) {
      this._callbackToJS(callbackId, {
        code: -1,
        message: `Method not found: ${method}`
      });
      return;
    }

    try {
      // 执行处理器
      const result = await handler(params);

      // 回调成功
      this._callbackToJS(callbackId, {
        code: 0,
        data: result
      });
    } catch (error) {
      // 回调失败
      this._callbackToJS(callbackId, {
        code: error.code || -1,
        message: error.message
      });
    }
  }

  /**
   * 回调给JS
   */
  _callbackToJS(callbackId, response) {
    if (!callbackId) return;

    const script = `
      window._JSBridgeCallback(
        '${callbackId}',
        ${JSON.stringify(response)}
      );
    `;

    this._evaluateJavaScript(script);
  }

  /**
   * 执行JavaScript
   */
  _evaluateJavaScript(script) {
    this._runOnUiThread(() => {
      if (Build.VERSION.SDK_INT >= 19) {
        // Android 4.4+
        this.webView.evaluateJavascript(script, null);
      } else {
        // Android 4.4以下
        this.webView.loadUrl(`javascript:${script}`);
      }
    });
  }

  /**
   * 在主线程运行
   */
  _runOnUiThread(action) {
    if (Looper.myLooper() === Looper.getMainLooper()) {
      action();
    } else {
      new Handler(Looper.getMainLooper()).post(action);
    }
  }

  /**
   * 注册Native方法
   */
  registerHandler(method, handler) {
    this.handlers.set(method, handler);
    console.log('[JSBridge] Register handler:', method);
  }

  /**
   * Native调用JS方法
   */
  callHandler(method, params) {
    return new Promise((resolve, reject) => {
      const callbackId = `native_cb_${Date.now()}`;

      // 构造调用脚本
      const script = `
        (function() {
          if (window.JSBridge && window.JSBridge['${method}']) {
            try {
              const result = window.JSBridge['${method}'](${JSON.stringify(params)});
              window.AndroidBridge.handleNativeCallback(
                '${callbackId}',
                JSON.stringify({ code: 0, data: result })
              );
            } catch (error) {
              window.AndroidBridge.handleNativeCallback(
                '${callbackId}',
                JSON.stringify({ code: -1, message: error.message })
              );
            }
          } else {
            window.AndroidBridge.handleNativeCallback(
              '${callbackId}',
              JSON.stringify({ code: -1, message: 'Method not found' })
            );
          }
        })();
      `;

      // 存储回调
      this.nativeCallbacks.set(callbackId, { resolve, reject });

      // 执行脚本
      this._evaluateJavaScript(script);

      // 设置超时
      setTimeout(() => {
        if (this.nativeCallbacks.has(callbackId)) {
          this.nativeCallbacks.delete(callbackId);
          reject(new Error('Timeout'));
        }
      }, 10000);
    });
  }
}

// 使用示例
const bridgeManager = new JSBridgeManager(webView, context);

// 注册方法
bridgeManager.registerHandler('getUserInfo', async (params) => {
  const userInfo = await UserManager.getCurrentUser();
  return userInfo;
});

bridgeManager.registerHandler('getLocation', async (params) => {
  const location = await LocationManager.getLocation(context, params);
  return location;
});

bridgeManager.registerHandler('showToast', async (params) => {
  Toast.makeText(context, params.message, Toast.LENGTH_SHORT).show();
  return { success: true };
});

4.2 权限处理

Android权限请求示例:

javascript 复制代码
// PermissionHandler.kt 伪代码

class PermissionHandler {
  constructor(activity) {
    this.activity = activity;
    this.pendingRequest = null;
  }

  async requestPermission(permission) {
    return new Promise((resolve, reject) => {
      // 检查权限
      if (ContextCompat.checkSelfPermission(this.activity, permission)
          === PackageManager.PERMISSION_GRANTED) {
        resolve(true);
        return;
      }

      // 请求权限
      this.pendingRequest = { permission, resolve, reject };

      ActivityCompat.requestPermissions(
        this.activity,
        [permission],
        PERMISSION_REQUEST_CODE
      );
    });
  }

  onRequestPermissionsResult(requestCode, permissions, grantResults) {
    if (requestCode !== PERMISSION_REQUEST_CODE) return;
    if (!this.pendingRequest) return;

    const granted = grantResults.length > 0
      && grantResults[0] === PackageManager.PERMISSION_GRANTED;

    if (granted) {
      this.pendingRequest.resolve(true);
    } else {
      this.pendingRequest.reject(new Error('Permission denied'));
    }

    this.pendingRequest = null;
  }
}

// 定位功能使用权限
bridgeManager.registerHandler('getLocation', async (params) => {
  try {
    // 请求权限
    await permissionHandler.requestPermission(
      Manifest.permission.ACCESS_FINE_LOCATION
    );

    // 获取位置
    const location = await LocationManager.getLocation(context);
    return location;
  } catch (error) {
    throw new Error('需要定位权限');
  }
});

五、通信协议优化

5.1 批量消息处理

优化消息队列机制:

javascript 复制代码
class MessageQueue {
  constructor(bridge) {
    this.bridge = bridge;
    this.queue = [];
    this.processing = false;
    this.batchSize = 10;        // 批量处理数量
    this.batchDelay = 50;       // 批量延迟(ms)
    this.timer = null;
  }

  /**
   * 添加消息到队列
   */
  enqueue(message) {
    this.queue.push(message);

    // 启动批处理
    if (!this.timer) {
      this.timer = setTimeout(() => {
        this.process();
      }, this.batchDelay);
    }
  }

  /**
   * 处理队列
   */
  process() {
    if (this.processing || this.queue.length === 0) {
      return;
    }

    this.processing = true;
    this.timer = null;

    // 取出一批消息
    const batch = this.queue.splice(0, this.batchSize);

    // 批量发送
    this._sendBatch(batch);

    this.processing = false;

    // 继续处理剩余消息
    if (this.queue.length > 0) {
      this.timer = setTimeout(() => {
        this.process();
      }, this.batchDelay);
    }
  }

  /**
   * 批量发送
   */
  _sendBatch(messages) {
    // 合并为一个请求
    const batchData = {
      protocol: 'jsbridge',
      version: '1.0',
      type: 'batch',
      messages: messages,
      timestamp: Date.now()
    };

    const url = `jsbridge://batch?data=${encodeURIComponent(
      JSON.stringify(batchData)
    )}`;

    // 发送
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);

    setTimeout(() => {
      document.body.removeChild(iframe);
    }, 100);
  }

  /**
   * 清空队列
   */
  clear() {
    this.queue = [];
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }
}

5.2 数据压缩

大数据传输优化:

javascript 复制代码
class DataCompressor {
  /**
   * 压缩数据
   */
  static compress(data) {
    const json = JSON.stringify(data);

    // 如果数据小于1KB,不压缩
    if (json.length < 1024) {
      return {
        compressed: false,
        data: json
      };
    }

    // 使用LZ压缩算法
    const compressed = LZString.compressToBase64(json);

    return {
      compressed: true,
      data: compressed,
      originalSize: json.length,
      compressedSize: compressed.length
    };
  }

  /**
   * 解压数据
   */
  static decompress(compressedData) {
    if (!compressedData.compressed) {
      return JSON.parse(compressedData.data);
    }

    const decompressed = LZString.decompressFromBase64(compressedData.data);
    return JSON.parse(decompressed);
  }
}

// 在JSBridge中使用
class JSBridgeWithCompression extends JSBridge {
  _buildMessage(method, params, callbackId) {
    const message = super._buildMessage(method, params, callbackId);

    // 压缩参数
    if (params && Object.keys(params).length > 0) {
      message.params = DataCompressor.compress(params);
    }

    return message;
  }

  _handleCallback(callbackId, response) {
    // 解压响应数据
    if (response.data && response.data.compressed) {
      response.data = DataCompressor.decompress(response.data);
    }

    super._handleCallback(callbackId, response);
  }
}

六、错误处理与调试

6.1 错误处理机制

javascript 复制代码
class ErrorHandler {
  constructor(bridge) {
    this.bridge = bridge;
    this.errorListeners = [];
  }

  /**
   * 添加错误监听器
   */
  onError(listener) {
    this.errorListeners.push(listener);
  }

  /**
   * 触发错误
   */
  triggerError(error, context) {
    const errorInfo = {
      message: error.message,
      code: error.code,
      stack: error.stack,
      context: context,
      timestamp: Date.now()
    };

    // 通知所有监听器
    this.errorListeners.forEach(listener => {
      try {
        listener(errorInfo);
      } catch (e) {
        console.error('[ErrorHandler] Listener error:', e);
      }
    });

    // 上报错误
    this._reportError(errorInfo);
  }

  /**
   * 上报错误
   */
  _reportError(errorInfo) {
    // 上报到监控系统
    if (this.bridge.hasNativeMethod('reportError')) {
      this.bridge.invoke('reportError', errorInfo).catch(() => {
        // 上报失败,使用本地存储
        this._saveToLocal(errorInfo);
      });
    }
  }

  /**
   * 保存到本地
   */
  _saveToLocal(errorInfo) {
    try {
      const errors = JSON.parse(localStorage.getItem('jsbridge_errors') || '[]');
      errors.push(errorInfo);

      // 最多保存100条
      if (errors.length > 100) {
        errors.splice(0, errors.length - 100);
      }

      localStorage.setItem('jsbridge_errors', JSON.stringify(errors));
    } catch (e) {
      console.error('[ErrorHandler] Save to local error:', e);
    }
  }
}

// 使用错误处理
const errorHandler = new ErrorHandler(bridge);

errorHandler.onError((error) => {
  console.error('[JSBridge Error]', error);

  // 显示用户友好的提示
  if (error.code === 'PERMISSION_DENIED') {
    alert('需要相应权限才能使用此功能');
  } else if (error.code === 'TIMEOUT') {
    alert('操作超时,请重试');
  } else {
    alert('操作失败,请稍后重试');
  }
});

6.2 调试工具

开发环境下的调试辅助:

javascript 复制代码
class JSBridgeDebugger {
  constructor(bridge) {
    this.bridge = bridge;
    this.enabled = false;
    this.logs = [];
  }

  /**
   * 启用调试模式
   */
  enable() {
    this.enabled = true;
    this._injectDebugPanel();
    this._interceptMethods();
  }

  /**
   * 注入调试面板
   */
  _injectDebugPanel() {
    const panel = document.createElement('div');
    panel.id = 'jsbridge-debug-panel';
    panel.innerHTML = `
      <div style="position:fixed;bottom:0;left:0;right:0;
                  background:#000;color:#0f0;padding:10px;
                  max-height:200px;overflow-y:auto;
                  font-size:12px;z-index:99999;">
        <div id="jsbridge-logs"></div>
        <button onclick="document.getElementById('jsbridge-debug-panel').remove()">
          关闭调试
        </button>
      </div>
    `;
    document.body.appendChild(panel);
  }

  /**
   * 拦截方法调用
   */
  _interceptMethods() {
    const originalInvoke = this.bridge.invoke.bind(this.bridge);

    this.bridge.invoke = (method, params, callback) => {
      const startTime = Date.now();

      this.log('→ CALL', method, params);

      return originalInvoke(method, params, callback)
        .then(result => {
          const duration = Date.now() - startTime;
          this.log('← SUCCESS', method, result, `${duration}ms`);
          return result;
        })
        .catch(error => {
          const duration = Date.now() - startTime;
          this.log('← ERROR', method, error.message, `${duration}ms`);
          throw error;
        });
    };
  }

  /**
   * 记录日志
   */
  log(...args) {
    const logEntry = {
      timestamp: new Date().toLocaleTimeString(),
      args: args
    };

    this.logs.push(logEntry);

    // 显示在面板上
    const logsDiv = document.getElementById('jsbridge-logs');
    if (logsDiv) {
      const logLine = document.createElement('div');
      logLine.textContent = `[${logEntry.timestamp}] ${args.map(a =>
        typeof a === 'object' ? JSON.stringify(a) : a
      ).join(' ')}`;
      logsDiv.appendChild(logLine);
      logsDiv.scrollTop = logsDiv.scrollHeight;
    }

    console.log('[JSBridge Debug]', ...args);
  }

  /**
   * 导出日志
   */
  exportLogs() {
    const blob = new Blob([JSON.stringify(this.logs, null, 2)], {
      type: 'application/json'
    });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = `jsbridge-logs-${Date.now()}.json`;
    a.click();

    URL.revokeObjectURL(url);
  }
}

// 在开发环境启用调试
if (process.env.NODE_ENV === 'development') {
  const debugger = new JSBridgeDebugger(bridge);
  debugger.enable();

  // 暴露到全局,方便控制台调用
  window.JSBridgeDebugger = debugger;
}

七、完整使用示例

7.1 实战场景:用户登录流程

javascript 复制代码
/**
 * 用户登录完整流程
 */
class LoginManager {
  constructor(bridge) {
    this.bridge = bridge;
  }

  /**
   * 执行登录
   */
  async login(username, password) {
    try {
      // 1. 显示加载
      await this.bridge.invoke('showLoading', {
        message: '登录中...'
      });

      // 2. 调用登录接口
      const loginResult = await this._callLoginAPI(username, password);

      // 3. 保存用户信息到Native
      await this.bridge.invoke('saveUserInfo', {
        userInfo: loginResult.userInfo,
        token: loginResult.token
      });

      // 4. 设置导航栏
      await this.bridge.invoke('setNavigationBarTitle', {
        title: `欢迎, ${loginResult.userInfo.nickname}`
      });

      // 5. 隐藏加载
      await this.bridge.invoke('hideLoading');

      // 6. 显示成功提示
      await this.bridge.invoke('showToast', {
        message: '登录成功',
        duration: 2000
      });

      // 7. 跳转到首页
      await this.bridge.invoke('navigateTo', {
        url: '/pages/home/index'
      });

      return loginResult;
    } catch (error) {
      // 隐藏加载
      await this.bridge.invoke('hideLoading');

      // 显示错误提示
      await this.bridge.invoke('showToast', {
        message: error.message || '登录失败',
        duration: 2000
      });

      throw error;
    }
  }

  /**
   * 调用登录API
   */
  async _callLoginAPI(username, password) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ username, password })
    });

    if (!response.ok) {
      throw new Error('登录失败');
    }

    return response.json();
  }

  /**
   * 退出登录
   */
  async logout() {
    try {
      // 1. 确认退出
      const confirmed = await this.bridge.invoke('showModal', {
        title: '提示',
        content: '确定要退出登录吗?',
        showCancel: true
      });

      if (!confirmed.confirm) {
        return;
      }

      // 2. 清除用户信息
      await this.bridge.invoke('clearUserInfo');

      // 3. 跳转到登录页
      await this.bridge.invoke('navigateTo', {
        url: '/pages/login/index'
      });
    } catch (error) {
      console.error('退出登录失败:', error);
    }
  }
}

// 使用
const loginManager = new LoginManager(bridge);

// 绑定登录按钮
document.getElementById('loginBtn').addEventListener('click', async () => {
  const username = document.getElementById('username').value;
  const password = document.getElementById('password').value;

  try {
    await loginManager.login(username, password);
  } catch (error) {
    console.error('登录失败:', error);
  }
});

7.2 实战场景:图片上传

javascript 复制代码
/**
 * 图片上传管理器
 */
class ImageUploadManager {
  constructor(bridge) {
    this.bridge = bridge;
  }

  /**
   * 选择并上传图片
   */
  async chooseAndUpload(options = {}) {
    try {
      // 1. 选择图片
      const images = await this.bridge.invoke('chooseImage', {
        count: options.count || 1,
        sizeType: options.sizeType || ['compressed'],
        sourceType: options.sourceType || ['album', 'camera']
      });

      if (!images || images.length === 0) {
        return [];
      }

      // 2. 显示上传进度
      await this.bridge.invoke('showLoading', {
        message: '上传中 0%'
      });

      // 3. 逐个上传
      const uploadedUrls = [];

      for (let i = 0; i < images.length; i++) {
        const localPath = images[i];

        // 更新进度
        const progress = Math.floor(((i + 1) / images.length) * 100);
        await this.bridge.invoke('updateLoading', {
          message: `上传中 ${progress}%`
        });

        // 上传单张图片
        const url = await this._uploadSingleImage(localPath);
        uploadedUrls.push(url);
      }

      // 4. 隐藏加载
      await this.bridge.invoke('hideLoading');

      // 5. 显示成功提示
      await this.bridge.invoke('showToast', {
        message: '上传成功',
        duration: 1500
      });

      return uploadedUrls;
    } catch (error) {
      await this.bridge.invoke('hideLoading');
      await this.bridge.invoke('showToast', {
        message: '上传失败',
        duration: 2000
      });
      throw error;
    }
  }

  /**
   * 上传单张图片
   */
  async _uploadSingleImage(localPath) {
    // 方式1: 通过Native上传(推荐)
    if (this.bridge.hasNativeMethod('uploadImage')) {
      const result = await this.bridge.invoke('uploadImage', {
        filePath: localPath,
        name: 'file',
        url: 'https://api.example.com/upload'
      });
      return result.url;
    }

    // 方式2: 通过H5上传
    const base64 = await this.bridge.invoke('getImageBase64', {
      filePath: localPath
    });

    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        image: base64
      })
    });

    const result = await response.json();
    return result.url;
  }
}

// 使用
const uploadManager = new ImageUploadManager(bridge);

document.getElementById('uploadBtn').addEventListener('click', async () => {
  try {
    const urls = await uploadManager.chooseAndUpload({
      count: 3
    });

    console.log('上传成功,图片URL:', urls);

    // 显示图片
    displayImages(urls);
  } catch (error) {
    console.error('上传失败:', error);
  }
});

八、总结

通过本文的详细讲解,我们完整实现了一个功能完善的JSBridge框架,涵盖了以下核心内容:

核心要点

  1. 架构设计: 清晰的分层架构,职责明确
  2. JavaScript端: 完整的类封装、消息队列、回调管理
  3. iOS端实现: WKWebView的URL拦截和消息处理
  4. Android端实现: WebView的多种通信方式
  5. 协议优化: 批量处理、数据压缩
  6. 错误处理: 完善的错误捕获和上报机制
  7. 调试工具: 开发环境下的调试辅助

最佳实践

  • 使用消息队列避免频繁通信
  • 合理设置超时时间
  • 完善的错误处理机制
  • 详细的日志记录
  • 跨平台API统一
  • 安全的权限校验

进阶方向

  1. 性能监控: 添加性能埋点,监控调用耗时
  2. 离线缓存: 支持离线调用和数据缓存
  3. 插件化: 支持动态注册和卸载功能模块
  4. TypeScript: 使用TypeScript增强类型安全
  5. 单元测试: 编写完整的测试用例

掌握JSBridge的实现原理和最佳实践,不仅能帮助我们更好地进行Hybrid开发,也为深入理解跨端通信机制打下坚实基础。

参考资料

相关推荐
JS_GGbond2 小时前
揭秘微信扫码登录:那个小绿框背后的魔法
前端
C_心欲无痕2 小时前
vue3 - 响应式数ref与reactive的深度解析
前端·javascript·vue.js
全栈老石2 小时前
TypeScript 中 Type 和 Interface 傻傻分不清?看完这篇就不纠结了
前端·typescript
沈千秋.2 小时前
xss.pwnfunction.com闯关(1~6)
java·前端·xss
浪浪山_大橙子2 小时前
吃透 CSS 常用函数:从布局到美化,18 个高频函数让样式写得又快又优雅
前端
我是ed.2 小时前
Vue3 图片标注插件 AILabel
前端·vue3·标注·ailabel
晚星star2 小时前
《深入浅出 Node.js》第四章:异步编程 详细总结
前端·node.js
无心使然2 小时前
vant实现自定义日期时间选择器(年月日时分秒)
前端·vue.js