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种
  1. WebView的addJavascriptInterface进行对象映射(低版本Android4以下好像有一些安全问题,本人没有验证)
  2. WebViewClient 的 shouldOverrideUrlLoading 方法回调拦截 url
  3. WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt方法回调拦截JS对话框alert()、confirm()、prompt() 消息一般常用onJsPrompt、prompt进行回调拦截。

H5和natvie通讯具体实现

1.通过DSBrige方式

1.1 H5和IOS 端源码解析

  • 初始化
  1. 首先通过js代码注入为window添加_dswk用于标注是Native使用。
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._dswk=true;"
                                              injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                           forMainFrameOnly:YES];
[configuration.userContentController addUserScript:script];

  1. 注册本地Native类并添加相应的namespacejavaScriptNamespaceInterfacesFDInternalApis类注册了部分需要的方法,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
  1. Native调用js使用方法:
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber * value){
    NSLog(@"%@",value);
}];
  1. js中需要注册相应方法(同步):
dsBridge.register('addValue', function (r, l) {
    return r + l;
})
  1. 对于异步js中的注册方式:
dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
     responseCallback(arg1+" "+arg2+" "+arg3);
})
  1. 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];
  1. js会去调用_handleMessageFromNative方法并对数据进行解析。 当js处理完毕会调用:
bridge.call("_dsb.returnValue", ret)
  1. 然后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
  1. js 先定义一个:
var str=dsBridge.call("testSyn","testSyn");
  1. 然后会执行到
ret = prompt("_dsbridge=" + method, arg);
  1. 通过prompt在iOS端会调用代理方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
  1. 可以看到其处理方式:
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);
}
  1. 这里会调用一个关键方法
-(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];
}
  1. 里面会去取两个方法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;
}
  1. 带异步方法的会执行因为args[@"_dscbstub"]不为空会带定义的completionHandler执行:
SuppressPerformSelectorLeakWarning(
                                                       [JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];
  1. 在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供原生调用.
  1. 初始化 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")
  1. 调用原生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版实例
  1. 先建立一个桥接文件:SwiftDemo-Bridging-Header.h
#import "dsbridge.h"
  1. 新建一个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
    }
    
}

  1. 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,就不进行内容的加载,而是执行相应的逻辑处理。嵌入webviewh5中的js一般是通过动态创建隐藏iframe标签,赋值上文提到的链接给srciframe不会引起页面调转、刷新。

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流程

  1. 设计出一个Native与JS交互的全局中间对象
  2. JS如何调用Native
  3. Native如何得知api被调用
  4. 分析url-参数和回调的格式
  5. Native如何调用JS
  6. 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

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页