dsBridge介紹
dsBridge是一個三端易用的現代跨平臺 Javascript bridge, 通過它,你可以在Javascript和原生之間同步或非同步的呼叫彼此的函式。
dsBridge之js原始碼分析
我對dsBridge
中js程式碼做了詳細註釋方便大家閱讀。
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中,用於Native端呼叫
}
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){//使用時Native會註冊_dswk引數
ret = prompt("_dsbridge=" + method, arg);//調起原生程式碼(ios端會呼叫WKUIDelegate的方法)
}
return JSON.parse(ret||'{}').data
},
//Native呼叫的方法使用此方法註冊
register: function (name, fun, asyn) {
//註冊的方法會儲存到_dsaf或_dsf中
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)
}
//object型別儲存到_obs下,方法直接儲存到_dsf(_dsaf)下
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 () {
//判斷是否需要給window進行引數新增,如果沒有新增會把ob內引數進行一次新增
if (window._dsf) return;
var ob = {
_dsf: {//儲存同步方法
_obs: {}//儲存同步方法相關object
},
_dsaf: {//儲存非同步方法
_obs: {}//儲存非同步方法相關object
},
dscb: 0,//避免方法同名每次加1
dsBridge: bridge,
close: function () {
bridge.call("_dsb.closePage")
},
//處理Native呼叫js方法
_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)//js方法處理完後回撥原生方法,並返回處理後的結果
}
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;
}
}
}
}
//將ob所有引數賦值給window
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])//js用!!進行bool轉換
}else{
// with namespace
var method=name.pop()
var namespace=name.join('.')
var ob=_dsf._obs[namespace]||_dsaf._obs[namespace]
return ob&&!!ob[method]
}
})
}();
複製程式碼
dsBridge之ios端原始碼分析
初始化
首先通過js程式碼注入為window
新增_dswk
用於標註是Native
使用。
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._dswk=true;"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
[configuration.userContentController addUserScript:script];
複製程式碼
註冊本地Native
類並新增相應的namespace
到javaScriptNamespaceInterfaces
。FDInternalApis
類註冊了部分需要的方法,namespace
是_dsb
。
FDInternalApis * interalApis= [[FDInternalApis alloc] init];
interalApis.webview=self;
[self addJavascriptObject:interalApis namespace:@"_dsb"];
if(object!=NULL){
[javaScriptNamespaceInterfaces setObject:object forKey:namespace];
NSLog(javaScriptNamespaceInterfaces.description);
}
複製程式碼
Native 呼叫 js
Native
呼叫js
使用方法:
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber * value){
NSLog(@"%@",value);
}];
複製程式碼
js
中需要註冊相應方法(同步):
dsBridge.register('addValue', function (r, l) {
return r + l;
})
複製程式碼
對於非同步js
中的註冊方式:
dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
responseCallback(arg1+" "+arg2+" "+arg3);
})
複製程式碼
native
端使用時會走以下方法:
NSString * json=[FDWebUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
@"data":[FDWebUtil objToJsonString: info.args]}];
[self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
completionHandler:nil];
複製程式碼
js
會去呼叫_handleMessageFromNative
方法並對資料進行解析。
當js
處理完畢會呼叫:
bridge.call("_dsb.returnValue", ret)
複製程式碼
然後native
會取出儲存在handerMap
的block
執行:
- (id) returnValue:(NSDictionary *) args{
void (^ completionHandler)(NSString * _Nullable)= handerMap[args[@"id"]];
if(completionHandler){
if(isDebug){
completionHandler(args[@"data"]);
}else{
@try{
completionHandler(args[@"data"]);
}@catch (NSException *e){
NSLog(@"%@",e);
}
}
if([args[@"complete"] boolValue]){
[handerMap removeObjectForKey:args[@"id"]];
}
}
return nil;
}
複製程式碼
js 呼叫 Native
var str=dsBridge.call("testSyn","testSyn");
複製程式碼
然後會執行到
ret = prompt("_dsbridge=" + method, arg);
複製程式碼
通過prompt
在iOS
端會呼叫代理方法
- (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);
}
複製程式碼
這裡會呼叫一個關鍵方法
-(NSString *)call:(NSString*) method :(NSString*) argStr
{
NSArray *nameStr=[FDWebUtil parseNamespace:[method stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
id JavascriptInterfaceObject= [javaScriptNamespaceInterfaces valueForKey:nameStr[0]];
NSString *error=[NSString stringWithFormat:@"Error! \n Method %@ is not invoked, since there is not a implementation for it",method];
NSMutableDictionary*result =[NSMutableDictionary dictionaryWithDictionary:@{@"code":@-1,@"data":@""}];
if(!JavascriptInterfaceObject){
NSLog(@"Js bridge called, but can't find a corresponded JavascriptObject , please check your code!");
}else{
method=nameStr[1];
NSString *methodOne = [FDWebUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
NSString *methodTwo = [FDWebUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
SEL sel=NSSelectorFromString(methodOne);
SEL selasyn=NSSelectorFromString(methodTwo);
NSDictionary * args=[FDWebUtil jsonStringToObject:argStr];
NSString *arg = [args safeObjectForKey:@"data"];
NSString * cb;
do{
if(args && (cb= args[@"_dscbstub"])){
if([JavascriptInterfaceObject respondsToSelector:selasyn]){
void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
NSString *del=@"";
result[@"code"]=@0;
if(value!=nil){
result[@"data"]=value;
}
value=[FDWebUtil objToJsonString:result];
value=[value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
if(complete){
del=[@"delete window." stringByAppendingString:cb];
}
NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
@synchronized(self)
{
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
self->jsCache=[self->jsCache stringByAppendingString:js];
if(t-self->lastCallTime<50){
if(!self->isPending){
[self evalJavascript:50];
self->isPending=true;
}
}else{
[self evalJavascript:0];
}
}
};
SuppressPerformSelectorLeakWarning(
[JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];
);
break;
}
}else if([JavascriptInterfaceObject respondsToSelector:sel]){
id ret;
SuppressPerformSelectorLeakWarning(
ret=[JavascriptInterfaceObject performSelector:sel withObject:arg];
);
[result setValue:@0 forKey:@"code"];
if(ret!=nil){
[result setValue:ret forKey:@"data"];
}
break;
}
NSString*js=[error stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
if(isDebug){
js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
[self evaluateJavaScript :js completionHandler:nil];
}
NSLog(@"%@",error);
}while (0);
}
return [FDWebUtil objToJsonString:result];
}
複製程式碼
裡面會去取兩個方法methodOne
和methodTwo
,當方法帶block
則會得到methodTwo
,否則得到methodOne
。
//return method name for xxx: or xxx:handle:
+(NSString *)methodByNameArg:(NSInteger)argNum selName:(NSString *)selName class:(Class)class
{
NSString *result = nil;
if(class){
NSArray *arr = [FDWebUtil allMethodFromClass:class];
for (int i=0; i<arr.count; i++) {
NSString *method = arr[i];
NSArray *tmpArr = [method componentsSeparatedByString:@":"];
if ([method hasPrefix:selName]&&tmpArr.count==(argNum+1)) {
result = method;
return result;
}
}
if (result == nil) {
NSArray *arr = [FDWebUtil allMethodFromSuperClass:class];
for (int i=0; i<arr.count; i++) {
NSString *method = arr[i];
NSArray *tmpArr = [method componentsSeparatedByString:@":"];
if ([method hasPrefix:selName]&&tmpArr.count==(argNum+1)) {
result = method;
return result;
}
}
}
}
return result;
}
複製程式碼
帶非同步方法的會執行因為args[@"_dscbstub"]
不為空會帶定義的completionHandler
執行:
SuppressPerformSelectorLeakWarning(
[JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];
複製程式碼
在completionHandler
中會每50
毫秒呼叫一次evalJavascript
NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
複製程式碼
[self evalJavascript:50];
複製程式碼
會執行到非同步方法,從而完成回撥。