Mobile Development 11 min read

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 registration, message handling, and call flow between WebView and native code on iOS, providing full code examples and a step‑by‑step process overview.

ByteFE
ByteFE
ByteFE
Understanding JSBridge: Implementation and Communication Between WebView and Native

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 also execute logic on the WebView through jsb.

2. Implementation Methods of JSB

Popular JSBridge implementations mainly achieve communication by intercepting URL requests.

We use the widely‑used WebViewJavascriptBridge as an example to analyze its implementation.

Source code: https://github.com/marcuswestin/WebViewJavascriptBridge

2-1. Registering the Bridge on Native and WebView

During registration, both sides store all 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. Injecting Initialization Code into the 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:

Creates an array WVJBCallbacks and stores the passed callback.

Creates an invisible iframe whose src is https://__bridge_loaded__ .

Sets a timer to remove the iframe.

2-3. Native Side URL Request Interception (iOS WKWebView example)

- (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 mainly:

Intercepts all URL requests and obtains the URL.

Checks whether the URL is the iframe‑triggered bridge URL via isWebViewJavascriptBridgeURL .

If it is a bridge‑loaded URL, injects JavaScript that creates the global WebViewJavascriptBridge object with methods such as registerHandler , callHandler , etc.

If it is a queue‑message URL, processes the queued messages (described later).

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

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;
}

The WebView creates a message object, pushes it into sendMessageQueue , and changes the iframe src to trigger native interception.

2-4-2. Native side flushMessageQueue

- (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.");
        return;
    }
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        if (![message isKindOfClass:[WVJBMessage class]]) { continue; }
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [_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) {};
            }
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            if (!handler) { NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); continue; }
            handler(message[@"data"], responseCallback);
        }
    }
}

If a message contains responseId , it is a callback from native; otherwise, native creates a response callback and invokes the registered handler.

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 });
                };
            } else {
                responseCallback = function(ignoreResponseData) {};
            }
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                handler(message.data, responseCallback);
            }
        }
    }
}

This code matches responses to stored callbacks and invokes the appropriate JavaScript handler.

2-4-4. Process Summary

Native registers the JSBridge.

WebView creates an invisible iframe with src __bridge_load__ .

Native intercepts the request, injects initialization JavaScript, and mounts the bridge object on window .

WebView calls callHandler , adds a callbackId , and changes iframe src.

Native receives the message, creates a response callback, and executes the registered native method.

After native finishes, it calls _handleMessageFromObjC in the WebView, retrieves the stored callback, and executes it.

2-5. Native Calls WebView Capabilities

Native can also invoke WebView methods directly (without iframe tricks). The flow is similar but initiated from native.

Native calls callHandler and adds a callbackId to responseCallback .

Native directly invokes _handleMessageFromObjC in the WebView.

WebView processes the call, generates a message with responseId , pushes it to sendMessageQueue , and updates iframe src to __wvjb_queue_message__ .

Native intercepts the URL change, fetches the message, and executes the corresponding callback.

mobile developmentiOSWebViewBridgeJSBridgeNative Communication
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.