DSBridge源码阅读

前言: 公司最近做项目,用到H5,的确,在RN、原生、H5混合,其实很多公司都已经慢慢使用H5来开发了,因为H5可以3端使用,这个的确是不能代替的属性,组内有人推荐用DSBridge,因此,就此简单地阅读了下源码,仅作为小记,以免健忘症发作…

[TOC]

原生代码

原生代码分成:

  • DWKWebView 处理web的通讯与交互(核心)

  • JSBUtil 格式封装、解析

  • InternalApis 默认注册的api,可以理解为消息处理

  • DSCallInfo 通讯model

  • JSBWebEventDelegate 作者实际上并没有用上

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
var bridge = {
default:this,// for typescript
call: function (method, args, cb) {
var ret = '';
if (typeof args == 'function') {
cb = args;
args = {};
}
var arg={data:args===undefined?null:args}
if (typeof cb == 'function') {
var cbName = 'dscb' + window.dscb++;
window[cbName] = cb;
arg['_dscbstub'] = cbName;
}
arg = JSON.stringify(arg)

//if in webview that dsBridge provided, call!
if(window._dsbridge){
ret= _dsbridge.call(method, arg)
}else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){
ret = prompt("_dsbridge=" + method, arg);//markJS-1: 此处可以调起
}

return JSON.parse(ret||'{}').data
},
register: function (name, fun, asyn) {
var q = asyn ? window._dsaf : window._dsf
if (!window._dsInit) {
window._dsInit = true;
//notify native that js apis register successfully on next event loop
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0)
}
if (typeof fun == "object") {
q._obs[name] = fun;
} else {
q[name] = fun
}
},
registerAsyn: function (name, fun) {
this.register(name, fun, true);
},
hasNativeMethod: function (name, type) {
return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"});
},
disableJavascriptDialogBlock: function (disable) {
this.call("_dsb.disableJavascriptDialogBlock", {
disable: disable !== false
})
}
};

!function () {
if (window._dsf) return;
var ob = {
_dsf: {
_obs: {}
},
_dsaf: {
_obs: {}
},
dscb: 0,
dsBridge: bridge,
close: function () {
bridge.call("_dsb.closePage")
},
_handleMessageFromNative: function (info) {
var arg = JSON.parse(info.data);
var ret = {
id: info.callbackId,
complete: true
}
var f = this._dsf[info.method];
var af = this._dsaf[info.method]
var callSyn = function (f, ob) {
ret.data = f.apply(ob, arg)
bridge.call("_dsb.returnValue", ret)
}
var callAsyn = function (f, ob) {
arg.push(function (data, complete) {
ret.data = data;
ret.complete = complete!==false;
bridge.call("_dsb.returnValue", ret)
})
f.apply(ob, arg)
}
if (f) {
callSyn(f, this._dsf);
} else if (af) {
callAsyn(af, this._dsaf);
} else {
//with namespace
var name = info.method.split('.');
if (name.length<2) return;
var method=name.pop();
var namespace=name.join('.')
var obs = this._dsf._obs;
var ob = obs[namespace] || {};
var m = ob[method];
if (m && typeof m == "function") {
callSyn(m, ob);
return;
}
obs = this._dsaf._obs;
ob = obs[namespace] || {};
m = ob[method];
if (m && typeof m == "function") {
callAsyn(m, ob);
return;
}
}
}
}
for (var attr in ob) {
window[attr] = ob[attr]
}
bridge.register("_hasJavascriptMethod", function (method, tag) {
var name = method.split('.')
if(name.length<2) {
return !!(_dsf[name]||_dsaf[name])
}else{
// with namespace
var method=name.pop()
var namespace=name.join('.')
var ob=_dsf._obs[namespace]||_dsaf._obs[namespace]
return ob&&!!ob[method]
}
})
}();

module.exports = bridge;

优缺点

横向对比

和iOS的WebViewJavascriptBridge、Android的JSBridge横向对比,其中JSBridge为WebViewJavascriptBridge的安卓版,通讯基本相同,可以融合

对比点 DSBridge WVJSB/JSBridge 评价
bridge初始化时间 1~2seconds 该点是W的最大缺点,也是DSBridge的挺大的优点
双平台支持 分别支持 需要整合 该点DSBridge也是做的很好的
稳定 ✨✨✨ ✨✨✨✨✨ 没办法,后者经过了很多年的风雨
框架架构/思路 ✨✨✨✨✨ 前者的代码实在是没法看,后者反而有很多技术博做🌰供新手学习
易用性 ✨✨✨ ✨✨✨✨✨ 对于易用性,可能有主观成分,其实个人不喜DSBridge那种方法型写法
解耦 - ✨✨✨✨✨ 框架最忌讳的就是继承!!!其实完全可以封装成分类
iOS 双webView支持 ✨✨✨✨✨ 仅仅支持了wkwebview
demo/文档 可读性 ✨✨✨ 有待提高
同步异步调用支持 支持 不支持 dsbridge走的是prompt的代理,在走这个代理的时候,web的js实际上是会被阻塞的,所以可以达到同步的效果

通讯

Web 调用 Native

WVJSB不同的是,DSBridge基于WKUIDelegate 来通讯的:

js代码请Command+F ( //markJS-1:)处

原生代码Command+F ( //markNative-1:) 处:

DWKWebView.m line 67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(NSString * _Nullable result))completionHandler
{
NSString * prefix=@"_dsbridge=";
if ([prompt hasPrefix:prefix])//markNative-1: 当带有该前缀,则认为属于本框架通讯
{
NSString *method= [prompt substringFromIndex:[prefix length]];
NSString *result=nil;
if(isDebug){
result =[self call:method :defaultText ];
}else{
@try {
result =[self call:method :defaultText ];
}@catch(NSException *exception){
NSLog(@"%@", exception);
}
}
completionHandler(result);

}
....
}

Native 调用 Web

DWKWebView.m line 372

1
2
3
4
NSString * json=[JSBUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
@"data":[JSBUtil objToJsonString: info.args]}];
[self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
completionHandler:nil];

