JSBridge插件化SDK的設(shè)計(jì)與實(shí)現(xiàn)

native與Javascript的三種交互方式

1. native對(duì)Javascript執(zhí)行代碼注入
// 點(diǎn)擊圖片預(yù)覽
NSString * LJJSInjectClickImage(void) {
#define __wvjb_js_func__(x) #x
    static NSString * JSCode = @__wvjb_js_func__(
         function getImages() {
             var objs = document.getElementsByTagName("img");
             var imgScr = '';
             for (var i = 0; i < objs.length; i++) {
                 imgScr = imgScr + objs[i].src + '+';
             };
             return imgScr;
         };

         function registerImagesClickAction() {
             var imgs = document.getElementsByTagName('img');
             var length = imgs.length;
             for (var i = 0; i < length; i++) {
                 img = imgs[i];
                 img.onclick = function () {
                     window.location.href = 'lj-js-clickimage:' + this.src
                 }
             }
         });
    #undef __wvjb_js_func__
    return JSCode;
}

// 執(zhí)行注入
[webView stringByEvaluatingJavaScriptFromString:LJJSInjectClickImage()];
2. native調(diào)用Javascript
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [_webView evaluateJavaScript:@"getImages()" completionHandler:^(NSString *imageString, NSError * _Nullable error) {
        NSLog(@"%@", imageString);
    }];
}
3. Javascript調(diào)用native
  • 1: 攔截URL
    WebViewJavascriptBridge乃沙,Cordova阻塑,EasyJSWebView都是基于攔截URL的原理來實(shí)現(xiàn)抑进。
  • 2: JavaScriptCore
    iOS7之后蘋果推出JavaScriptCore框架抱冷,從而讓web頁面和本地原生應(yīng)用炮温,交互起來非常方便巴碗,而且使用此框架可以做Android那邊和iOS相對(duì)統(tǒng)一,web前端寫一套代碼就可以適配客戶端的兩個(gè)平臺(tái),從而減少了web前端的工作量议蟆,但只適用于UIWebView。
  • 3: MessageHandler
    iOS8以后出現(xiàn)萎战,web前端需要對(duì)iOS Android不同處理咐容,不允許跨域,無法發(fā)送POST參數(shù)蚂维。只適用于WKWebView戳粒。
基于攔截URL實(shí)現(xiàn)的插件化JSBridge SDK

以下是一個(gè)設(shè)備信息插件的聲明文件路狮,前端工程師可通過引入該聲明文件獲取設(shè)備信息插件的相關(guān)能力。

當(dāng)收到plusready事件名稱時(shí)蔚约,代表插件初始化代碼已經(jīng)成功注入奄妨,此時(shí)注冊該插件。

/* ************************ 設(shè)備信息插件 ************************ */
document.addEventListener("plusready",
function registerPluginFunction() {
    var _pluginName = 'DeviceInfoPlugin',
    p = window.plus;
    p[_pluginName] = {
        appVersion: function (param, callback) {
            p.requestNative(_pluginName, "appVersion", param, callback)
        },
        uniqueId: function (param, callback) {
            p.requestNative(_pluginName, "uniqueId", param, callback)
        }
    };
    document.removeEventListener("plusready", registerPluginFunction, true);
},
true);

調(diào)用插件代碼:

<html>
    <head>
        <script type="text/javascript" src="DeviceInfoPlugin.js"></script>
        <script>

        function appVersion(){
            window.plus.DeviceInfoPlugin.appVersion(null, function (responseData) {
                alert(JSON.stringify(responseData));
                                               })
        }
        </script>

    </head>
    
    <body>
        <img src="eg.jpg"  width=100 height=100 /><br/>
        <img src="zq.jpg"  width=100 height=100 /><br/>
        <input type="button" id="enter32" value="appVersion" onclick="appVersion();" /><br/>

    </body>
    
</html>

可以看到對(duì)插件的調(diào)用最終都會(huì)觸發(fā) window.plus.requestNative(插件名苹祟,方法名砸抛,參數(shù),回調(diào))树枫。
window.plus.requestNative(...)的實(shí)現(xiàn)是在native加載webView時(shí)注入到WebView中直焙。

注入的關(guān)鍵代碼如下:

