JSBridge(Android和IOS平臺)的設計和實現
前言
對於商務類的app,隨著app註冊使用人數遞增,app的運營者們就會逐漸考慮在應用中開展一些推廣活動。大多數活動具備時效性強、運營時間短的特徵,一般產品們和運營者們都是通過wap頁面快速投放到產品的活動模組。Wap頁面可以聲文並茂地介紹活動,但活動的最終目標是通過獲取特權、跳轉進入本地功能模組,最後達成交易。如何建立wap頁面和本地Native頁面的深度互動,這就需要用到本文介紹的JSBridge。
此外一些平臺類的產品,如大家每天都在使用的微信、支付寶、手機qq等,無一例外都在使用整合JSBridge的webContainer完成眾多業務元件功能,大大減少了客戶端Native開發的工作量,不僅節約了大量人力開發成本,還能避開產品上線更新的版本稽核週期限制(特別是IOS平臺)。當然這些超級APP有強大的技術力量支撐,通過JSBridge有計劃的進行API規範介面,不斷向前端Wap開發人員開放,並在版本上向下相容。但對於我們剛起步運營的中小級app來說暫時還沒有必要如此大張旗鼓,相反前面提到的wap活動推廣則是我們的主要需求。
為了滿足這個需求,本文通過提煉JSBridge的核心部分改造成JSService方式供各個不同的產品零修改方式使用。各個不同的產品只需要按照外掛的方式提供Native擴充套件介面,並在各自封裝的webContainer中呼叫JSService對Wap呼叫進行攔截處理。
具體產品應用
目前該框架同時覆蓋了Android和IOS平臺,在我司的幾個電商類產品中都得到了很好的使用,並趨於穩定。
本文的Demo工程執行效果如下:

image

