H5与原生通讯之二 (DSBrige,H5,IOS,Android源码实例)
H5和natvie通讯方式简介
1.H5和IOS通讯
IOS主要有三种方式:WKWebView 、UIWebView、JavaScriptCoreWKWebView:
- WKWebView:WKWebView优点很多,支持更多H5特性,刷新效率及内置手势等,更加强大,性能也更优,不一一列举,如果大家app不需要兼容7及以下版本,不需要拦截一些请求,直接解析本地一些文件,建议使用WKWebView。
- UIWebView:较老webview,第一代。其中stringByEvaluatingJavaScriptFromString方法提供了OC与js交互的能力。JavaScriptCore(ios7及以后版本)。
- JavaScriptCore:JavaScriptCore框架是webkit重要组成部分,主要是对JS进行解析和提供执行环境,Javascript的虚拟机,有点类似v8引擎。
2.H5和Android通讯
- Android调用JS代码的方法主要有2种: WebView的loadUrlWebView的evaluateJavascript
- JS调用Android代码的方法主要有3种:
- WebView的addJavascriptInterface进行对象映射(低版本Android4以下好像有一些安全问题,本人没有验证)
- WebViewClient 的 shouldOverrideUrlLoading 方法回调拦截 url
- WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt方法回调拦截JS对话框alert()、confirm()、prompt() 消息一般常用onJsPrompt、prompt进行回调拦截。
H5和natvie通讯具体实现
1.通过DSBrige方式
- DSBirdge-iOS 源码下载:DSBirdge-iOS
- DSBirdge-Android源码下载:DSBirdge-Android
1.1 H5和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
- js 先定义一个:
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];
1.2 dbbridge 安卓端源码解析
1.3 dbbrige 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]
}
})
}();
1.4 IOS端使用DSBridge框架API
1.4.1 集成源码
pod "dsBridge"
1.4.2 Api使用
- 新建一个类,实现API
#import "dsbridge.h"
...
@implementation JsApiTest
//同步API
- (NSString *) testSyn:(NSString *) msg
{
return [msg stringByAppendingString:@"[ syn call]"];
}
//异步API
- (void) testAsyn:(NSString *) msg :(JSCallback)completionHandler
{
completionHandler([msg stringByAppendingString:@" [ asyn call]"],YES);
}
@end
- 添加API类实例到 DWKWebView
DWKWebView * dwebview=[[DWKWebView alloc] initWithFrame:bounds];
// register api object without namespace
[dwebview addJavascriptObject:[[JsApiTest alloc] init] namespace:nil];
- 在Javascript中调用原生 (Java/Object-c/swift) API ,并注册一个 javascript API供原生调用.
- 初始化 dsBridge
//cdn方式引入初始化代码(中国地区慢,建议下载到本地工程)
//<script src="https://cdn.jsdelivr.net/npm/dsbridge@3.1.4/dist/dsbridge.js"> //</script>
//npm方式安装初始化代码
//npm install dsbridge@3.1.4
var dsBridge=require("dsbridge")
- 调用原生API ,并注册一个 javascript API供原生调用.
//同步调用
var str=dsBridge.call("testSyn","testSyn");
//异步调用
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v);
})
//注册 javascript API
dsBridge.register('addValue',function(l,r){
return l+r;
})
- 在Object-c中调用Javascript API
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber* value){
NSLog(@"%@",value);
}];
1.4.3 Api注意事项
- 调试模式:
通过下面代码可以开启调式模型
// open debug mode
[dwebview setDebugMode:true];
但是需要注意:
在调试模式时,发生一些错误时,将会以弹窗形式提示,并且原生API如果触发异常将不会被自动捕获,因为在调试阶段应该将问题暴露出来。如果调试模式关闭,错误将不会弹窗,并且会自动捕获API触发的异常,防止crash。强烈建议在开发阶段开启调试模式
- 进度回调
通常情况下,调用一个方法结束后会返回一个结果,是一一对应的。但是有时会遇到一次调用需要多次返回的场景,比如在javascript钟调用端上的一个下载文件功能,端上在下载过程中会多次通知javascript进度, 然后javascript将进度信息展示在h5页面上,这是一个典型的一次调用,多次返回的场景,如果使用其它Javascript bridge, 你将会发现要实现这个功能会比较麻烦,而DSBridge本省支持进度回调,你可以非常简单方便的实现一次调用需要多次返回的场景
下面我们实现一个倒计时的例子:
- ( void )callProgress:(NSDictionary *) args :(JSCallback)completionHandler
{
value=10;
hanlder=completionHandler;
timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(onTimer:)
userInfo:nil
repeats:YES];
}
-(void)onTimer:t{
if(value!=-1){
hanlder([NSNumber numberWithInt:value--],NO);
}else{
hanlder(@"",YES);
[timer invalidate];
}
}
- Javascript 弹出框
DSBridge已经实现了 Javascript的弹出框函数(alert/confirm/prompt),这些对话框按钮、标签文字默认都是中文的,如果你想自定义这些文本可以参考 customJavascriptDialogLabelTitles API,如果你不想使用DSBridge实现的对话框,你可以通过设置DSUIDelegate 属性(是WKUIDelegate的代理属性)完全自定义。
另外注意,DSBridge实现的弹出框都是模态的,这会阻塞UI线程,如果你需要非模态的对话框,请参考disableJavascriptDialogBlock API.
1.4.4 Api使用swift版实例
- 先建立一个桥接文件:SwiftDemo-Bridging-Header.h
#import "dsbridge.h"
- 新建一个DSbridgeViewController
import UIKit
class DSbridgeViewController: UIViewController {
private var webview:DWKWebView = {
let webview:DWKWebView = DWKWebView.init()
return webview
}()
override func viewDidLoad() {
super.viewDidLoad()
//设置webview
setupWebview()
//JS调用原生
addJsMethod()
//原生调用JS
nativeCallJS()
}
// MARK: - 设置Webview
private func setupWebview() {
webview.frame = self.view.bounds
self.view.addSubview(webview)
#if DEBUG
webview.setDebugMode(true)
#endif
webview.customJavascriptDialogLabelTitles(["alertTitle" : "Notification", "alertBtn" : "OK"])
webview.navigationDelegate = self
let baseUrl = URL.init(fileURLWithPath: Bundle.main.bundlePath)
let htmlPath = Bundle.main.path(forResource: "test", ofType: "html") ?? ""
let htmlContent = (try? String.init(contentsOfFile: htmlPath, encoding: String.Encoding.utf8)) ?? ""
webview.loadHTMLString(htmlContent, baseURL: baseUrl)
}
// MARK: - JS调用原生
private func addJsMethod() {
//添加原生方法类
webview.addJavascriptObject(JsApiTestSwift.init(), namespace: nil)
webview.addJavascriptObject(JsApiTestSwift.init(), namespace: "swift")//增加命名空间, JS在调用的时候可以用 swift.methodName 方便管理功能模块可增强阅读
}
// MARK: - 原生调用JS
private func nativeCallJS() {
//原生调用js的addValue方法, 参数是[3, 4], 返回值是value
webview.callHandler("addValue", arguments: [3, 4]) { (value) in
print(value ?? "")
}
//拼接字符串
webview.callHandler("append", arguments: ["I", "love", "you"]) { (value) in
print(value ?? "")
}
//传递json
let dic: Dictionary = ["name": "weixiang", "sex": "male"]
let jsonStr: String = dicToString(dic) ?? ""
webview.callHandler("showJson", arguments: [jsonStr]) { (value) in
print(value ?? "")
}
//收到result 为json
let dic1: Dictionary = ["name": "zhangsan", "sex": "male"]
let jsonStr1: String = dicToString(dic1) ?? ""
webview.callHandler("showResult", arguments: [jsonStr1]) { (value) in
print(value ?? "")
}
webview.callHandler("startTimer") { (value) in
print(value ?? "")
}
//带有命名空间的方法
webview.callHandler("syn.addValue", arguments: [5, 6]) { (value) in
print(value as Any)
}
//测试是否js有这个方法
webview.hasJavascriptMethod("addValue") { (isHas) in
print(isHas)
}
//如果H5调用了window.close方法就会监听到
webview.setJavascriptCloseWindowListener {
print("监听到关闭H5页面")
}
}
}
extension DSbridgeViewController: WKNavigationDelegate {
// MARK: 字典转json字符串
func dicToString(_ dic:[String : Any]) -> String? {
let data = try? JSONSerialization.data(withJSONObject: dic, options: [])
let str = String(data: data!, encoding: String.Encoding.utf8)
return str
}
}
- js调用原生
- 新建一个JsApiTestSwift.swift
//JS调用原生方法类
import Foundation
typealias JSCallback = (String, Bool)->Void
class JsApiTestSwift: NSObject {
var value = 10
var feedbankHandler : JSCallback?
var valueTimer: Timer?
// MARK: - 测试同步方法
//MUST use "_" to ignore the first argument name explicitly。
@objc func testSyn( _ arg:String) -> String {
print("js调用了原生的testSyn方法")
return String(format:"%@[Swift sync call:%@]", arg, "test")
}
// MARK: - 测试异步有回调
@objc func testAsyn( _ arg:String, handler: JSCallback) {
print("js调用了原生的testAsyn方法")
handler(String(format:"%@[Swift async call:%@]", arg, "test"), true)
}
// MARK: - 带有dic参数的
@objc func testNoArgSyn( _ args:Dictionary<String, Any>) -> String{
print("js调用了原生的testNoArgSyn方法")
return String("带有dic参数的的方法")
}
// MARK: - 持续返回进度
@objc func callProgress( _ args:Dictionary<String, Any> , handler: @escaping JSCallback ){
print("js调用了原生的callProgress方法")
feedbankHandler = handler
valueTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(feedbackValue), userInfo: nil, repeats: true)
}
//返回进度value
@objc func feedbackValue() {
if let handler = feedbankHandler {
if value > 0{
handler(String(value), false)//上传中
value -= 1
}else {
handler(String(value), true)//上传完成
}
}
}
}
- js代码如下:
<!DOCTYPE html>
<html>
<head lang="zh-cmn-Hans">
<meta charset="UTF-8">
<title>DSBridge Test</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=0.5,user-scalable=no"/>
<!--require dsbridge init js-->
<script src="https://cdn.jsdelivr.net/npm/dsbridge/dist/dsbridge.js"> </script>
</head>
<style>
.btn {
text-align: center;
background: #eee;
color: #000;
padding: 20px;
margin: 30px;
font-size: 24px;
border-radius: 4px;
box-shadow: 4px 2px 10px #999;
}
.btn:active {
opacity: .7;
box-shadow: 4px 2px 10px #555;
}
</style>
<body>
<div class="btn" onclick="callSyn()">Synchronous call</div>
<div class="btn" onclick="callAsyn()">Asynchronous call</div>
<div class="btn" onclick="callSynSwift()">Syn call (swift)</div>
<div class="btn" onclick="callAsynSwift()">Async call(swift)</div>
<div class="btn" onclick="callNoArgSyn()">Sync call without argument</div>
<div class="btn" onclick="callNoArgAsyn()">Async call without argument</div>
<div class="btn" onclick="echoSyn()">echo.syn</div>
<div class="btn" onclick="echoAsyn()">echo.asyn</div>
<div class="btn" onclick="callAsyn_()">Stress test,2K times consecutive asynchronous API calls</div>
<div class="btn" onclick="callNever()">Never call because without @JavascriptInterface
annotation<br/>( This test is
just for Android ,should be ignored in IOS )
</div>
<div class="btn" onclick="callProgress()">call progress <span id='progress'></span></div>
<div class="btn" onclick="hasNativeMethod('xx')">hasNativeMethod("xx")</div>
<div class="btn" onclick="hasNativeMethod('testSyn')">hasNativeMethod("testSyn")</div>
<script>
function callSyn() {
alert(dsBridge.call("testSyn", "Hello"))
}
function callAsyn() {
dsBridge.call("testAsyn","hello", function (v) {
alert(v)
})
}
function callSynSwift() {
alert(dsBridge.call("swift.testSyn", "hello"))
}
function callAsynSwift() {
dsBridge.call("swift.testAsyn","hello", function (v) {
alert(v)
})
}
function callAsyn_() {
for (var i = 0; i < 2000; i++) {
dsBridge.call("testAsyn", "js+" + i, function (v) {
if (v == "js+1999 [ asyn call]") {
alert("All tasks completed!")
}
})
}
}
function callNoArgSyn() {
alert(dsBridge.call("testNoArgSyn"));
}
function callNoArgAsyn() {
dsBridge.call("testNoArgAsyn", function (v) {
alert(v)
});
}
function callNever() {
alert(dsBridge.call("testNever", {msg: "testSyn"}))
}
function echoSyn() {
// call function with namespace
var ret=dsBridge.call("echo.syn",{msg:" I am echoSyn call", tag:1});
alert(JSON.stringify(ret))
}
function echoAsyn() {
// call function with namespace
dsBridge.call("echo.asyn",{msg:" I am echoAsyn call",tag:2},function (ret) {
alert(JSON.stringify(ret));
})
}
function callProgress() {
dsBridge.call("callProgress", function (value) {
if(value==0) value="";
document.getElementById("progress").innerText = value
})
}
function hasNativeMethod(name) {
alert(dsBridge.hasNativeMethod(name))
}
dsBridge.register('addValue', function (r, l) {
return r + l;
})
dsBridge.registerAsyn('append', function (arg1, arg2, arg3, responseCallback) {
responseCallback(arg1 + " " + arg2 + " " + arg3);
})
dsBridge.registerAsyn('showJson', function (arg1, responseCallback) {
responseCallback(arg1);
})
dsBridge.registerAsyn('showResult', function (arg1, responseCallback) {
var json = {
"result" : {
"value": "hahahah",
"error": "haha"
}
}
responseCallback(json);
})
dsBridge.registerAsyn('startTimer', function (responseCallback) {
var t = 0;
var timer = setInterval(function () {
if (t == 5) {
responseCallback(t)
clearInterval(timer)
} else {
// if the 2nd argument is false, the java callback handler will be not removed!
responseCallback(t++, false)
}
}, 1000)
})
// namespace test for syn functions
dsBridge.register("syn", {
tag: "syn",
addValue:function (r,l) {
return r+l;
},
getInfo: function () {
return {tag: this.tag, value:8}
}
})
// namespace test for asyn functions
dsBridge.registerAsyn("asyn", {
tag: "asyn",
addValue:function (r,l, responseCallback) {
responseCallback(r+l);
},
getInfo: function (responseCallback) {
responseCallback({tag: this.tag, value:8})
}
})
</script>
</body>
</html>
1.5 Android端使用DSBridge框架API
2.通过JSBrige方式
2.1 H5和IOS
目前兼顾兼容性、比较成熟的方案还是通过拦截URL的方式。UIWebView
的特性,在UIWebView
内发起的所有网络请求,都可以在Native层被捕捉到。利用这一特性,就可以在UIWebView
内发起一个自定义的网络请求,一般格式:jsbridge://method?参数1=value1&参数2=value2
于是在UIWebView
中,只要发现是jsbridge://
开头的url
,就不进行内容的加载,而是执行相应的逻辑处理。嵌入webview
的h5
中的js
一般是通过动态创建隐藏iframe标签,赋值上文提到的链接给src
,iframe
不会引起页面调转、刷新。
var src= 'jsbridge://method?参数1=value1&参数2=value2';
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = src;
document.body.appendChild(iframe);
//再删除iframesetTimeout(function() {
iframe.remove();
}, 50);
2.2 H5和android
2.3 JS端
2.3.1 jsbridge流程
- 设计出一个Native与JS交互的全局中间对象
- JS如何调用Native
- Native如何得知api被调用
- 分析url-参数和回调的格式
- Native如何调用JS
- H5中api方法的注册以及格式
2.3.2 js核心代码
(function() {
(function() {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var JSBridge = window.JSBridge || (window.JSBridge = {});
//jsbridge协议定义的名称
var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge';
//最外层的api名称
var API_Name = 'namespace_bridge';
//进行url scheme传值的iframe
var messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
document.documentElement.appendChild(messagingIframe);
//定义的回调函数集合,在原生调用完对应的方法后,会执行对应的回调函数id
var responseCallbacks = {};
//唯一id,用来确保每一个回调函数的唯一性
var uniqueId = 1;
//本地注册的方法集合,原生只能调用本地注册的方法,否则会提示错误
var messageHandlers = {};
//当原生调用H5注册的方法时,通过回调来调用(也就是变为了异步执行,加强安全性)
var dispatchMessagesWithTimeoutSafety = true;
//本地运行中的方法队列
var sendMessageQueue = [];
//实际暴露给原生调用的对象
var Inner = {
/**
* @description 注册本地JS方法通过JSBridge给原生调用
* 我们规定,原生必须通过JSBridge来调用H5的方法
* 注意,这里一般对本地函数有一些要求,要求第一个参数是data,第二个参数是callback
* @param {String} handlerName 方法名
* @param {Function} handler 对应的方法
*/
registerHandler: function(handlerName, handler) {
messageHandlers[handlerName] = handler;
},
/**
* @description 调用原生开放的方法
* @param {String} handlerName 方法名
* @param {JSON} data 参数
* @param {Function} callback 回调函数
*/
callHandler: function(handlerName, data, callback) {
//如果没有 data
if(arguments.length == 3 && typeof data == 'function') {
callback = data;
data = null;
}
_doSend({
handlerName: handlerName,
data: data
}, callback);
},
/**
* iOS专用
* @description 当本地调用了callHandler之后,实际是调用了通用的scheme,通知原生
* 然后原生通过调用这个方法来获知当前正在调用的方法队列
*/
_fetchQueue: function() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
},
/**
* @description 原生调用H5页面注册的方法,或者调用回调方法
* @param {String} messageJSON 对应的方法的详情,需要手动转为json
*/
_handleMessageFromNative: function(messageJSON) {
setTimeout(_doDispatchMessageFromNative);
/**
* @description 处理原生过来的方法
*/
function _doDispatchMessageFromNative() {
var message;
try {
message = JSON.parse(messageJSON);
} catch(e) {
//TODO handle the exception
console.error("原生调用H5方法出错,传入参数错误");
return;
}
//回调函数
var responseCallback;
if(message.responseId) {
//这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseId
responseCallback = responseCallbacks[message.responseId];
if(!responseCallback) {
return;
}
//执行本地的回调函数
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//否则,代表原生主动执行h5本地的函数
if(message.callbackId) {
//先判断是否需要本地H5执行回调函数
//如果需要本地函数执行回调通知原生,那么在本地注册回调函数,然后再调用原生
//回调数据有h5函数执行完毕后传入
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
//默认是调用EJS api上面的函数
//然后接下来原生知道scheme被调用后主动获取这个信息
//所以原生这时候应该会进行判断,判断对于函数是否成功执行,并接收数据
//这时候通讯完毕(由于h5不会对回调添加回调,所以接下来没有通信了)
_doSend({
handlerName: message.handlerName,
responseId: callbackResponseId,
responseData: responseData
});
};
}
//从本地注册的函数中获取
var handler = messageHandlers[message.handlerName];
if(!handler) {
//本地没有注册这个函数
} else {
//执行本地函数,按照要求传入数据和回调
handler(message.data, responseCallback);
}
}
}
}
};
/**
* @description JS调用原生方法前,会先send到这里进行处理
* @param {JSON} message 调用的方法详情,包括方法名,参数
* @param {Function} responseCallback 调用完方法后的回调
*/
function _doSend(message, responseCallback) {
if(responseCallback) {
//取到一个唯一的callbackid
var callbackId = Util.getCallbackId();
//回调函数添加到集合中
responseCallbacks[callbackId] = responseCallback;
//方法的详情添加回调函数的关键标识
message['callbackId'] = callbackId;
}
var uri;
//android中,可以通过onJsPrompt或者截取Url访问都行
var ua = navigator.userAgent;
if(ua.match(/(iPhone\sOS)\s([\d_]+)/)||ua.match(/(iPad).*OS\s([\d_]+)/)) {
//ios中,通过截取客户端url访问
//因为ios可以不暴露scheme,而是由原生手动获取
//正在调用的方法详情添加进入消息队列中,原生会主动获取
sendMessageQueue.push(message);
uri = Util.getUri();
}else{
//android中兼容处理,将所有的参数一起拼接到url中
uri = Util.getUri(message);
}
//获取 触发方法的url scheme
//采用iframe跳转scheme的方法
messagingIframe.src = uri;
}
var Util = {
getCallbackId: function() {
//如果无法解析端口,可以换为Math.floor(Math.random() * (1 << 30));
return 'cb_' + (uniqueId++) + '_' + new Date().getTime();
},
//获取url scheme
//第二个参数是兼容android中的做法
//android中由于原生不能获取JS函数的返回值,所以得通过协议传输
getUri: function(message) {
var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
if(message) {
//回调id作为端口存在
var callbackId, method, params;
if(message.callbackId) {
//第一种:h5主动调用原生
callbackId = message.callbackId;
method = message.handlerName;
params = message.data;
} else if(message.responseId) {
//第二种:原生调用h5后,h5回调
//这种情况下需要原生自行分析传过去的port是否是它定义的回调
callbackId = message.responseId;
method = message.handlerName;
params = message.responseData;
}
//参数转为字符串
params = this.getParam(params);
//uri 补充
uri += ':' + callbackId + '/' + method + '?' + params;
}
return uri;
},
getParam: function(obj) {
if(obj && typeof obj === 'object') {
return JSON.stringify(obj);
} else {
return obj || '';
}
}
};
for(var key in Inner) {
if(!hasOwnProperty.call(JSBridge, key)) {
JSBridge[key] = Inner[key];
}
}
})();
//注册一个测试函数
JSBridge.registerHandler('testH5Func', function(data, callback) {
alert('测试函数接收到数据:' + JSON.stringify(data));
callback && callback('测试回传数据...');
});
/*
***************************API********************************************
* 开放给外界调用的api
* */
window.jsapi = {};
/**
***app 模块
* 一些特殊操作
*/
jsapi.app = {
/**
* @description 测试函数
*/
testNativeFunc: function() {
//调用一个测试函数
JSBridge.callHandler('testNativeFunc', {}, function(res) {
callback && callback(res);
});
}
};
})();
2.4
参考文档:https://juejin.im/post/5abfbb7251882555731c434a
https://juejin.im/post/5c8774db6fb9a049e82c5dd6