APP原生与H5互调Bridge技术原理及基础使用
API使用
js调用原生插件功能
调用命名为'11'的插件里的一个定时器api:jsCallTimer
带回调结果带参数的调用方式:
YN.callNative('11',"jsCallTimer",'我是传到原生端的参数',function (value) {
if (a == 1){
document.getElementById("progress1").innerText = value
}else{
document.getElementById("progress2").innerText = value
}
},function (error) {
alert(error)
})
不带回调结果带参数的调用方式:
YN.callNative('11',"jsCallTimer",'我是传到原生端的参数')
不带回调结果不带参数的调用方式:
YN.callNative('11',"jsCallTimer")
原生调用js插件功能
调用命名为'asynObj'的插件里的一个定时器api:startTimer
带回调结果带参数的调用方式:
[dwebview callHandler:@"asynObj" action:@"startTimer" arguments:@"我是传到js端的参数" completionHandler:^(CallbackStatus status, id _Nonnull value, NSString * _Nonnull callId, BOOL complete) {
[sender setTitle:[NSString stringWithFormat:@"%@-%@",value,callId] forState:0];
}];
不带回调结果的调用方式:
[dwebview callHandler:@"asynObj" action:@"startTimer" arguments:@"我是传到js端的参数" completionHandler:nil];
一些全局约定
-
js调原生和原生调js的参数传递必须是json字符串格式。
-
api调用,底层逻辑必须使用命名空间方式即:namespace.apixxx的形式。
-
还有很多规范和约定,后续补充。
js call native
关键技术点
原生Android端向浏览器注入供js调用的对象‘_anbridge’,对象里实现‘call()’方法,并且方法需要加上@JavascriptInterface注解,代码示例:
WebSettings webSettings = wv.getSettings();
webSettings.setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JsApp(),"_anbridge");
class JsApp{
public JsApp(){}
@JavascriptInterface
public void call(Object obj){
}
}
原生iOS端
向浏览器配置对象里注入‘window._ynwk=true;’这段js代码,并且设置注入时机为开始加载时即:injectionTime=WKUserScriptInjectionTimeAtDocumentStart,代码实现:
///初始化注入js标记
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._ynwk=true;"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
[configuration.userContentController addUserScript:script];
实现js端换起原生通信的关键是实现wk的h5输入框拦截回调方法- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler
当js端执行代码‘prompt()’时原生端就会自动调起该方法
在上面实现的基础上,js端判断window._anbridge为true则为与Android通信,执行代码:_anbridge.call(api, arg),如果判断window._ynwk为true则为与iOS端通信,执行代码:prompt('_ynbridge=' + api, arg),js端代码实现:
var natiValue = '';
if (window._anbridge)
natiValue = _anbridge.call(api, arg);//调用android对象的call()
else if (window._ynwk)
natiValue = prompt('_ynbridge=' + api, arg);
原生端、js端提供的api都要通过命名空间的方式管理,如:api_1在‘namespace1’这个命名空间下的类里面,则js端调用api_1书写形式为‘namespace1.api_1’。
原生端和js端提供的功能都以插件的方式提供,插件(除基础插件)都继承自一个基础插件类,插件结果回调都是走异步回传值方式,同步方式也可以但暂没实现。
iOS端逻辑步骤
基础插件对象是处理js通讯和插件扩展的必要条件,wk浏览器初始化好后将基础插件类注册进插件集合,然后读取配置文件里可用的其他插件,将每个插件类注册进插件集合,代码实现:
//注册基础插件
[self addJavascriptObject:self.ynPlugin namespace:baseNameSpace];
//注册已有插件
NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"applyPlugPlist" ofType:@"plist"];
NSArray *modules = [NSArray arrayWithContentsOfFile:plistPath];
for (NSDictionary *obj in modules) {
Class class = NSClassFromString(obj[@"plug"]);
if (class != nil && ![class isKindOfClass:[NSNull class]]) {
[self addJavascriptObject:[[class alloc] init] namespace:obj[@"namespace"]];
}
}
-
js端的第一个信号来自wk的h5输入框拦截回调方法,参数prompt里携带js端要调用的api名字,参数值为字符串:_ynbridge=namespace1.api_1,_ynbridge=为YNBridge框架调用的标记,如果不是以这个标记开头则不做任何处理,只弹出正常的系统弹框。
-
通过api名,去插件集合里找有没有注册对应的插件对象,如果没有找到或找到了但插件下没有对应api则将错误结果返回js端
-
js调起的api,参数由defaultText携带。defaultText是json字符串,需要转换为json对象来解析出数据,参数值示例:{"data":null,"callId":"callId0"} data:真实参数值。 callId:api调用事件id或叫回传值队列id,当次api调用js需要回传值时此参数不为空,如果为空则表示当次api调用js端不需要结果回调
-
-(BOOL)exec:(YNJsCallInfo*)arg 此方法是插件接收数据的入口,这是个工厂方法子类必须实现,解析和组装好js过来的api和参数后用反射的方式执行对应插件的exec:方法,该方法同步方式返回个bool值,表示调用成功或失败,如果失败则将失败结果返回给js,代码实现:
BOOL(*action)(id,SEL,id) = (BOOL(*)(id,SEL,id))objc_msgSend;
BOOL ret=action(JavascriptInterfaceObject,sel,info);
if (ret) {
return YES;
}
return [self nativeCallBackWithCode:ret ? OK : ERROR value:ret ? @"OK" : error complete:YES callId:info.callId];
-
exec:方法的形参是YNJsCallInfo对象,该对象携带的参数:
action:api名,或叫动作标识字符串,各业务通过该字段判断该执行什么功能,如果插件内没有处理该api则返回调用失败的错误值false反之返回true。
callId:api调用事件id或叫回传值队列id,当给js回传值时需要带上该值返回去。
data:js给过来的参数值。
callBack:block变量,结果回调入口,回传值时需要指定四个参数status、value、callId、complete,参数用处后面讲解。 -
功能实现完成后需要调用YNJsCallInfo对象的callBack回调方法,方法参数:
status:结果状态值,此值为一个枚举类型,OK表示成功ERROR表示失败。
value:结果值,该值最后在调用js回传值api时会转换为json字符串格式。
callId:api调用事件id或叫回传值队列id。
complete:bool值,当次api任务是否全部执行完毕,处理需要保活服务的长连接状态,false执行完毕,true服务需要继续保持。 -
api调用完毕,需要给js回传值时,调用wk的- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler方法 执行这段js代码:window.nativeCallBack('%@',%ld,%@,%d),nativeCallBack()是js端接收原生端回传值的方法,接收四个参数,即为YNJsCallInfo对象的callBack回调参数。
-
原生功能通过插件的形式实现,要新增一个插件只需要: 第一步新建一个继承'YNPlugin'基础插件类的对象,然后在对象里实现方法-(BOOL)exec:(YNJsCallInfo*)arg; 第二步在YNBridgePlugPlist.plist文件里添加以下形式的代码
<dict>
<key>namespace</key>
<string>命名空间</string>
<key>plug</key>nativeCallBack
<string>插件类名</string>
</dict>
然后将命名空间名和相应的api名告诉js端即可
js端
调起一个原生插件时,执行YN对象里面的callNative: function (service,action,actionArgs,successCallback,failCallback)方法,方法参数:
service:原生api对应的命名空间名。
action:api名。
actionArgs:需要给原生端的参数。
successCallback:成功的回调。
failCallback:失败的回调。 比如我要调起原生端11命名空间下的jsCallTimer这个api,让原生端执行一个定时器功能,代码实现:
YN.callNative('11',"jsCallTimer",undefined,function (value) {
if (a == 1){
document.getElementById("progress1").innerText = value
}else{
document.getElementById("progress2").innerText = value
}
},function (error) {
alert(error)
})
-
执行YN.call()方法,实现调起原生和结果回调队列的维护,如果注入过安卓js对象‘window._anbridge’则执行_anbridge.call(api, arg)调起安卓端,如果注入过‘window._ynwk’值为true则执行prompt('_ynbridge=' + api, arg)调起iOS端,如果需要有回传值,则arg对象将给callId字段赋一个唯一值,并且在window.nativeCallBackIds缓存集合里新增callId值,值即为回调函数。
-
所有插件调用的前提基础是js端和原生端都已正常初始化,并且通讯已建立,即deviceReady已为ture,deviceReady的询问会在js入口函数里执行,即通过YN.call()方法,执行一个原生YNBase.init的api,如果结果返回为OK则为deviceReady成功
-
原生端插件执行结果回调通过‘nativeCallBack = function (callId,status,args,complete)’方法接收值,方法内部通过callId在window.nativeCallBackIds对象里找到回调方法然后执行,将args值由json字符串转json对象后传入,判断complete字段,为true则执行:delete window.nativeCallBackIds[callId]代码,将该服务回调移除队列。
native call js
js端
- 实现思路和设计方式同js call native,即只是其一个反向过程,实现基础依然是需要实现和注册基础插件类,各子插件继承基础插件,结果回调都是通过异步回传值,所以细节不做重复阐述。
- 在入口函数执行基础插件和各插件对象的注册,注册完成后可以调用原生YNBase.jsinit这个api告诉原生端,代码实现:
YN.register('asynObj',new YNPlugin());
YN.register('YNPlugin1',new YNPlugin1());
//告诉原生js初始化了,调原生初始化api(在js初始化前原生就要求执行的js方法可在jsinit方法里开始执行了)
if (deviceReady){
YN.call('YNBase.jsinit');
}
register()方法内部实现同原生注册插件的形式,将插件和对应的命名空间添加进window.nativeNamespaceInterfaces集合。
-
接收原生端第一个信号由nativeCallJs = function(callId,service,action,actionArgs)方法接收,参数:
callId:api调用事件id或叫回传值队列id。
service:js api对应的命名空间名。
action:api名。
actionArgs:原生端的参数。 方法内部实现同原生插件调用,也是找到插件并执行插件方法exec(action,args,responseCallback)。 -
插件回传值结果和api调用结果通过调用原生的YNBase.returnValue这个api实现,即执行YN.call('YNBase.returnValue', value); value是参数对象,包含data、callId、complete、status四个字段,含义和用途同原生回调那里。
iOS端
-
调起一个js端的插件功能,执行wk对象的方法-(void)callHandler:(NSString*)server action:(NSString *)action arguments:(id)args completionHandler:(JSCallback)completionHandler;该方法逻辑同js call native时调用的YN.call()方法,通过维护一个callid服务队列来处理结果回传。
-
组装好参数后浏览器执行window.nativeCallJs('%@','%@','%@',%@)这个js代码即可调起js,代码示例:
[self evaluateJavaScript:[NSString stringWithFormat:@"window.nativeCallJs('%@','%@','%@',%@)",info.callId,info.service,info.action,[JSBUtil objToJsonString:info.args]]];
接收插件结果回传值在基础插件里监听returnValue这个api的执行,逻辑处理同js端nativeCallBack()方法。也是如果complete字段值为true时将该服务对象从队列里移除







