一站式解决 WKWebView 支持离线包,Ajax/Fetch 请求和 Cookie 同步的问题 (基于 Ajax Hook,Fetch Hook 和 Cookie Hook)
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 复用
从复用池取出缓存的 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);
CocoaPods
//In Podfile
# 分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案,可以根据具体需求自由选择。
# 只能选择其中一个方案,默认是 ajax protocol hook。
pod 'KKJSBridge/AjaxProtocolHook'
pod 'KKJSBridge/AjaxHook'
这里只对比方案间相互比较的优缺点,共同的优点,就不赘述了。如果对私有 API 不敏感的,我是比较推荐使用方案一的。
这个方案对应的是这里的 KKJSBridge/AjaxProtocolHook
。
原理介绍:此种方案,是只需要 hook ajax 中的 open/send 方法,在 hook 的 send 方法里,先把要发送的 body 通过 JSBridge 发送到 Native 侧去缓存起来。缓存成功后,再去执行真实的 send 方法,NSURLProtocol 此时会拦截到该请求,然后取出之前缓存的 body 数据,重新拼接请求,就可以发送出去了,然后通过 NSURLProtocol 把请求结果返回给 WebView 内核。
优点:
缺点:
这个方案对应的是这里的 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 知道有请求结果了。
优点:
缺点: