Принципиальный анализ JSBridge - как пример реализации WebViewJavascriptbridge

внешний интерфейс JavaScript

1. Что такое JSBridge?

JSBridge — это средство связи между стороной веб-просмотра и нативной стороной.Веб-просмотр может вызывать нативную способность через jsb, а натив также может выполнять некоторую логику в веб-просмотре через jsb.

Во-вторых, реализация JSB

В JSBRIDGE популярном, в первую очередь путем перехвата URL-адреса запроса обменивается друг с другом для достижения эффекта коренного конца и конца веб-просмотра.

Здесь мы возьмем популярный WebviewJavascriptBridge в качестве примера для анализа его реализации.

Адрес источника:GitHub.com/Маркус Уэст и…

2-1, зарегистрирован на коренном конце моста и конец WebView

function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

2-2. Вставьте код инициализации в веб-просмотр

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

Этот код в основном делает следующие вещи:

(1) Создайте массив с именем WVJBCallbacks и поместите параметр входящего обратного вызова в массив.

(2) Создайте iframe, установите невидимый, установите src наhttps://__bridge_loaded__

В iOS есть два типа веб-просмотров: один — UIWebview, а другой — WKWebview.Вот пример WKWebview:

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

Этот код в основном предназначен для выполнения следующих действий:

(1) Перехватить все запросы URL и получить URL

(2) Первый судьяisWebViewJavascriptBridgeURL, чтобы определить, активируется ли URL-адрес iframe веб-представления, что может быть определено хостом.

(3) Продолжайте судить, если даisBridgeLoadedURL, то он будет выполнятьсяinjectJavascriptFileМетод, впрыскиваю какую-то логику в веб-просмотр снова, наиболее важным для того, чтобы установить некоторые глобальные переменные иWebViewJavascriptBridgeАтрибуты, конкретные значения следующие:

window.WebViewJavascriptBridge = {
	registerHandler: registerHandler,
	callHandler: callHandler,
	disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
	_fetchQueue: _fetchQueue,
	_handleMessageFromObjC: _handleMessageFromObjC
};

var sendMessageQueue = [];
var messageHandlers = {};

var responseCallbacks = {};
var uniqueId = 1;

(4) Продолжайте судить, если это isQueueMessageURL, то это callback для обработки сообщений, и необходимо выполнить некоторые методы обработки сообщений (подробно будет описан четвертый шаг)

2–4, веб-просмотр вызывает собственные возможности

Когда и нативное, и веб-представление зарегистрировали мост, обе стороны могут звонить друг другу.Здесь сначала вводится процесс вызова нативной возможности веб-представления.

2-4-1, 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;
}

На самом деле нужно сначала сгенерировать сообщение, затем отправить его в sendMessageQueue, а затем изменить src iframe.

2-4-2, родная сторона промывки

Затем, когда терминал обнаруживает изменение родного IFRAME SRC, проходит через логику решения ISQUEEMESSAGEURL, затем выполнил функцию WKLUSHMESSAGEQUEUE, получите все сообщения SendMessageQueue JS в сторону.

- (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. 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);
        }
    }
}

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}

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

Если в сообщении, полученном от натива, есть responseId, это означает, что сообщение является сообщением, полученным обратным вызовом после того, как JS вызывает натив, поэтому responseCallbacks, добавленные в sendData с самого начала, основаны на responseId (callbackId, используемый в начало, два значения совпадают) возьмите эту функцию обратного вызова и выполните ее, тем самым завершив процесс однократного вызова Native с помощью JS.

2-4-4, подведем итоги процесса

Процесс выглядит следующим образом:

截屏2021-06-14 23.28.32.png

1, РОДНОЙ Логис JSB

2. Создайте iframe на стороне веб-просмотра и установите для src значение__bridge_load__

3. Нативная сторона перехватывает запрос, вводит код инициализации jsb и монтирует связанные объекты и методы в окне.

4. Побочный вызов веб-просмотраcallHandlerметод, а вresponseCallbackдобавитьcallbackId: responseCallbackи измените SRC IFrame, чтобы вызвать захват

5. Натив получает сообщение и генерируетresponseCallbackИ выполняет родную сторону зарегистрированного хорошего способа

6, после того, как родной завершен, выполнение через веб-просмотр_handleMessageFromObjCметод, выньте функцию обратного вызова и выполните

截屏2021-06-14 23.28.26.png

callHandlerresponseCallbackдобавитьcallbackId: responseCallback

2. Активный вызов на родной стороне_handleMessageFromObjCметод для выполнения соответствующей логики в веб-просмотре

3. После того, как выполнение стороны веб-просмотра завершено,responseIdСообщение, Добавить вsendMessageQueueи измените src iframe на__wvjb_queue_message__

4. Нативная сторона перехватывает изменение URL-адреса, вызывает логику веб-просмотра для получения сообщения и получает его.responseIdи выполнить соответствующую функцию обратного вызова

Автор: Чжао Юэ