Mobile Development 15 min read

Bridge Communication Between Native and Webview in Hybrid Development: Methods, Implementation, and Event Handling

This article explains from a frontend perspective how JavaScript and native code communicate in hybrid apps, covering injection and interception bridge methods, their implementation details, SDK initialization, message flow, and native event listening with code examples and compatibility considerations.

ByteFE
ByteFE
ByteFE
Bridge Communication Between Native and Webview in Hybrid Development: Methods, Implementation, and Event Handling

During hybrid development, differences in understanding between frontend and client developers create communication costs and information asymmetry when solving bridge problems. This article approaches the topic from a frontend perspective, describing how bridge methods interact with the client and the intermediate processing involved.

Native and Webview Communication Methods

JavaScript Calls Native Methods

In a Webview, JavaScript can call native methods in three main ways:

Native injects an object into the Webview window that either exposes specific native methods (Android) or receives JavaScript messages (iOS).

Intercept specific URL schemes in the Webview and execute the corresponding native method based on the URL.

Intercept JavaScript functions such as console.log , alert , prompt , or confirm and execute the corresponding native method.

Most mainstream JSSDK implementations use the first two methods, primarily injection with interception as a fallback. The injection method offers better performance and developer experience but is not compatible with all system versions.

On Android, JavascriptInterface before version 4.2 exposed system classes like java.lang.Runtime , creating security risks. On iOS, WKScriptMessageHandler only supports iOS 8.0 and above. Therefore, on lower system versions, the interception method is used.

Native Calls JavaScript Methods

Native can call specific JavaScript methods in the Webview via two ways:

Directly execute JavaScript statements through a URL, e.g., javascript:alert('calling...');

Use platform‑specific methods like evaluateJavascript() to execute JavaScript and obtain a return value, which is the recommended approach.

Calling Native Methods

The general flow for invoking native methods in current JSSDKs is illustrated below:

Calling Compatibility Methods

We refer to the entry point where JavaScript calls a native method or listens to a native event as a compatibility method . Compatibility methods map to a specific native method or event listener based on the host environment.

For example, the toast() compatibility method can be implemented as:

import core from "./core"
import rules from "./toast.rule"

interface JSBridgeRequest { content: string }
interface JSBridgeResponse { code: number }

function toast(params?: JSBridgeRequest, callback?: (_: JSBridgeResponse) => void, options?: { namespace?: string; sdkVersion?: string | number }) {
  return core.pipeCall({ method: "toast", params, callback, rules }) as Promise
}

toast.rules = rules
export default toast

The core entry core.pipeCall() receives several key parameters:

method : the name of the compatibility method.

params : input parameters for the compatibility method.

callback : function invoked after the native side returns.

rules : an array of rule objects that define native method mapping, input/output preprocessing, host ID, and version compatibility.

SDK Bridge Entry

When pipeCall() is entered, it validates the container environment, then uses rules to resolve the real native method name ( realMethod ) and processed parameters ( realParams ). Finally, it calls the injected window.JSBridge.call() .

The callback performs global post‑processing, method‑specific post‑processing (derived from the matched rule), executes the user‑provided callback, and finally triggers the lifecycle hook onInvokeEnd() .

return new Promise((resolve, reject) => {
  this.bridge.call(
    realMethod,
    realParams,
    (realRes) => {
      let res = realRes
      try {
        if (globalPostprocess && typeof globalPostprocess === 'function') {
          res = globalPostprocess(res, { params, env })
        }
        if (rule.postprocess && typeof rule.postprocess === 'function') {
          res = rule.postprocess(res, { params, env })
        }
      } catch (error) {
        if (this.onInvokeEnd) {
          this.onInvokeEnd({ error, config: hookConfig })
        }
        reject(error)
      }
      if (typeof callback === 'function') {
        callback(res)
      }
      resolve(res)
      if (this.onInvokeEnd) {
        this.onInvokeEnd({ response: res, config: hookConfig })
      }
    },
    Object.assign(this.options, options),
  )
})

Calling Bridge Methods

The method window.JSBridge.call() builds a Message object, registers the callback in a global callbackMap using a callbackId , and sends the message to native via window.JSBridge.sendMessageToNative() . If a native injector is present, the message is sent directly; otherwise, it falls back to an iframe URL‑scheme approach.

export interface JavaScriptMessage {
  func: string; // native method name
  params: object;
  __msg_type: JavaScriptMessageType;
  __callback_id?: string;
  __iframe_url?: string;
}

When the native side injects JS2NativeBridge , the SDK creates a nativeMethodInvoker that directly calls the native bridge with a JSON string. If the native implementation is synchronous, the result is returned immediately; otherwise, the native side invokes the callback later.

const nativeMessageJSON = this.nativeMethodInvoker(message);
if (nativeMessageJSON) {
  const nativeMessage = JSON.parse(nativeMessageJSON);
  this.handleMessageFromNative(nativeMessage);
}

Injection‑Based Calls

If the native object is injected, the SDK adds nativeMethodInvoker under window.JSBridge to call native bridge APIs directly.

Interception‑Based Calls

When no injection is detected, the SDK queues messages and uses an iframe to trigger a URL scheme that the native side intercepts.

this.javascriptMessageQueue.push(message);
if (!this.dispatchMessageIFrame) {
  this.tryCreateIFrames();
  return;
}
this.dispatchMessageIFrame.src = `${this.scheme}${this.dispatchMessagePath}`;

Listening to Native Events

The flow for listening to native events follows a similar pattern, using core.pipeEvent() as the entry point.

import core from "./core"
import rules from "./onAppShow.rule"

interface JSBridgeRequest {}
interface JSBridgeResponse {}

interface Subscription {
  remove: () => void
  listener: (_: JSBridgeResponse) => void
}

function onAppShow(callback: (_: JSBridgeResponse) => void, once?: boolean): Subscription {
  return core.pipeEvent({ event: "onAppShow", callback, rules, once })
}

onAppShow.rules = rules
export default onAppShow

core.pipeEvent() processes the event name, callback, and rules, performing global and method‑specific post‑processing before invoking the user callback.

public on(event: string, callback: Callback, once: boolean = false): string {
  if (!event || typeof event !== 'string' || typeof callback !== 'function') {
    return;
  }
  const callbackId = this.registerCallback(event, callback);
  this.eventMap[event] = this.eventMap[event] || {};
  this.eventMap[event][callbackId] = { once };
}

Removing Event Listeners

public off(event: string, callbackId: string): boolean {
  if (!event || typeof event !== 'string') {
    return true;
  }
  const callbackMetaMap = this.eventMap[event];
  if (!callbackMetaMap || typeof callbackMetaMap !== 'object' || !callbackMetaMap.hasOwnProperty(callbackId)) {
    return true;
  }
  this.deregisterCallback(callbackId);
  delete callbackMetaMap[callbackId];
  return true;
}

If You Are Interested...

ByteDance's Dali Smart invites you to submit your resume. The business is growing rapidly with many open positions.

We work on Dali Smart work lights, Dali tutoring apps, and related education products across H5, Flutter, mini‑programs, and various hybrid scenarios. Our team also explores monorepo, micro‑frontend, serverless, and other cutting‑edge frontend technologies, using stacks such as React, TypeScript, and Node.js.

Scan the QR code below for an internal referral code:

First batch of ByteDance Youth Training Camp – Frontend recruitment is now open: https://mp.weixin.qq.com/s/Pw7Ffi1DNfpYsk00f0gx6w

mobileNativesdkJavaScriptWebViewhybridBridge
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.