Before introducing the Hybrid container I implemented, I suggest that we first understand how many ways commonly used JavaScript and Native communicate with each other?
Take a look at this article: Cleaning up a Hybrid Framework from Scratch (I) – Start by choosing a JS communication solution
Below, you are assumed to have a basic understanding of JS and Native communication methods.
In order to be compatible with UIWebView, the common three-party library WebViewJavascriptBridge continues to adopt the way of fake jump intercepting Request. Instead of intercepting, you can use the API provided by WKWebView itself.
1. Analyze how to block a Request first (skip it if you already know it)
WebViewJavascriptBridge source analysis. It loads the local exampleApp.html file. ExampleApp. HTML load time, run a js method: setupWebViewJavascriptBridge (the callback).
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)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}
bridge.registerHandler('testJavascriptHandler'.function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha! ' }
log('JS responding with', responseData)
responseCallback(responseData)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
Copy the code
At first, the WebViewJavascriptBridge and WVJBCallbacks variables on the Window object have no values. Then, we define the WVJBCallbacks array, which is an array of methods that hold the operations that register the events. It then declares an iframe whose SRC is a false address (which is why it is called a false jump). Simply put, an IFrame in HTML opens a web page. When it shows up on the webView, it jumps to a new link. For this fake link, the client does not jump, but instead injects the JS script file. JS files are mainly used to deal with the communication between Web and native. They are used to receive and send events in the form of queues. What does the injected JS file do? Instead of a bunch of declarations, you can see the following call:
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout".disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for(var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); }}Copy the code
Here we create an iframe with a SRC of a specific format: https://__wvjb_queue_message__. We also see that the _callWVJBCallbacks method iterates through the WVJBCallbacks array and passes the WebViewJavascriptBridge. Looking back at the methods in the array, you will see that at this point: the interaction event is registered. Back to the client side, SRC is not a jump link, but a “trigger” for web and Native event interaction.
Summary: webView javascriptBridge will trigger two types of false jumps. The first type is for the client to inject JS into the webView, and the second type is for the interaction between Web and native events. The principle is to intercept the request in the process of the jump, the request to do a specific processing.
2. WKWebView API implementation
The topic of this article is not to intercept requests, and if you do, we can see two problems from the process above.
- How do I inject JS files?
- How do I “deliver interaction” events?
Problem Solver 2: After Apple provided JavaScriptCore, JS in iOS worked like a fish in water. There is a key class in Webkit: WKUserContentController. Look at its introduction:
A WKUserContentController object provides a way for JavaScript to post messages to a web view. The user content controller associated with a web view is specified by its web view configuration.
To put it simply, we can realize JS and Native interaction of WebView by registering events. (I won’t provide code examples, see the API documentation)
Problem solver 1: Webkit also provides a class: WKUserScript. It provides only a public method for injecting script files. There are two options: one is when the page is loaded, and the other is when the page is loaded.
At this point, the outline of the scheme is already complete. The short answer is to use the WKWebView API. However, there is still room for optimization.
To optimize the
As we can see in the exampleapp.html file of WebViewJavascriptBridge, each interactive event requires a separate registration. Can it be handled with an event? That’s perfectly ok. We know that when events interact, data is passed, so we can: pass the name of the method to be called as a parameter to the client. The client generates the function signature using the NSMethodSignature class, and finally invokes the corresponding method through the Runtime.
For example, in an injected JS file, we can do this:
/ /... Other processingfunction_on(event, callback) { //... Slightly _event_hook_map [event] = callback; }function_handleMessageFromApp(message) { //... A switch (the message) {case 'event': {/ /... }case 'init': { var ret = _event_hook_map[xxx]; }}}function _setDefaultEventHandlers() {
_on('sys:init'.function(ses){
if (window.RbJSBridge._hasInit) {
console.log('hasInit, no need to init again');
return;
}else{
console.log('init event');
}
window.RbJSBridge._hasInit = true;
// bridge ready
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('RbJSBridge');
doc.dispatchEvent(readyEvent);
});
}
var doc = document;
_setDefaultEventHandlers();
Copy the code
1. When the webView is loaded, js is injected, which calls _setDefaultEventHandlers(); Methods. The _event_HOOk_map holds the method for registering events, but it waits for the page to load successfully before registering.
2. When the client page loads successfully, the client actively calls the _handleMessageFromApp() method in the didFinish proxy method of the webView to tell the Web to start registering the event.
When a client receives an interactive event from the Web, what we need to do is “translate” the method name into a function. There needs to be a uniform set of rules. That is, we stipulate (according to our own needs, the people at both ends) that all client methods called have only two arguments, a dictionary argument and a block callback. Then, generate the method as follows:
+ (id)ur_performSelectorWithTargetName:(NSString *)targetName selector:(SEL)aSelector withObjects:(NSArray *)objects {
URWebWidgetManager *manager = [URWebWidgetManager shareInstance];
id realInstance = [manager.widgets objectForKey:targetName];
if ([realInstance respondsToSelector:aSelector]) {
NSMethodSignature *signature = [realInstance methodSignatureForSelector:aSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:realInstance];
[invocation setSelector:aSelector];
NSUInteger i = 1;
for (id object in objects) {
id tempObject = object;
[invocation setArgument:&tempObject atIndex:++i]; } [invocation invoke]; // The method is executedif ([signature methodReturnLength]) {
id data;
[invocation getReturnValue:&data];
returndata; }}return nil;
}
Copy the code
This way, we don’t need to register interaction events in the HTML and client side every time. Instead, the front end specifies the client method name for the call, and the client provides the corresponding implementation.
The last
Not intercepting a Request is just one way to do it, and I didn’t test whether it would be better than intercepting. However, I feel that providing unique event registration, and doing so in an injected JS file, greatly reduces the amount of work developers on both ends have to do. When the Web side calls the client, provide the method name and parameters. The client only needs to implement the corresponding method name function. At present, H5 containers based on this scheme have been used in our company’s online products. I have not sorted out the demo separately, and I will update it as soon as I sorted out it.