优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案

简介:

简介

本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge

它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发送、接收、消息处理器的注册与调用以及设置消息处理的回调。

就像项目的名称一样,它是连接UIWebView和Javascript的bridge。在加入这个项目之后,他们之间的交互处理方式变得很友好。

在native code中跟UIWebView中的js交互的时候,像下面这样:

//发送一条消息给UI端并定义回调处理逻辑
 [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) {
        if (error) { NSLog(@"Uh oh - I got an error: %@", error); }
        NSLog(@"objc got response! %@ %@", error, responseData);
 }];


而在UIWebView中的js跟native code交互的时候也变得很简洁,比如在调用处理器的时候,就可以定义回调处理逻辑:

//调用名为testObjcCallback的native端处理器,并传递参数,同时设置回调处理逻辑
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
	log('Got response from testObjcCallback', response)
})

一起来看看它的实现吧,它总共就包含了三个文件:

WebViewJavascriptBridge.h
WebViewJavascriptBridge.m
WebViewJavascriptBridge.js.txt

它们是以如下的模式进行交互的:


很明显:WebViewJavascriptBridge.js.txt主要用于衔接UIWebView中的web page,而WebViewJavascriptBridge.h/m则主要用于与ObjC的native code打交道。他们作为一个整体,其实起到了一个“桥梁”的作用,这三个文件封装了他们具体的交互处理方式,只开放出一些对外的涉及到业务处理的API,因此你在需要UIWebView与Native code交互的时候,引入该库,则无需考虑太多的交互上的问题。整个的Bridge对你来说都是透明的,你感觉编程的时候,就像是web编程的前端和后端一样清晰。

简单地罗列一下它可以实现哪些功能吧:

出于表达上的需要,对于UIWebView相关的我就称之为UI端,而objc那端的处理代码称之为Native端。

【1】UI端

(1) UI端在初始化时支持设置消息的默认处理器(这里的消息指的是从Native端接收到的消息)

(2) 从UI端向Native端发送消息,并支持对于Native端响应后的回调处理的定义

(3) UI端调用Native定义的处理器,并支持Native端响应后的回调处理定义

(4) UI端注册处理器(供Native端调用),并支持给Native端响应处理逻辑的定义

【2】 Native端

(1) Native端在初始化时支持设置消息的默认处理器(这里的消息指的是从UI端发送过来的消息)

(2) 从Native端向UI端发送消息,并支持对于UI端响应后的回调处理逻辑的定义

(3) Native端调用UI端定义的处理器,并支持UI端给出响应后在Native端的回调处理逻辑的定义

(4) Native端注册处理器(供UI端调用),并支持给UI端响应处理逻辑的定义


UI端以及Native端完全是对等的两端,实现也是对等的。一段是消息的发送端,另一段就是接收端。这里为引起混淆,需要解释一下我这里使用的“响应”、“回调”在这个上下文中的定义:

(1) 响应:接收端给予发送端的应答

(2) 回调:发送端收到接收端的应答之后在接收端调用的处理逻辑

下面来分析一下源码:

WebViewJavascriptBridge.js.txt:

主要完成了如下工作:

(1) 创建了一个用于发送消息的iFrame(通过创建一个隐藏的ifrmae,并设置它的URL 来发出一个请求,从而触发UIWebView的shouldStartLoadWithRequest回调协议)

(2) 创建了一个核心对象WebViewJavascriptBridge,并给它定义了几个方法,这些方法大部分是公开的API方法

(3) 创建了一个事件:WebViewJavascriptBridgeReady,并dispatch(触发)了它。

代码解读

UI端实现

对于(1),相应的代码如下:

/*
 *创建一个iFrame,设置隐藏并加入到DOM中
 */
	function _createQueueReadyIframe(doc) {
		messagingIframe = doc.createElement('iframe')
		messagingIframe.style.display = 'none'
		doc.documentElement.appendChild(messagingIframe)
	}

对于(2)中的WebViewJavascriptBridge,其对象拥有如下方法:

window.WebViewJavascriptBridge = {
		init: init,
		send: send,
		registerHandler: registerHandler,
		callHandler: callHandler,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	}

方法的实现:

	/*
	 *初始化方法,注入默认的消息处理器
	 *默认的消息处理器用于在处理来自objc的消息时,如果该消息没有设置处理器,则采用默认处理器处理
	 */
	function init(messageHandler) {
		if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }
		WebViewJavascriptBridge._messageHandler = messageHandler
		var receivedMessages = receiveMessageQueue
		receiveMessageQueue = null
		//如果接收队列有消息,则处理
		for (var i=0; i<receivedMessages.length; i++) {
			_dispatchMessageFromObjC(receivedMessages[i])
		}
	}


	/*
	 *发送消息并设置回调
	 */
	function send(data, responseCallback) {
		_doSend({ data:data }, responseCallback)
	}
	
	/*
	 *注册消息处理器
	 */
	function registerHandler(handlerName, handler) {
		messageHandlers[handlerName] = handler
	}
	

	/*
	 *调用处理器并设置回调
	 */
	function callHandler(handlerName, data, responseCallback) {
		_doSend({ data:data, handlerName:handlerName }, responseCallback)
	}

