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工程運行效果如下:

 

 

 

關於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的交互方式共有三種:

 

  1. ExposedJsApi:js直接調用java對象的方法;(同步)

  2. 重載chromeClient的prompt 截獲方案;(異步)

  3. 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中注冊服務並調用:

     

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM