前言
對于商務類的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工程運行效果如下:
關于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的引擎原理如圖所示:
一般app中都有自己定制的Webcontainer,為了更好的跟已有項目相融合,在Cordova的基礎上我們進行了簡化,通過JSAPIService服務的方式進行插件擴展開發如圖所示:
本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
鏈接:http://www.lxweimin.com/p/90d1bee2f3c9
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。