preface
Reference sources
This paper refers to the following sources
Wedge
This paper introduces the complete implementation of JSBridge, including JS part, Android native part and iOS native part
JS implementation part
explain
This is a JSbridge implementation code (JS part) after excluding the business. JS implementation code on a set
realization
The implementation code is as follows
(function() { (function() { var hasOwnProperty = Object.prototype.hasOwnProperty; var JSBridge = window.JSBridge || (window.JSBridge = {}); //The name of the jsbridge protocol definition var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge'; //The name of the outermost api var API_Name = 'namespace_bridge'; //iframe for url scheme value transfer var messagingIframe = document.createElement('iframe'); messagingIframe.style.display = 'none'; messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name; document.documentElement.appendChild(messagingIframe); //After the corresponding method is called, the corresponding callback function id will be executed var responseCallbacks = {}; //Unique id, used to ensure the uniqueness of each callback function var uniqueId = 1; //Locally registered method collection. Native methods can only call locally registered methods, otherwise an error will be prompted var messageHandlers = {}; //When a method registered in H5 is called natively, it is called through callback (that is, it is changed into asynchronous execution to enhance security) var dispatchMessagesWithTimeoutSafety = true; //Method queue in local running var sendMessageQueue = []; //Objects actually exposed to native calls var Inner = { /** * @description Register local JS method to call native through JSBridge * We stipulate that the native must call the H5 method through JSBridge * Note that there are some requirements for local functions. The first parameter is data and the second parameter is callback * @param handlerName Method name * @param handler Corresponding methods */ registerHandler: function(handlerName, handler) { messageHandlers[handlerName] = handler; }, /** * @description Calling native open methods * @param handlerName Method name * @param data parameter * @param callback Callback function */ callHandler: function(handlerName, data, callback) { //If there is no data if(arguments.length == 3 && typeof data == 'function') { callback = data; data = null; } _doSend({ handlerName: handlerName, data: data }, callback); }, /** * iOS special-purpose * @description When the callHandler is called locally, the generic scheme is actually called to notify the native * This method is then called natively to know the method queue that is currently being called */ _fetchQueue: function() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; return messageQueueString; }, /** * @description Call the registered method of H5 page, or call the callback method * @param messageJSON The details of the corresponding method need to be manually converted to json */ _handleMessageFromNative: function(messageJSON) { setTimeout(_doDispatchMessageFromNative); /** * @description Dealing with native methods */ function _doDispatchMessageFromNative() { var message; try { message = JSON.parse(messageJSON); } catch(e) { //TODO handle the exception console.error("Native call H5 Method error,Error in pass in parameter"); return; } //Callback function var responseCallback; if(message.responseId) { //It is specified here that when the native execution method is ready to notify h5 to execute the callback, the callback function id is responseId responseCallback = responseCallbacks[message.responseId]; if(!responseCallback) { return; } //Execute the local callback function responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { //Otherwise, it represents the native active execution of h5 local functions if(message.callbackId) { //First determine whether the local H5 is required to execute the callback function //If you need a local function to perform callback notification native, register the callback function locally, and then call native //The callback data is passed in after the h5 function is executed var callbackResponseId = message.callbackId; responseCallback = function(responseData) { //The default is to call the functions above the EJS api //Then, after the native knows that scheme is called, it will take the initiative to obtain this information //Therefore, the native should judge whether the function is successfully executed and receive data //At this point, the communication is finished (since h5 will not add a callback to the callback, there is no communication in the next step) _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData }); }; } //Get from locally registered functions var handler = messageHandlers[message.handlerName]; if(!handler) { //This function is not registered locally } else { //Execute local functions, pass in data and callbacks as required handler(message.data, responseCallback); } } } } }; /** * @description JS Before calling the native method, it will send here for processing * @param message Details of the method to be called, including method name and parameters * @param responseCallback Callback after calling method */ function _doSend(message, responseCallback) { if(responseCallback) { //Get a unique callbackid var callbackId = Util.getCallbackId(); //Callback functions are added to the collection responseCallbacks[callbackId] = responseCallback; //Method details add the key identifier of the callback function message['callbackId'] = callbackId; } var uri; //In android, you can access it through onJsPrompt or intercepting Url var ua = navigator.userAgent; if(ua.match(/(iPhone\sOS)\s([\d_]+)/)||ua.match(/(iPad).*OS\s([\d_]+)/)) { //In ios, through intercepting the client url to access //Because ios can be obtained manually without exposing scheme //The details of the method being called are added to the message queue, and will be obtained by the native sendMessageQueue.push(message); uri = Util.getUri(); }else{ //android compatible processing, all parameters are spliced into the url uri = Util.getUri(message); } //Get the url scheme of the trigger method //Using iframe to jump to scheme messagingIframe.src = uri; } var Util = { getCallbackId: function() { //If the port cannot be resolved, it can be replaced by Math.floor ( Math.random () * (1 << 30)); return 'cb_' + (uniqueId++) + '_' + new Date().getTime(); }, //Get url scheme //The second parameter is compatible with the practice in android //In android, the return value of JS function cannot be obtained by native, so it has to be transmitted through protocol getUri: function(message) { var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name; if(message) { //The callback id exists as a port var callbackId, method, params; if(message.callbackId) { //First: h5 calls native callbackId = message.callbackId; method = message.handlerName; params = message.data; } else if(message.responseId) { //The second: after native call h5, h5 callback //In this case, you need to analyze whether the port passed is a callback defined by it callbackId = message.responseId; method = message.handlerName; params = message.responseData; } //The parameter is converted to a string params = this.getParam(params); //uri supplement uri += ':' + callbackId + '/' + method + '?' + params; } return uri; }, getParam: function(obj) { if(obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return obj || ''; } } }; for(var key in Inner) { if(!hasOwnProperty.call(JSBridge, key)) { JSBridge[key] = Inner[key]; } } })(); //Register a test function JSBridge.registerHandler('testH5Func', function(data, callback) { alert('The test function received data:' + JSON.stringify(data)); callback && callback('Test returned data...'); }); /* ***************************API******************************************** * Open api for external calls * */ window.jsapi = {}; /** ***app modular * Some special operations */ jsapi.app = { /** * @description Test function */ testNativeFunc: function() { //Call a test function JSBridge.callHandler('testNativeFunc', {}, function(res) { callback && callback(res); }); } }; })();
Android implementation
explain
This is the supporting JSBridge implementation code in Android native. The implementation of Android is more complex than JS, including many parts
JSBridge class implementation
The implementation code is as follows
public class JSBridge { private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>(); //Native registration API method public static void register(String exposedName, Class<? extends IBridge> clazz) { if (!exposedMethods.containsKey(exposedName)) { try { exposedMethods.put(exposedName, getAllMethod(clazz)); } catch (Exception e) { e.printStackTrace(); } } } //Get all the registration methods private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception { HashMap<String, Method> mMethodsMap = new HashMap<>(); Method[] methods = injectedCls.getDeclaredMethods(); for (Method method : methods) { String name; if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) { continue; } Class[] parameters = method.getParameterTypes(); if (null != parameters && parameters.length == 4) { if (parameters[0] == BaseWebLoader.class && parameters[1] == WebView.class && parameters[2] == JSONObject.class && parameters[3] == Callback.class) { mMethodsMap.put(name, method); } } } return mMethodsMap; } //Calling methods in Hava //BaseWebLoader is the webview container of JSBridge (secondary encapsulation) //After the method is executed, if it returns, it will be called automatically public static String callJava(BaseWebLoader webLoader,WebView webView, String uriString) { String methodName = ""; String className = ""; String param = "{}"; String port = ""; if (!TextUtils.isEmpty(uriString) && uriString.startsWith("EpointJSBridge")) { Uri uri = Uri.parse(uriString); className = uri.getHost(); param = uri.getQuery(); port = uri.getPort() + ""; String path = uri.getPath(); if (!TextUtils.isEmpty(path)) { methodName = path.replace("/", ""); } } if (exposedMethods.containsKey(className)) { HashMap<String, Method> methodHashMap = exposedMethods.get(className); if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) { Method method = methodHashMap.get(methodName); if (method != null) { try { method.invoke(null,webLoader, webView, new JSONObject(param), new Callback(webView, port)); } catch (Exception e) { e.printStackTrace(); } } } } return null; } }
The purpose of this class is to natively define some exposed APIs
Callback class implementation
The implementation code is as follows
public class Callback { private static Handler mHandler = new Handler(Looper.getMainLooper()); private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge._handleMessageFromNative(%s);"; private String mPort; private WeakReference<WebView> mWebViewRef; public Callback(WebView view, String port) { mWebViewRef = new WeakReference<>(view); mPort = port; } public void apply(JSONObject jsonObject) throws JSONException { JSONObject object = new JSONObject(); object.put("responseId", mPort); object.putOpt("responseData", jsonObject); final String execJs = String.format(CALLBACK_JS_FORMAT, String.valueOf(object)); //If the activity has been closed, no callback is made if (mWebViewRef != null && mWebViewRef.get() != null && !((BaseWebLoader) mWebViewRef.get().getContext()).getActivity().isFinishing()) { mHandler.post(new Runnable() { @Override public void run() { mWebViewRef.get().loadUrl(execJs); } }); } } }
The purpose of this class is to define callback functions in native
Key code implementation of Webview container
The implementation code is as follows
Register api methods//Define api collection JSBridge.register("namespace_bridge",BridgeImpl.class);Code that captures the url scheme and executes the method
public boolean shouldOverrideUrlLoading(WebView view, String url){ //After reading the url, it is called through callJava analysis JSBridge.callJava(BaseWebLoader.this,view,url); //If false is returned, WebView processes the link url. If true is returned, it means WebView executes the url according to the program return true; }
The key code is to register function, capture url, execute method and so on
API class implementation
The implementation code is as follows
public class BridgeImpl implements IBridge { /** * Testing native methods */ public static void testNativeFunc(final BaseWebLoader webLoader, WebView wv, JSONObject param, final Callback callback) { //You can get the parameters like this param.optString (key value); //Execute in a new thread new Thread(new Runnable() { @Override public void run() { try { //Do something of your own JSONObject object = new JSONObject(); //Add test information object.put("test", "test"); //Execute callback callback.apply(getJSONObject(1, "", object)); } catch (JSONException e) { e.printStackTrace(); } } }).start(); } }
This class is the specific implementation of some APIs, and the webview registers these corresponding APIs
iOS implementation
explain
This is the supporting JSBridge implementation code in iOS native. The code in iOS is based on UIWebview, which comes from an open source project on github marcuswestin/WebViewJavascriptBridge
Implementation of WebView javascriptbridgebase
The implementation code is as follows
@implementation WebViewJavascriptBridgeBase { __weak id _webViewDelegate; long _uniqueId; } static bool logging = false; static int logMaxLength = 500; + (void)enableLogging { logging = true; } + (void)setLogMaxLength:(int)length { logMaxLength = length;} -(id)init { self = [super init]; self.messageHandlers = [NSMutableDictionary dictionary]; self.startupMessageQueue = [NSMutableArray array]; self.responseCallbacks = [NSMutableDictionary dictionary]; _uniqueId = 0; return(self); } - (void)dealloc { self.startupMessageQueue = nil; self.responseCallbacks = nil; self.messageHandlers = nil; } - (void)reset { self.startupMessageQueue = [NSMutableArray array]; self.responseCallbacks = [NSMutableDictionary dictionary]; _uniqueId = 0; } - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { NSMutableDictionary* message = [NSMutableDictionary dictionary]; if (data) { message[@"data"] = data; } if (responseCallback) { NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId]; self.responseCallbacks[callbackId] = [responseCallback copy]; message[@"callbackId"] = callbackId; } if (handlerName) { message[@"handlerName"] = handlerName; } [self _queueMessage:message]; } - (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); } } } /*This code is not used in this article * In the original project, the js Library of JSBridge is placed in the iOS local sandbox, so it needs to be injected manually * But in the example in this article, JSBridge is referenced directly in Html, so no injection is required - (void)injectJavascriptFile { NSString *js = WebViewJavascriptBridge_js(); [self _evaluateJavascript:js]; if (self.startupMessageQueue) { NSArray* queue = self.startupMessageQueue; self.startupMessageQueue = nil; for (id queuedMessage in queue) { [self _dispatchMessage:queuedMessage]; } } } */ -(BOOL)isCorrectProcotocolScheme:(NSURL*)url { if([[url scheme] isEqualToString:kCustomProtocolScheme]){ return YES; } else { return NO; } } -(BOOL)isQueueMessageURL:(NSURL*)url { if([[url host] isEqualToString:kQueueHasMessage]){ return YES; } else { return NO; } } -(BOOL)isBridgeLoadedURL:(NSURL*)url { return ([[url scheme] isEqualToString:kCustomProtocolScheme] && [[url host] isEqualToString:kBridgeLoaded]); } -(void)logUnkownMessage:(NSURL*)url { NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", kCustomProtocolScheme, [url path]); } -(NSString *)webViewJavascriptCheckCommand { return @"typeof WebViewJavascriptBridge == \'object\';"; } -(NSString *)webViewJavascriptFetchQueyCommand { return @"JSBridge._fetchQueue();"; } - (void)disableJavscriptAlertBoxSafetyTimeout { [self sendData:nil responseCallback:nil handlerName:@"_disableJavascriptAlertBoxSafetyTimeout"]; } // Private // ------------------------------------------- - (void) _evaluateJavascript:(NSString *)javascriptCommand { [self.delegate _evaluateJavascript:javascriptCommand]; } - (void)_queueMessage:(WVJBMessage*)message { // if (self.startupMessageQueue) { // [self.startupMessageQueue addObject:message]; // } else { // [self _dispatchMessage:message]; // } [self _dispatchMessage:message]; } - (void)_dispatchMessage:(WVJBMessage*)message { NSString *messageJSON = [self _serializeMessage:message pretty:NO]; [self _log:@"SEND" json:messageJSON]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\ withString:@""\\\\"]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\""" withString:@""\\\"""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\'"" withString:@""\\\'""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\n"" withString:@""\\n""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\r"" withString:@""\\r""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\f"" withString:@""\\f""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\u2028"" withString:@""\\u2028""]; messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@""\u2029"" withString:@""\\u2029""]; NSString* javascriptCommand = [NSString stringWithFormat:@""JSBridge._handleMessageFromNative('%@');""