热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

WebViewJavaScriptBridge深入剖析

前一篇文章中,我们大致的讲述了一下JavaScriptCore这个库在iOS开发中的应用。在文中最后的阶段,我们提到了简单的来说,在最开始的UIWebView时,原生跟JS之间的交互一般是两种方式:当然这个其实也就是

前一篇文章中,我们大致的讲述了一下JavascriptCore这个库在iOS开发中的应用。在文中最后的阶段,我们提到了 WebViewJavascriptBridge 这个库。提到这个库,可能有一些人就要说了,现在都什么时代了,谁还会用这个库啊?全是坑!不错,早在三年前,这个库有过一段辉煌的时光,在苹果除了WKWebView之后,渐渐的使用这个库的人越来越少,尽管这个库也是支持了WKWebView的。 但是一个事物的存在就有他的价值,就算使用也不是那么频繁了,尽管他有很多的坑。但是对于一个开发者来说,我们应该取其精华去其糟粕,现如今出的很多的交互的bridge依旧是有部分交互逻辑沿用了WebViewJavascriptBridge的思想。 这里就不得不提味精大神的一片文章,这篇文章里面深入浅出的谈了谈现如今Hybrid开发时常用的一些桥方法。有兴趣的可以去关注一下。废话不多说,那么我们今天就从源码开始解析这个库的使用以及原理。

简介

简单的来说,在最开始的UIWebView时,原生跟JS之间的交互一般是两种方式:

  • Native -> JS :这种方式很简单,只是是原生调用 stringByEvaluatingJavascriptFromString: 方法,传入要执行的JS代码就可以实现;
  • JS -> Native :这种方式是在网页上面加载一串Custom URL Scheme的URL,然后通过原生去 UIWebView 的代理方法 webView:shouldStartLoadWithRequest:navigationType: 中拦截相应的URL做处理。

当然这个其实也就是 WebViewJavascriptBridge 的理论核心。但是上面这种实现方法为什么没有人使用呢?原因就是,通过在代理方法里面拦截,我们就必不可少的要写很多的 if else 的代码。在项目中的混合插件越来越多的时候,就导致了这个代理方法里面的逻辑越来越臃肿,越来越难以维护。 那么 WebViewJavascriptBridge 的作用就是以更加优雅的方式,去实现Native与JS之间的互调。让Native能像调用OC的方法一样调用JS,同时JS也能像调用JS方法一样去调用OC。这就在OC和JS中间搭起了一座友谊的桥梁。

使用

这里使用我就不多说了,直接 pod 'WebViewJavascriptBridge' 就可以引入到项目了。 附上源码地址: WebViewJavascriptBridge

目录结构

WebViewJavascriptBridge深入剖析
  • WebViewJavascriptBridgeBase :bridge的核心类,用来初始化以及消息的处理;
  • WebViewJavascriptBridge :判断WebView的类型,并通过不同的类型进行分发。针对UIWebView和WebView做的一层封装,主要从来执行JS代码,以及实现UIWebView和WebView的代理方法,并通过拦截URL来通知WebViewJavascriptBridgeBase做的相应操作;
  • WKWebViewJavascriptBridge :主要是针对WKWebView做的一些封装,主要也是执行JS代码和实现WKWebView的代理方法的。同上面这个类类似;
  • WebViewJavascriptBridge_JS:里面主要写了一些JS的方法,JS端与Native”互动“的JS端的方法基 本上都在这个里面;

主要流程

WebViewJavascriptBridge参与交互的流程包括三个部分:初始化、JS调用Native、Native调用JS。接下来我们就一一分析其中的过程。

1、初始化

这里必须要说一下,WebViewJavascriptBridge的这个设计很巧妙,他在JS端和Native端,都各自初始化了一个WebViewJavascriptBridge对象,就像是两边各自安排了一个”通讯兵“,让这两个对象去完成消息的收发工作。同时两边还各自维护一个管理相应事件的messageHandlers容器、一个管理回调的callbackId容器。所以这里的初始化,我们得分为两个部分的初始化,一个部分是Native端的初始化,一个是JS端的初始化。这里我们都以UIWebView为例子讲解,WKWebView其实也是相类似的原理,可以类比一下。

(1)、Native端的初始化

  • 首先初始化 WebViewJavascriptBridge 并且设置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigatiOnDelegate= self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

然后其内部初始化了 WebViewJavascriptBridgeBase 类和相关的属性

- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.respOnseCallbacks= [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}
  • 注册 handler ,这个 handler 是提供给JS调用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];

注册其实就是在 messageHandlers 这个 NSMutableDictionary 里面保存一下

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

(2)、web view端的初始化

  • 当我们通过 loadRequest 加载URL之后,网页一加载就会执行网页JS中的bridge的初始化方法 setupWebViewJavascriptBridge 函数
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)
    }

