开源库WebViewJavascriptBridge

简单说明下WebViewJavascriptBridge执行流程

简单说明下WebViewJavascriptBridge执行流程

原理: 原理其实很好理解,主要就内置的webview,通过一个基类的信息存储,通过webview处理js中函数事件,以及基类中数据存储再执行,数据再封装成指定格式,来实现callbackID和responseID的实现;;并且js那边也有个jsbridge对象来处理之前约定的回调事件,如客户端定义的handler,也就是handlerName对应的回调函数在js对象中其实有一个对应的回调方法可以触发执行js的方法

介绍

WebViewJavascriptBridgeBase,一个存储类,用户消息记录,responseCallbackId记录以及messageHandler的记录,分别对应下面指定的属性定义

1
2
3
4
5
6
7
8
9
@property (assign) id <WebViewJavascriptBridgeBaseDelegate> delegate;
@property (strong, nonatomic) NSMutableArray* startupMessageQueue;
OC这边一个存储器,当我们开始从原生发送消息给webview时,
我们还不能确定我们需要添加的js文件已经加载完毕或者js中的bridge对象是否已经初始化成功,如果没有,则需要在这个存储的类开始存储消息;;
@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;
OC中的回调函数的存储器,key为一次递增的uniqueID,value是传入的block的copy,用户后续指定的消息体返回时,OC模块代码的回调
@property (strong, nonatomic) NSMutableDictionary* messageHandlers;
存储约定的事件,跟js约定相同的事件,已经key来执行指定的block事件,区分于responseCallbacks的执行事件
@property (strong, nonatomic) WVJBHandler messageHandler;

主要罗列主要的方法,但由于三方库在不断更新,可能方法、实现以及调用方式都会修改

初始化webview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView
webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate
handler:(WVJBHandler)messageHandler {
return [self bridgeForWebView:webView
webViewDelegate:webViewDelegate
handler:messageHandler
resourceBundle:nil];
}

+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView
webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate
handler:(WVJBHandler)messageHandler
resourceBundle:(NSBundle*)bundle
{
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView
webViewDelegate:webViewDelegate
handler:messageHandler
resourceBundle:bundle];
return bridge;
}
//内置的webview通过参数跟外部的webview实现关联关系
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView
webViewDelegate:(id<UIWebViewDelegate>)webViewDelegate
handler:(WVJBHandler)messageHandler
resourceBundle:(NSBundle*)bundle{
_webView = webView;
_webView.delegate = self;
_webViewDelegate = webViewDelegate;
//创建对象时,对应的存储空间进行相对应的初始化
_base = [[WebViewJavascriptBridgeBase alloc] initWithHandler:(WVJBHandler)messageHandler
resourceBundle:(NSBundle*)bundle];
_base.delegate = self;
//代理的回调事件还是放在基类中实现,
}

js代码块加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (BOOL)webView:(UIWebView *)webView 
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isCorrectProcotocolScheme:url]) {//之前的版本是直接引入 现在的版本加载完成后html,函数初始化后进行js块代码的初始化 比之前的版本做了些强容错
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}

//判断js文件中这个对象是否已经初始化,如果没有初始化 说明这个文件还没被加载
-(NSString *)webViewJavascriptCheckCommand {
return @"typeof WebViewJavascriptBridge == \'object\';";
}

- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();//通过OC类声明的js块 之前的版本是直接加载一个js文件,但区别不是很大
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}

注册handler

注册的handler会存放在messagehandlers里面,handler是个block,后续直接执行handler就可以了

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

通过OC直接调用js函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
- (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调用的部分
function _handleMessageFromObjC(messageJSON) {
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON)
console.log('handle message from objective' + 'push ' + messageJSON);
} else {
_dispatchMessageFromObjC(messageJSON)
console.log('handle message from objective ' + 'pop' + messageJSON);
}
}

我们找到页面初始化的部分,可以在html文件找到对象的注册事件,通过注册的handler来实现回调;接着通过callbackID实现的
function _dispatchMessageFromObjC(messageJSON) {
setTimeout(function _timeoutDispatchMessageFromObjC() {
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({ responseId:callbackResponseId, responseData:responseData })
}//如果有客户端的回调事件 需要处理客户端这里的时间 直接执行dosent的方式
}

var handler = WebViewJavascriptBridge._messageHandler
if (message.handlerName) {//直接执行html中的注册事件
handler = messageHandlers[message.handlerName]
}

try {
handler(message.data, responseCallback)
} catch(exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
}
}
}
})
}

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
}

