Understanding JSBridge: Implementation and Communication Between WebView and Native
This article explains what JSBridge is, details its implementation using URL interception and iframe tricks, and walks through the full communication flow between a WebView and native code on iOS, including code examples for registration, message handling, and callbacks.
1. What is JSBridge?
JSBridge is a mechanism for communication between the WebView side and the native side; the WebView can call native capabilities via jsb, and native can execute logic on the WebView via jsb.
2. Implementation of JSBridge
Popular JSBridge implementations mainly achieve communication by intercepting URL requests.
We take the popular WebViewJavascriptBridge as an example to analyze its implementation.
2-1. Register Bridge on native and WebView sides
During registration, both sides store functions in an object.
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
} - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}2-2. Inject initialization code into WebView
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function(){ document.documentElement.removeChild(WVJBIframe) }, 0);
}This code performs the following actions:
(1) Creates an array WVJBCallbacks and stores the passed callback.
(2) Creates an invisible iframe whose src is https://__bridge_loaded__ .
(3) Sets a timer to remove the iframe.
2-3. Native side listens to URL requests
iOS provides UIWebView and WKWebView ; the example uses WKWebView .
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
if (webView != _webView) { return; }
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) {
[strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationResponsePolicyAllow);
}
}This code does the following:
(1) Intercepts all URL requests and obtains the URL.
(2) Checks whether the URL is a bridge‑loaded URL via isWebViewJavascriptBridgeURL (typically by examining the host).
(3) If it is a bridge‑loaded URL, injectJavascriptFile is called, which injects initialization logic and adds global variables and the WebViewJavascriptBridge object to window :
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var sendMessageQueue = [];
var messageHandlers = {};
var responseCallbacks = {};
var uniqueId = 1;(4) If the URL matches isQueueMessageURL , the message‑handling logic (described later) is triggered.
2-4. WebView calls native capabilities
After both sides have registered the bridge, they can invoke each other. The following describes the WebView‑to‑native flow.
2-4-1. WebView side callHandler
When the WebView calls native, it uses callHandler . Example usage and implementation:
bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
console.log("JS received response:", responseData);
});
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}In short, a message object is created, pushed to sendMessageQueue , and the invisible iframe’s src is changed to trigger native interception.
2-4-2. Native side flushMessageQueue
When native detects the iframe src change, it executes WKFlushMessageQueue to fetch the queued messages from the WebView.
- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString *result, NSError *error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
[_base flushMessageQueue:result];
}];
}
- (void)flushMessageQueue:(NSString *)messageQueueString {
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) { responseData = [NSNull null]; }
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) { /* Do nothing */ };
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}Messages containing responseId are replies; messages without it are new calls, for which native creates a responseCallback to send back later.
2-4-3. WebView side handleMessageFromObjC
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var responseCallback;
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) { return; }
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}If a message contains responseId , the stored callback is invoked; otherwise the appropriate handler is called.
2-4-4. Process summary
Summary of the communication flow (diagram omitted):
1. Native registers the JSBridge.
2. WebView creates an invisible iframe with src set to __bridge_load__ .
3. Native intercepts the request, injects initialization code, and mounts the bridge object on window .
4. WebView calls callHandler , adds a callbackId , and changes the iframe src .
5. Native receives the message, creates a responseCallback , and executes the registered native method.
6. After native finishes, it calls _handleMessageFromObjC , which retrieves the stored callback and executes it, completing the round‑trip.
2-5. Native calls WebView capabilities
Native can also invoke WebView methods directly, without using an iframe.
1. Native calls callHandler and adds a callbackId to a responseCallback .
2. Native invokes _handleMessageFromObjC inside the WebView.
3. WebView generates a message containing responseId , pushes it to sendMessageQueue , and changes the iframe src to __wvjb_queue_message__ .
4. Native intercepts the URL change, fetches the message queue, and executes the corresponding callback.
ByteDance ADFE Team
Official account of ByteDance Advertising 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.