这里主要做了两件事情,一个是保存要执行的一直自定义初始化函数,比如注册JS中的 handler ,第二个就是通过添加一个 iframe 加载初始化链接 https://__bridge_loaded__

  • Native端会拦截 https://__bridge_loaded__ 这个URL
WebViewJavascriptBridge深入剖析
  • 在webview中执行本地 WebViewJavascriptBridge_JS 中的代码,初始化 window.WebViewJavascriptBridge 对象:首先在JS中创建一个 WebViewJavascriptBridge 对象,设置成window一个属性,然后定义几个用于管理消息的全局变量,接着给 WebViewJavascriptBridge 对象定义几个处理消息的方法和函数,执行Native端 startupMessageQueue 中保存的消息,也就是本地JS文件还未加载时就发送了的消息。
window.WebViewJavascriptBridge = {
		registerHandler: registerHandler,
		callHandler: callHandler,
		disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	};

2、JS调用Native

  • JS中调用 callHandler() 方法,发消息给原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('JS got response', response)
			})

然后我们看看 callHandler 是怎么定义的

function callHandler(handlerName, data, responseCallback) {
		if (arguments.length == 2 && typeof data == 'function') {
			respOnseCallback= data;
			data = null;
		}
		_doSend({ handlerName:handlerName, data:data }, responseCallback);
	}

那么这个 _doSend 是干嘛的?我们顺着往下看

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

这下我们清楚了,原来我们在传入 handlerNamedata 被包装成了一个 message 传入到 _doSend 函数,然后生成一个 callbackId ,也一道包装到 message 中去。这样三个数据都被打包成了一个 message 传到Native。 当然为什么要传入一个 callbackId 进去呢?这是因为用于处理原生回调的 responseCallback 是一个函数,是不能直接传给原生的,所以这里就把这个 responseCallback 存到了一个全局的 responseCallbacks 对象的属性里面去,属性名就是 responseCallback 对应的id。这个地方就是为了后面Native回调JS时,根据id找到对应的 responseCallback

  • 在上图中的最后一步指的是JS会在 iframe 中加载发送消息的URL,此时原生就可以在相应的代理中拦截到这个URL,然后就知道JS端给我传递消息了,然后Native端会去调用JS,把 sendMessageQueue 中的 message 取出来,转成 JSON string 的格式。接着原生把 JSON string 解析成字典,取出相应的 datacallbackIdhandlerName 。最后根据 handlerName 去先前的 messageHanlers 里面取出相对应的 block(handler) ,然后调用这个 blockdata 作为第一个参数,第二个参数是根据 callbackId 创建的 responseCallback(block) ,然后原生就可以在 block(handler )中处理接收到的 data 以及回调JS了。

  • 如果说需要原生给JS回调的话,当这个 responseCallback 被回调的时候,会执行下面的代码

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

这里就是直接创建了一个 message (NSMutableDictionary)对象,把 datacallbackIdhandlerName 封装之后转换成为JSON string,最后调用 WebViewJavascriptBridge._handleMessageFromObjC('%@') 这个方法,把 message 传给JS。

- (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:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:JavascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:JavascriptCommand];
        });
    }
}

在JS接收到了这个 message 之后,会根据里面的 callbackId 找到之前的 responseCallback ,把 data 作为参数,回调这个 responseCallback

2、Native调用JS

其Native调用JS和上面JS调用Native是有很多的相似之处的。当然,其实也是可以直接通过web view执行JS脚本去实现的。但是 WebViewJavascriptBridge 使用了一套更加规范的调用方式。接下来来介绍一下这种方式。

  • Native调用callHandler()方法,把消息发送给JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];

