1 Introduction
Hybrid mobile development has become the mainstream choice for balancing multi-platform development efficiency with native experience. While WebView hosts Web UI, it needs access to native capabilities like camera, geolocation, etc. However, JavaScript and Native code run in separate runtimes, making direct mutual invocation impossible. JSBridge is the core bridge that solves this problem—it defines a reliable communication protocol that allows JavaScript to call native functions, and also allows Native to actively notify JavaScript of events.
This article explains the communication principles of JSBridge from scratch, and gradually implements a simple Bridge that supports bidirectional invocation, covering Android integration and common pitfalls. After reading this article, you will master the injection API implementation of JSBridge and be able to build or improve a Bridge solution independently in your project.
2 What is JSBridge? Why is it needed?
2.1 Core Concepts and Background
JSBridge is a communication middleware that runs in the WebView environment. It allows JavaScript to call methods of native code, and also allows native code to execute JavaScript functions. In hybrid development, web pages use HTML/CSS/JavaScript to build UI, offering advantages like rapid iteration and cross-platform reuse; but some system-level functions (e.g., camera, gyroscope, file selection) can only be accessed via native APIs.
Through JSBridge, web pages can use these capabilities as if they were calling local functions, while the native layer can also actively send data back after page loading is complete or when the user interacts.
The history of JSBridge can be traced back to desktop software embedding web UI into native functionality, and it became popular on mobile with the rise of Hybrid App frameworks like Cordova and PhoneGap. Today, not only traditional hybrid solutions rely on it, but also React Native, WeChat Mini Programs, etc., adopt similar bridging ideas, though the communication underlying layer is managed by different runtimes. Understanding JSBridge helps in mastering any cross-platform technology.
2.2 Basic Communication Principles
The communication modes of JSBridge are mainly divided into two types:
- JavaScript calling Native: There are two mainstream implementation methods. The first is API injection, where the native side injects a native object into the JavaScript global object (
window) through the WebView interface. The properties and methods of this object correspond to native functions. When JS calls them, native code is actually executed.
The second is URL Scheme interception: JS initiates a URL request with a custom protocol (e.g., jsbridge://openCamera?param=xx), and the native side parses the URL in the WebView’s shouldOverrideUrlLoading callback, matches the corresponding method, and executes it. The API injection method has better performance and more flexible data transfer, making it the current mainstream choice. This article uses this approach.
- Native calling JavaScript: This is simpler than the former. The native side directly executes a JavaScript string via the WebView API. On Android, use
webView.evaluateJavascript(script, callback); on iOS, usewebView.evaluateJavaScript(script, completionHandler:).
This JS code can invoke predefined functions in the Bridge, thereby allowing Native to send messages to JS.
The combination of these two directions forms a bidirectional communication loop. The key lies in the unification of the message protocol and the handling of callbacks.
3 Hands-on Implementation: Write Your Own JSBridge in Three Steps
The following steps guide you through implementing a JSBridge from scratch that supports bidirectional calls and a callback mechanism. The code uses JavaScript and Android Native as examples; the iOS principle is similar.
3.1 Define Bridge Interface and Message Format
First, define a message format that both sides can correctly parse. It is recommended to use a JSON object with three core fields:
1 | |
bridgeName: The name of the native function to be called. The native side dispatches it to the corresponding business module based on this name.data: Parameters passed to the native function, in JSON format, supporting complex structures.callbackId: A unique ID for identifying a call’s callback. Generated by the JS side, the native side uses this ID to find the corresponding callback function and execute it after the logic is completed. Supports asynchronous callback scenarios.
Why is callbackId needed? Native functions are often asynchronous (e.g., taking a photo, getting location), and JS cannot block and wait. Therefore, a mechanism is needed: JS first sends a request, and after the native side completes, it sends back the result with the original callbackId, and the JS side retrieves the stored callback based on the ID and executes it.
3.2 JavaScript Side (Web Side) Implementation
The following is the JavaScript code that needs to be included in the browser or WebView. It mounts a JSBridge object on window, providing callNative and onNativeCall methods externally.
1 | |
Key points:
NativeBridge.postMessageis the API injected by Native, used to pass JSON strings to Native.callbackMapmanages callbacks using closures, deleting immediately after execution to avoid memory leaks.onNativeCallis the entry point for Native to call JS. The native side needs to callwindow.JSBridge.onNativeCall(jsonString)viaevaluateJavascript.
3.3 Native Side: Android Example (WebView Injection + Message Reception)
On Android, expose methods to JavaScript using the @JavascriptInterface annotation. Below is a complete integration example:
Step 1: Create the injected Java class
1 | |
Step 2: Message dispatch handler
1 | |
Step 3: Configure WebView in Activity
1 | |
iOS brief implementation: Use WKUserContentController‘s addScriptMessageHandler:name: to register a message handler named NativeBridge. Implement the userContentController:didReceiveScriptMessage: method on the native side to receive messages from JS.
To send back, use webView.evaluateJavaScript:completionHandler:. Note that addScriptMessageHandler in WKUserContentController can cause strong reference cycles; remove it at an appropriate time.
3.4 Simple Implementation of Native Calling JavaScript
Scenarios where Native actively calls JS include: after native location is completed, notify the page; incoming push messages, etc. Simply execute a piece of JS code:
1 | |
1 | |
Note: jsonMessage needs to have its single or double quotes escaped to prevent JS parsing errors. It is recommended to process it on the frontend using JSON.stringify, or have the native side construct a JSON string and parse it again using JSON.parse in JS, rather than directly concatenating objects.
Thus, a simple but functional JSBridge is complete. It supports synchronous/asynchronous calls, bidirectional communication, and maintains callback relationships through callbackId.
4 Quick Integration with Open Source Library: DSBridge Practice
While manually implementing Bridge helps understand the principles, using mature open source libraries in production can save a lot of time and avoid common mistakes. DSBridge is an excellent open source JSBridge library with good performance, supporting synchronous/asynchronous and type-safe calls (GitHub: wendux/DSBridge-Android / lzan13/DSBridge-iOS).
4.1 DSBridge Overview and Dispatch Mechanism
The core idea of DSBridge is: the developer only needs to define a class with the @DWebApi annotation on the native side, and all public methods are automatically exposed to JavaScript. On the JS side, call dsBridge.call("apiName", args, callback). The return value of the native method is the synchronous response; if the return type is void or requires asynchrony, use handler.complete(data) to send back.
It automatically handles callbackId mapping, thread switching, and JSON serialization, greatly simplifying development.
4.2 Integration Steps (Android Example)
- Add dependency (build.gradle)
1
implementation 'com.github.wendux:DSBridge-Android:3.0-SNAPSHOT' - Create API class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class MyApi {
@DWebApi
public String getDeviceInfo(JSONObject args) {
return Build.MODEL;
}
@DWebApi
public void showToast(JSONObject args, Completer completer) {
String msg = args.optString("msg");
// Simulate async operation
new Handler(Looper.getMainLooper()).postDelayed(() -> {
completer.complete("toast shown");
}, 1000);
}
} - Register to WebView
1
2
3
4
5
6WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
// DSBridge will handle message passing
webView.addJavascriptInterface(new MyApi(), "dsBridge");
// Load page, the page needs to include dsbridge.js
webView.loadUrl("file:///android_asset/index.html"); - JS side call
1
2
3
4
5
6
7
8
9
10<script src="dsbridge.js"></script>
<script>
// Synchronous call
var device = dsBridge.call('getDeviceInfo', {});
console.log(device);
// Asynchronous call
dsBridge.call('showToast', {msg: 'Hello'}, function(response) {
console.log('callback:', response);
});
</script>
Compared to manual implementation, DSBridge reduces code by about 70% and automatically handles threading issues (calls from JS to Native run on a background thread by default to avoid blocking UI). For most business scenarios, it is recommended to directly use this library, and only consider writing manually when deep customization or special security policies are needed.
5 Advanced Tips and Typical Pitfalls
Even when using open source libraries, understanding the underlying pitfalls helps with debugging and optimization. The following are common issues encountered in hybrid development.
5.1 Pitfall 1: URL Length Limit and Protocol Hijacking
If using the URL Scheme method (instead of API injection) for JSBridge, passing large data (e.g., base64 encoded images) may cause the URL to exceed the WebView’s character limit (usually around 2KB), resulting in truncation or complete failure. The API injection method avoids this problem because data is passed directly as a JSON string, not limited by URL length.
If URL scheme must be used, split the data into multiple fragments and reassemble through sequential requests, or switch to the postMessage method. In practice, API injection is recommended directly for better compatibility and performance.
5.2 Pitfall 2: Memory Leaks and Thread Safety
Unreleased callbacks: On the JavaScript side, if the native side does not call back for a long time (e.g., user cancels location), the corresponding callback will remain in
callbackMapforever. Solution: set a timeout timer for eachcallbackId(e.g., 10s), automatically remove it after timeout and call back with an error. The JS code above did not include timeout logic; production environment needs to add it.Strong reference on Native side: In Android, objects injected via
addJavascriptInterfaceare held by the WebView’s internal JS engine with strong references. If that object also holds a reference to Activity, it prevents the Activity from being garbage collected. Common scenario: holdingActivity contextorWebViewin theNativeBridgeclass.
It is recommended that the injected object only hold a WeakReference<WebView>, and manually remove the injection when the Activity is destroyed (webView.removeJavascriptInterface("NativeBridge")).
- Thread issues: Methods annotated with
@JavascriptInterfacerun on the WebView kernel thread and cannot directly operate UI.
Need to post to the main thread before executing. DSBridge automatically handles this conversion, but it is easy to overlook when writing manually.
5.3 Advanced Tips: Security and Debugging
Disable dangerous configurations: Besides the necessary
setJavaScriptEnabled(true), disable permissions likesetAllowFileAccess(true),setAllowContentAccess(true)to prevent XSS attacks from using Bridge to read local files. For sensitive operations (e.g., payment, private data), verify the call source on the native side (e.g., check ifrefereris a legitimate domain).Debug communication logs: During development, enable WebView remote debugging. On Android, after calling
WebView.setWebContentsDebuggingEnabled(true), you can view WebView console output via Chrome DevTools atchrome://inspect.
Add console.log statements in onNativeCall and callNative on the JS side to print message details, helping quickly locate communication failure causes.
- Message integrity check: In production, it is recommended to sign or verify the message content to prevent man-in-the-middle attacks from tampering with Bridge messages. Especially in financial apps, verify whether
bridgeNameis in the whitelist.
6 Summary and Extensions
6.1 Core Recap of This Article
JSBridge is the bridge for communication between JavaScript and Native in hybrid development. The core principles are API injection and executing JavaScript strings.
Writing a JSBridge manually requires three things: designing a unified message protocol (bridgeName/data/callbackId), maintaining a callback queue on the JS side, and handling dispatch and result return on the Native side.
In production, it is recommended to use mature libraries like DSBridge to save development costs, but understanding the underlying mechanism helps troubleshoot complex issues.
Common pitfalls include URL length limits, memory leaks (unreleased callbacks or strong references from injected objects), thread safety, and security configurations.
6.2 Extension Learning Directions
Comparison with other cross-platform solutions: React Native uses a JavaScript engine (JSC/Hermes) as an intermediate layer, bridging through Native module registration and serialization, with asynchronous messages; Flutter communicates with native via Platform Channel, using binary serialization. Deeply comparing the performance differences of these solutions can help choose the right tech stack more precisely.
Packaging a Bridge SDK: Encapsulate the JS code and Native code from this article into an SDK module for business teams. Abstract an API registration interface so that business side only needs to focus on business logic, not communication details.
Special restrictions in Mini Programs or WKWebView: In WeChat Mini Programs, the view layer and logic layer are separate; JSBridge messages need to be forwarded through Native. In iOS WKWebView, due to inter-process communication (IPC) limitations, the frequency and size of
evaluateJavascriptcalls have upper limits; large data may need to be split or use alternative solutions.
Through the above practices, you have mastered the core implementation and engineering key points of JSBridge. It is recommended to first assess the complexity of the project’s requirements: for simple scenarios, a few lines of manual code suffice; for complex scenarios, directly choose DSBridge, and focus on testing edge cases after integration (large data, rapid clicks, page closing, etc.). With a solid bridge, the road to hybrid development can be more stable.