最后还是吐槽下他的架构吧:

  • 继承问题:真的,在没必要的情况下,继承是魔鬼!
  • api,写了代理,其实没有用上,其实可以从InternalApis可以看出他想通过注册很多api进去提供给web端调用,但事实上缺乏很好地管理
  • 代码基本都在DWKWebView,可读性非常差
  • 对于web端,作为开发,我肯定是想要判断两种情况,一种是在web打开,一种是在app打开,但这个代码块没有经过封装,对于一份代码在两个端跑,在前端页面开发的时候,每次都要写判断

同步/异步

背景js在调用alert函数正常情况下只要用户没有关闭alert对话框,js代码是会阻塞的,但是考虑到alert 对话框只有一个确定按钮,也就是说无论用户关闭还是确定都不会影响js代码流程,所以DWebview中在弹出alert对话框时会先给js返回,这样一来js就可以继续执行,而提示框等用户关闭时在关闭即可。如果你就是想要阻塞的alert,可以自定义。而DWebview的promptcomfirm实现完全符合ecma标准,都是阻塞的。

消息来了

js代码请Command+F ( //markJS-1:)处

DWKWebView.m line 67

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
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(NSString * _Nullable result))completionHandler
{
NSString * prefix=@"_dsbridge=";
if ([prompt hasPrefix:prefix])
{
NSString *method= [prompt substringFromIndex:[prefix length]];
NSString *result=nil;
if(isDebug){
result =[self call:method :defaultText ];
}else{
@try {
result =[self call:method :defaultText ];
}@catch(NSException *exception){
NSLog(@"%@", exception);
}
}
completionHandler(result);

}else {
//...
}
//...
}

消息处理了

由上代码可以看到

1
[self call:method :defaultText ];

是具体处理web信息的方法。

瞧瞧返回了啥

我们不妨debug下,看看同步和异步的completionHandler(result)都返回了什么

同步

1
{"data":"testSyn[ syn call]","code":0}

异步

1
{"data":"","code":-1}

仔细看代码

注意看Mark— :

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
-(NSString *)call:(NSString*) method :(NSString*) argStr
{
//...
SEL sel=NSSelectorFromString(methodOne);
SEL selasyn=NSSelectorFromString(methodTwo);
NSDictionary * args=[JSBUtil jsonStringToObject:argStr];
NSString *arg=args[@"data"];
NSString * cb;
do{
if(args && (cb= args[@"_dscbstub"])){
if([JavascriptInterfaceObject respondsToSelector:selasyn]){//Mark- : 如果是异步加载,则创建了一个回调(代码块),该代码块会成为参数,传进api里的方法。
void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
NSString *del=@"";
result[@"code"]=@0;
if(value!=nil){
result[@"data"]=value;
}
value=[JSBUtil objToJsonString:result];
value=[value stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];

if(complete){
del=[@"delete window." stringByAppendingString:cb];
}
NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];//Mark- : 可以看到,此处的数据返回跟同步的时候是有差别的,同步通讯是通过prompt的代理直接返回结果,此处是直接调用js的接受native传输的方法

@synchronized(self)
{
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
jsCache=[jsCache stringByAppendingString:js];
if(t-lastCallTime<50){
if(!isPending){
[self evalJavascript:50];
isPending=true;
}
}else{
[self evalJavascript:0];
}
}

};
SuppressPerformSelectorLeakWarning(
[JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];

);

break;
}
}else if([JavascriptInterfaceObject respondsToSelector:sel]){
//...
}
}while (0);
}
return [JSBUtil objToJsonString:result];
}

demo执行回调

1
2
3
4
5
6
- (void) testAsyn:(NSString *) msg :(void (^)(NSString * _Nullable result,BOOL complete))completionHandler
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
completionHandler([msg stringByAppendingString:@" [ asyn call]"],YES);
});
}

执行了回调,可以看回上面代码

值得一提

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
//...
@synchronized(self)
{
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
jsCache=[jsCache stringByAppendingString:js];
if(t-lastCallTime<50){
if(!isPending){
[self evalJavascript:50];
isPending=true;
}
}else{
[self evalJavascript:0];
}
}

};
SuppressPerformSelectorLeakWarning(
[JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];

);

@synchronized(self)中间那段干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
@synchronized(self)
{
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
jsCache=[jsCache stringByAppendingString:js];
if(t-lastCallTime<50){
if(!isPending){ //Mark- : 是否在等待状态
[self evalJavascript:50];
isPending=true;
}
}else{ //Mark- : 距离上次异步调用时间大于50毫秒,直接执行
[self evalJavascript:0];
}
}

首先看

1
[self evalJavascript:50];
1
2
3
4
5
6
7
8
9
10
11
12
- (void) evalJavascript:(int) delay{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
@synchronized(self){
if([jsCache length]!=0){
[self evaluateJavaScript :jsCache completionHandler:nil];
isPending=false;//Mark- : 等待标志取消
jsCache=@"";//清空缓存
lastCallTime=[[NSDate date] timeIntervalSince1970]*1000;
}
}
});
}

哦,其实是一个异步的执行js的方法。

而且,从上面的代码得知,每个异步调用间隔必须大于50毫秒

思考,为什么是要延时50毫秒再执行呢?

猜测,避免大量异步同时回调到web而造成数据丢失?

由于时间有限,不做过多测评,但是作者做了这一系列的动作就肯定有他的道理。