这个方法跟JS里面的这个方法名是一样的,当然实际的作用其实也是相似的。 在这里都是将 handlerNamedataresponseCallback 对应的id包装成一个 message 。然后把这个 message 对象转成JSON string。最后在调用 WebViewJavascriptBridge._handleMessageFromObjC(messageJSON) 方法把数据给到JS。这里至于为什么也是传id,其实原理跟上面是一样的,block也是不能直接传给JS的,所以这里把 responseCallback 的这个block存到了全局的 responseCallbacks 字典里面去了,key就是 responseCallback 对应的id。JS回调Native的时候,就会来这个字典里面去取对应的block。其实思想都是差不多的。

  • JS端拿到了这个message之后,会将它解析成为JS对象,然后去使用 datacallbackIdhandlerName 。然后根据 handlerNamemessageHandlers 里面去对应的handler函数,然后去执行这个函数。第一个参数是传过来的 data ,第二个参数就是根据 callbackId 创建的 responseCallback 的function。这里就可以在 handler 里面处理接收到的回调了。
  • 这里与前面JS调Native时Native回调JS的处理不太一样,因为JS调Native是不能直接调的。但是怎么去通知Native呢?其实他这里就是直接走了JS调用Native的流程,就是上面提到的这个流程。不过还是有不同的:
    • 一是 message 里面的东西不一样了;
    • 二是Native对message的处理:
      • 跟上面JS调用Native不一样的就是 message 里面现在不需要你传一个 callbackId 了,因为这里本来就是JS回调给Native的,再传这个,两边就一直在回调来回调去了。但是呢,多了一个 responseId ,这是因为Native执行JS回调的时候,会根据这个 responseIdresponseCallbacks 中去取对应的block

        ``
        WVJBResponseCallback respOnseCallback= _responseCallbacks[responseId];
        ```
        
        • Native在收到JS回调之后,会根据 responseId 找到之前保存的 responseCallback 的block,然后把 message 中的 responseData (其实就是data)作为参数回调给这个responseCallback。与JS调用Native不同的其实就是这里的 responseCallback 只有一个 data 参数了,是没有用于再次回调JS的block了。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们


推荐阅读
  • 如何使用Ping命令来测试网络连接?当网卡安装和有关参数配置完成后,可以使用ping命令来测试一下网络是否连接成功。以winXP为例1、打开XP下DOS窗口具体操作是点击“开始”菜 ... [详细]
  • 鼠标悬停出现提示信息怎么做
    概述–提示:指启示,提起注意或给予提醒和解释。在excel中会经常用到给某个格子增加提醒信息,比如金额提示输入数值或最大长度值等等。设置方式也有多种,简单的,仅为单元格插入批注就可 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
  • 本文介绍了如何在React和React Native项目中使用JavaScript进行日期格式化,提供了获取近7天、近半年及近一年日期的具体实现方法。 ... [详细]
  • 本文详细介绍如何在 iOS 7 环境下申请苹果开发者账号,涵盖从访问开发者网站到最终激活账号的完整流程。包括选择个人或企业账号类型、付款方式及注意事项等。 ... [详细]
  • 深入解析:FlameGraph 火焰图在性能分析中的应用
    本文详细介绍了 FlameGraph 火焰图作为性能分析工具的原理、使用方法及其应用场景,帮助开发者更好地理解和利用这一强大的可视化工具。 ... [详细]
  • 深入解析动态代理模式:23种设计模式之三
    在设计模式中,动态代理模式是应用最为广泛的一种代理模式。它允许我们在运行时动态创建代理对象,并在调用方法时进行增强处理。本文将详细介绍动态代理的实现机制及其应用场景。 ... [详细]
  • 通常情况下,修改my.cnf配置文件后需要重启MySQL服务才能使新参数生效。然而,通过特定命令可以在不重启服务的情况下实现配置的即时更新。本文将详细介绍如何在线调整MySQL配置,并验证其有效性。 ... [详细]
  • JavaScript中的数组是数据集合的核心结构之一,内置了多种实用的方法。掌握这些方法不仅能提高开发效率,还能显著提升代码的质量和可读性。本文将详细介绍数组的创建方式及常见操作方法。 ... [详细]
  • Python自动化测试入门:Selenium环境搭建
    本文详细介绍如何在Python环境中安装和配置Selenium,包括开发工具PyCharm的安装、Python环境的设置以及Selenium包的安装方法。此外,还提供了编写和运行第一个自动化测试脚本的步骤。 ... [详细]
  • 本文探讨了如何在Classic ASP中实现与PHP的hash_hmac('SHA256', $message, pack('H*', $secret))函数等效的哈希生成方法。通过分析不同实现方式及其产生的差异,提供了一种使用Microsoft .NET Framework的解决方案。 ... [详细]
  • 本文介绍了如何通过Java代码计算一个整数的位数,并展示了多个基础编程示例,包括求和、平均分计算、条件判断等。 ... [详细]
  • 本题要求在一组数中反复取出两个数相加,并将结果放回数组中,最终求出最小的总加法代价。这是一个经典的哈夫曼编码问题,利用贪心算法可以有效地解决。 ... [详细]
  • 本篇文章介绍如何将两个分别表示整数的链表进行相加,并生成一个新的链表。每个链表节点包含0到9的数值,如9-3-7和6-3相加得到1-0-0-0。通过反向处理链表、逐位相加并处理进位,最终再将结果链表反向,即可完成计算。 ... [详细]
  • Spring Boot 中静态资源映射详解
    本文深入探讨了 Spring Boot 如何简化 Web 应用中的静态资源管理,包括默认的静态资源映射规则、WebJars 的使用以及静态首页的处理方法。通过本文,您将了解如何高效地管理和引用静态资源。 ... [详细]
author-avatar
手机用户2702937271
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有