涉及到的两个内部方法:

	/*
	 *内部方法:消息的发送
	 */
	function _doSend(message, responseCallback) {
		//如果定义了回调
		if (responseCallback) {
			//为回调对象产生唯一标识
			var callbackId = 'js_cb_'+(uniqueId++)
			//并存储到一个集合对象里
			responseCallbacks[callbackId] = responseCallback
			//新增一个key-value对- 'callbackId':callbackId
			message['callbackId'] = callbackId
		}
		sendMessageQueue.push(JSON.stringify(message))
		messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
	}


	/*
	 *内部方法:处理来自objc的消息
	 */
	function _dispatchMessageFromObjC(messageJSON) {
		setTimeout(function _timeoutDispatchMessageFromObjC() {
			var message = JSON.parse(messageJSON)
			var messageHandler
			
			if (message.responseId) {
				//取出回调函数对象并执行
				var responseCallback = responseCallbacks[message.responseId]
				responseCallback(message.error, message.responseData)
				delete responseCallbacks[message.responseId]
			} else {
				var response
				if (message.callbackId) {
					var callbackResponseId = message.callbackId
					response = {
						respondWith: function(responseData) {
							_doSend({ responseId:callbackResponseId, responseData:responseData })
						},
						respondWithError: function(error) {
							_doSend({ responseId:callbackResponseId, error:error })
						}
					}
				}
				
				var handler = WebViewJavascriptBridge._messageHandler
				//如果消息中已包含消息处理器,则使用该处理器;否则使用默认处理器
				if (message.handlerName) {
					handler = messageHandlers[message.handlerName]
				}
				
				try {
					handler(message.data, response)
				} catch(exception) {
					console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
				}
			}
		})
	}

还有两个js方法是供native端直接调用的方法(它们本身也是为native端服务的):

	/*
	 *获得队列,将队列中的每个元素用分隔符分隔之后连成一个字符串【native端调用】
	 */
	function _fetchQueue() {
		var messageQueueString = sendMessageQueue.join(MESSAGE_SEPARATOR)
		sendMessageQueue = []
		return messageQueueString
	}


	/*
	 *处理来自ObjC的消息【native端调用】
	 */
	function _handleMessageFromObjC(messageJSON) {
		//如果接收队列对象存在则入队该消息,否则直接处理
		if (receiveMessageQueue) {
			receiveMessageQueue.push(messageJSON)
		} else {
			_dispatchMessageFromObjC(messageJSON)
		}
	}

最后还有一段代码就是,定义一个事件并触发,同时设置设置上面定义的WebViewJavascriptBridge对象为事件的一个属性:

	var doc = document
	_createQueueReadyIframe(doc)
	//创建并实例化一个事件对象
	var readyEvent = doc.createEvent('Events')
	readyEvent.initEvent('WebViewJavascriptBridgeReady')
	readyEvent.bridge = WebViewJavascriptBridge
	//触发事件
	doc.dispatchEvent(readyEvent)

Native端实现

其实大致跟上面的类似,只是因为语法不同(所以我上面才说两端是对等的):

WebViewJavascriptBridge.h/.m

它其实可以看作UIWebView的Controller,实现了UIWebViewDelegate协议:

@interface WebViewJavascriptBridge : NSObject <UIWebViewDelegate>
+ (id)bridgeForWebView:(UIWebView*)webView handler:(WVJBHandler)handler;
+ (id)bridgeForWebView:(UIWebView*)webView webViewDelegate:(id <UIWebViewDelegate>)webViewDelegate handler:(WVJBHandler)handler;
+ (void)enableLogging;
- (void)send:(id)message;
- (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
@end

方法的实现其实是跟前面类似的,这里我们只看一下UIWebView的一个协议方法

shouldStartLoadWithRequest:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (webView != _webView) { return YES; }
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:CUSTOM_PROTOCOL_SCHEME]) {
		//队列中有数据
        if ([[url host] isEqualToString:QUEUE_HAS_MESSAGE]) {
			//刷出队列中数据
            [self _flushMessageQueue];
        } else {
            NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", CUSTOM_PROTOCOL_SCHEME, [url path]);
        }
        return NO;
    } else if (self.webViewDelegate) {
        return [self.webViewDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
    } else {
        return YES;
    }
}

使用示例

