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.
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 toastThe 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 onAppShowcore.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
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.