JSBridge 原理与实现

实现 Web 与 Native 双向通信的桥接方案

问题

JSBridge 是 Hybrid App 中 Web 与 Native 通信的桥梁。请说明其原理,包括 Native 调用 Web、Web 调用 Native 的实现方式。

解答

整体架构

┌─────────────────────────────────────────┐
│              WebView                     │
│  ┌─────────────────────────────────┐    │
│  │         JavaScript              │    │
│  │    window.JSBridge.call()       │    │
│  └──────────────┬──────────────────┘    │
│                 │                        │
│    ┌────────────▼────────────┐          │
│    │       JSBridge          │          │
│    │  (URL Schema / 注入API)  │          │
│    └────────────┬────────────┘          │
└─────────────────┼───────────────────────┘

┌─────────────────▼───────────────────────┐
│              Native                      │
│         (iOS / Android)                  │
└─────────────────────────────────────────┘

Web 调用 Native

方式一:URL Schema 拦截

// Web 端发起调用
function callNative(method, params, callback) {
  // 生成唯一回调 ID
  const callbackId = `cb_${Date.now()}_${Math.random().toString(36).slice(2)}`
  
  // 注册回调函数
  window.__callbacks = window.__callbacks || {}
  window.__callbacks[callbackId] = callback
  
  // 构造 URL Schema
  const url = `myapp://${method}?params=${encodeURIComponent(JSON.stringify(params))}&callback=${callbackId}`
  
  // 通过 iframe 发起请求(不会引起页面跳转)
  const iframe = document.createElement('iframe')
  iframe.style.display = 'none'
  iframe.src = url
  document.body.appendChild(iframe)
  
  // 移除 iframe
  setTimeout(() => {
    document.body.removeChild(iframe)
  }, 100)
}

// 使用示例
callNative('share', { title: 'Hello', url: 'https://example.com' }, (result) => {
  console.log('分享结果:', result)
})
// iOS 端拦截 URL Schema (WKWebView)
func webView(_ webView: WKWebView, 
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
    guard let url = navigationAction.request.url,
          url.scheme == "myapp" else {
        decisionHandler(.allow)
        return
    }
    
    // 解析方法名和参数
    let method = url.host ?? ""
    let params = parseQueryParams(url)
    
    // 执行对应的 Native 方法
    handleJSCall(method: method, params: params)
    
    decisionHandler(.cancel)
}

方式二:注入 API(推荐)

// Native 端注入全局对象后,Web 端直接调用
// iOS: WKScriptMessageHandler
// Android: addJavascriptInterface

// Web 端封装
class JSBridge {
  static call(method, params = {}) {
    return new Promise((resolve, reject) => {
      const callbackId = `cb_${Date.now()}`
      
      // 注册回调
      window.__bridgeCallbacks = window.__bridgeCallbacks || {}
      window.__bridgeCallbacks[callbackId] = { resolve, reject }
      
      // 调用 Native 注入的方法
      if (window.webkit?.messageHandlers?.nativeBridge) {
        // iOS WKWebView
        window.webkit.messageHandlers.nativeBridge.postMessage({
          method,
          params,
          callbackId
        })
      } else if (window.AndroidBridge) {
        // Android WebView
        window.AndroidBridge.call(JSON.stringify({
          method,
          params,
          callbackId
        }))
      } else {
        reject(new Error('JSBridge not available'))
      }
    })
  }
  
  // 供 Native 调用的回调处理
  static _handleCallback(callbackId, result, error) {
    const callback = window.__bridgeCallbacks?.[callbackId]
    if (callback) {
      error ? callback.reject(error) : callback.resolve(result)
      delete window.__bridgeCallbacks[callbackId]
    }
  }
}

// 使用示例
async function share() {
  try {
    const result = await JSBridge.call('share', {
      title: '分享标题',
      content: '分享内容'
    })
    console.log('分享成功', result)
  } catch (e) {
    console.error('分享失败', e)
  }
}
// Android 端注入 API
public class AndroidBridge {
    private WebView webView;
    
