Latest 1.0.0
Homepage https://github.com/Dcell/DHWebViewJavascriptBridge
License MIT
Platforms ios 8.0
Frameworks WebKit
Authors

DHWebViewJavascriptBridge

前言

作为移动端开发人员,经常会碰到H5调用native的交互方式(我们暂时不考虑,Native调用JS函数);比较常见WebApp调用本地摄像头相关功能。
比较成熟的框架,如Cordova,Cordova提供了一整套的解决方案,可非常方便的集成/添加插件。
但是有时候我们只需要一些简单的交互,需要更加简洁的方案来支持。

现有的方案

我大概搜索了下Github开源的项目

  1. JSBridge 6000+🌟
  2. WebViewJavascriptBridge 12000+🌟
  3. DsBridge

JSBridge很久没人维护了,issues也比较多;
WebViewJavascriptBridge 相对来说比较好,但是只支持iOS端;
DsBridge 支持android和iOS,属于后起之秀;

我们向往的方式

  1. JS 不需要加载某个.js文件;js代码由Native注入
  2. Native代码侵入少;最好不需要继承自定义的WebView组件

其实WebViewJavascriptBridge已经满足了如上2个条件,我们来学习下他实现的原理。

  • JS注入的时机
function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';  -------(1)
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

查看标记(1)的代码,JS在加载这段代码的时候,会主动告诉native

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

Native在截获到 https://__bridge_loaded__ 后,主动注入JS代码。


这样的话就产生了一个问题,如果JS注入还没执行,JS Call Native 或 Native Call JS,将产生无法挽回的严重问题。
该作者通过队列的方式来解决JS注册前后问题,如果是JS调用Native,并且在未注入JS前调用了注册的函数,在注入JS函数后,会主动执行队列里面的函数

setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

反之Native调用JS也一样

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

  • JS 调用 Native
    JS每次调用Native函数,会先把HandleName CallBackId CallbackFunction 封装成一个Message 存储起来;然后发送一个 ‘https://__wvjb_queue_message__‘ 告诉Native端
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

Native截获这个URL后,会主动去调用JS函数 _fetchQueue();

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    console.log('_fetchQueue:' + messageQueueString);
    sendMessageQueue = [];
    return messageQueueString;
}

把JS存储的Message都拉下来,解析,判断HandleName,执行对应的Native函数,并且将结果通过 function _dispatchMessageFromObjC(messageJSON) {}
告诉JS,一个完整的执行流程就结束了。


WebViewJavascriptBridge最主要的2个功能,我们大概分析了下,其他的功能大家可以通过源码去学习。
iframe.src 这种通知方式非常的通用,Cordova,JSBridge 都采用了这种方式。


我们现在的开发环境

其实iOS 8 WKWebView 和 android4.4+ Webview 都支持 addScriptMessageHandler 本地函数的注入
并且注入的时机 和 Webview 加载URL无关(android例外后面再讲)
如果只考虑,iOS 8+ android 4.4+ ,我们可以换一种思路去实现JS 和 Native的交互。

JS代码实现

