JSBridge插件化SDK的设计与实现

native与Javascript的三种交互方式

1. native对Javascript执行代码注入
// 点击图片预览
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;
}

// 执行注入
[webView stringByEvaluatingJavaScriptFromString:LJJSInjectClickImage()];
2. native调用Javascript
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [_webView evaluateJavaScript:@"getImages()" completionHandler:^(NSString *imageString, NSError * _Nullable error) {
        NSLog(@"%@", imageString);
    }];
}
3. Javascript调用native
  • 1: 拦截URL
    WebViewJavascriptBridge,Cordova,EasyJSWebView都是基于拦截URL的原理来实现。
  • 2: JavaScriptCore
    iOS7之后苹果推出JavaScriptCore框架,从而让web页面和本地原生应用,交互起来非常方便,而且使用此框架可以做Android那边和iOS相对统一,web前端写一套代码就可以适配客户端的两个平台,从而减少了web前端的工作量,但只适用于UIWebView。
  • 3: MessageHandler
    iOS8以后出现,web前端需要对iOS Android不同处理,不允许跨域,无法发送POST参数。只适用于WKWebView。
基于拦截URL实现的插件化JSBridge SDK

以下是一个设备信息插件的声明文件,前端工程师可通过引入该声明文件获取设备信息插件的相关能力。

当收到plusready事件名称时,代表插件初始化代码已经成功注入,此时注册该插件。

/* ************************ 设备信息插件 ************************ */
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);

调用插件代码:

<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>

可以看到对插件的调用最终都会触发 window.plus.requestNative(插件名,方法名,参数,回调)
window.plus.requestNative(...)的实现是在native加载webView时注入到WebView中。

注入的关键代码如下:

(function(){

    if(window.plus){
        return;
    }
    window.plus = {};
    // messageMap存储callbackId与回调的对应关系
    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转换为window.LJJSBridge.requestNative,并在结尾发送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调用native的方法执行顺序如下。
window.LJJSBridge.openNativeURL()内将方法调用转换为URL跳转,对参数进行URI编码。在native进行URL拦截。

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

iOS native对URL进行拦截:

// 插件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负责对单个webView中所有的插件进行管理,插件采用反射+懒加载方式创建。

/**
 Js调用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;
        }
    }];

    // 创建插件并执行插件方法
    void (^createAndExecPlugBlock)(void) = ^() {
        LJBaseJSPlugin *baseJSPlugin = [self createJSPluginWithPlugName:plugName];
        if (baseJSPlugin != nil) {
            // 找到并创建该插件,将该插件加入插件列表
            [plugsAr addObject:baseJSPlugin];
            // 执行该插件的native方法
            [self callFunctionWithObj:baseJSPlugin functionName:functionName message:[LJBridgeUtil convertStringToMessage:msg]];
        }
    };
    
    if (plug == nil) {
        // 未找到该插件,创建该插件
            createAndExecPlugBlock();
    }
    else {
        // 找到该插件
        if (plug.webView != nil && plug.vc != nil) {
            // 执行该插件的native方法
            [self callFunctionWithObj:plug functionName:function message:[LJBridgeUtil convertStringToMessage:msg]];
        }
        else {
            // 若插件不可用,则移除该插件并重新创建它
            [plugsAr removeObject:plug];
            
            createAndExecPlugBlock();
        }
    }
}

使用反射创建插件:

/**
 创建插件
 
 @param plugNameStr 插件名称
 */
- (LJBaseJSPlugin *)createJSPluginWithPlugName:(NSString *)plugName
{
    // 创建该插件
//    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插件,若未找到则动态创建它。
对DeviceInfoPlugin插件调用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
}

设备信息插件实现如下:

@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

基础插件实现如下:
nativeCalled属性提供插件给native调用的能力,默认值为true。插件不仅支持js调用,也支持本地native调用。

/**
 插件基类(公开).
 */
@interface LJBaseJSPlugin : NSObject

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

// 是否是native调用,默认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;
    }
    
    // 回调native
    if (self.isNativeCalled) {
        if (resMsg.nativeResponseBlock) {
            resMsg.nativeResponseBlock(resMsg);
        }
    }
    else {
        if (self.webView) {
            // native回调JS,回传返回值
            [LJBridgeUtil webView:self.webView callJSWithMessage:resMsg];
        }
    }
}

@end

callJSWithMessage()负责native回调js,将version信息回传。

// js处理native调用
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,并执行回调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)
},

成功执行回调。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容