iOS和H5交互离不开原生层面的支持
- WKWebView 执行一段js代码,可以通过:
webView.evaluateJavaScript("console.log('hello word!')")
- JS可以通过prompt传递参数到Native ,WKUIDelegate中的prompt方法会拦截到
var rsult = prompt(method,args)
WKUIDelegate中的prompt:
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
以上就是iOS Native和H5交互的基础。
DSBridge是跨平台JavaScript Bridge,通过它,您可以在JavaScript和Native之间同步或异步调用彼此的函数。
集成方式:
pod "dsBridge"
初始化:
- H5需要引入dsbridge.js
<script src="https://cdn.jsdelivr.net/npm/dsbridge/dist/dsbridge.js"> </script>
- iOS需要:
- 创建DWKWebView对象,或者继承DWKWebView
- 注册方法:可以扩展InternalApis类型,或者自己实现,然后通过addJavascriptObject:注册相应的方法
InternalApis * interalApis= [[InternalApis alloc] init];
interalApis.webview=self;
[self addJavascriptObject:interalApis namespace:@"_dsb"];
原生调用js方法
- 需要js先注册对应的方法,比如:
addValue
<script>
dsBridge.register('addValue', function (r, l) {
return r + l;
})
</script>
register方法做了如下事情:
- 通过_dsb.dsinit初始化 dsbridge
- 待注册方法保存到_dsaf或_dsf,object 保存到 _dsaf._obs或_dsf._obs
window属性下有如下数据结构:
/*
// 存储 同步方法,对象
_dsf: { _obs: {} },
// 存储 异步方法,对象
_dsaf: { _obs: {} },
*/
//js注册方法
register: function (b, a, c) {
c = c ? window._dsaf : window._dsf;
if (window._dsInit) {
window._dsInit = true;
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0);
}
// 方法直接保存到 _dsaf或_dsf
// object 保存到 _dsaf._obs或_dsf._obs
if ("object" == typeof a) {
c._obs[b] = a;
} else {
c[b] = a;
}
}
- 原生通过
callHandler:
调用js方法,最终会在js中执行_handleMessageFromNative
- Native会创建一个唯一的callbackId和handle对应
- 保存handle到全局,传递callbackId到js
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber* value){
NSLog(@"%@",value);
}];
...
// evaluateJavaScript 执行 js的 _handleMessageFromNative(p),参数p:包含{method,callbackId,data}
- (void) dispatchJavascriptCall:(DSCallInfo*) info{
NSString * json=[JSBUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
@"data":[JSBUtil objToJsonString: info.args]}];
[self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
completionHandler:nil];
}
_handleMessageFromNative()的实现如下:
- json序列化,取出参数和callbackId
- 定义同步/异步方法
- 执行对应的方法,然后通过_dsb.returnValue将callbackId回传给Native
// 原生调用js会来到这 {method,callbackId,data}
_handleMessageFromNative: function (a) {
var e = JSON.parse(a.data);
var b = { id: a.callbackId, complete: !0 };
var c = this._dsf[a.method];
var d = this._dsaf[a.method];
// 同步js方法
var h = function (a, c) {
b.data = a.apply(c, e);
// js 执行完成处理回调,原生returnValue会被执行
bridge.call("_dsb.returnValue", b);
};
// 异步js方法
var k = function (a, c) {
// 异步保存 block
e.push(function (a, c) {
b.data = a;
b.complete = !1 !== c;
bridge.call("_dsb.returnValue", b);
});
// js 执行
a.apply(c, e);
};
if (c) {
// 同步
h(c, this._dsf);
} else if (d) {
// 异步
k(d, this._dsaf);
} else {
c = a.method.split(".");
// c.length >= 2 , 命名空间对象
if (!(2 > c.length)) {
a = c.pop();
var c = c.join("."),
d = this._dsf._obs, // 同步
d = d[c] || {},
f = d[a];
}
if (f && "function" == typeof f) {
h(f, d);
} else {
var d = this._dsaf._obs; // 异步
d = d[c] || {};
f = d[a];
if (f && "function" == typeof f) {
k(f, d);
}
}
}
}
最后,js处理完,通过原生returnValue回传参数(包含最初的callbackId),根据 callbackId 取出对应的 handle并执行;
整个native->H5过程结束
- (id) returnValue:(NSDictionary *) args{
// 通过 callbackId 取出对应的 block 完成整个过程
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调用原生方法
- js调用原生方法 ,需要等原生先注册好对应的方法,注册方式写法如下:
可以创建自己的javascriptObject,namespace,也可通过扩展InternalApis添加交互方法。
InternalApis * interalApis= [[InternalApis alloc] init];
interalApis.webview=self;
[self addJavascriptObject:interalApis namespace:@"_dsb"];
- js通过 prompt调用原生方法:
dsBridge.call()调用原生,方法内部会将callback保存到window下,比将callback id传给原生
dsBridge.call("testAsyn","hello", function (v) {
alert(v)
})
call方法的定义如下:
- 处理无参数,有callback的情况
- callback:id 保存到window下,id传给Native
- 通过prompt函数传递方法名和参数
call: function (b, a, c) {
var e = "";
// 处理无参数,有callback的情况
if ("function" == typeof a) {
c = a;
a = {};
}
a = { data: void 0 === a ? null : a };
// callback,保存到window下
if ("function" == typeof c) {
// callback id
var g = "dscb" + window.dscb++;
window[g] = c;
a._dscbstub = g;
}
a = JSON.stringify(a);
if (window._dsbridge) {
e = _dsbridge.call(b, a);
} else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) {
// iOS WKUIDelegate中的prompt方法会被调用
e = prompt("_dsbridge=" + b, a);
}
return JSON.parse(e || "{}").data;
}
prompt会触发WKUIDelegate中的runJavaScriptTextInputPanelWithPrompt方法:(方法很长,只看关键部分)
- 获得方法名prompt,参数defaultText
- call:method:
- (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);
}
// ......
}
此处有一个关键的方法,call:method:
- 截取method中的前缀namespace
- 根据namespace取出对应的javascriptObject;通过runtime遍历出javascriptObject 类中的所有方法。
- 匹配method,封装返回参数(包括id) 并执行
- navite处理完成,通过evalJavascript执行对应的js回掉
整个JS->Navite调用过程完毕
-(NSString *)call:(NSString*) method :(NSString*) argStr
{
NSArray *nameStr=[JSBUtil parseNamespace:[method stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
// 根据命名空间取 interfaceObject
id JavascriptInterfaceObject=javaScriptNamespaceInterfaces[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];
// methodOne xxx:
NSString *methodOne = [JSBUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
// methodTwo xxx:handle:
NSString *methodTwo = [JSBUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
SEL sel=NSSelectorFromString(methodOne);
SEL selasyn=NSSelectorFromString(methodTwo);
// 解析参数 argStr
NSDictionary * args=[JSBUtil jsonStringToObject:argStr];
id arg=args[@"data"];
if(arg==[NSNull null]){
arg=nil;
}
NSString * cb;
do{
if(args && (cb= args[@"_dscbstub"])){
if([JavascriptInterfaceObject respondsToSelector:selasyn]){
__weak typeof(self) weakSelf = self;
// 对于异步方法,创建completionHandler
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];
}
/**
cb(JSON.parse(decodeURIComponent("value")).data);
delete window.cb
*/
// 通过window下的cb,回调数据
NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
__strong typeof(self) strongSelf = weakSelf;
@synchronized(self)
{
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
(*self).jsCache=[(*self).jsCache stringByAppendingString:js];
if(t-(*self).lastCallTime<50){
if(!(*self).isPending){
[strongSelf evalJavascript:50];
(*self).isPending=true;
}
}else{
[strongSelf evalJavascript:0];
}
}
};
void(*action)(id,SEL,id,id) = (void(*)(id,SEL,id,id))objc_msgSend;
action(JavascriptInterfaceObject,selasyn,arg,completionHandler);
break;
}
}
// 无handle,同步
else if([JavascriptInterfaceObject respondsToSelector:sel]){
id ret;
id(*action)(id,SEL,id) = (id(*)(id,SEL,id))objc_msgSend;
ret=action(JavascriptInterfaceObject,sel,arg);
[result setValue:@0 forKey:@"code"];
if(ret!=nil){
[result setValue:ret forKey:@"data"];
}
break;
}
NSString*js=[error stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
if(isDebug){
js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
[self evaluateJavaScript :js completionHandler:nil];
}
NSLog(@"%@",error);
}while (0);
}
return [JSBUtil objToJsonString:result];
}
源码地址:DSBridge
最后附上包含详细注释的 js源码
var bridge = {
default: this,
// js调用原生(method,参数,callback)
call: function (b, a, c) {
var e = "";
// 处理无参数,有callback的情况
if ("function" == typeof a) {
c = a;
a = {};
}
a = { data: void 0 === a ? null : a };
// callback,保存到window下
if ("function" == typeof c) {
var g = "dscb" + window.dscb++;
window[g] = c;
a._dscbstub = g;
}
a = JSON.stringify(a);
if (window._dsbridge) {
e = _dsbridge.call(b, a);
} else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) {
// iOS WKUIDelegate中的prompt方法会被调用
e = prompt("_dsbridge=" + b, a);
}
return JSON.parse(e || "{}").data;
},
//js注册方法
register: function (b, a, c) {
c = c ? window._dsaf : window._dsf;
if (window._dsInit) {
window._dsInit = true;
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0);
}
// 方法直接保存到 _dsaf或_dsf
// object 保存到 _dsaf._obs或_dsf._obs
if ("object" == typeof a) {
c._obs[b] = a;
} else {
c[b] = a;
}
},
registerAsyn: function (b, a) {
this.register(b, a, true);
},
hasNativeMethod: function (b, a) {
// 原生中提前写好的 hasNativeMethod
return this.call("_dsb.hasNativeMethod", { name: b, type: a || "all" });
},
disableJavascriptDialogBlock: function (b) {
// 原生中提前写好的,禁用Javascript对话框
this.call("_dsb.disableJavascriptDialogBlock", { disable: !1 !== b });
},
};
// 立即执行函数
!(function () {
if (!window._dsf) {
var b = {
// 存储 同步方法,对象
_dsf: { _obs: {} },
// 存储 异步方法,对象
_dsaf: { _obs: {} },
dscb: 0,
dsBridge: bridge,
close: function () {
bridge.call("_dsb.closePage");
},
// 原生调用js会来到这 {method,callbackId,data}
_handleMessageFromNative: function (a) {
var e = JSON.parse(a.data);
var b = { id: a.callbackId, complete: !0 };
var c = this._dsf[a.method];
var d = this._dsaf[a.method];
// 同步js方法
var h = function (a, c) {
b.data = a.apply(c, e);
// js 执行完成处理回调,原生returnValue会被执行
bridge.call("_dsb.returnValue", b);
};
// 异步js方法
var k = function (a, c) {
// 异步保存 block
e.push(function (a, c) {
b.data = a;
b.complete = !1 !== c;
bridge.call("_dsb.returnValue", b);
});
// js 执行
a.apply(c, e);
};
if (c) {
// 同步
h(c, this._dsf);
} else if (d) {
// 异步
k(d, this._dsaf);
} else {
c = a.method.split(".");
// c.length >= 2 , 命名空间对象
if (!(2 > c.length)) {
a = c.pop();
var c = c.join("."),
d = this._dsf._obs, // 同步
d = d[c] || {},
f = d[a];
}
if (f && "function" == typeof f) {
h(f, d);
} else {
var d = this._dsaf._obs; // 异步
d = d[c] || {};
f = d[a];
if (f && "function" == typeof f) {
k(f, d);
}
}
}
},
},
a;
//将b的所有成员赋值给window
for (a in b) window[a] = b[a];
// 注册 _hasJavascriptMethod
bridge.register("_hasJavascriptMethod", function (a, b) {
b = a.split(".");
if (2 > b.length) return !(!_dsf[b] && !_dsaf[b]);
a = b.pop();
b = b.join(".");
return (b = _dsf._obs[b] || _dsaf._obs[b]) && !!b[a];
});
}
})();