WebViewJavascriptBridge的使用和封裝

WebViewJavascriptBridge的使用和封裝

iOS原生和UIWebView交互一般採用JavaScript來做,而目前第三方中框架比較好用,還是WebViewJavascriptBridge。這裏基於WebViewJavascriptBridge(版本5.0.5)封裝了些代碼,作用是最後能實現Web傳參到原生進行原生頁面的跳轉或者方法的直接調用以及原生傳至給Web讓Web根據得到的值進行業務或者界面的編寫。

一.前期工作

1.1 下載庫

你需要下載WebViewJavascriptBridge第三方庫,點擊這裏進入,測試工程是使用pod下載,版本爲5.0.5

1.2 界面搭建

用到的主要文件如下:
主要使用文件.png

入口使用原始的ViewController,讓它帶了NavigationController,中間按鈕點擊後會push出BridgeVc,BridgeVc帶了一個UIWebview並加載網頁WebTest.html,該網頁利用了寫好的JavaScript文件WebBridge.js來和原生界面進行交互。WebTest.html中有3個按鈕,分別點擊交互後對應push或者present出BViewController,BViewController帶2個屬性name和sex,BViewController界面出現後會打印這兩個屬性值,3種方式跳轉後打印的屬性值不同。

主頁面.png
Web.png

打印屬性.png

二.代碼

2.1 BridgeVc代碼

該控制器主要負責顯示加載UIWebview內容並且進行交互

//
//  BridgeVc.m
//  NewWebTest
//
//  Created by Jeffrey on 2017/5/14.
//  Copyright © 2017年 Jeffrey. All rights reserved.
//

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
#pragma ide diagnostic ignored "CannotResolve"

#import "BridgeVc.h"
#import "WebViewJavascriptBridge.h"

@interface BridgeVc ()
@property WebViewJavascriptBridge *bridge;
@property(nonatomic, strong) UIWebView *webView;

@end

@implementation BridgeVc

- (void)viewDidLoad {
    [super viewDidLoad];
    /**基礎設置,並初始化bridge*/
    [self baseConfig];
    /**配置WebBridge*/
    [self bridgeConfig];
    /**加載本地網頁*/
    [self loadExamplePage:self.webView];
    // Do any additional setup after loading the view.
}

/**常規設置,添加網頁*/
- (void)baseConfig {
    self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.webView];
}

/**配置WebBridge*/
- (void)bridgeConfig {
    /**初始化bridge*/
    self.bridge = [WebViewJavascriptBridge bridgeForWebView:(WVJB_WEBVIEW_TYPE *) self.webView];

    /***
     * 向Web發送數據data,並接收Web返回的數據responseData
     * data: 發送給Web的數據
     * responseData:Web接收到數據後執行了callHandler,帶回來responseData數據
     */
    NSDictionary *customData = @{
            @"token": @"tokenValue",
            @"phoneType": @"iPhone7P"
    };
    [self.bridge callHandler:@"LocalToWeb" data:customData responseCallback:^(id responseData) {
        NSLog(@"發送給網頁的內容爲:%@,接收到網頁的內容爲:%@", customData, responseData);
    }];



    /***
     * 註冊handler,接收Web發送的數據data並回傳給Web本地的數據
     * data:Web發送過來的數據
     * responseCallback:是個block,可以帶數據執行,例如responseCallback(dataForWeb),dataForWeb爲id類型
     * 目測常規的數組字典字符串等都可以傳,Web在接受數據後可以進行一些自定義操作
     * 目的:註冊完這個方法後,可以實現:Web裏調用js的pushViewControllerWithParameters方法後,原生界面根據方法內的具體參數進行界面跳轉
     * 具體見WebTest.html文件
     * 另外,可以實現Web裏調用js的performMethod方法後,直接調用本網頁控制器內寫好的方法,這裏測試的爲其繼承類BaseViewController的
     * presentBWithName:sex:方法。
     */
    [self.bridge registerHandler:@"WebToLocal" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"網頁傳過來的數據是%@", data);
        NSArray *p = data[@"parameters"];
        NSString *methodName = data[@"methodName"];
        /**push控制器並帶參數*/
        if ([methodName isEqualToString:@"push"]) {
            NSString *controllerName = data[@"controllerName"];
            NSDictionary *parameters = data[@"parameters"];
            Class controllerClass = NSClassFromString(controllerName);
            UIViewController *controller = (UIViewController *) [[controllerClass alloc] init];
            @try {
                /**Web裏傳過來的參數必須和要push出去的控制器屬性一一對應,否則會拋出下面的異常*/
                [controller setValuesForKeysWithDictionary:parameters];
            }
            @catch (NSException *exception) {
                if ([exception.name isEqualToString:@"NSUnknownKeyException"]) {
                    /**屬性沒對應上,會拋出異常,並告知是哪個字段設置錯誤*/
                    NSLog(@"要push出的控制器沒有該屬性:%@,請在js上重新確認", exception.userInfo[@"NSUnknownUserInfoKey"]);
                    @throw exception;
                }
            }
            [self.navigationController pushViewController:controller animated:YES];
        } else {
            /**如果不是push方法,則執行常規的方法名帶參數的方法*/
            [self performWithTarget:self MethodName:methodName withParameters:p];
        }
        /**回傳內容*/
    }];
}

/**執行單個方法*/
- (void)performWithTarget:(id)target MethodName:(NSString *)methodName withParameters:(NSArray *)parameters {
    SEL method = NSSelectorFromString(methodName);
    NSMethodSignature *methodSignature = [[target class] instanceMethodSignatureForSelector:method];
    if (!methodSignature) {
        NSLog(@"無此方法名");
        return;
    }

    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = target;
    invocation.selector = method;
    if (parameters.count > methodSignature.numberOfArguments - 2) {
        NSLog(@"參數個數不匹配");
    } else {
        for (int i = 0; i < parameters.count; i++) {
            NSString *value = parameters[(NSUInteger) i];
            [invocation setArgument:&value atIndex:i + 2];
        }
        [invocation invoke];
    }
}

