{"id":13610086,"url":"https://github.com/karosLi/KKJSBridge","last_synced_at":"2025-04-12T22:32:44.017Z","repository":{"id":38360724,"uuid":"205123310","full_name":"karosLi/KKJSBridge","owner":"karosLi","description":"一站式解决 WKWebView 支持离线包，Ajax/Fetch 请求，表单请求和 Cookie 同步的问题 (基于 Ajax Hook，Fetch Hook 和 Cookie Hook)","archived":false,"fork":false,"pushed_at":"2022-02-16T09:37:03.000Z","size":10794,"stargazers_count":706,"open_issues_count":25,"forks_count":121,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-04-12T09:04:49.294Z","etag":null,"topics":["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"],"latest_commit_sha":null,"homepage":"","language":"Objective-C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karosLi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":["https://github.com/karosLi/BlogImageBed/blob/master/pay.jpg"]}},"created_at":"2019-08-29T09:01:54.000Z","updated_at":"2025-03-02T17:08:37.000Z","dependencies_parsed_at":"2022-07-14T04:20:36.772Z","dependency_job_id":null,"html_url":"https://github.com/karosLi/KKJSBridge","commit_stats":null,"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karosLi%2FKKJSBridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karosLi%2FKKJSBridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karosLi%2FKKJSBridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karosLi%2FKKJSBridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karosLi","download_url":"https://codeload.github.com/karosLi/KKJSBridge/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248641307,"owners_count":21138181,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["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"],"created_at":"2024-08-01T19:01:41.086Z","updated_at":"2025-04-12T22:32:43.553Z","avatar_url":"https://github.com/karosLi.png","language":"Objective-C","readme":"# KKJSBridge\n\n[![GitHub stars](https://img.shields.io/github/stars/karosLi/KKJSBridge)](https://github.com/karosLi/KKJSBridge/stargazers) [![GitHub forks](https://img.shields.io/github/forks/karosLi/KKJSBridge)](https://github.com/karosLi/KKJSBridge/network) [![GitHub issues](https://img.shields.io/github/issues/karosLi/KKJSBridge)](https://github.com/karosLi/KKJSBridge/issues) [![GitHub license](https://img.shields.io/github/license/karosLi/KKJSBridge)](https://github.com/karosLi/KKJSBridge/blob/master/LICENSE)\n\n一站式解决 WKWebView 支持离线包，Ajax/Fetch 请求和 Cookie 同步的问题 (基于 Ajax Hook，Fetch Hook 和 Cookie Hook)\n\n[更详细的介绍](http://karosli.com/2019/08/30/%E4%B8%80%E7%AB%99%E5%BC%8F%E8%A7%A3%E5%86%B3WKWebView%E5%90%84%E7%B1%BB%E9%97%AE%E9%A2%98/)\n\n## KKJSBridge 支持的功能\n- JSBrdige 相关\n\n    - 基于 MessageHandler 搭建通信层\n\n    - 支持模块化的管理 JSAPI\n\n    - 支持模块共享上下文信息\n\n    - 支持模块消息转发\n\n    - 支持 JSBridge 同步调用\n    \n    - 兼容 WebViewJavascriptBridge\n\n- 请求相关\n\n    - 支持离线资源\n\n    - 支持 ajax/fetch hook 避免 body 丢失\n    \n    - 支持 ajax/fetch 同步请求\n\n    - Native 侧控制 ajax/fetch hook\n\n    - 支持表单数据，支持图片上传\n\n    - 支持 ajax/fetch 请求外部代理\n    \n    - 分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案，可以根据具体需求自由选择\n     \n\n- WebView 相关\n\n    - Cookie 统一管理\n\n    - WKWebView 复用\n\n\n\n## Demo\n### demo 概览\n\n![模块化调用 JSAPI](https://github.com/karosLi/KKJSBridge/blob/master/Demo0.gif)\n\n### 模块化调用 JSAPI\n\n![模块化调用 JSAPI](https://github.com/karosLi/KKJSBridge/blob/master/Demo1.gif)\n\n### ajax hook 演示\n\n![ajax hook 演示](https://github.com/karosLi/KKJSBridge/blob/master/Demo2.gif)\n\n### 淘宝 ajax hook 演示\n\n![淘宝 ajax hook 演示](https://github.com/karosLi/KKJSBridge/blob/master/Demo3.gif)\n\n### ajax 发送表单 演示\n![淘宝 ajax hook 演示](https://github.com/karosLi/KKJSBridge/blob/master/Demo4.gif)\n\n## 用法\n\n从复用池取出缓存的 WKWebView，并开启 ajax hook\n\n```objectivec\n+ (void)load {\n    __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {\n        [self prepareWebView];\n        [[NSNotificationCenter defaultCenter] removeObserver:observer];\n    }];\n}\n\n+ (void)prepareWebView {\n    // 预先缓存一个 webView\n    [KKWebView configCustomUAWithType:KKWebViewConfigUATypeAppend UAString:@\"KKJSBridge/1.0.0\"];\n    [[KKWebViewPool sharedInstance] makeWebViewConfiguration:^(WKWebViewConfiguration * _Nonnull configuration) {\n        // 必须前置配置，否则会造成属性不生效的问题\n        configuration.allowsInlineMediaPlayback = YES;\n        configuration.preferences.minimumFontSize = 12;\n    }];\n    [[KKWebViewPool sharedInstance] enqueueWebViewWithClass:KKWebView.class];\n    KKJSBridgeConfig.ajaxDelegateManager = (id\u003cKKJSBridgeAjaxDelegateManager\u003e)self; // 请求外部代理处理，可以借助 AFN 网络库来发送请求\n}\n\n- (void)dealloc {\n    // 回收到复用池\n    [[KKWebViewPool sharedInstance] enqueueWebView:self.webView];\n}\n\n- (void)commonInit {\n    _webView = [[KKWebViewPool sharedInstance] dequeueWebViewWithClass:KKWebView.class webViewHolder:self];\n    _webView.navigationDelegate = self;\n    _jsBridgeEngine = [KKJSBridgeEngine bridgeForWebView:self.webView];\n    _jsBridgeEngine.config.enableAjaxHook = YES; // 开启 ajax hook\n\n    [self registerModule];\n}\n\n#pragma mark - KKJSBridgeAjaxDelegateManager\n+ (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request callbackDelegate:(NSObject\u003cKKJSBridgeAjaxDelegate\u003e *)callbackDelegate {\n    return [[self ajaxSesstionManager] dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {\n        // 处理响应数据\n        [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveResponse:response];\n        if ([responseObject isKindOfClass:NSData.class]) {\n            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseObject];\n        } else if ([responseObject isKindOfClass:NSDictionary.class]) {\n            NSData *responseData = [NSJSONSerialization dataWithJSONObject:responseObject options:0 error:nil];\n            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseData];\n        } else {\n            NSData *responseData = [NSJSONSerialization dataWithJSONObject:@{} options:0 error:nil];\n            [callbackDelegate JSBridgeAjax:callbackDelegate didReceiveData:responseData];\n        }\n        [callbackDelegate JSBridgeAjax:callbackDelegate didCompleteWithError:error];\n    }];\n}\n\n```\n\n注册模块\n\n```objectivec\n- (void)registerModule {\n ModuleContext *context = [ModuleContext new];\n context.vc = self;\n context.scrollView = self.webView.scrollView;\n context.name = @\"上下文\";\n // 注册 默认模块\n [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleDefault.class];\n // 注册 模块A\n [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleA.class];\n // 注册 模块B 并带入上下文\n [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleB.class withContext:context];\n // 注册 模块C\n [self.jsBridgeEngine.moduleRegister registerModuleClass:ModuleC.class];\n}\n```\n\n模块定义\n\n```objectivec\n@interface ModuleB()\u003cKKJSBridgeModule\u003e\n\n@property (nonatomic, weak) ModuleContext *context;\n\n@end\n\n@implementation ModuleB\n\n// 模块名称\n+ (nonnull NSString *)moduleName {\n    return @\"b\";\n}\n\n// 单例模块\n+ (BOOL)isSingleton {\n    return YES;\n}\n\n// 模块初始化方法，支持上下文带入\n- (instancetype)initWithEngine:(KKJSBridgeEngine *)engine context:(id)context {\n    if (self = [super init]) {\n        _context = context;\n        NSLog(@\"ModuleB 初始化并带上 %@\", self.context.name);\n    }\n\n    return self;\n}\n\n// 模块提供的方法\n- (void)callToGetVCTitle:(KKJSBridgeEngine *)engine params:(NSDictionary *)params responseCallback:(void (^)(NSDictionary *responseData))responseCallback {\n    responseCallback ? responseCallback(@{@\"title\": self.context.vc.navigationItem.title ? self.context.vc.navigationItem.title : @\"\"}) : nil;\n}\n```\n\nJS 侧调用方式\n\n```javascript\n// 异步调用\nwindow.KKJSBridge.call('b', 'callToGetVCTitle', {}, function(res) {\n    console.log('receive vc title：', res.title);\n});\n\n// 同步调用\nvar res = window.KKJSBridge.syncCall('b', 'callToGetVCTitle', {});\nconsole.log('receive vc title：', res.title);\n```\n\n\n\n## 安装\n\n1. CocoaPods\n   \n   ```objectivec\n   //In Podfile\n   \n   # 分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案，可以根据具体需求自由选择。\n   # 只能选择其中一个方案，默认是 ajax protocol hook。\n   pod 'KKJSBridge/AjaxProtocolHook'\n   pod 'KKJSBridge/AjaxHook'\n\n   ```\n  \n## Ajax Hook 方案对比\n\n这里只对比方案间相互比较的优缺点，共同的优点，就不赘述了。如果对私有 API 不敏感的，我是比较推荐使用方案一的。\n\n### 方案一：Ajax Hook 部分 API + NSURLProtocol \n这个方案对应的是这里的 `KKJSBridge/AjaxProtocolHook`。\n\n原理介绍：此种方案，是只需要 hook ajax 中的 open/send 方法，在 hook 的 send 方法里，先把要发送的 body 通过 JSBridge 发送到 Native 侧去缓存起来。缓存成功后，再去执行真实的 send 方法，NSURLProtocol 此时会拦截到该请求，然后取出之前缓存的 body 数据，重新拼接请求，就可以发送出去了，然后通过 NSURLProtocol 把请求结果返回给 WebView 内核。\n\n优点：\n\n- 兼容性会更好，网络请求都是走 webview 原生的方式。\n- hook 的逻辑会更少，会更加稳定。\n- 可以更好的支持 ajax 获取二进制的数据。例如 H5 小游戏场景（白鹭引擎是通过异步获取图片资源）。\n\n缺点：\n\n- 需要使用到私有 API browsingContextController 去注册 http/https。（其实现在大部分的离线包方案也是使用了这个私有 API 了）\n                                                                                                                                          \n\n### 方案二：Ajax Hook 全部 API\n这个方案对应的是这里的 `KKJSBridge/AjaxHook`。\n\n原理介绍：此种方案，是使用 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 知道有请求结果了。\n\n优点：\n\n- 没有使用私有 API。\n\n\n缺点：\n\n- 需要 hook XMLHttpRequest 的所有方法。\n- 请求结果是通过 JSBrdige 来进行传输的，性能上肯定没有原生的性能好。\n- 不能支持 ajax 获取二进制的数据。要想支持的话，还需要额外的序列化工作。\n\n\n## 更新历史\n### 2020.11.24 (1.3.5)\n- 模块化管理 TS，方便代码阅读和维护\n- 移除 await 关键字，优化表单转 base64 的 js 代码\n\n### 2020.11.21 (1.3.4)\n- 支持 JSBridge 同步调用\n- 支持 ajax 同步请求\n- 支持通过 document.cookie 同步从 NSHTTPCookieStorage 读取最新的 Cookie\n\n### 2020.10.21 (1.2.1)\n- 正式版本，分别提供了 ajax hook 和 ajax urlprotocol hook 两种方案，可以根据具体需求自由选择。\n\n### 2020.7.14 (1.1.5-beta9)\n- 可以根据需求选择是 ajax hook 还是 ajax urlprotocol hook\n\n### 2020.6.23 (1.1.5-beta2)\n\n- 使用新的 hook 方式，提升 hook 兼容性 \n- 支持 iframe 和 form 标签\n\n### 2020.6.18 (1.1.0)\n- 支持 fetch hook\n\n### 2020.5.18\n- 支持通过 off 方法取消事件监听\n\n### 2020.3.3\n- 回收 webView 到复用池时，清除 sessionStorage\n- 支持 on 事件广播，让 H5 可以在多个地方注册事件监听\n\n### 2019.10.23\n- 提供统一配置 configuration 方法，有些属性必须前置配置，否则会不生效\n\n### 2019.10.22\n- 增加模块注册支持首次初始化\n\n### 2019.9.29\n- 仅保留 Native 侧控制 ajax hook，主要是避免 ajax hook 时机不对时，会造成首次 hook 失败\n- 支持表单数据，支持图片上传\n- 支持 ajax 请求外部代理\n\n\n## 参考\n\n- [Ajax-hook](https://github.com/wendux/Ajax-hook)\n\n- [Fetch](https://github.com/github/fetch)\n\n- [HybridPageKit](https://github.com/dequan1331/HybridPageKit)\n\n- [kerkee_ios](https://github.com/kercer/kerkee_ios)\n\n\n","funding_links":["https://github.com/karosLi/BlogImageBed/blob/master/pay.jpg"],"categories":["Objective-C","Foundation"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FkarosLi%2FKKJSBridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FkarosLi%2FKKJSBridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FkarosLi%2FKKJSBridge/lists"}