;(function(){
    if(window.DhBridge){
        return;
    }
    window.DhBridge = {
        callHandler:_callHandler,
        handleMessage:_handleMessage,
        callBackQueue:{}
    };
    //调用Native注入的函数
    function _jsToNative(message){
        var result = false;
        try {
                //android
            dh_android_Bridge.dhBridge(message);
            result = true;
        } catch (error) {
            console.log('[_jsToNative] error:'+error);
        }
        try {
                //iOS
            window.webkit.messageHandlers.dhBridge.postMessage(message);
            result = true;
        } catch (error) {
            console.log('[_jsToNative] error:'+error);
        }
        return result;
    }

      //调用Native注册的函数
    function _callHandler(handlerName,args,success,fail){
        console.log('[_callHandler] '+handlerName+" " +args);
        var message = {handlerName:handlerName,args:args};
        if(success || fail){
            var callbackId = 'dhbridge_callbackId_'+new Date().getTime();
            window.DhBridge.callBackQueue[callbackId] = {success:success,fail:fail};
            message['callbackId'] = callbackId;
        }
        var result = _jsToNative(JSON.stringify(message));
        if(!result){
            if(fail){
                fail(400,"bad request");
            }
            delete window.DhBridge.callBackQueue[message['callbackId']];
        }
    }
    //Natvie执行完成后,回调的函数
    function _handleMessage(message){
        console.log('[_handleMessage] '+message);
        var json = JSON.parse(message);
        var callbackId = json['callbackId'];
        if(callbackId !== undefined){
            var callback = window.DhBridge.callBackQueue[callbackId];
            var code = json['code'];
            if(code === 200){
                callback.success(json['data']);
            }else{
                callback.fail(code,json['data']);
            }
            delete window.DhBridge.callBackQueue[callbackId];
        }
    }
 }
)();

逻辑非常简单,JS调用Native注册的函数,会生成的一个唯一的CallBackID,Native执行完,通过CallBackID找到对应的JS回调函数,流程走完。


iOS代码


@interface DHBridgeWKWebView()<WKScriptMessageHandler>
@property (nonatomic,strong) NSMutableDictionary *dictionary;
@property (nonatomic,strong) NSLock *lock;
@property (nonatomic,strong) WKWebView *webview;
@end

@implementation DHBridgeWKWebView

- (instancetype)initWithWKWebView:(WKWebView *)webview{
    self = [super init];
    if (self) {
        self.dictionary = [NSMutableDictionary new];
        self.lock = [NSLock new];
        self.webview = webview;
        [webview.configuration.userContentController addScriptMessageHandler:self name:SMH];
        NSString *js = DHBridgeJS();
        [self _evaluateJavaScript:js];
    }
    return self;
}

- (void)destroy{
    [self.webview.configuration.userContentController removeScriptMessageHandlerForName:SMH];
    [self.lock lock];
    [self.dictionary removeAllObjects];
    [self.lock unlock];
    self.webview = nil;
}

- (void)registerHandler:(NSString *)handlerName handler:(DHBHandler)handler{
    if (handlerName.length == 0) {return;}
    [self.lock lock];
    self.dictionary[handlerName] = handler;
    [self.lock unlock];
}

- (void)removeHandler:(NSString *)handlerName{
    if (handlerName.length == 0) {return;}
    [self.lock lock];
    [self.dictionary removeObjectForKey:handlerName];
    [self.lock unlock];
}

- (void)didReceiveScriptMessage:(WKScriptMessage *)message{
    if (message == nil) {return;}
    NSString *handlername = message.name;
    if (![handlername isEqualToString:SMH]) {
        return;
    }
    NSString *body = message.body;
    NSDictionary *dic =  [NSJSONSerialization JSONObjectWithData:[body dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];
    NSString *handlerName = [dic objectForKey:@"handlerName"];
    id args = [dic objectForKey:@"args"];
    NSString *callbackId = [dic objectForKey:@"callbackId"];
    [self.lock lock];
    if ([self.dictionary.allKeys containsObject:handlerName]) {
        DHBHandler handler = [self.dictionary valueForKey:handlerName];
        DHBCallback callback = ^(int code,id responseData) {
            NSMutableDictionary* message = [NSMutableDictionary dictionary];
            [message setObject:@(code) forKey:@"code"];
            [message setObject:callbackId forKey:@"callbackId"];
            [message setObject:responseData forKey:@"data"];

            NSString * evaluateJavaScriptCommond = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(0) error:nil] encoding:NSUTF8StringEncoding];
            evaluateJavaScriptCommond = [NSString stringWithFormat:EJS,evaluateJavaScriptCommond];
            [self _evaluateJavaScript:evaluateJavaScriptCommond];
        };
        handler(args,callback);
    }else{
        NSMutableDictionary* message = [NSMutableDictionary dictionary];
        [message setObject:@(400) forKey:@"code"];
        [message setObject:callbackId forKey:@"callbackId"];
        [message setObject:@"bad request" forKey:@"data"];

        NSString * evaluateJavaScriptCommond = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(0) error:nil] encoding:NSUTF8StringEncoding];
        evaluateJavaScriptCommond = [NSString stringWithFormat:EJS,evaluateJavaScriptCommond];
        [self _evaluateJavaScript:evaluateJavaScriptCommond];
    }
    [self.lock unlock];
}

