美文网首页Hybird开发WebViewiOS学习开发
OC和JS交互(UIWebView)中级篇1

OC和JS交互(UIWebView)中级篇1

作者: bigParis | 来源:发表于2017-11-23 16:03 被阅读38次

    作为一名合格的iOS开发, 光了解Native是不够的, 在很多情况下, 我们都要和Web去做交互, 了解OC和Web交互的原理有助于我们更好的对底层框架进行改动优化.

    还没有了解OC和JS交互的基本原理的可以快速浏览下, 在这里还要继续深入的探讨下OC和JS交互.

    WebViewJavascriptBridge

    这是Github传送门, 其实OC和JS交互有很多种, 其中一种主要的方式叫做JS注入, 这是一种比较传统也比较经典的方式, WebViewJavascriptBridge也是这种方式.

    1 首先在你要加载的Web页面里会有这样的一段JS代码,
    // 这段代码是固定的,必须要放到js中
    1       function setupWebViewJavascriptBridge(callback) {
    2            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    3            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    4            window.WVJBCallbacks = [callback];
    5            var WVJBIframe = document.createElement('iframe');
    6            WVJBIframe.style.display = 'none';
    7            WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    8            document.documentElement.appendChild(WVJBIframe);
    9            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
            }
    

    连注释都copy过来了, 这里是一个脚本方法, 下面的代码直接调用了setupWebViewJavascriptBridge这个方法
    第2行, 意思是window.WebViewJavascriptBridge存在就调用callback(WebViewJavascriptBridge)并返回
    第3行, 意思是window.WVJBCallbacks存在就入栈callbackWVJBCallbacks并返回
    第4行, 意思是将数组WVJBCallbacks初始化为[callback]
    第5行, 意思是创建一个iframe对象WVJBIframe
    第6行, 设置 WVJBIframe的display属性
    第7行, 设置 WVJBIframe的src属性为'wvjbscheme://__BRIDGE_LOADED__', 这行代码很关键, 这里相当于改变了iframe的src, 导致了UIWebView的代理方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType被执行,
    第8行, 将WVJBIframet添加到document
    第9行, 调用setTimeout(func, 0)方法, 传入一个callback方法function() { document.documentElement.removeChild(WVJBIframe) }, 0) }

    2 在ViewController中, 初始化bridge
    VC
    self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
    [self.bridge setWebViewDelegate:self];
    
    WebViewJavascriptBridge
    1 初始化bridge
    + (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView {
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    2 给webView绑定代理, 并把base的代理设置成自己
    - (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
        _webView = webView;
        _webView.delegate = self;
        _base = [[WebViewJavascriptBridgeBase alloc] init];
        _base.delegate = self;
    }
    3 代理方法被执行
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
        if (webView != _webView) { return YES; }
        NSURL *url = [request URL];
        __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
        if ([_base isCorrectProcotocolScheme:url]) {
            if ([_base isBridgeLoadedURL:url]) {
                [_base injectJavascriptFile];
            } else if ([_base isQueueMessageURL:url]) {
                NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
                [_base flushMessageQueue:messageQueueString];
            } else {
                [_base logUnkownMessage:url];
            }
            return NO;
        } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
            return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
        } else {
            return YES;
        }
    }
    

    上面的4段代码显示了, 一个web是如何传入WebViewJavascriptBridge并进行代理绑定的, 这里最终的目的是想让所有web的代理在WebViewJavascriptBridge被执行, 也就是我们要在WebViewJavascriptBridge里面进行统一的拦截, shouldStartLoadWithRequest被执行的时候传了NSURLRequest类型参数, 而这个requestURL正是'wvjbscheme://__BRIDGE_LOADED__', 也就是网页内容中设置的src的值. [_base isCorrectProcotocolScheme:url][_base isBridgeLoadedURL:url] 是判断scheme和URL是否等于wvjbscheme__BRIDGE_LOADED__, 如果相等, 就执行下面的代码

    WebViewJavascriptBridgeBase
    - (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];
            }
        }
    }
    
    - (void) _evaluateJavascript:(NSString *)javascriptCommand {
        [self.delegate _evaluateJavascript:javascriptCommand];
    }
    

    这里是先从WebViewJavascriptBridge_js文件中取出js脚本并进行执行,

    @protocol WebViewJavascriptBridgeBaseDelegate <NSObject>
    - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand;
    @end
    
    WebViewJavascriptBridge
    - (NSString*) _evaluateJavascript:(NSString*)javascriptCommand
    {
        return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
    }
    

    这段脚本最终被WebViewJavascriptBridge中的_webView执行了, 这里可以看出比较强的设计思想, WebViewJavascriptBridgeBase中只是提供通用的方法, 而不保存_webView的实例, _webView的实例保存在WebViewJavascriptBridge, 这个UIWebView和JS交互用到的bridge, 并且请求的拦截也是在这里完成的, 至此就完成了JS的注入, 下面来看下, 到底注入了什么东西

    // This file contains the source for the Javascript side of the
    // WebViewJavascriptBridge. It is plaintext, but converted to an NSString
    // via some preprocessor tricks.
    //
    // Previous implementations of WebViewJavascriptBridge loaded the javascript source
    // from a resource. This worked fine for app developers, but library developers who
    // included the bridge into their library, awkwardly had to ask consumers of their
    // library to include the resource, violating their encapsulation. By including the
    // Javascript as a string resource, the encapsulation of the library is maintained.
    
    #import "WebViewJavascriptBridge_JS.h"
    
    NSString * WebViewJavascriptBridge_js() {
        #define __wvjb_js_func__(x) #x
        
        // BEGIN preprocessorJSCode
        static NSString * preprocessorJSCode = @__wvjb_js_func__(
    ;(function() {
        if (window.WebViewJavascriptBridge) {
            return;
        }
    
        if (!window.onerror) {
            window.onerror = function(msg, url, line) {
                console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
            }
        }
        window.WebViewJavascriptBridge = {
            registerHandler: registerHandler,
            callHandler: callHandler,
            disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
            _fetchQueue: _fetchQueue,
            _handleMessageFromObjC: _handleMessageFromObjC
        };
    
        var messagingIframe;
        var sendMessageQueue = [];
        var messageHandlers = {};
        
        var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme';
        var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__';
        
        var responseCallbacks = {};
        var uniqueId = 1;
        var dispatchMessagesWithTimeoutSafety = true;
    
        function registerHandler(handlerName, handler) {
            messageHandlers[handlerName] = handler;
        }
        
        function callHandler(handlerName, data, responseCallback) {
            if (arguments.length == 2 && typeof data == 'function') {
                responseCallback = data;
                data = null;
            }
            _doSend({ handlerName:handlerName, data:data }, responseCallback);
        }
        function disableJavscriptAlertBoxSafetyTimeout() {
            dispatchMessagesWithTimeoutSafety = false;
        }
        
        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;
        }
    
        function _fetchQueue() {
            var messageQueueString = JSON.stringify(sendMessageQueue);
            sendMessageQueue = [];
            return messageQueueString;
        }
    
        function _dispatchMessageFromObjC(messageJSON) {
            if (dispatchMessagesWithTimeoutSafety) {
                setTimeout(_doDispatchMessageFromObjC);
            } else {
                 _doDispatchMessageFromObjC();
            }
            
            function _doDispatchMessageFromObjC() {
                var message = JSON.parse(messageJSON);
                var messageHandler;
                var responseCallback;
    
                if (message.responseId) {
                    responseCallback = responseCallbacks[message.responseId];
                    if (!responseCallback) {
                        return;
                    }
                    responseCallback(message.responseData);
                    delete responseCallbacks[message.responseId];
                } else {
                    if (message.callbackId) {
                        var callbackResponseId = message.callbackId;
                        responseCallback = function(responseData) {
                            _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        };
                    }
                    
                    var handler = messageHandlers[message.handlerName];
                    if (!handler) {
                        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                    } else {
                        handler(message.data, responseCallback);
                    }
                }
            }
        }
        
        function _handleMessageFromObjC(messageJSON) {
            _dispatchMessageFromObjC(messageJSON);
        }
    
        messagingIframe = document.createElement('iframe');
        messagingIframe.style.display = 'none';
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
        document.documentElement.appendChild(messagingIframe);
    
        registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
        
        setTimeout(_callWVJBCallbacks, 0);
        function _callWVJBCallbacks() {
            var callbacks = window.WVJBCallbacks;
            delete window.WVJBCallbacks;
            for (var i=0; i<callbacks.length; i++) {
                callbacks[i](WebViewJavascriptBridge);
            }
        }
    })();
        ); // END preprocessorJSCode
    
        #undef __wvjb_js_func__
        return preprocessorJSCode;
    };
    
    

    这是一段通用的注入代码, 因为本人JS能力有限, 大致是创建一些JS对象, 并对一些后面需要用到的数据进行初始化操作, 这里主要说几个前面用到的, 下面有说错的地方, 请大神们看到了轻拍

    window.WebViewJavascriptBridge = {
            registerHandler: registerHandler,
            callHandler: callHandler,
            disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
            _fetchQueue: _fetchQueue,
            _handleMessageFromObjC: _handleMessageFromObjC
        };
    

    定义了一个叫WebViewJavascriptBridge的作用域, 里面有几个方法
    registerHandler, callHandler等, 至于这些方法有什么用, 等后面再说. 这里先看代码的结构.

    messagingIframe = document.createElement('iframe');
        messagingIframe.style.display = 'none';
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
        document.documentElement.appendChild(messagingIframe);
    

    这里还是和之前类似的, 创建一个messagingIframeiframe, 并改变src, 触发UIWebView代理方法回调.

    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    

    注册一个JS方法, 后面供OC调用

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

    调用setTimeout方法, 并传递一个_callWVJBCallbacks函数指针, 给setTimeout, 至于这个setTimeout到底有什么作用, 小编暂时还没搞清楚.

    至此, 我们看下OC中最关键的一个方法

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    

    拦截请求的方法, 这里一共回调了几次
    1 url = file:///Users/bigparis/Library/Developer/CoreSimulator/Devices/F2CEEE61-7EAC-43BA-8412-BB886AA1E4D4/data/Containers/Bundle/Application/D099E6F5-BE84-49BC-988F-AF6D6E9622F4/WebViewJSBridgeDemo.app/index.html这次是加载网页的时候回调的.
    2 url = url wvjbscheme://__BRIDGE_LOADED__ 这次是网页内容执行到WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';这句的时候触发的. 这里会导致把事先准备好的文件WebViewJavascriptBridge_js中的内容全部注入, 这里host已经说明了, 是LOADED, 也就是加载.
    3 url=wvjbscheme://__WVJB_QUEUE_MESSAGE__这次是在注入的过程中执行到了messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;这句触发的, 这里host已经说明了, 是QUEUE_HAS_MESSAGE是因为有消息触发, 但是由于实际上这里只是在初始化注入, JS中的消息队列sendMessageQueue中并没有实际内容, 所以没有进行后续的执行了.

    WebViewJavascriptBridgeBase.m
    - (void)flushMessageQueue:(NSString *)messageQueueString{
        if (messageQueueString == nil || messageQueueString.length == 0) {
            NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
            return;
        }
        // something else...
    }
    

    初始化注入到这里就截止了.

    当然, 对于不同的网页, 可能不止这3次, 但是这3次是必要的, 本文至此就已经详细说明了如何对JS进行注入. 下一篇将详细说明下利用注入的JS, 如何进行交互.

    相关文章

      网友评论

        本文标题:OC和JS交互(UIWebView)中级篇1

        本文链接:https://www.haomeiwen.com/subject/mviavxtx.html