    @JavascriptInterface
    public void call(String jsonStr) {
        try {
            JSONObject json = new JSONObject(jsonStr);
            String method = json.getString("method");
            JSONObject params = json.getJSONObject("params");
            String callbackId = json.getString("callbackId");
            
            // 处理调用
            handleMethod(method, params, callbackId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // 执行回调
    private void callback(String callbackId, Object result) {
        String js = String.format(
            "JSBridge._handleCallback('%s', %s, null)",
            callbackId,
            new Gson().toJson(result)
        );
        webView.evaluateJavascript(js, null);
    }
}

// 注入到 WebView
webView.addJavascriptInterface(new AndroidBridge(), "AndroidBridge");

Native 调用 Web

// Web 端注册方法供 Native 调用
window.JSBridge = window.JSBridge || {}

window.JSBridge.registerHandler = function(name, handler) {
  window.JSBridge._handlers = window.JSBridge._handlers || {}
  window.JSBridge._handlers[name] = handler
}

// Native 调用此方法
window.JSBridge._invokeHandler = function(name, params, callbackId) {
  const handler = window.JSBridge._handlers?.[name]
  if (handler) {
    const result = handler(params)
    // 如果有回调,通知 Native
    if (callbackId) {
      JSBridge.call('_callback', { callbackId, result })
    }
  }
}

// 注册处理函数
JSBridge.registerHandler('updateUserInfo', (data) => {
  console.log('收到用户信息更新:', data)
  // 更新页面
  return { success: true }
})
// iOS 调用 Web 方法
func callJS(method: String, params: [String: Any], callback: ((Any?) -> Void)?) {
    let paramsJson = try? JSONSerialization.data(withJSONObject: params)
    let paramsStr = String(data: paramsJson ?? Data(), encoding: .utf8) ?? "{}"
    
    let js = "window.JSBridge._invokeHandler('\(method)', \(paramsStr), '\(callbackId)')"
    
    webView.evaluateJavaScript(js) { result, error in
        callback?(result)
    }
}

// 使用
callJS(method: "updateUserInfo", params: ["name": "张三", "age": 18]) { result in
    print("JS 返回: \(result)")
}

完整的 JSBridge 封装

// bridge.js - 完整的 JSBridge 实现
(function() {
  const callbacks = {}
  const handlers = {}
  let callbackId = 0
  
  const JSBridge = {
    // Web 调用 Native
    call(method, params = {}) {
      return new Promise((resolve, reject) => {
        const id = `cb_${++callbackId}`
        callbacks[id] = { resolve, reject }
        
        const message = { method, params, callbackId: id }
        
        if (window.webkit?.messageHandlers?.bridge) {
          window.webkit.messageHandlers.bridge.postMessage(message)
        } else if (window.AndroidBridge?.postMessage) {
          window.AndroidBridge.postMessage(JSON.stringify(message))
        } else {
          // 降级到 URL Schema
          this._callBySchema(message)
        }
        
        // 超时处理
        setTimeout(() => {
          if (callbacks[id]) {
            callbacks[id].reject(new Error('Bridge call timeout'))
            delete callbacks[id]
          }
        }, 10000)
      })
    },
    
    // URL Schema 降级方案
    _callBySchema(message) {
      const url = `myapp://bridge?data=${encodeURIComponent(JSON.stringify(message))}`
      const iframe = document.createElement('iframe')
      iframe.style.cssText = 'display:none;width:0;height:0;'
      iframe.src = url
      document.body.appendChild(iframe)
      setTimeout(() => iframe.remove(), 200)
    },
    
    // 注册供 Native 调用的方法
    register(name, handler) {
      handlers[name] = handler
    },
    
    // Native 调用 Web(由 Native 触发)
    _invoke(name, params, cbId) {
      const handler = handlers[name]
      if (handler) {
        Promise.resolve(handler(params)).then(result => {
          if (cbId) {
            this.call('_nativeCallback', { callbackId: cbId, result })
          }
        })
      }
    },
    
    // Native 回调 Web(由 Native 触发)
    _callback(id, result, error) {
      const cb = callbacks[id]
      if (cb) {
        error ? cb.reject(new Error(error)) : cb.resolve(result)
        delete callbacks[id]
      }
    }
  }
  
  window.JSBridge = JSBridge
})()

关键点

  • URL Schema 拦截:通过 iframe 发起自定义协议请求,Native 拦截并解析,兼容性好但效率较低
  • 注入 API:Native 向 WebView 注入全局对象,Web 直接调用,效率高且支持同步返回
  • 回调机制:通过 callbackId 关联请求和响应,实现异步回调
  • Native 调用 Web:通过 evaluateJavaScript 执行 JS 代码,调用预先注册的处理函数
  • 安全考虑:Android 4.2+ 需要 @JavascriptInterface 注解,避免反射漏洞