WKWebView的NSUrlProtocol的各种问题

KKJSBridge

转自:https://github.com/karosLi/KKJSBridge

一站式解决 WKWebView 支持离线包,Ajax/Fetch 请求和 Cookie 同步的问题 (基于 Ajax Hook,Fetch Hook 和 Cookie Hook)

更详细的介绍

KKJSBridge 支持的功能

  • JSBrdige 相关

    • 基于 MessageHandler 搭建通信层

    • 支持模块化的管理 JSAPI

    • 支持模块共享上下文信息

    • 支持模块消息转发

    • 支持 JSBridge 同步调用

    • 兼容 WebViewJavascriptBridge

  • 请求相关

    • 支持离线资源

    • 支持 ajax/fetch hook 避免 body 丢失

    • 支持 ajax/fetch 同步请求

    • Native 侧控制 ajax/fetch hook

    • 支持表单数据,支持图片上传

    • 支持 ajax/fetch 请求外部代理

    • 分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案,可以根据具体需求自由选择

  • WebView 相关

    • Cookie 统一管理

    • WKWebView 复用

Demo

demo 概览

[站外图片上传中...(image-3e31fd-1677660759674)]

模块化调用 JSAPI

[站外图片上传中...(image-ad48b6-1677660759674)]

ajax hook 演示

[站外图片上传中...(image-a06bf8-1677660759674)]

淘宝 ajax hook 演示

[站外图片上传中...(image-2f9abb-1677660759674)]

ajax 发送表单 演示

[站外图片上传中...(image-e98295-1677660759674)]

用法

从复用池取出缓存的 WKWebView,并开启 ajax hook

+ (void)load {
    __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        [self prepareWebView];
        [[NSNotificationCenter defaultCenter] removeObserver:observer];
    }];
}

+ (void)prepareWebView {
    // 预先缓存一个 webView
    [KKWebView configCustomUAWithType:KKWebViewConfigUATypeAppend UAString:@"KKJSBridge/1.0.0"];
    [[KKWebViewPool sharedInstance] makeWebViewConfiguration:^(WKWebViewConfiguration * _Nonnull configuration) {
        // 必须前置配置,否则会造成属性不生效的问题
        configuration.allowsInlineMediaPlayback = YES;
        configuration.preferences.minimumFontSize = 12;
    }];
    [[KKWebViewPool sharedInstance] enqueueWebViewWithClass:KKWebView.class];
    KKJSBridgeConfig.ajaxDelegateManager = (id<KKJSBridgeAjaxDelegateManager>)self; // 请求外部代理处理,可以借助 AFN 网络库来发送请求
}

- (void)dealloc {
    // 回收到复用池
    [[KKWebViewPool sharedInstance] enqueueWebView:self.webView];
}

- (void)commonInit {
    _webView = [[KKWebViewPool sharedInstance] dequeueWebViewWithClass:KKWebView.class webViewHolder:self];
    _webView.navigationDelegate = self;
    _jsBridgeEngine = [KKJSBridgeEngine bridgeForWebView:self.webView];
    _jsBridgeEngine.config.enableAjaxHook = YES; // 开启 ajax hook

    [self registerModule];
}

#pragma mark - KKJSBridgeAjaxDelegateManager
+ (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request callbackDelegate:(NSObject<KKJSBridgeAjaxDelegate> *)callbackDelegate {
    return [[self ajaxSesstionManager] dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        // 处理响应数据
        [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveResponse:response];
        if ([responseObject isKindOfClass:NSData.class]) {
            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseObject];
        } else if ([responseObject isKindOfClass:NSDictionary.class]) {
            NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseObject options:0 error:nil];
            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseData];
        } else {
            NSData *responseData = [NSJSONSerialization dataWithJSONObject:@{} options:0 error:nil];
            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseData];
        }
        [callbackDelegate JSBridgeAjax:callbackDelegate didCompleteWithError:error];
    }];
}

注册模块

- (void)registerModule {
 ModuleContext *context = [ModuleContext new];
 context.vc = self;
 context.scrollView = self.webView.scrollView;
 context.name = @"上下文";
 // 注册 默认模块
 [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleDefault.class];
 // 注册 模块A
 [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleA.class];
 // 注册 模块B 并带入上下文
 [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleB.class withContext:context];
 // 注册 模块C
 [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleC.class];
}

模块定义

@interface ModuleB()<KKJSBridgeModule>

@property (nonatomic, weak) ModuleContext *context;

@end

@implementation ModuleB

// 模块名称
+ (nonnull NSString *)moduleName {
    return @"b";
}

// 单例模块
+ (BOOL)isSingleton {
    return YES;
}

// 模块初始化方法,支持上下文带入
- (instancetype)initWithEngine:(KKJSBridgeEngine *)engine context:(id)context {
    if (self = [super init]) {
        _context = context;
        NSLog(@"ModuleB 初始化并带上 %@", self.context.name);
    }

    return self;
}