(function(){

    if(window.plus){
        return;
    }
    window.plus = {};
    // messageMap存儲(chǔ)callbackId與回調(diào)的對(duì)應(yīng)關(guān)系
    var messageMap = new Map();
    var uniqueId = 1;
    window.LJJSBridge = {
        requestNative : function(scheme, plugin, func, param, callback) {
            var message = {};
            message.plugin = plugin;
            message.param = param;
            if (!!callback) {
                var callbackId ='cb_'+ (uniqueId++) + '_' + new Date().getTime();
                message.callbackId = callbackId;
                message.callback = callback;
                messageMap.set(callbackId,message);
            }
            window.LJJSBridge.openNativeURL(scheme,plugin,func,JSON.stringify(message));
        },
        openNativeURL : function (scheme, plugin, func, args) {
            var formattedArgs = (args.length > 0 ? encodeURIComponent(args):"");
            var iframe = document.createElement("IFRAME");
            iframe.setAttribute("src", scheme + ":" + plugin + ":" + encodeURIComponent(func) +":" + formattedArgs);
            document.documentElement.appendChild(iframe);
            iframe.parentNode.removeChild(iframe);
            iframe = null;
        },
        _handleMessageFromNative : function(nativeMessage) {
            setTimeout(function() {
                var message;
                if(nativeMessage.callbackId){
                    message = messageMap.get(nativeMessage.callbackId);
                    console.log(nativeMessage.responseData);
                    if (!message||(!message.responseCallback)) {
                        return;
                    }
                    message.responseCallback(nativeMessage.responseData);
                    messageMap.delete(message.callbackId);
                }
            },0)
        },
    }
})();

window.plus.requestNative 帶上scheme轉(zhuǎn)換為window.LJJSBridge.requestNative茄蚯,并在結(jié)尾發(fā)送plusready事件具被。

(function(){
    if (window.plus.requestNative) {
        return;
    }
    function requestNative(plugin,func,param,callback) {
        window.LJJSBridge.requestNative("lj-js-plugin",plugin, func, param,callback);
    }
    var plus = {
        requestNative:requestNative,
    };
    window.plus = plus;
    var readyEvent = document.createEvent('Events');
    readyEvent.initEvent('plusready');
    readyEvent.bridge = plus;
    document.dispatchEvent(readyEvent);
})();

js調(diào)用native的方法執(zhí)行順序如下。
window.LJJSBridge.openNativeURL()內(nèi)將方法調(diào)用轉(zhuǎn)換為URL跳轉(zhuǎn)顽分,對(duì)參數(shù)進(jìn)行URI編碼舔清。在native進(jìn)行URL攔截丝里。

window.plus.DeviceInfoPlugin.appVersion()
window.plus.requestNative()
window.LJJSBridge.requestNative()
window.LJJSBridge.openNativeURL()

iOS native對(duì)URL進(jìn)行攔截:

// 插件Bridge處理
@property (nonatomic, strong) LJPluginsJSBridge *pluginsBridge;

// URL攔截
NSURL *url = [navigationAction.request URL];
NSString *urlString = [url absoluteString];
NSArray *components = [urlString componentsSeparatedByString:@":"];
NSString *scheme = components[0];
if ([scheme isEqualToString:[@"lj-js-plugin" copy]] && [components count] > 3) {
    NSString *pluginName = components[1];
    NSString *funcName = components[2];
    NSString *argsString = [components[3] length] ? [components[3] stringByRemovingPercentEncoding] : nil;
    [self.pluginsBridge execPlugWithPlugName:pluginName Function:funcName Message:argsString];
}

LJPluginsJSBridge負(fù)責(zé)對(duì)單個(gè)webView中所有的插件進(jìn)行管理曲初,插件采用反射+懶加載方式創(chuàng)建体谒。

/**
 Js調(diào)用native方法

 @param plugName 插件名稱
 @param functionName native方法名
 @param msg 屬性json
 */
