YKWebViewJavascriptBridge设计总结

目录

  • 前沿
  • YKWebViewJavascriptBridge
    • 关于『自定义文本协议』
    • 关于『协议传输』
    • 关于 YKNativeBridgeEngine
      • 在应用中的初始化流程
      • 关于『解码后的逻辑』
    • 关于 YKWebBridgeEngine
  • 其他
  • 参考资料

前沿

在 iOS 开发中,你应该会碰到以下这个问题:

如何实现 APP 内的 web 页面和 Native 页面的交互功能?
该问题的本质是:实现 JS 和 OC 的相互调用。

YKWebViewJavascriptBridge 是我针对上述问题实现的库。下面将会具体介绍该库的设计思路和具体实现。


YKWebViewJavascriptBridge

YKWebViewJavascriptBridge 是基于 WKWebView+messageHandler+自定义文本协议 进行实现的,协议的传输采取的是请求响应模式。

YKWebViewJavascriptBridge 的设计很简单,其内部只有2个类:

  • YKNativeBridgeEngine:提供 Native 层调用 JS 方法的接口;管理供 JS 调用的 OC 方法;与 Web 层进行协议通讯(对协议进行编码、解码以及传输)
  • YKWebBridgeEngine:提供 Web 层调用 OC 方法的接口;管理供OC 调用的 JS 方法;与 Native 层进行协议通讯(对协议进行编码、解码以及传输)

关于『自定义文本协议』

YKWebViewJavascriptBridge 里设计的『协议』是一个 JSON 格式的文本协议,其具体格式如下:

  • 请求方发出的协议数据:
{
    "reqId":100, // 请求ID
    "name":"GetLocation", // 请求响应方执行的方法
    "data":"some data from Requester" // 给响应方的数据(若没有数据给响应方,则不存在该key)
}
  • 响应方返回的协议数据:
{
    "respId":100, // 响应ID,值源自reqId
    "data":"some data from Responser" // 给请求方的响应数据(若没有响应数据,则不存在该key)
}

关于『编码、解码』

YKWebViewJavascriptBridge 的编码和解码,其实就是把数据对象序列化为 JSON 和把 JSON 反序列化为数据对象。

关于『协议传输』

Native层的协议传输和Web层的协议传输分别由 YKNativeBridgeEngineYKWebBridgeEngine 各自实现。其传输接口实现如下:

  • YKNativeBridgeEngine的协议传输接口
- (void)_bridgeEngineDidWriteData:(NSString *)jsonMessage
{
    if (!jsonMessage) {
        return;
    }
    
    if (![jsonMessage isKindOfClass:[NSString class]]) {
        return;
    }
    
    // 调用YKWebBridgeEngine的_bridgeEngineDidReadData方法传递协议数据给YKWebBridgeEngine
    NSString *jsCode = [NSString stringWithFormat:@"YKWebBridgeEngine._bridgeEngineDidReadData('%@')",jsonMessage];
    [self.webView evaluateJavaScript:jsCode completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"bridgeEngineDidWriteData:%@-%@",result,error);
    }];
}

- (void)_bridgeEngineDidReadData:(NSString *)jsonMessage
{
    if (!jsonMessage) {
        return;
    }
    
    if (![jsonMessage isKindOfClass:[NSString class]]) {
        return;
    }
    
    // 对接收到的json文本协议进行解码,转换层字典对象
    NSDictionary *messgaeDict = [self deserializeJSONMessage:jsonMessage];
    if (!messgaeDict) {
        return;
    }
    
    // 根据解码后的协议,执行具体操作
    ...
}
  • YKWebBridgeEngine的协议传输接口
function _bridgeEngineDidWriteData(jsonMessage) {
    //通过WKWebView的messageHandler功能,传递数据给YKNativeBridgeEngine     
    window.webkit.messageHandlers.NativeBridgeEngineDidReadData.postMessage(jsonMessage);
}

function _bridgeEngineDidReadData(jsonMessage) {
    // 对接收到的json文本协议进行解码,转换层字典对象
    var messageDict = JSON.parse(jsonMessage);

    // 根据解码后协议内容,执行具体的操作
    ...
}

熟悉WKWebView+messageHandler 的童鞋看到这,应该知道,YKWebViewJavascriptBridge 的协议传输完全是基于 WKWebView 实现的:

  • [WKWebView evaluateJavaScript:completionHandler:] 提供了 Native 层发数据给 Web 层的能力

  • window.webkit.messageHandlers. NativeMethod.postMessage(messageData); 提供了 Web 层发送数据给 Native 层的能力

YKNativeBridgeEngine 设计要点

在应用中的初始化流程

YKNativeBridgeEngine 在应用中的初始化代码如下:

    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [WKUserContentController new];
    
    WKPreferences *preferences = [WKPreferences new];
    preferences.javaScriptCanOpenWindowsAutomatically = YES;
    preferences.minimumFontSize = 40.0;
    configuration.preferences = preferences;
    
    // 1、根据WKWebViewConfiguration生成nativeBridgeEngine实例
    self.nativeBridgeEngine = [[YKNativeBridgeEngine alloc] initWithWebViewConfiguration:configuration];;
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, self.view.frame.size.height - 100) configuration:self.nativeBridgeEngine.configuration];
    // 2、绑定具体的WKWebView实例
    [self.nativeBridgeEngine bridgeForWebView:self.webView];

