WebViewJavascriptBridge源码分析

博客链接 WebViewJavascriptBridge源码分析

在APP的开发过程中,都会通过H5来实现部分功能,H5页面是内嵌在原生应用的WebView组件中。在有的场景下,当两端需要相互通信,但是JavaScript的权限受到限制,比如不能修改系统配置等,这个时候需要委托Native去实现某个功能,并在完成后将结果通知JavaScript。所以我们需要在Native和JavaScript之间就搭建一个通信的桥梁,这个桥梁就是我们所说的JavaScript Bridge,简称 JS Bridge。

通常实现Native与JS桥接的方式有两种:

  1. 通过JavaScriptCore框架
  2. 通过Webview拦截请求的方式(WebViewJavascriptBridge使用的方式)

marcuswestin/WebViewJavascriptBridge是使用第2种方式实现在用于在WKWebView和UIWebView中,JS与Native相互发送消息。

与其他OC的三方库不同,WebViewJavascriptBridge的实现包括OC和JS两部分,因此只看OC部分的代码我们是无法理解这个bridge是如何实现两端通信的。

WebViewJavascriptBridge中的类的作用

  • WebViewJavascriptBridgeBase:OC端桥接基础服务类,维护OC端开放给JS端的方法以及OC回调方法,实现OC向JS发送数据的具体逻辑。
  • WebViewJavascriptBridge_JS:维护了一份JS代码,用于JS环境的注入。同时维护JS端的bridge对象,管理JS端注册的方法集合以及回调方法集合,面向Web端提供注册JS方法、调用OC端方法的接口。
  • WKWebViewJavascriptBridge:基于WKWebView的OC端交互逻辑处理类,面向OC业务层,提供了注册OC方法、调用JS方法等接口。
  • WebViewJavascriptBridge:基于UIWebView的的OC端交互逻辑处理类,与WKWebViewJavascriptBridge的功能一致。

WebViewJavascriptBridge源码解析

WebViewJavascriptBridge的实现可以说是双向的过程,无论是JS端还是Native端都包含以下三部分内容:

  • bridge初始化
  • 本端注册函数共另一端调用
  • 调用另一端函数

目前App已经取消对UIWebView的支持,所以我们只需要看WKWebView相关部分的实现即可。

bridge初始化

bridge初始化分为Native初始化bridge和JS初始化bridge。在使用WebView的时候,都是从Native端打开页面开始,因此先分析Native初始化bridge。

Native初始化bridge

+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _setupInstance:webView];
    [bridge reset];
    
    return bridge;
}

- (void)_setupInstance:(WKWebView*)webView {
    _webView = webView;
    // 将webView的navigationDelegate设为WKWebViewJavascriptBridge对象自身
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    // WKWebViewJavascriptBridge对象需要实现_evaluateJavascript:代理方法
    _base.delegate = self;
}

- (void)reset {
    [_base reset];
}

WKWebViewJavascriptBridge_evaluateJavascript:方法的实现:

- (NSString*)_evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

关于JS代码的注入,可以使用WKUserContentController,也可以使用evaluateJavaScript:completionHandler:这个函数,WebViewJavascriptBridge使用后者实现JS注入,因此需要将webView的navigationDelegate设为WKWebViewJavascriptBridge对象自身,并在代理方法中调用evaluateJavaScript:completionHandler:函数。

在Native初始化后,就要使用load之类的方法加载页面与JS代码,这进入到JS初始化bridge过程。

JS初始化bridge

相对于Native初始化bridge来说,JS初始化bridge就要显得难一些。