- (void)execPlugWithPlugName:(NSString *)plugName Function:(NSString *)functionName Message:(NSString *)msg
{
    if (plugName == nil) {
        return;
    }
    // 從插件列表中找到該插件
    NSMutableArray<LJBaseJSPlugin *> *plugsAr = [self plugsAr];
    __block LJBaseJSPlugin *plug = nil;
    [plugsAr enumerateObjectsUsingBlock:^(LJBaseJSPlugin * tempPlug, NSUInteger idx, BOOL *stop) {
        if ([tempPlug.name isEqualToString:plugName]) {
            plug = tempPlug;
            *stop = YES;
        }
    }];

    // 創(chuàng)建插件并執(zhí)行插件方法
    void (^createAndExecPlugBlock)(void) = ^() {
        LJBaseJSPlugin *baseJSPlugin = [self createJSPluginWithPlugName:plugName];
        if (baseJSPlugin != nil) {
            // 找到并創(chuàng)建該插件,將該插件加入插件列表
            [plugsAr addObject:baseJSPlugin];
            // 執(zhí)行該插件的native方法
            [self callFunctionWithObj:baseJSPlugin functionName:functionName message:[LJBridgeUtil convertStringToMessage:msg]];
        }
    };
    
    if (plug == nil) {
        // 未找到該插件臼婆,創(chuàng)建該插件
            createAndExecPlugBlock();
    }
    else {
        // 找到該插件
        if (plug.webView != nil && plug.vc != nil) {
            // 執(zhí)行該插件的native方法
            [self callFunctionWithObj:plug functionName:function message:[LJBridgeUtil convertStringToMessage:msg]];
        }
        else {
            // 若插件不可用抒痒,則移除該插件并重新創(chuàng)建它
            [plugsAr removeObject:plug];
            
            createAndExecPlugBlock();
        }
    }
}

使用反射創(chuàng)建插件:

/**
 創(chuàng)建插件
 
 @param plugNameStr 插件名稱
 */
- (LJBaseJSPlugin *)createJSPluginWithPlugName:(NSString *)plugName
{
    // 創(chuàng)建該插件
//    Class plugClass = [[LJAbilityConfig sharedInstance] classOfPlugName:plugName];
    Class plugClass = NSClassFromString(plugName);
    if (plugClass == nil) {
        return nil;
    }
    LJBaseJSPlugin *baseJSPlugin = nil;
    if (plugClass != nil) {
        baseJSPlugin = [[plugClass alloc] init];
        baseJSPlugin.name = plugName;
        baseJSPlugin.nativeCalled = NO;
        baseJSPlugin.webView = self.webView;
        baseJSPlugin.vc = self.vc;
    }
    return baseJSPlugin;
}

如下url被分解為四部分:

lj-js-plugin:DeviceInfoPlugin:appVersion:%7B%22plugin%22%3A%22DeviceInfoPlugin%22%2C%22param%22%3Anull%2C%22callbackId%22%3A%22cb_1_1631588855957%22%7D
  • scheme:lj-js-plugin,
  • plugin:DeviceInfoPlugin,
  • function:appVersion,
  • args:%7B%22plugin%22%3A%22DeviceInfoPlugin%22%2C%22param%22%3Anull%2C%22callbackId%22%3A%22cb_1_1631588855957%22%7D

在屬于webView的插件列表中查找DeviceInfoPlugin插件,若未找到則動(dòng)態(tài)創(chuàng)建它颁褂。
對(duì)DeviceInfoPlugin插件調(diào)用appVersion方法故响。

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

NSString *ocFunctionName = [NSString stringWithFormat:@"%@:",functionName];
if ([obj respondsToSelector:NSSelectorFromString(ocFunctionName)]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    dispatch_main_async_safe(^{
            [obj performSelector:NSSelectorFromString(ocFunctionName) withObject:msg];
        })
    #pragma clang diagnostic pop
}

設(shè)備信息插件實(shí)現(xiàn)如下:

@interface DeviceInfoPlugin : LJBaseJSPlugin

- (void)appVersion:(LJMessage *)msg;
- (void)uniqueId:(LJMessage *)msg;

@end


@implementation DeviceInfoPlugin

- (void)appVersion:(LJMessage *)msg
{
    NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];

    NSString *versionStr = [infoDict objectForKey:@"CFBundleShortVersionString"];
    NSString *buildStr = [infoDict objectForKey:@"CFBundleVersion"];
    
    msg.responseDic = @{
                        @"version":ACNotNilStr(versionStr),
                        @"build":ACNotNilStr(buildStr),
                        };
    [self respondJSWithMsg:msg];
}

