dsBridge是一個三端易用的現代跨平臺 Javascript bridge, 經過它,你能夠在Javascript和原生之間同步或異步的調用彼此的函數。java
我對dsBridge
中js代碼作了詳細註釋方便你們閱讀。ios
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]
}
})
}();
複製代碼
首先經過js代碼注入爲window
添加_dswk
用於標註是Native
使用。git
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._dswk=true;"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
[configuration.userContentController addUserScript:script];
複製代碼
註冊本地Native
類並添加相應的namespace
到javaScriptNamespaceInterfaces
。FDInternalApis
類註冊了部分須要的方法,namespace
是_dsb
。github
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
使用方法:web
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber * value){
NSLog(@"%@",value);
}];
複製代碼
js
中須要註冊相應方法(同步):typescript
dsBridge.register('addValue', function (r, l) {
return r + l;
})
複製代碼
對於異步js
中的註冊方式:json
dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
responseCallback(arg1+" "+arg2+" "+arg3);
})
複製代碼
native
端使用時會走如下方法:api
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
處理完畢會調用:bash
bridge.call("_dsb.returnValue", ret)
複製代碼
而後native
會取出保存在handerMap
的block
執行:app
- (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;
}
複製代碼
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"])){ //此處cb即時前面call方法中的cbName,cbName對應的方法也保存在了window下,用於後面調用
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];
複製代碼
會執行到異步方法,從而完成回調。