/**加載網頁*/
- (void)loadExamplePage:(UIWebView *)webView {
    NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"WebTest" ofType:@"html"];
    NSString *appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
    NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
    [webView loadHTMLString:appHtml baseURL:baseURL];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

#pragma clang diagnostic pop

原則上如果網頁端寫好,項目中只需要加入上面對應的bridgeConfig方法即可,會自動根據Web裏的JavaScript來進行調整或者方法的執行。

2.2 JavaScript代碼(WebBridge.js文件)

/**
 * Created by jeffrey on 2017/5/15.
 */

/**
 * 原始方法,兩個註冊都需要調用這個方法才能使用
 * @param callback
 * @returns {*}
 */
function setupWebViewJavascriptBridge(callback) {

    if (window.WebViewJavascriptBridge) {
        return callback(WebViewJavascriptBridge);
    } else {

    }
    if (window.WVJBCallbacks) {
        return window.WVJBCallbacks.push(callback);
    }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    /**注意,git上原來的readme文檔上下面的資源有問題,會導致交互不能正常記性,下面這個是正常的*/
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function () {
        document.documentElement.removeChild(WVJBIframe)
    }, 0)
}


/**
 * push方法,讓原生界面進行push跳轉
 * @param controllerName 原生界面控制器名稱
 * @param parameters 原生界面控制器的各個屬性值(有的界面需要傳值跳轉),爲字典類型,且鍵值對必須和控制器內屬性一一對應,否則原生會拋異常。
 */
function pushViewControllerWithParameters(controllerName, parameters) {
    setupWebViewJavascriptBridge(function (bridge) {
        /**這裏接收原生控制器的數據*/
        bridge.registerHandler('JS Echo', function (data, responseCallback) {
            responseCallback(data)
        });
        let data = {'methodName': 'push', 'controllerName': controllerName, 'parameters': parameters};
        bridge.callHandler('WebToLocal', data, function responseCallback(responseData) {
            console.log("JS received response:", responseData);
            alert(responseData['good'])
        })
    })
}

/**
 * 執行原生網頁所在控制器的方法
 * @param methodName 方法名
 * @param parameters 所帶參數,爲數組。參數將按照方法裏的順序賦值
 */
function performMethod(methodName,parameters) {
    setupWebViewJavascriptBridge(function (bridge) {
        let data = {'methodName': methodName, 'parameters': parameters};
        bridge.callHandler('WebToLocal', data, function responseCallback(responseData) {
            alert(responseData['good'])
        })
    })
}


/**
 * 註冊接收原生數據
 * 原生界面直接傳數據過來,接收後可以根據接收的數據寫出項目中的網頁需要的邏輯
 */
function registerHandlerLocalToWeb() {
    window.onload = function () {
        /**註冊接收原生數據*/
        setupWebViewJavascriptBridge(function (bridge) {
            /**
             * data: 原生傳過來的數據
             * responseCallback:傳入數據執行後原生會接收到,數據爲responseData
             */

            bridge.registerHandler('LocalToWeb', function (data, responseCallback) {
                /**下面後臺在獲取相關數據後可以對網頁進行一些操作,並返回原生一些數據*/
                alert('token是' + data['token']);
                let responseData = {'key1': 'this is a test', 'key2': 'Web To Local'};
                responseCallback(responseData);
            });
        });
    };
}

2.3 網頁內使用JavaScript文件(WebTest.html)
注意:如果自己重新寫,記得把上面的JavaScript文件添加到Copy Bundle Resources,否則不會在html裏生效。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>測試</title>
    <script src="WebBridge.js"></script>
    <script>
        /**註冊接收原生發過來的數據*/
        registerHandlerLocalToWeb();
    </script>
</head>
<body>
<p>這是個測試</p>
<script>
    let parametersA = {'name': 'Jeff', 'sex': 'M'};
    let parametersB = {'name': 'Tom', 'sex': 'F'};
    let parametersC = ['Lucy','F'];
</script>
<button onclick="pushViewControllerWithParameters('BViewController', parametersA)">點我A</button>
<button onclick="pushViewControllerWithParameters('BViewController', parametersB)">點我B</button>
<button onclick="performMethod('presentBWithName:sex:', parametersC)">點我present</button>
</body>
</html>

這裏可以看到,html中在點擊事件中執行js的pushViewControllerWithParameters方法,並傳入iOS原生控制器名稱(類名稱)和該控制器的各屬性(按照字典形式傳入),即會push出對應控制器,這裏還可以在push完後給出回調讓html使用具體的返回數據

點擊執行的performMethod方法,需要傳入原生控制器的方法名字和參數,例如上面的第三個按鈕,點擊後調用了BridgeVc的方法presentBWithName:sex:(父類方法也可以調用),傳入參數parametersC爲數組,爲方法裏的各個參數,順序從左到右。

另外在<head>標籤執行的js:registerHandlerLocalToWeb()爲註冊去收到原生頁面的傳至,具體傳至後如何使用可以在方法內部定義或者另行根據項目封裝。

三.總結

最後根據項目的需求,先在原生中集成交互的代碼(bridgeConfig相關代碼,可以單獨摘出來作爲工具類也行),然後把js文件適當修改,扔給後臺,在需要用到的網頁上直接調用對應的方法即可。也可以根據自己項目中的邏輯進行再修改來使用。
本工程git地址:https://github.com/JeffreyWW/JFWebBridgeTest

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章