//iframe的创建 相对于window.location来使用不能设定超时时间
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe')
messagingIframe.style.display = 'none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
doc.documentElement.appendChild(messagingIframe)
}

if ([_base isCorrectHost:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}

总结一下调用:分为3个模块,客户端代码调用事件,传入对应的data, responseCallBackID 事件处理之后,分为了handler的模块和responsecallbackID的模块,handler事件直接执行html中初始化函数的回调【函数回调是data和根据callbackID 新生成的responseID】,新生成的responseID需要通过iframe对象想客户端回传,进行url的触发,js在自己的sendMessageQueue中保存一份,通过flushMessageQueue来执行。

web页面执行OC里面的函数;

前端页面执行buttonClick事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)
})
}

调用js中的实例对象

function callHandler(handlerName, data, responseCallback) {
_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
}

返回给OC的webview去回调这个事件,然后在客户端内部实现对返回的message进行处理,通过_fetchQueue来获取message内部信息,解析封装成指定的responseID,将获取到最新的数据封装成指定格式,解析,最后再次通过_handleMessageFromObjC实现对应的结果

OC,webView(webview中分为html和js)的交互过程

解析一种相对复杂的交互模式

1
2
3
4
5
6
7
8
9
10
11
12
13
//初始化部分
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
//加载html
NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"ExampleApp" ofType:@"html"];
NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
[webView loadHTMLString:appHtml baseURL:baseURL];
//开始注册对应时间 图片3 4 5的执行的顺序其实是不确定的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];

常见场景交互

  1. 初始化webViewJavaScriptBridge:进行messageHandlers,startupMessageQueue,responseCallbacks等存储器以及_uniqueId等计数器的初始化;
  2. 加载本地的html文件,主要是针对html的script的对象进行初始化,详细看下setupWebViewJavascriptBridge(callback)函数,里面涉及了我们现在js的交互方式,通过iframe来重定向url(此处可以了解下这种方式比起window.location的优势是什么),【注意几个点,window.WVJBCallbacks = [callback]将块的匿名函数初始化给window.WVJBCallbacks,后续在js文件初始化之后,会依次初始化callback的内容;另外初始化中的log以及resgisterHandler的事件也注意下,后续在dosend的数据封装二次发送的时候需要用到】
  3. 通过回调给原生一个有效的scheme来使得客户端这里知道我们此时需要加载一段js代码,从而触发了injectJavascriptFile()函数调用,js必须全部加载完成并对应对象初始化完成,检测到相应的对象才有效,此时如果发送的消息会一次寄存在startupMessageQueue数组中,之前注册的testObjcCallback事件也会存放到messageHandlers的映射表中…初始化完成后,会对startupMessageQueue置空,便于后面的消息进行直接消费,同此同时存储在startupMessageQueue的消息也会立即消费
  4. 发送消息,不管是存放在startupMessageQueue中的消息还是立即会被消费的消息,都会先进行加工,所谓的加工就是将数据区分data,callbackId,handlerName,并在回调的responseCallbacks存储一份key为callbackId的block,用于后续的回调。
  5. WebViewJavascriptBridge._handleMessageFromObjC通过这段js代码将我们的消息发送到js端进行实现处理【注意要使用主线程来进行js代码处理】,解析,重新封装,再次使用js中的_dosend();[进行对responseId+responseData的数据封装];messageHandlers[message.handlerName];调用之前js初始化的匿名函数事件
  6. dosend的发送消息后,提示客户端有消息处理,并将当前的消息内容全部存储到js的bridge对象sendMessageQueue的存储器中,OC获取消息后,先通过js方法WebViewJavascriptBridge._fetchQueue()获取消息体,然后针对消息体的内容进行解析分析,回调对应事件

仓库地址链接:

https://github.com/marcuswestin/WebViewJavascriptBridge.git

优点:

  1. 在交互的过程中消息体一直通过js传参或者返回值来通信,规避通过iframe过于暴露消息结构体;
  2. 整体的设计思路还是很棒的

缺点:

  1. 理解起来还是有点复杂的,尤其是OC和JS交互过程的代码理解