<script>
    function setupWebViewJavascriptBridge(callback) {
        // window表示浏览器窗口
        // WebViewJavascriptBridge就是bridge对象。
        // 如果有bridge对象则直接调用callback并传入bridge对象。
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        // 如果有WVJBCallbacks则将回调函数push到数组里,后面初始化bridge时会统一遍历调用callback,并传入bridge。
        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是Native端注入的一个函数
        setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

    // 执行调用setupWebViewJavascriptBridge函数,bridge就是对象。
    // 在WebViewJavascriptBridge_JS.m文件中对bridge的定义
    // window.WebViewJavascriptBridge = {
    //      registerHandler: registerHandler,
    //      callHandler: callHandler,
    //      disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    //      _fetchQueue: _fetchQueue,
    //      _handleMessageFromObjC: _handleMessageFromObjC
    // };
    setupWebViewJavascriptBridge(function (bridge) {
        // ...
        // JS注册函数供Native调用
        bridge.registerHandler('testJavascriptHandler', function (data, responseCallback) {
           // ...
        })
        // ...
    })
</script>

在JS初始化bridge过程中,会直接调用setupWebViewJavascriptBridge(callback)函数,callback相当于block/闭包。在这个过程中,callHandlerregisterHandler等函数都是通过JS代码注入的,这些代码都在Native端,那它是如何成功执行这些函数的呢?关键在于https://__bridge_loaded__这个url。在使用了这个url后,WKWebView的NavigationDelegate会拦截这个请求,并注入JS代码。相关实现如下:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    // ...
    // isBridgeLoadedURL函数中的kBridgeLoaded即为__bridge_loaded__
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    }
    // ...
}

用泳道图来描述初始化bridge的过程


WebViewJavascriptBridge初始化bridge

JS注册函数,Native调用JS

JS注册函数

// JS注册函数给Native调用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
    var responseData = { 'Javascript Says':'Right back atcha!' }
    responseCallback(responseData)
})

// WebViewJavascriptBridge_JS.m中的JS代码
// 保存JS函数与函数名的映射关系
var messageHandlers = {};
    
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

Native调用JS函数

id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
    NSLog(@"testJavascriptHandler responded: %@", response);
}];

内部调用了WebViewJavascriptBridgeBasesendData:responseCallback:handlerName:方法。

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    // JS函数所需的参数
    if (data) message[@"data"] = data;
    // responseCallback:Native调用JS函数后的回调函数
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        // 保存Native回调
        self.responseCallbacks[callbackId] = [responseCallback copy];
        // 保存回调方法的id
        message[@"callbackId"] = callbackId;
    }
    // JS函数名
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

在Native调用JS的函数时,有时Native需要JS调用Native的回调函数返回一些数据,因此需要保存回调函数的一些信息,关于Native的回调函数是如何调用的,在后面会讲到。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

在开发过程中,有可能Native调用JS函数的时候,JS端还没有完成bridge准备工作。bridge是在decidePolicyForNavigationAction:的代理方法中执行injectJavascriptFile方法才完成的,但是
callHandler可能在viewWillAppear的时候调用,此时没有完成JS端bridge的初始化,所以先存入startupMessageQueue中,等准备完成后, 再统一调用 startupMessageQueue中的Message到JS,并将startupMessageQueue置为空。

- (void)injectJavascriptFile {
    // 注入JS bridge的环境代码
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    // 对于一些提前调用的callHandler,在注入JS初始化代码后,会统一发送,并清空startupMessageQueue
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
- (void)_dispatchMessage:(WVJBMessage*)message {
    // 序列化message
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    // 省略对messageJSON的处理
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

_dispatchMessage将之前拼装好的message传给JS,用JS bridge的 _handleMessageFromObjC函数处理Native的调用请求,_handleMessageFromObjC函数是在JS bridge初始化的时候注入的。

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

function _dispatchMessageFromObjC(messageJSON) {
    // 忽略dispatchMessagesWithTimeoutSafety部分
    _doDispatchMessageFromObjC();

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        // 是否有responseId,对于Native调用JS函数所传过来的message来说是没有该字段的
        // if (message.responseId) {
        // // ...
        // } 
        
        // 处理Native调用JS函数的message
        if (message.callbackId) {
            // 是否含有Native回调
            var callbackResponseId = message.callbackId;
            responseCallback = function (responseData) {
                // _doSend函数只传了message,另外没有responseCallback参数
                _doSend({
                    handlerName: message.handlerName,
                    // 如果Native传过来的message有回调,那么JS端需要传入一个responseId,这样Native端才能通过responseId这个key
                    // 在responseCallbacks字典中找到对应的Native回调
                    responseId: callbackResponseId,
                    responseData: responseData
                });
            };
        }

        // 通过handlerName获取到对应的JS函数,并调用。
        // messageHandlers保存了JS bridge的函数名和回调函数
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            handler(message.data, responseCallback);
        }
    }
}