UI端

	//给WebViewJavascriptBridgeReady事件注册一个Listener
	document.addEventListener('WebViewJavascriptBridgeReady', onBridgeReady, false)
    	//事件的响应处理
	function onBridgeReady(event) {
		var bridge = event.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 + (data ? ': ' + JSON.stringify(data) : '')
			if (log.children.length) { log.insertBefore(el, log.children[0]) }
			else { log.appendChild(el) }
		}
        	//初始化操作,并定义默认的消息处理逻辑
		bridge.init(function(message) {
			log('JS got a message', message)
		})
        	//注册一个名为testJavascriptHandler的处理器,并定义用于响应的处理逻辑
		bridge.registerHandler('testJavascriptHandler', function(data, response) {
			log('JS handler testJavascriptHandler was called', data)
			response.respondWith({ 'Javascript Says':'Right back atcha!' })
		})

        	//创建一个发送消息给native端的按钮
		var button = document.getElementById('buttons').appendChild(document.createElement('button'))
		button.innerHTML = 'Send message to ObjC'
		button.ontouchstart = function(e) {
			e.preventDefault()
            		//发送消息
			bridge.send('Hello from JS button')
		}

		document.body.appendChild(document.createElement('br'))

        	//创建一个用于调用native端处理器的按钮
		var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
		callbackButton.innerHTML = 'Fire testObjcCallback'
		callbackButton.ontouchstart = function(e) {
			e.preventDefault()
			log("Calling handler testObjcCallback")
            //调用名为testObjcCallback的native端处理器,并传递参数,同时设置回调处理逻辑
			bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('Got response from testObjcCallback', response)
			})
		}
	}

Native端

//实例化一个webview并加入到window中去
    UIWebView* webView = [[UIWebView alloc] initWithFrame:self.window.bounds];
    [self.window addSubview:webView];
    
    //启用日志记录
    [WebViewJavascriptBridge enableLogging];
    
    //实例化WebViewJavascriptBridge并定义native端的默认消息处理器
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) {
        NSLog(@"ObjC received message from JS: %@", data);
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
    }];
    
    //注册一个供UI端调用的名为testObjcCallback的处理器,并定义用于响应的处理逻辑
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponse *response) {
        NSLog(@"testObjcCallback called: %@", data);
        [response respondWith:@"Response from testObjcCallback"];
    }];
    
    //发送一条消息给UI端并定义回调处理逻辑
    [_bridge send:@"A string sent from ObjC before Webview has loaded." responseCallback:^(id error, id responseData) {
        if (error) { NSLog(@"Uh oh - I got an error: %@", error); }
        NSLog(@"objc got response! %@ %@", error, responseData);
    }];
    
    //调用一个在UI端定义的名为testJavascriptHandler的处理器,没有定义回调
    [_bridge callHandler:@"testJavascriptHandler" data:[NSDictionary dictionaryWithObject:@"before ready" forKey:@"foo"]];
    
    [self renderButtons:webView];
    [self loadExamplePage:webView];
    
    //单纯发送一条消息给UI端
    [_bridge send:@"A string sent from ObjC after Webview has loaded."];

项目运行截图:





原文发布时间为:2012-11-21

本文作者:vinoYang

本文来自云栖社区合作伙伴 CSDN博客,了解相关信息可以关注CSDN博客。
目录
相关文章
|
iOS开发
iOS Error Domain=NSURLErrorDomain Code=-999
iOS Error Domain=NSURLErrorDomain Code=-999
187 0
|
6天前
|
iOS开发 开发者
uniapp开发ios打包Error code = -5000 Error message: Error: certificate file(p12) import failed!报错问题如何解决
uniapp开发ios打包Error code = -5000 Error message: Error: certificate file(p12) import failed!报错问题如何解决
uniapp开发ios打包Error code = -5000 Error message: Error: certificate file(p12) import failed!报错问题如何解决
|
7月前
|
前端开发 数据安全/隐私保护
Uncaught (in promise) Error: Request failed with status code 404 at createError (createError.js:
Uncaught (in promise) Error: Request failed with status code 404 at createError (createError.js:
261 2
|
3月前
|
移动开发 网络协议 小程序
基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v9.1版已发布
RainbowChat是一套基于开源IM聊天框架 MobileIMSDK 的产品级移动端IM系统。RainbowChat源于真实运营的产品,解决了大量的屏幕适配、细节优化、机器兼容问题
76 5
|
3月前
|
机器学习/深度学习 JSON JavaScript
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
66 0
|
5月前
|
iOS开发
IOS编译出现Command PhaseScriptExecution failed with a nonzero exit code
IOS编译出现Command PhaseScriptExecution failed with a nonzero exit code
437 2
|
7月前
|
JavaScript 前端开发 Android开发
kotlin开发 webview如何在收到JS调用后,native返回数据给到JS
这段内容描述了在Hybrid App开发中,使用Kotlin的Compose构建的Web视图(WebView)如何通过JsBridge实现JavaScript与原生代码的交互
|
8月前
|
移动开发 网络协议 小程序
基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v9.0版已发布
MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架,超轻量级、高度提炼,一套API优雅支持 UDP 、TCP 、WebSocket 三种协议,支持 iOS、Android、H5、标准Java、小程序、Uniapp,服务端基于Netty编写。
142 0
|
Web App开发 JavaScript 前端开发
解决DevTools failed to load SourceMap Could not load content for .js.map HTTP error code 404 问题
解决DevTools failed to load SourceMap Could not load content for .js.map HTTP error code 404 问题
958 0
|
8月前
|
前端开发 安全 Linux
React Native 打包 App 发布 iOS 及加固混淆过程
本文将介绍如何使用 React Native 打包并发布 iOS 应用到 App Store,并介绍了如何进行应用的加固和混淆过程。

热门文章

最新文章