In the past year, I have been busy with mixed development in the company. We mainly implement h5 business. Native provides capabilities, such as downloading files, synchronizing meeting information to local calendar on mobile phones, and so on. The communication between H5 and native is inevitably involved in the middle. At first, it is implemented by itself, which is very difficult to use. For example, the ways of accepting parameters on both sides of iOS and Android are different, and the incoming JS callback is not supported, and so on. So I found a very good open source bridge, the link is below.
Github.com/wendux/DSBr…
Github.com/wendux/DSBr…
This sparked interest in the internal implementation of jsBridge and led to this project.
This project takes the communication between JS and Android as an example to explain the implementation principle of JSBridge. The methods mentioned below have corresponding methods in iOS (UIWebview or WKWebview).
Project address: github.com/mcuking/JSB…
1. native to js
Both native methods call JS methods. Note that the method to be called needs to be in the js global context
loadUrl
evaluateJavascript
1.1 loadUrl
mWebview.loadUrl("javascript: func()");
Copy the code
1.2 evaluateJavascript
mWebview.evaluateJavascript("javascript: func()".new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
return; }});Copy the code
The above two native methods of calling JS are compared in the following table:
way | advantages | disadvantages |
---|---|---|
loadUrl | Good compatibility | 2. Cannot obtain the js method execution result |
evaluateJavascript | Good performance. 2. You can obtain the return value after the JS execution | Available only on Android 4.4 or above |
2. js to native
Three js calls to native methods
Intercepting Url Schema (fake request)
Intercept Prompt Alert Confirm
Injecting JS context
2.1 Intercepting Url Schemas
That is, H5 issues a new jump request, and native obtains the data sent by H5 by intercepting the URL.
The destination of the jump is an invalid URL, for example:
"jsbridge://methodName? {"data": arg, "cbName": cbName}"
Copy the code
The following is an example:
"jsbridge://openScan? {"data": {"scanType":"qrCode"},"cbName":"handleScanResult"}"
Copy the code
H5 and native agree on a communication protocol, such as jsbridge. They also agree to call the native methodName methodName as the domain name, followed by the parameter arg that calls the method, and the js methodName cbName that receives the result of the method execution.
Specifically, related methods can be encapsulated on the JS side for unified invocation by the business side. The code is as follows:
window.callbackId = 0;
function callNative(methodName, arg, cb) {
const args = {
data: arg === undefined ? null : JSON.stringify(arg),
};
if (typeof cb === 'function') {
const cbName = 'CALLBACK' + window.callbackId++;
window[cbName] = cb;
args['cbName'] = cbName;
}
const url = 'jsbridge://' + methodName + '? ' + JSON.stringify(args); . }Copy the code
The more ingenious part of the above encapsulation is to mount the JS callback method cb to the window, which is used to receive the execution results of native. To prevent name conflicts, it is distinguished by global callbackId. Then pass the name of the callback function on the window as an argument to the native end. After native gets THE cbName and executes the method, it calls JS (the two methods mentioned above) through native to call cb and pass the result to H5 (for example, pass the scan result to H5).
To initiate a request in H5, you can either set window.location.href or create a new iframe to jump to.
function callNative(methodName, arg, cb) {... const url ='jsbridge://' + method + '? ' + JSON.stringify(args);
// Jump to the location
window.location.href = url;
// Jump by creating a new iframe
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.width = 0;
iframe.style.height = 0;
document.body.appendChild(iframe);
window.setTimeout(function() {
document.body.removeChild(iframe);
}, 800);
}
Copy the code
Native intercepts requests made by H5. When it detects that the protocol is Jsbridge rather than HTTP/HTTPS /file, it intercepts the request and resolves methodName, ARG, and cbName in the URL. Execute the method and call the JS callback function.
Let’s take android as an example. This is done by overriding the shouldOverrideUrlLoading method of the WebViewClient class, which is explained in a separate section below.
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class JSBridgeViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
JSBridge.call(view, url);
return true; }}Copy the code
Interception of URL Schema
- Messages are lost when sent continuously
The following code:
window.location.href = "jsbridge://callNativeNslog? {"data":"111","cbName":""}";
window.location.href = "jsbridge://callNativeNslog? {"data":"222","cbName":""}";
Copy the code
Js’s appeal at this time is to quickly send two communication requests in succession within the same running logic, and print 111,222 in sequence with the IDE log of the client itself, so the actual result is that the communication message of 222 cannot be received at all, and will be directly discarded by the system.
Cause: H5’s request is an analog jump in the final analysis, and webView has limitations on jump. When H5 sends multiple hops in succession, webView will directly filter out subsequent jump requests, so the second message cannot be received at all. How to receive the second message? Js to delay the second message.
// Send the first message
location.href = "jsbridge://callNativeNslog? {"data":"111","cbName":""}";
// Delay sending the second message
setTimeout(500.function(){
location.href = "jsbridge://callNativeNslog? {"data":"222","cbName":""}";
});
Copy the code
To solve this problem for the system, the JS side can encapsulate a layer of queue. All the messages called by JS code are first put into the queue and not sent immediately. Then H5 will periodically, for example, empty the queue for 500 milliseconds. Never send two requests in a row in a very short time.
- URL length limit
If the data to be transmitted is long, such as many method parameters, some data will still be lost due to the URL length limit.
2.2 Intercept Prompt Alert Confirm
That is, H5 initiates an alert confirm Prompt, and Native obtains the data transmitted by H5 by intercepting the Prompt.
Since alert confirm is more commonly used, prompt communication is generally carried out.
The convention for the combination of transmitted data and the JS side encapsulation method can be similar to the one mentioned in the intercepting URL Schema above.
function callNative(methodName, arg, cb) {... const url ='jsbridge://' + method + '? ' + JSON.stringify(args);
prompt(url);
}
Copy the code
Native intercepts prompt issued by H5. When detecting that the protocol is Jsbridge rather than common HTTP/HTTPS /file, native intercepts the request and resolves methodName, ARG and cbName in the URL. Execute the method and call the JS callback function.
Using Android as an example, the onJsPrompt method overwrites WebChromeClient class for interception. The specific encapsulation of Android side will be explained in a separate section below.
import android.webkit.JsPromptResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
public class JSBridgeChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.call(view, message));
return true; }}Copy the code
This way does not have too big disadvantage, also does not exist when continuous send information loss. However, iOS UIWebView does not support this method (WKWebView does).
2.3 Injecting JS context
That is, the instance object is injected into js global context by native through the method provided by WebView, and JS can communicate by calling the instance method of native.
There’s addJavascriptInterface for Android WebView, JSContext for iOS UIWebview, scriptMessageHandler for iOS WKWebview.
The following is an example of addJavascriptInterface for Android WebView.
First of all, native side inject the instance object into js global context, the code is roughly as follows, the specific encapsulation will be explained in the separate section below:
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mWebView = (WebView) findViewById(R.id.mWebView); .// Convert the js methods provided by the NativeMethods class to a hashMap
JSBridge.register("JSBridge", NativeMethods.class);
// Insert an instance of JSBridge into the js global context named _jsbridge, which has the call method
mWebView.addJavascriptInterface(new JSBridge(mWebView), "_jsbridge"); }}public class NativeMethods {
// The method to be called by js
public static void methodName(WebView view, JSONObject arg, CallBack callBack) {}}public class JSBridge {
private WebView mWebView;
public JSBridge(WebView webView) {
this.mWebView = webView;
}
private static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();
// Static method, which is used to convert the interface provided to Javacript under the class passing in the second argument to Map, named as the first argument
public static void register(String exposeName, Class
classz) {... }// Instance method, which is used to provide methods for js unified invocation
@JavascriptInterface
public String call(String methodName, String args) {... }}Copy the code
H5 can then call the window._jsbridge instance in js, and the incoming data combination can be similar to the above two methods. The specific code is as follows:
window.callbackId = 0;
function callNative(method, arg, cb) {
let args = {
data: arg === undefined ? null : JSON.stringify(arg)
};
if (typeof cb === 'function') {
const cbName = 'CALLBACK' + window.callbackId++;
window[cbName] = cb;
args['cbName'] = cbName;
}
if (window._jsbridge) {
window._jsbridge.call(method, JSON.stringify(args)); }}Copy the code
The problem of injecting JS context
Take addJavascriptInterface of Android WebView as an example. Before Android 4.2 version, JS can make use of Java Reflection API to obtain the internal information of the class that constructs the instance object, and can directly operate the internal properties and methods of the object. This method will cause security risks, for example, if the external web page is loaded, the malicious JS script of the web page can obtain the information on the memory card of the phone.
After Android version 4.2, it is possible to indicate that only the Java method can be called by JS by adding the decorator @javascriptInterface before the Java method provided for js calls.
The comparison of the above three methods of js calling native is shown in the following table:
way | advantages | disadvantages |
---|---|---|
Intercepting Url Schema (fake request) | No security holes | 2. The Url length is limited and the size of transmitted data is limited |
Intercept Prompt Alert Confirm | No security holes | The iOS UIWebView doesn’t support this |
Injecting JS context | Provided by the official, convenient and simple | There are security vulnerabilities under Android 4.2 |
3. Encapsulation of Android-based Java
The code for the native/H5 interaction section has been mentioned above, but here is how the native side encapsulates the methods exposed to H5.
The first step is to encapsulate a separate class, NativeMethods, that writes the methods for H5 calls as public and static methods. As follows:
public class NativeMethods {
public static void showToast(WebView view, JSONObject arg, CallBack callBack) {... }}Copy the code
Next consider how to build a bridge in front of NativeMethods and H5. The JSBridge class is born for transportation. There are two main static methods under the JSBridge class: Register and Call. The register method is used to convert the method called by H5 into a Map form for query. The call method is mainly to receive the call from h5 end, decompose the parameters from H5 end, and find and call the corresponding Native method in the Map.
Register the static method of the JSBridge class
Declare a static attribute exposeMethods (HashMap) in JSBridge class first. Then declare the static method register (string exposeName and classz) and group all of the static methods of exposeName and classz into a single map, for example:
{ jsbridge: { showToast: ... openScan: ... }}Copy the code
The code is as follows:
private static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();
public static void register(String exposeName, Class
classz) {
if (!exposeMethods.containsKey(exposeName)) {
exposeMethods.put(exposeName, getAllMethod(classz));
}
}
Copy the code
We need to define a getAllMethod to convert the methods in our class to a HashMap data format. Declare a HashMap in the method and convert the methods that meet the condition into a Map, with key as the method name and value as the method.
The first argument is an instance of the Webview class, the second is an instance of the JSONObject class, and the third is an instance of the CallBack class. (CallBack is a custom class, which will be covered later) The code looks like this:
private static HashMap<String, Method> getAllMethod(Class injectedCls) {
HashMap<String, Method> methodHashMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method: methods) {
if(method.getModifiers()! =(Modifier.PUBLIC | Modifier.STATIC) || method.getName()==null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if(parameters! =null && parameters.length==3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == CallBack.class) { methodHashMap.put(method.getName(), method); }}}return methodHashMap;
}
Copy the code
The static method call of the JSBridge class
Because of the JS context injection and the other two, the h5 side passes the parameters in a different form, so the way the parameters are handled is slightly different. The following takes interception Prompt as an example to explain. In this method, the first parameter of the call receiving is webView, and the second parameter is ARG, which is the parameter passed from h5. Remember how native and H5 agreed to transmit data when intercepting Prompt?
"jsbridge://openScan? {"data": {"scanType":"qrCode"},"cbName":"handleScanResult"}"
Copy the code
The call method first determines if the string starts with jsbridge, then converts the string to the Uri format, then gets the host name, which is the method name, and then gets the query. The object that is the combination of the method parameter and the js callback function name. Finally, find the mapping of exposeMethods, find the corresponding method and execute it.
public static String call(WebView webView, String urlString) {
if(! urlString.equals("") && urlString! =null && urlString.startsWith("jsbridge")) {
Uri uri = Uri.parse(urlString);
String methodName = uri.getHost();
try {
JSONObject args = new JSONObject(uri.getQuery());
JSONObject arg = new JSONObject(args.getString("data"));
String cbName = args.getString("cbName");
if (exposeMethods.containsKey("JSBridge")) {
HashMap<String, Method> methodHashMap = exposeMethods.get("JSBridge");
if(methodHashMap! =null&& methodHashMap.size()! =0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if(method! =null) {
method.invoke(null, webView, arg, newCallBack(webView, cbName)); }}}}catch(Exception e) { e.printStackTrace(); }}return null;
}
Copy the code
The CallBack class
After JS calls native method successfully, it is necessary for native to return some feedback to JS, such as whether the interface is successfully called, or the data obtained after the execution of Native (such as scanning code). So native needs to execute js callbacks.
The essence of executing JS callback function is that native calls THE JS method of H5, and the method is still the two methods mentioned above: evaluateJavascript and loadUrl. In simple terms, you can directly pass the js callback function name to the corresponding Native method, which executes through the evaluateJavascript call.
But to unify the way callbacks are called, we can define a CallBack class that defines a static method called apply that calls js callbacks directly.
Note: Native executes JS methods on the main thread.
public class CallBack {
private String cbName;
private WebView mWebView;
public CallBack(WebView webView, String cbName) {
this.cbName = cbName;
this.mWebView = webView;
}
public void apply(JSONObject jsonObject) {
if(mWebView! =null) {
mWebView.post(() -> {
mWebView.evaluateJavascript("javascript:" + cbName + "(" + jsonObject.toString() + ")".new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
return; }}); }); }}}Copy the code
At this point, the general principles of JSBridge are covered. But the features can still be improved, such as:
When native executes JS methods, it can accept data returned asynchronously by JS methods, such as requesting an interface to return data in JS methods. Calling WebView directly to evaluateJavascript, the instance method onReceiveValue in the second parameter of ValueCallback class does not receive the data returned by JS asynchronously.
We will continue to improve the native call JS method later.
In addition, I am writing a converter to compile Vue code to React code. Welcome to check it out.
Github.com/mcuking/vue…