// 这个_doSend是精简之后的实现,_doDispatchMessageFromObjC中的_doSend函数没有传递responseCallback参数
function _doSend(message) {
    // sendMessageQueue保存message信息,这个message信息是给Native回调时候用的
    sendMessageQueue.push(message);
    // src = https://__wvjb_queue_message__,WKWebView的代理方法优惠拦截这个url,从而调用WKWebViewJavascriptBridge的KFlushMessageQueue方法
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

当WKWebView的代理方法拦截到https://__wvjb_queue_message__这个url的时候,就会调用WKFlushMessageQueue方法

if ([_base isQueueMessageURL:url]) {
    [self WKFlushMessageQueue];
}

- (void)WKFlushMessageQueue {
    [_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
        NSLog(@"%@", result);
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

执行_fetchQueue()这个JS函数,并在completionHandler这个block内返回JS中sendMessageQueue的信息,从而获取带responseId的message。接着执行Native的flushMessageQueue方法。

// 在flushMessageQueue方法中完成了Native调用JS函数后的回调
- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判断
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略对Message类型的校验
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        }
    }
}

用泳道图来描述Native调用JS的过程


WebViewJavascriptBridgeNative调用JS

Native注册函数,JS调用Native

Native注册函数

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"testObjcCallback called: %@", data);
    //
    responseCallback(@"Response from testObjcCallback");
}];

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    // messageHandlers用来保存OC函数与函数名的映射关系
    _base.messageHandlers[handlerName] = [handler copy];
}

JS调用Native函数

bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
    log('JS got response', response)
})

// JS端callHandler的实现
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

在Native调用JS的过程也使用了_doSend函数,它的作用是为了能调用Native调用JS函数之后的回调函数。在JS调用Native的过程中,_doSend函数是为了调用OC函数(与函数名对应的block),responseCallback则是代表JS调用OC函数后的回调函数。

function _doSend(message, responseCallback) {
    // 如果有JS回调,则使用responseCallbacks保存JS回调
    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;
}

之后的逻辑在OC调用JS中已经描述过了,直到执行flushMessageQueue:方法前都是一样的
这里就不再重复。接着看一下flushMessageQueue:方法在JS调用Native过程中的实现:

- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判断
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略对Message类型的校验
        // 忽略关于responseId的实现
        // 对于JS调用Native所传过来的message来说是没有responseId字段的
        WVJBResponseCallback responseCallback = NULL;
        NSString* callbackId = message[@"callbackId"];
        // 判断是否有JS回调
        if (callbackId) {
            responseCallback = ^(id responseData) {
                if (responseData == nil) {
                    responseData = [NSNull null];
                }
                
                WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                // _queueMessage: -> _dispatchMessage: -> JS: _handleMessageFromObjC -> JS: _dispatchMessageFromObjC
                [self _queueMessage:msg];
            };
        } else {
            responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        }
        // 通过handlerName获取到对应的Native函数,并调用。
        // messageHandlers保存了Native bridge的函数名和回调函数
        WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
        
        if (!handler) {
            NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
            continue;
        }
        
        // 执行Native函数
        handler(message[@"data"], responseCallback);
    }
}

关于_queueMessage:方法前面已经分析过,这里就不再重复,接着看一下_dispatchMessageFromObjC函数在JS调用Native过程中的实现:

// 精简了_doDispatchMessageFromObjC,只保留调用JS回调的部分
function _doDispatchMessageFromObjC() {
    var message = JSON.parse(messageJSON);
    var responseCallback;
    // 如果有JS回调,那么OC传过来的message必然存在responseId字段
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        // 调用JS回调
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    } 
}

接着用泳道图来描述下JS调用Native的过程


WebViewJavascriptBridgeJS调用Native

NNWKWebViewJSBridge

NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的实现过程后,基于这个项目,实现一个轻量级Swift版本JSBridge,并且它仅需要支持WKWebView即可。
相对于WebViewJavascriptBridge,我使用了WKUserContentController简化了初始化和消息传递的实现过程,相对来说会更好理解,消息传递性能也要比拦截Requests的方式要高。

项目地址:NNWKWebViewJSBridge
项目截图:

WKWebViewJSBridge_demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容