如上述代码所示,YKNativeBridgeEngine 在应用中的初始化需要2步:

  1. 根据 WKWebViewConfiguration 生成 nativeBridgeEngine 实例

    为什么需要 WKWebViewConfiguration 来生成nativeBridgeEngine 实例?
    看代码:

    
    - (instancetype)initWithWebViewConfiguration:(WKWebViewConfiguration *)configuration
    {
        self = [self init];
        if (self) {
            if (configuration) {
                self.configuration = configuration;
            }else{
                WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
                configuration.userContentController = [[WKUserContentController alloc] init];
                
                self.configuration = configuration;
            }
            
            if (!self.configuration.userContentController) {
                self.configuration.userContentController = [WKUserContentController new];
            }
            
            //通过JS注入方式,初始化WebBridgeEngine
            NSString *js = YKWebBridgeEngine_js();
            WKUserScript *script = [[WKUserScript alloc] initWithSource:js
                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                       forMainFrameOnly:YES];
            [self.configuration.userContentController addUserScript:script];
            
            [self.configuration.userContentController addScriptMessageHandler:self name:@"NativeBridgeEngineDidReadData"];
        }
        return self;
    }
    
    

    WKWebViewConfiguration 的作用主要是:

  • 注入 JS 代码到 WebView,在 Document 元素开始生成时,执行 YKWebBridgeEngine 初始化代码
  • 添加 ScriptMessageHandler,提供方法给 YKWebBridgeEngine 发送数据给 YKNativeBridgeEngine
  1. 绑定具体的 WKWebView 实例

为什么需要绑定具体的 WKWebView 实例?
看代码:

- (void)_bridgeEngineDidWriteData:(NSString *)jsonMessage
{
    if (!jsonMessage) {
        return;
    }
    
    if (![jsonMessage isKindOfClass:[NSString class]]) {
        return;
    }
    
    NSString *jsCode = [NSString stringWithFormat:@"YKWebBridgeEngine._bridgeEngineDidReadData('%@')",jsonMessage];
    [self.webView evaluateJavaScript:jsCode completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        NSLog(@"bridgeEngineDidWriteData:%@-%@",result,error);
    }];
}

YKNativeBridgeEngine 内部持有 webView 的用处就是在于借助 webViewevaluateJavaScript:completionHandler: 功能,达到发数据给 YKWebBridgeEngine 的目的。

关于『解码后的逻辑』

YKNativeBridgeEngine 收到 YKWebBridgeEngine 发来的 JSON文本数据后,YKNativeBridgeEngine 会对数据进行解码:反序列化JSON,得到一个 messageDict ,然后根据其内容进行具体的逻辑操作。核心代码如下:

- (void)_bridgeEngineDidReadData:(NSString *)jsonMessage
{
    if (!jsonMessage) {
        return;
    }
    
    if (![jsonMessage isKindOfClass:[NSString class]]) {
        return;
    }
    
    NSDictionary *messgaeDict = [self deserializeJSONMessage:jsonMessage];
    if (!messgaeDict) {
        return;
    }
    
    NSArray *allKeys = messgaeDict.allKeys;
    
    //收到WebBridgeEngine发来的请求
    if ([allKeys containsObject:@"reqId"]) {
        NSInteger reqId = [[messgaeDict objectForKey:@"reqId"] integerValue];
        id data = [messgaeDict objectForKey:@"data"];
        NSString *name = [messgaeDict objectForKey:@"name"];
        
        if (!name) {
            return;
        }
        
        YKMessageHandler handler = [self.messageHandlerDict objectForKey:name];
        
        if (!handler) {
            return;
        }
        
        __weak typeof(self) weakSelf = self;
        YKResponseCallback responseCallback = ^(id responseData) {
            NSMutableDictionary *respMessageDict = [[NSMutableDictionary alloc] initWithCapacity:8];
            [respMessageDict setObject:@(reqId) forKey:@"respId"];
            
            if (responseData) {
                [respMessageDict setObject:responseData forKey:@"data"];
            }
            
            NSString *jsonMessage = [weakSelf serializeMessage:respMessageDict];
            [weakSelf _bridgeEngineDidWriteData:jsonMessage];
        };
        
        handler(data,responseCallback);

    }
    //收到WebBridgeEngine返回的响应数据
    else if ([allKeys containsObject:@"respId"]) {
        NSInteger respId = [[messgaeDict objectForKey:@"respId"] integerValue];
        id responseData = [messgaeDict objectForKey:@"data"];
        YKResponseCallback responseCallback = [self.responseCallbackDict objectForKey:@(respId)];
        if (responseCallback) {
            responseCallback(responseData);
            [self.responseCallbackDict removeObjectForKey:@(respId)];
        }
    }
}

在收到数据后,会根据解码后的 messgaeDict 的 key 判断这是一个『请求类型的数据』还是一个『响应类的数据』:若 key 中存在 reqId ,则是『请求类型的数据』,然后执行对应的请求,并返回响应数据;若是存在 respId ,则是一个『响应类型的数据』,然后把返回的响应数据通过回调告知应用层。

关于 YKWebBridgeEngine

YKWebBridgeEngine 的设计(接口和功能) 和 YKNativeBridgeEngine 是一样的,区别在于前者是 OC 语言实现的,后者是 JS 语言实现而已。另外需要注意的是,YKWebBridgeEngine初始化依赖与YKNativeBridgeEngine,通过 JS注入,让webview在开始加载 Document 元素时执行YKWebBridgeEngine的初始化代码。


其他

想详细了解WKWebViewmessageHandler 的童鞋,可以参考下面文章:


参考资料

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

推荐阅读更多精彩内容