美文网首页WebView 各种相关
WKWebView的NSUrlProtocol的各种问题

WKWebView的NSUrlProtocol的各种问题

作者: YY110 | 来源:发表于2023-02-28 16:52 被阅读0次

    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 请求外部代理

    参考

    相关文章

      网友评论

        本文标题:WKWebView的NSUrlProtocol的各种问题

        本文链接:https://www.haomeiwen.com/subject/iyjdertx.html