之前寫的一篇 關於H5與App原生交互方案,不少人問有沒有實例代碼,今天來講一個對iOS與Android通用的代碼實踐javascript
場景:如今有一個H5活動頁面,上面有一個登錄按鈕,要求點擊登錄按鈕之後,喚出App內部的登陸界面,當登陸成功之後將用戶的手機號返回給H5頁面,顯示出來。
這個場景應該算是比較完整的一次H5中的JavaScript與App原生代碼進行交互了,這個過程,咱們制定的方案知足一下幾點:前端
上一篇文章裏提到,當H5頁面上的JavaScript代碼要調用原生的頁面或者組件的時候,調用最好是雙向的,一來一回,這樣比較容易知足一些比較複雜的業務場景,就像上面的場景同樣,有調用,有回調告知H5調用的結果。前端開發寫的JavaScript代碼基本上都是異步風格的,就拿上面的場景,若是登陸是H5前端的,那麼這個流程就會是:java
代碼以下:ios
function loginClick() {
loginComponent.login(function (error,result) {
//處理登陸完成之後的邏輯
});
}
var loginComponent = {
callBack:null,
"login":function (callBack) {
this.show();
this.callBack = callBack;
},
show:function (loginComponent) {
//登陸組件顯示的邏輯
},
confirm:function (userName,password) {
ajax.post('https://xxxx.com/login',function (error,result) {
if(this.callBack !== null){
this.callBack(error,result);
}
});
}
}複製代碼
若是要改爲調用原生登陸,那麼這個流程就應該是這樣:git
爲了實現上述流程,而且能讓H5的前端開發儘量少的語法損失,咱們須要構建一個JavaScript與原生App進行交互的橋樑,這個橋樑來處理與App的協議交互,兼容iOS與Android的交互實現。github
Android與iOS都支持在打開H5頁面的時候,向H5頁面的window對象上注入一個JavaScript能夠訪問到的對象,Android端使用的是web
webView.addJavascriptInterface(myJavaScriptInterface, 「bridge」);複製代碼
iOS則可使用JavaScriptCore來完成:ajax
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PICBridgeExport <JSExport>
@end
@interface PICBridge : NSObject<PICBridgeExport>
@end
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.bridge =[[PICBridge alloc]init];複製代碼
這裏面Android的myJavaScriptInterface與PICBridge都是做爲與JavaScript進行通訊的橋樑。
咱們使用設計這個橋樑的時候,須要使用一個具體的語法約定和數據約定,比方說,當前端開發調用App登陸的時候,他必定是但願就像調用其餘JavaScript的組件同樣,而登陸的結果經過傳入callBack的函數來完成,對於callBack函數,咱們但願藉助NodeJS的規範:objective-c
function(error,res) {
//回調函數第一個參數是錯誤,第二個參數是結果
}複製代碼
以上咱們能夠看到,bridge必須有能力將前端開發寫的JavaScript回調函數傳入到App內部,而後App處理完邏輯之後經過回調函數來告知前端處理,而且這個須要經過約定好的數據格式來傳遞入參和返回值。
爲了完成雙向通訊,咱們就須要在JavaScript設置一個bridge,原生再注入一個bridge,這兩個bridge按照必定的數據約定來進行雙向通訊和分發邏輯。swift
經過使用JavaScriptCore這個庫,咱們能很容易的將JavaScript傳入的回調函數在objective-c或者是swift端持有,並回去回調這個回調函數。
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PICBridgeExport <JSExport>
JSExportAs(callRouter, -(void)callRouter:(JSValue *)requestObject callBack:(JSValue *)callBack);
@end
@interface PICBridge : NSObject<PICBridgeExport>
-(void)addActionHandler:(NSString *)actionHandlerName forCallBack:(void(^)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)))callBack;
@end複製代碼
須要說明的是,JavaScript沒有函數參數標籤的概念,JSExportAs是用來將objective-c的方法映射爲JavaScript的函數。
-(void)callRouter:(JSValue )requestObject callBack:(JSValue )callBack);
這個方法是暴露給JavaScript端調用的。
第一個參數requestObject是一個JavaScript對象,傳入到objective-c中之後就能夠轉換爲key-value結構的字典,那麼這個字典的數據約定是:
{
'Method':'Login',
'Data':null
}複製代碼
其中Method是App內部對外提供的API,而這個Data則是該API須要的入參。
第二個參數是一個callBack函數,該類型的JSValue能夠調用callWithArguments:方法來invoke這個回調函數。
前面已經說明,回調函數的第一個參數是error,第二個參數是一個結果,而回調的結果咱們也進行一下約定,那就是:
{
'result':{}
}複製代碼
這樣的好處是,業務邏輯能夠講返回的結果放入result中,跟result同級別的咱們還能夠加入統一的簽名認證的東西,在此暫時不延伸。
原生端的bridge的來實現一下callRouter:
-(void)callRouter:(JSValue *)requestObject callBack:(JSValue *)callBack{
NSDictionary * dict = [requestObject toDictionary];
NSString * methodName = [dict objectForKey:@"Method"];
if (methodName != nil && methodName.length>0) {
NSDictionary * params = [dict objectForKey:@"Data"];
__weak PICBridge * weakSelf = self;
//由於JavaScript是單線程的,須要儘快完成調用邏輯,耗時操做須要異步提交到主線程中執行
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf callAction:methodName params:params success:^(NSDictionary *responseDict) {
if (responseDict != nil) {
NSString * result = [weakSelf responseStringWith:responseDict];
if (result) {
[callBack callWithArguments:@[@"null",result]];
}
else{
[callBack callWithArguments:@[@"null",@"null"]];
}
}
else{
[callBack callWithArguments:@[@"null",@"null"]];
}
} failure:^(NSError *error) {
if (error) {
[callBack callWithArguments:@[[error description],@"null"]];
}
else{
[callBack callWithArguments:@[@"App Inner Error",@"null"]];
}
}];
});
}
else{
[callBack callWithArguments:@[@NO,[PICError ErrorWithCode:PICUnkonwError].description]];
}
return;
}
//將返回的結果字典轉換爲字符串經過回調函數傳回給JavaScript
-(NSString *)responseStringWith:(NSDictionary *)responseDict{
if (responseDict) {
NSDictionary * dict = @{@"result":responseDict};
NSData * data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
NSString * result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
return result;
}
else{
return nil;
}
}複製代碼
callAction函數實際上就是分發業務邏輯用的
-(void)callAction:(NSString *)actionName params:(NSDictionary *)params success:(void(^)(NSDictionary * responseDict))success failure:(void(^)(NSError * error))failure{
void(^callBack)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)) = [self.handlers objectForKey:actionName];
if (callBack != nil) {
callBack(params,failure,success);
}
}複製代碼
這個callBack Block是在self.handlers的字典中存儲,比較複雜,block第一個參數是傳入的入參,後面兩個參數是成功之後的回調和失敗之後的回調,以便業務邏輯完成後進行回調給JavaScript。
同時會有註冊業務邏輯的方法:
-(void)addActionHandler:(NSString *)actionHandlerName forCallBack:(void(^)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)))callBack{
if (actionHandlerName.length>0 && callBack != nil) {
[self.handlers setObject:callBack forKey:actionHandlerName];
}
}複製代碼
至此,原生端路由實現完畢。
先貼上完整代碼:
(function(win) {
var ua = navigator.userAgent;
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r !== null) return unescape(r[2]);
return null;
}
function isAndroid() {
return ua.indexOf('Android') > 0;
}
function isIOS() {
return /(iPhone|iPad|iPod)/i.test(ua);
}
var mobile = {
/** *經過bridge調用app端的方法 * @param method * @param params * @param callback */
callAppRouter: function(method, params, callback) {
var req = {
'Method': method,
'Data': params
};
if (isIOS()) {
win.bridge.callRouter(req, function(err, result) {
var resultObj = null;
var errorMsg = null;
if (typeof(result) !== 'undefined' && result !== 'null' && result !== null) {
resultObj = JSON.parse(result);
if (resultObj) {
resultObj = resultObj['result'];
}
}
if (err !== 'null' && typeof(err) !== 'undefined' && err !== null) {
errorMsg = err;
}
callback(err, resultObj);
});
} else if (isAndroid()) {
//生成回調函數方法名稱
var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
//掛載一個臨時函數到window變量上,方便app回調
win[cbName] = function(err, result) {
var resultObj;
if (typeof(result) !== 'undefined' && result !== null) {
resultObj = JSON.parse(result)['result'];
}
callback(err, resultObj);
//回調成功以後刪除掛載到window上的臨時函數
delete win[cbName];
};
win.bridge.callRouter(JSON.stringify(req), cbName);
}
},
login: function() {
// body...
this.callAppRouter('Login', null, function(errMsg, res) {
// body...
if (errMsg !== null && errMsg !== 'undefined' && errMsg !== 'null') {
} else {
var name = res['phone'];
if (name !== 'undefined' && name !== 'null') {
var button = document.getElementById('loginButton');
button.innerHTML = name;
}
}
});
}
};
//將mobile對象掛載到window全局
win.webBridge = mobile;
})(window);複製代碼
在window上掛在一個叫webBridge的對象,其餘業務JavaScript能夠經過webBridge.login來進行調用原生端開放的API。
callAppRouter方法的實現咱們來分析一下:
若是判斷是iOS設備,則使用iOS註冊的bridge對象進行調用callRouter方法:
if (isIOS()) {
win.bridge.callRouter(req, function(err, result) {
var resultObj = null;
var errorMsg = null;
if (typeof(result) !== 'undefined' && result !== 'null' && result !== null) {
resultObj = JSON.parse(result);
if (resultObj) {
resultObj = resultObj['result'];
}
}
if (err !== 'null' && typeof(err) !== 'undefined' && err !== null) {
errorMsg = err;
}
callback(err, resultObj);
});
}複製代碼
req是標準的包含Method和Data的對象,緊接着傳入回調函數,回調函數有err與result,裏面作好各類類型檢查。
着重說一下Android端的實現,由於Android端的JavaScript方法註冊,參數類型只能字符串,java語言自己沒有匿名函數的概念,因此只能給Java端傳入回調函數的名字,而回調函數的實現則在JavaScript端持有。
else if (isAndroid()) {
//生成回調函數方法名稱
var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
//掛載一個臨時函數到window變量上,方便app回調
win[cbName] = function(err, result) {
var resultObj;
if (typeof(result) !== 'undefined' && result !== null) {
resultObj = JSON.parse(result)['result'];
}
callback(err, resultObj);
//回調成功以後刪除掛載到window上的臨時函數
delete win[cbName];
};
win.bridge.callRouter(JSON.stringify(req), cbName);
}複製代碼
本質上就是將其餘業務JavaScript代碼傳入的callBack函數經過隨機生成函數名,掛在到window變量上,回調之後將其刪除:delete win[cbName]。
當調用Java端的bridge.callRouter(JSON.stringify(req), cbName),Java端拿到cbName,在完成業務邏輯後,按照標準數據格式,在JavaScript執行的上下文中,回調這個名字的方法。
至此,前端的webBridge完成。
最後附上Demo地址:
github.com/Neojoke/Pic…此Demo是經過H5調用原生的登陸界面,登陸成功之後將手機號在H5上的登陸按鈕顯示出來,完成一整套邏輯交互。喜歡的給個✨,有任何問題你們多多交流!