karosLi / KKJSBridge

一站式解决 WKWebView 支持离线包,Ajax/Fetch 请求,表单请求和 Cookie 同步的问题 (基于 Ajax Hook,Fetch Hook 和 Cookie Hook)
MIT License
693 stars 120 forks source link
ajax ajax-body ajax-hook cookie cookie-hook fetch fetch-hook formdata ifame jsapi jsapi-module-based jsbridge messagehandler webviewjavascriptbridge wkwebview wkwebview-ajax-cookie wkwebview-reuse

KKJSBridge

GitHub stars GitHub forks GitHub issues GitHub license

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

更详细的介绍

KKJSBridge 支持的功能

Demo

demo 概览

模块化调用 JSAPI

模块化调用 JSAPI

模块化调用 JSAPI

ajax hook 演示

ajax hook 演示

淘宝 ajax hook 演示

淘宝 ajax hook 演示

ajax 发送表单 演示

淘宝 ajax hook 演示

用法

从复用池取出缓存的 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 内核。

优点:

缺点:

方案二: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 知道有请求结果了。

优点:

缺点:

更新历史

2020.11.24 (1.3.5)

2020.11.21 (1.3.4)

2020.10.21 (1.2.1)

2020.7.14 (1.1.5-beta9)

2020.6.23 (1.1.5-beta2)

2020.6.18 (1.1.0)

2020.5.18

2020.3.3

2019.10.23

2019.10.22

2019.9.29

参考