- (void)_evaluateJavaScript:(NSString *)evaluateJavaScriptCommond{
    if ([[NSThread currentThread] isMainThread]) {
        [self.webview evaluateJavaScript:evaluateJavaScriptCommond completionHandler:^(id _Nullable data, NSError * _Nullable error) {

        }];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.webview evaluateJavaScript:evaluateJavaScriptCommond completionHandler:nil];
        });
    }
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    [self didReceiveScriptMessage:message];
}

@end

逻辑也很简单,注入WkWebView后,向JS注入本地函数 dhBridge,这样就可以监听JS调用的函数,通过解析,执行,然后返回结果,流程完毕。


android代码


public class DHBridgeWebView {
    private static String JSI = "dh_android_Bridge";
    private static String EJS = "window.DhBridge.handleMessage('%s')";
    private HashMap<String,DHBridgeHandlerInterface> handlerHashMap;
    private WebView webView;

    @SuppressLint("JavascriptInterface")
    public DHBridgeWebView(WebView webView){
        super();
        this.webView = webView;
        this.webView.addJavascriptInterface(this,JSI);
        this.handlerHashMap = new HashMap<String,DHBridgeHandlerInterface>();
        this.injectJS();
    }
    public void injectJS(){
        InputStream in = null;
        try {
            in = webView.getContext().getAssets().open("dhwebviewbridge.js");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
            String line = null;
            StringBuilder sb = new StringBuilder();
            do {
                line = bufferedReader.readLine();
                sb.append(line);
            } while (line != null);

            bufferedReader.close();
            in.close();
            this.evaluateJavaScript(sb.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /*注册handler*/
    public void registerHandler(String handlerName,DHBridgeHandlerInterface handler){
        if (handlerName == null || handlerName.isEmpty()){ return; }
        if (handler == null){return;}
        synchronized(this){
            this.handlerHashMap.put(handlerName,handler);
        }
    }
    /*删除Handler*/
    public void removeHandler(String handlerName){
        if (handlerName == null || handlerName.isEmpty()){ return; }
        synchronized(this){
            this.handlerHashMap.remove(handlerName);
        }
    }
    /*清空资源信息*/
    public void destroy(){
        synchronized(this){
            this.webView.removeJavascriptInterface(JSI);
            this.webView = null;
            this.handlerHashMap.clear();
            this.handlerHashMap = null;
        }
    }

    private void didReceiveScriptMessage(final DHBridgeMessage dhbMessage){
        synchronized(this){
            if (!dhbMessage.handlerName.isEmpty() && this.handlerHashMap.containsKey(dhbMessage.handlerName)){
                DHBridgeHandlerInterface handler = this.handlerHashMap.get(dhbMessage.handlerName);
                handler.callback(dhbMessage.args, new DHBridgeHandlerResponse() {
                    @Override
                    public void callBack(int code, String data) {
                        JsonObject json = new JsonObject();
                        json.addProperty("code",code);
                        json.addProperty("data",data);
                        json.addProperty("callbackId",dhbMessage.callbackId);
                        String commond = String.format(EJS,json.toString());
                        DHBridgeWebView.this.evaluateJavaScript(commond);
                    }
                });
            }else{
                JsonObject json = new JsonObject();
                json.addProperty("code",400);
                json.addProperty("data","bad request");
                json.addProperty("callbackId",dhbMessage.callbackId);
                String commond = String.format(EJS,json.toString());
                this.evaluateJavaScript(commond);
            }
        }
    }

    private void evaluateJavaScript(final String evaluateJavaScriptCommond){
        if (Looper.getMainLooper().getThread().getId() == Thread.currentThread().getId()){
            this.webView.evaluateJavascript(evaluateJavaScriptCommond, new ValueCallback<String>() {
                @Override
                public void onReceiveValue(String s) {
                    Log.d("tag",s);
                }
            });
        }else{
            Handler mainThread = new Handler(Looper.getMainLooper());
            mainThread.post(new Runnable() {
                @Override
                public void run() {
                    DHBridgeWebView.this.webView.evaluateJavascript(evaluateJavaScriptCommond, null);
                }
            });
        }
    }

    @JavascriptInterface
    public void dhBridge(String message){
        Gson gson = new Gson();
        DHBridgeMessage dhbMessage = gson.fromJson(message,DHBridgeMessage.class);
        if (dhbMessage != null){
            this.didReceiveScriptMessage(dhbMessage);
        }
    }
}

class DHBridgeMessage{
    String handlerName;
    String args;
    String callbackId;
}

原理和iOS一致,不再复述。


JS测试代码

callbackButton.onclick = function(e) {
    var success = function(data){
        alert("Success");
        log('success',data);
    }
    var fail = function(code,data){
        log('fail',data);
        alert("fail");
    }
window.DhBridge.callHandler('TestOC','TestOC from JS',success,fail)
};

添加一个按钮,执行Native函数,并且打印成功/失败日志


看下效果

DHWebViewJavaScriptBridgeDHWebViewJavaScriptBridge


看下异常情况

  • 如果Native没有注册对应的Handler函数呢?
    JS Fail函数会回调400 Bad Request错误
    DHWebViewJavaScriptBridge

碰到的问题

在测试过程中,android碰到了个问题

this.dhBridgeWebView = new DHBridgeWebView(this.webview);
this.dhBridgeWebView.registerHandler("TestOC", new DHBridgeHandlerInterface() {
    @Override
    public void callback(String data, DHBridgeHandlerResponse response) {
        response.callBack(200,"OK");
    }
});
//this.webview.loadUrl("file:///android_asset/html/ExampleApp.html");

我们只注入JS,不加载URL,然后去看下JS日志
我们的JS代码是注入成功的
DHWebViewJavaScriptBridge

我们再加载URL,再查看JS代码注入情况
this.webview.loadUrl("file:///android_asset/html/ExampleApp.html");

DHWebViewJavaScriptBridge

注入的JS没有了,这个问题我寻找了很多资料都没有找到答案,如果有人知道的话,麻烦告诉我声。


android 问题解决方案

原本想通过addScriptMessageHandler方式,在加载URL之前,把想要的JS注入进去,这样就可以避免iFrame.src这种方式带来的JS注入前后的问题。
但是android 这种情况,还是无法避免。

  • 解决方案
    android webView onPageFinished 回调中,执行dhBridgeWebView.injectJS(); 重新把JS函数注入进去。
    但是带来的问题就是,类似React/Vue应用,在组件Creat的时候去调用Native函数,这个时候BridgeJS还没注入,调用函数会全部失败。

我们可以稍微进行下封装,如果bridge还没有注入,则用一个队列缓存起来

function callDhBridgeHandler(handleName,args,success,fail){
    console.log("call handle:" + handleName);
    log("call handle:" , handleName);
    if(window.DhBridge){
        window.DhBridge.callHandler(handleName,args,success,fail)
    }else{
        var message = {
            handleName:handleName,
            args:args,
            success:success,
            fail:fail
        };
        if(window.cacheCallQueue){
            window.cacheCallQueue.push(message);
        }else{
            window.cacheCallQueue = [message];
        }
    }
}

然后再对注入的JS添加下代码,如果发现有缓存的队列,则主动去执行下。

setTimeout(_clearCacheCallQueue, 0);
function _clearCacheCallQueue() {
    if(window.cacheCallQueue === undefined){return;}
    var queue = window.cacheCallQueue;
    delete window.cacheCallQueue;
    for (var i=0; i<queue.length; i++) {
        window.DhBridge.callHandler(queue[i].handleName,queue[i].args,queue[i].success,queue[i].fail)
    }
}

我们在android上测试一下,在未注入JS之前,就调用函数会如何呢?

DHWebViewJavaScriptBridge

未注入前执行的函数,也执行成功了。


集成

iOS with CocoaPods

pod 'DHWebViewJavaScriptBridge', '~> 1.0.0'

android

添加Github 自定义的Maven 到 build.gradle

repositories {
    google()
    jcenter()
    maven { url 'https://raw.githubusercontent.com/Dcell/DHWebViewJavascriptBridge/master' }
}

在使用的模块里面添加

implementation 'com.dcell:dhWebviewJavascriptBridge:1.0.0'

使用

无论 android / iOS,都需要把Webview 的 JavaScriptEnabled 开启

iOS

  • 传入WKWebView,生成Bridge对象
self.dHBridgeWKWebView = [[DHBridgeWKWebView alloc] initWithWKWebView:self.webView];
  • 注册本地函数
[self.dHBridgeWKWebView registerHandler:@"TestOC" handler:^(id data, DHBCallback responseCallback) {
    NSLog(@"TestOC from js:%@",data);
    responseCallback(200,@"OK");
}];

返回错误码,默认200 成功,其他为失败

  • 销毁对象
[self.dHBridgeWKWebView destroy];

android

  • 传入WebView,生成Bridge对象
this.dhBridgeWebView = new DHBridgeWebView(this.webview);
  • 注册本地函数
this.dhBridgeWebView.registerHandler("TestOC", new DHBridgeHandlerInterface() {
    @Override
    public void callback(String data, DHBridgeHandlerResponse response) {
        response.callBack(200,"OK");
    }
});
  • 注入JS函数!!!!!(比iOS多一步)
@Override
public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);
    this.dhBridgeWebView.injectJS();
}
  • 销毁对象
this.dhBridgeWebView.destroy();

JS代码

function callDhBridgeHandler(handleName,args,success,fail){
    if(window.DhBridge){
        window.DhBridge.callHandler(handleName,args,success,fail)
    }else{
        var message = {
            handleName:handleName,
                args:args,
                success:success,
                fail:fail
            };
            if(window.cacheCallQueue){
                window.cacheCallQueue.push(message);
            }else{
                window.cacheCallQueue = [message];
            }
        }
}

如何使用

var success = function(data){
    //success
}
var fail = function(code,data){
    //fail
}
callDhBridgeHandler('TestOC','TestOC from JS',success,fail)

结束

封装的代码和Demo都已经上传,需要交流的同学可以下载试试。
只支持iOS WKWebviewandroid 4.4+
有问题或者好的建议,欢迎提issues

Latest podspec

{
    "name": "DHWebViewJavaScriptBridge",
    "version": "1.0.0",
    "summary": "An iOS/android bridge for sending messages between Obj-C and java in WKWebViews, WebViews",
    "description": "An iOS/android bridge for sending messages between Obj-C and java in WKWebViews, WebViewsnandroid 4.4+ & iOS8 WkWebview 8+",
    "homepage": "https://github.com/Dcell/DHWebViewJavascriptBridge",
    "license": {
        "type": "MIT"
    },
    "authors": {
        "Dcell": "[email protected]"
    },
    "platforms": {
        "ios": "8.0"
    },
    "source": {
        "git": "https://github.com/Dcell/DHWebViewJavascriptBridge.git",
        "tag": "1.0.0"
    },
    "source_files": [
        "Classes",
        "iOS/**/*.{h,m}"
    ],
    "exclude_files": "Classes/Exclude",
    "frameworks": "WebKit"
}

Pin It on Pinterest

Share This