@end

基礎(chǔ)插件實(shí)現(xiàn)如下:
nativeCalled屬性提供插件給native調(diào)用的能力,默認(rèn)值為true颁独。插件不僅支持js調(diào)用彩届,也支持本地native調(diào)用。

/**
 插件基類(公開).
 */
@interface LJBaseJSPlugin : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, weak) id webView;
@property (nonatomic, weak) UIViewController *vc;

// 是否是native調(diào)用誓酒,默認(rèn)yes
@property (nonatomic, assign, getter=isNativeCalled) BOOL nativeCalled;

- (void)respondJSWithMsg:(LJMessage *)resMsg;

@end

@implementation LJBaseJSPlugin

- (id)init
{
    self = [super init];
    if (self) {
        _nativeCalled = YES;
    }
    return self;
}

- (void)respondJSWithMsg:(LJMessage *)resMsg
{
    if (!resMsg) {
        return;
    }
    
    // 回調(diào)native
    if (self.isNativeCalled) {
        if (resMsg.nativeResponseBlock) {
            resMsg.nativeResponseBlock(resMsg);
        }
    }
    else {
        if (self.webView) {
            // native回調(diào)JS樟蠕,回傳返回值
            [LJBridgeUtil webView:self.webView callJSWithMessage:resMsg];
        }
    }
}

@end

callJSWithMessage()負(fù)責(zé)native回調(diào)js,將version信息回傳靠柑。

// js處理native調(diào)用
static const NSString *kLJJSHandleMessageFormat = @"window.LJJSBridge._handleMessageFromNative(%@);";

+ (void)webView:(id)webView callJSWithMessage:(LJMessage *)msg
{
    NSString *messageStr = [msg json];

    NSString *formatStr = [kLJJSHandleMessageFormat copy];
    NSString *jsStr = [NSString stringWithFormat:formatStr,messageStr];
    
    [self webView:webView callJsWithString:jsStr];
}

_handleMessageFromNative通過callbackId找到Map中的message寨辩,并執(zhí)行回調(diào)message.callback(nativeMessage.responseData);

_handleMessageFromNative : function(nativeMessage) {
    setTimeout(function() {
        var message;
        if(nativeMessage.callbackId){
            message = messageMap.get(nativeMessage.callbackId);
            console.log(nativeMessage.responseData);
            if (!message||(!message.callback)) {
                return;
            }
            message.callback(nativeMessage.responseData);
            messageMap.delete(message.callbackId);
        }
    },0)
},

成功執(zhí)行回調(diào)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末歼冰,一起剝皮案震驚了整個(gè)濱河市靡狞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隔嫡,老刑警劉巖甸怕,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甘穿,死亡現(xiàn)場離奇詭異,居然都是意外死亡蕾各,警方通過查閱死者的電腦和手機(jī)扒磁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來式曲,“玉大人妨托,你說我怎么就攤上這事×咝撸” “怎么了兰伤?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钧排。 經(jīng)常有香客問我敦腔,道長,這世上最難降的妖魔是什么恨溜? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任符衔,我火速辦了婚禮,結(jié)果婚禮上糟袁,老公的妹妹穿的比我還像新娘判族。我一直安慰自己,他們只是感情好项戴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布形帮。 她就那樣靜靜地躺著,像睡著了一般周叮。 火紅的嫁衣襯著肌膚如雪辩撑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天仿耽,我揣著相機(jī)與錄音合冀,去河邊找鬼。 笑死项贺,一個(gè)胖子當(dāng)著我的面吹牛君躺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敬扛,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼晰洒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了啥箭?” 一聲冷哼從身側(cè)響起谍珊,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎急侥,沒想到半個(gè)月后砌滞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侮邀,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年贝润,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绊茧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡打掘,死狀恐怖华畏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尊蚁,我是刑警寧澤亡笑,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站横朋,受9級(jí)特大地震影響仑乌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜琴锭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一晰甚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧决帖,春花似錦厕九、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腺阳。三九已至落君,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亭引,已是汗流浹背绎速。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焙蚓,地道東北人纹冤。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像购公,于是被迫代替她去往敵國和親萌京。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容