// 模块提供的方法
- (void)callToGetVCTitle:(KKJSBridgeEngine *)engine params:(NSDictionary *)params responseCallback:(void (^)(NSDictionary *responseData))responseCallback {
    responseCallback ? responseCallback(@{@"title": self.context.vc.navigationItem.title ? self.context.vc.navigationItem.title : @""}) : nil;
}

JS 侧调用方式

// 异步调用
window.KKJSBridge.call('b', 'callToGetVCTitle', {}, function(res) {
    console.log('receive vc title:', res.title);
});

// 同步调用
var res = window.KKJSBridge.syncCall('b', 'callToGetVCTitle', {});
console.log('receive vc title:', res.title);

安装

  1. CocoaPods

    //In Podfile
    
    # 分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案,可以根据具体需求自由选择。
    # 只能选择其中一个方案,默认是 ajax protocol hook。
    pod 'KKJSBridge/AjaxProtocolHook'
    pod 'KKJSBridge/AjaxHook'
    
    

Ajax Hook 方案对比

这里只对比方案间相互比较的优缺点,共同的优点,就不赘述了。如果对私有 API 不敏感的,我是比较推荐使用方案一的。

方案一:Ajax Hook 部分 API + NSURLProtocol

这个方案对应的是这里的 KKJSBridge/AjaxProtocolHook

原理介绍:此种方案,是只需要 hook ajax 中的 open/send 方法,在 hook 的 send 方法里,先把要发送的 body 通过 JSBridge 发送到 Native 侧去缓存起来。缓存成功后,再去执行真实的 send 方法,NSURLProtocol 此时会拦截到该请求,然后取出之前缓存的 body 数据,重新拼接请求,就可以发送出去了,然后通过 NSURLProtocol 把请求结果返回给 WebView 内核。

优点:

  • 兼容性会更好,网络请求都是走 webview 原生的方式。
  • hook 的逻辑会更少,会更加稳定。
  • 可以更好的支持 ajax 获取二进制的数据。例如 H5 小游戏场景(白鹭引擎是通过异步获取图片资源)。

缺点:

  • 需要使用到私有 API browsingContextController 去注册 http/https。(其实现在大部分的离线包方案也是使用了这个私有 API 了)

方案二:Ajax Hook 全部 API

这个方案对应的是这里的 KKJSBridge/AjaxHook

原理介绍:此种方案,是使用 hook 的 XMLHttpRequest 对象来代理真实的 XMLHttpRequest 去发送请求,相当于是需要 hook ajax 中的所有方法,在 hook 的 open 方法里,调用 JSBridge 让 Native 去创建一个 NSMutableRequest 对象,然后在 hook 的 send 方法,把要发送的 body 通过 JSBridge 发送到 Native 侧,并把 body 设置给刚才创建的 NSMutableRequest 对象,并在 Native 侧完成请求后,通过 JS 执行函数,把请求结果通知给 JS 侧,JS 侧找到 hook 的 XMLHttpRequest 对象,最后调用 onreadystatechange 函数,让 H5 知道有请求结果了。

优点:

  • 没有使用私有 API。

缺点:

  • 需要 hook XMLHttpRequest 的所有方法。
  • 请求结果是通过 JSBrdige 来进行传输的,性能上肯定没有原生的性能好。
  • 不能支持 ajax 获取二进制的数据。要想支持的话,还需要额外的序列化工作。

更新历史

2020.11.24 (1.3.5)

  • 模块化管理 TS,方便代码阅读和维护
  • 移除 await 关键字,优化表单转 base64 的 js 代码

2020.11.21 (1.3.4)

  • 支持 JSBridge 同步调用
  • 支持 ajax 同步请求
  • 支持通过 document.cookie 同步从 NSHTTPCookieStorage 读取最新的 Cookie

2020.10.21 (1.2.1)

  • 正式版本,分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案,可以根据具体需求自由选择。

2020.7.14 (1.1.5-beta9)

  • 可以根据需求选择是 ajax hook 还是 ajax urlprotocol hook

2020.6.23 (1.1.5-beta2)

  • 使用新的 hook 方式,提升 hook 兼容性
  • 支持 iframe 和 form 标签

2020.6.18 (1.1.0)

  • 支持 fetch hook

2020.5.18

  • 支持通过 off 方法取消事件监听

2020.3.3

  • 回收 webView 到复用池时,清除 sessionStorage
  • 支持 on 事件广播,让 H5 可以在多个地方注册事件监听

2019.10.23

  • 提供统一配置 configuration 方法,有些属性必须前置配置,否则会不生效

2019.10.22

  • 增加模块注册支持首次初始化

2019.9.29

  • 仅保留 Native 侧控制 ajax hook,主要是避免 ajax hook 时机不对时,会造成首次 hook 失败
  • 支持表单数据,支持图片上传
  • 支持 ajax 请求外部代理

参考

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

推荐阅读更多精彩内容