image
關於JSAPI的介面封裝
JSAPI的封裝包括核心JS和對外開放介面JS兩個部分。 核心JS部分通過攔截某Q的wap請求頁面獲取,獲取的JS進行編碼混淆處理,已經通過除錯進行了註釋,其主要過程就是對引數和回撥進行封裝,並構建一個url連結通過建立一個隱藏的iframe進行傳送。 核心JS程式碼閱讀
對引數和回撥進行封裝部分的程式碼如下:
//invoke //mapp.invoke("device", "getDeviceInfo", e); //@param e 類 必須 //@param n 類方法 必須 //@param i 同步回撥的js方法 //@param s function k(e, n, i, s) { if (!e || !n) return null; var o, u; i = r.call(arguments, 2), //相當於呼叫Array.prototype.slice(arguments) == arguments.slice(2),獲取argument陣列2以後的元素 //令s等於回撥函式 s = i.length && i[i.length - 1], s && typeof s == "function" ? i.pop() : typeof s == "undefined" ? i.pop() : s = null, //u為當前儲存回撥函式的index; u = b(s); //如果當前版本支援Bridge if (C(e, n)) { //將傳進來的所有引數生成一個url字串; o = "ldjsbridge:" + "/" + "/" + encodeURIComponent(e) + "/" + encodeURIComponent(n), i.forEach(function(e, t) { typeof e == "object" && (e = JSON.stringify(e)), t === 0 ? o += "?p=": o += "&p" + t + "=", o += encodeURIComponent(String(e)) }), (o += "#" + u); //帶上儲存回撥的陣列index; //執行生成的url, 有些函式是同步執行完畢,直接呼叫回撥函式;而有些函式的呼叫要通過非同步呼叫執行,需要通過 //全域性呼叫去完成; var f = N(o); if (t.iOS) { f = f ? f.result: null; if (!s) return f; //如果無回撥函式,直接返回結果; } }else { console.log("mappapi: the version don't support mapp." + e + "." + n); } }
建立iframe傳送JSBridge呼叫請求:
//建立一個iframe,執行src,供攔截 function N(n, r) { console.log("logOpenURL:>>" + n); var i = document.createElement("iframe"); i.style.cssText = "display:none;width:0px;height:0px;"; var s = function() { //通過全域性執行函式執行回撥函式;監聽iframe是否載入完畢 E(r, { r: -201, result: "error" }) }; //ios平臺,令iframe的src為url,onload函式為全域性回撥函式 //並將iframe插入到body或者html的子節點中; t.iOS && (i.onload = s, i.src = n); var o = document.body || document.documentElement; o.appendChild(i), t.android && (i.onload = s, i.src = n); // var u = t.__RETURN_VALUE; //當iframe執行完成之後,最後執行settimeout 0語句 return t.__RETURN_VALUE = e, setTimeout(function() { i.parentNode.removeChild(i) }, 0), u }
對外開放介面的封裝:(使用者只需要對該部分進行介面擴充套件即可)
mapp.build("mapp.device.getDeviceInfo", { iOS: function(e) { return mapp.invoke("device", "getDeviceInfo", e); }, android: function(e) { var t = e; e = function(e) { try { e = JSON.parse(e) } catch(n) {} t && t(e) }, mapp.invoke("device", "getDeviceInfo", e) }, support: { iOS: "1.0", android: "1.0" } }),
核心JS程式碼呼叫說明
mapp.version: mappAPI自身版本號 mapp.iOS: 如果在ios app中,值為true mapp.android: 如果在android app中,值為true mapp.support: 檢查當前app環境是否支援該介面,支援返回true mapp.support("mqq.device.getClientInfo") mapp.callback: 用於生成回撥名字,跟著invoke引數傳給客戶端,供客戶端回撥 var callbackName = mapp.callback(function(type, index){ console.log("type: " + type + ", index: " + index); }); mapp.invoke 方法: mapp核心方法,用於呼叫客戶端介面。 @param {String} namespace 名稱空間 @param {String} method 介面名字 @param {Object/String} params 可選,API呼叫的引數 @param {Function} callback 可選,API呼叫的回撥 * 呼叫普通的無引數介面: mapp.invoke("ns", "method"); * 呼叫有非同步回撥函式的介面: mapp.invoke("ns", "method", function(data){ console.log(data); }); 或 mapp.invoke("ns", "method", { "params" : params//引數通過json封裝 "callback" : mapp.callback(handler), //生成回撥名字 }); * 如果有多個引數呼叫: mapp.invoke("ns", "method", param1, param2 /*,...*/,callback);
JSService的具體實現-外掛執行機制
JSService部分是基於Phonegap的Cordova引擎的基礎上簡化而來,其基本原理參照Cordova的引擎原理如圖所示:

image
一般app中都有自己定製的Webcontainer,為了更好的跟已有專案相融合,在Cordova的基礎上我們進行了簡化,通過JSAPIService服務的方式進行外掛擴充套件開發如圖所示:

image
本JSBridge是基於Phonegap的Cordova引擎的基礎上簡化而來, Android平臺Webview和JS的互動方式共有三種:
- ExposedJsApi:js直接呼叫java物件的方法;(同步)
- 過載chromeClient的prompt 截獲方案;(非同步)
- url截獲+webview.loadUrl回撥的方案;(非同步)
為了和IOS保持一致的JSAPI,只能選用第三套方案;
基於JSService的外掛開發、配置和使用
IOS平臺
git地址: https://github.com/Lede-Inc/LDJSBridge_IOS.git
在Native部分,定義一個模組外掛對應於建立一個外掛類, 模組中的每個外掛介面對應外掛類中某個方法。
整合LDJSBridge_IOS框架之後,只需要繼承框架中的外掛基類LDJSPlugin,如下所示:
- 外掛介面定義
#import "LDJSPlugin.h" @interface LDPDevice : LDJSPlugin {} //@func 獲取裝置資訊 - (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command; @end
- 自定義外掛介面實現
@implementation LDPDevice /** *@func 獲取裝置資訊 */ - (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command{ //讀取裝置資訊 NSMutableDictionary* deviceProperties = [NSMutableDictionary dictionaryWithCapacity:4]; UIDevice* device = [UIDevice currentDevice]; [deviceProperties setObject:[device systemName] forKey:@"systemName"]; [deviceProperties setObject:[device systemVersion] forKey:@"systemVersion"]; [deviceProperties setObject:[device model] forKey:@"model"]; [deviceProperties setObject:[device modelVersion] forKey:@"modelVersion"]; [deviceProperties setObject:[self uniqueAppInstanceIdentifier] forKey:@"identifier"]; LDJSPluginResult* pluginResult = [LDJSPluginResult resultWithStatus:LDJSCommandStatus_OK messageAsDictionary:[NSDictionary dictionaryWithDictionary:deviceProperties]]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } @end
- 在plugin.json檔案中對plugin外掛的統一配置
{ "update": "", "module": "mapp", "plugins": [ { "pluginname": "device", "pluginclass": "LDPDevice", "exports": [ { "showmethod": "getDeviceInfo", "realmethod": "getDeviceInfo" } ] } ] }
- 在webContainer中對JSService初始化, 當初始化完成之後,向前端頁面傳送一個ReadyEvent,前端即可開始呼叫JSAPI介面;
//註冊外掛Service if(_bridgeService == nil){ _bridgeService = [[LDJSService alloc] initBridgeServiceWithConfig:@"PluginConfig.json"]; } [_bridgeService connect:_webview Controller:self]; /** Called when the webview finishes loading.This stops the activity view. */ - (void)webViewDidFinishLoad:(UIWebView*)theWebView{ NSLog(@"Finished load of: %@", theWebView.request.URL); //當webview finish load之後,發event事件通知前端JSBridgeService已經就緒 //監聽事件由各個產品自行決定 [_bridgeService readyWithEvent:@"LDJSBridgeServiceReady"]; }
Android平臺
git地址: https://github.com/Lede-Inc/LDJSBridge_Android.git
- 外掛介面定義
public class LDPDevice extends LDJSPlugin { public static final String TAG = "Device"; /** * Constructor. */ public LDPDevice() { } }
- LDJSPlugin 屬性方法說明
/** * Plugins must extend this class and override one of the execute methods. */ public class LDJSPlugin { public String id; //在外掛初始化的時候,會初始化當前外掛所屬的webview和controller //供外掛方法介面 返回處理結果 public WebView webView; public LDJSActivityInterface activityInterface; //所有自定義外掛需要過載此方法 public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException { return false; } }
- 自定義外掛介面實現
@Override public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException { if (action.equals("getDeviceInfo")) { JSONObject r = new JSONObject(); r.put("uuid", LDPDevice.uuid); r.put("version", this.getOSVersion()); r.put("platform", this.getPlatform()); r.put("model", this.getModel()); callbackContext.success(r); } else { return false; } return true; }
- 在封裝的webContainer中註冊服務並呼叫:
/** * 初始化Activity,開啟網頁,註冊外掛服務 */ public void initActivity() { //建立webview和顯示view createGapView(); createViews(); //註冊外掛服務 if(jsBridgeService == null){ jsBridgeService = new LDJSService(_webview, this, "PluginConfig.json"); } //載入請求 if(this.url != null && !this.url.equalsIgnoreCase("")){ _webview.loadUrl(this.url); } } /** * 初始化webview,如果需要呼叫JSAPI,必須為Webview註冊WebViewClient和WebChromeClient */ @SuppressLint("SetJavaScriptEnabled") public void createGapView(){ if(_webview == null){ _webview = new WebView(LDPBaseWebViewActivity.this, null); //設定允許webview和javascript互動 _webview.getSettings().setJavaScriptEnabled(true); _webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); //繫結webviewclient _webviewClient = new WebViewClient(){ public void onPageStarted(WebView view, String url, Bitmap favicon){ super.onPageStarted(view, url, favicon); isWebviewStarted = true; } public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); //傳送事件通知前端 if(isWebviewStarted){ //在page載入之後,載入核心JS,前端頁面可以在document.ready函式中直接呼叫了; jsBridgeService.onWebPageFinished(); jsBridgeService.readyWithEventName("LDJSBridgeServiceReady"); } isWebviewStarted = false; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if(url.startsWith("about:")){ return true; } if(url.startsWith(LDJSService.LDJSBridgeScheme)){ //處理JSBridge特定的Scheme jsBridgeService.handleURLFromWebview(url); return true; } return false; } }; _webview.setWebViewClient(_webviewClient); //繫結chromeClient _webviewChromeClient = new WebChromeClient(){ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { return super.onJsAlert(view, url, message, result); } }; _webview.setWebChromeClient(_webviewChromeClient); } }
結束
第一次寫部落格,寫得糙和不好的地方望見諒,本人將會不斷改善和提高自身能力;所以本部落格主要提供大概的解決方案,望能夠和有需要的人士交流溝通具體實現方式的差異。
作者:philon
連結: https://www.jianshu.com/p/90d1bee2f3c9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。