由于当时公司的旧 Web 容器已无法持续保护(懂得都懂),所以需要重构一套新的来支撑越来越多的在线页面事务系统。但在阻拦资源,本地缓存加快这个过程中,踩了特别多的坑,这儿特别记载一下,让咱们们能少走些弯路。

布景

简略聊下布景,曾经事务上更多是运用 Web 离线包下载到本地,然后加载本地资源渲染,类似小程序相同的规划。但随之问题也许多,究竟没有一套成熟的开发系统,最重要的是开发的离线包都和盲盒相同,联调排查问题的时刻都赶的上开发时刻了。

在降本增效的条件下,已经不或许有人力单独为 App 开发一套离线包来支撑事务的状况下,必然就需要融合前端系统,直接加载在线页面。

那随之而来的一个结果便是对用户来说呈现了体验降级的状况,曾经秒开的页面变慢了,乃至在网络差的状况下白屏状况变得非常明显。

当然,这肯定是不能承受的,所以要重新建造整个 Web 容器。

总体规划

Web 容器 建造总览.png

其他建造暂时不提,这儿只聊聊,怎么让在线 URL 页面到达秒开加载这件事。

方案选型

image2023-2-23_14-18-25.png

怎么提高秒开率?便是减少整个建立连接到渲染完成的这段时刻。

其实说白了,无非便是资源缓存加上提早预加载,而这也有几种办法能挑选。

WebView 自带 Cache

最常见的便是 WebView 自带缓存 Cache,缓存规则也是依托于前端开发,但这个缓存战略上常常会有问题,比方版别不对、意外白屏、缓存丢掉加载过慢等问题。

简略代码示意(来源 GPT-3.5)

// 装备缓存战略以及是否可运用cookie
WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
WKWebsiteDataStore *nonPersistentDataStore = [store nonPersistentDataStore];
WKWebsiteDataStore *ephermeralDataStore = [store ephemeralDataStore];
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
                                                  diskCapacity:500 * 1024 * 1024
                                                      diskPath:nil];
configuration.websiteDataStore = nonPersistentDataStore;
// 装备 URL 缓存战略
configuration.urlCache = cache;
configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
// 例如,设置一个自界说的Cookie战略
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
NSString *cookieScript = @"document.cookie = 'cookie_name=cookie_value; domain=your_domain.com;';";
WKUserScript *cookieScriptObj = [[WKUserScript alloc] initWithSource:cookieScript
                                                       injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                    forMainFrameOnly:YES];

运用前端方案service worker

service worker是一种 Web API,它允许开发者将脚本文件运行在用户浏览器的后台进程中,独立于当时页面/标签,供应了强大的离线数据存储、网络恳求阻拦和消息推送等功用。

但其最大的硬伤是,在 iOS App 中是不被支撑的,这在苹果开发文档中是有清晰的。只在 iOS Safari 浏览器上才被支撑。

阻拦资源恳求

以上两种还有一个硬伤,便是有必要依托 WebView 环境,那在预加载上就有必要发动一个 WebView 容器,这一点其实有性能损耗的,假如有多个页面需要预缓存,那就有必要开多个容器或许供应一个预加载队列,控制起来也是尤为费事,特别在预加载完之前用户就进入页面的情景下,缓存意外也是有或许的。

那咱们还能怎么做呢?

其实思路很简略,在总体规划上也说的很明白。

咱们阻拦资源加载的恳求,让它去拜访咱们本地资源,假如本地资源不存在就先下载到本地再回来给 Web 页面。

那提早预加载,就让前端供应资源清单,咱们依据资源清单提早做一个资源更新即可。这样的好处还在于就算预加载未完成用户就进入了 Web 页面,那也不要紧,已下载好的资源仍然可以供应加快才能。

iOS 的坑

方案大致讲了下, Android 完成很顺利的就完成了。但 iOS 的确就被 WKWebView 这货卡住了。

想象真的很夸姣,但网上的资源乃至 GPT-3.5 给出的方案有真有假的,尝试了许多种,走了许多歪门邪道。

邪道一: NSURLProtocol

亲们,这个的确是不能用的,尽管咱们可以用它阻拦 http / https 恳求,也可以通过 + (BOOL)canInitWithRequest:(NSURLRequest *)request 办法来放过网络恳求,从 safari 调试下看 post 恳求内容也还在,但的确是在发送的时分就丢掉了 …

总结:仍是会阻拦掉网络恳求,而且丢掉了 post 恳求中的 body 信息。而且会影响整个全局。

邪道二: Hook XMLHttpRequest / fetch

这个是基于上一个“邪道”的延伸,已然恳求必定会被阻拦,那要不然就重写 XMLHttpRequest / fetch,让网络恳求走咱们的桥接办法,这样前端事务侧也无需修正任何代码。

代码示意(来源:反复问询 GPT-3.5 并修正)

function replaceXHR() {
  // 保存原始的 XMLHttpRequest 结构函数
  const origXHR = window.XMLHttpRequest
  // 界说新的 XMLHttpRequest 结构函数
  function NewXHR() {
    const xhr = new origXHR()
    let status = 200
    let statusText = 'OK'
    let response = ''
    // 重写 open 办法
    xhr.open = function (method, url, async, user, pass) {
      this.url = url
      this.method = method
      origXHR.prototype.open.call(this, method, url, async, user, pass)
    }
    // 重写 send 办法
    xhr.send = function (data) {
      var urlObj
      if (isUrlComplete(this.url)) {
        urlObj = new URL(this.url)
      } else {
        urlObj = new URL('https://gaoding.com' + this.url)
      }
      const path = urlObj.pathname
      const params = new URLSearchParams(urlObj.search)
      // 非稿定恳求,不阻拦
      if (!urlObj.host.includes('gaoding.com')) {
        origXHR.prototype.send.call(this, data)
        return
      }
      const paramObj = {}
      for (const [key, value] of params) {
        paramObj[key] = value
      }
      const config = {
        method: this.method,
        path: path,
        query: paramObj,
      }
      if (this.method.toUpperCase() === 'POST' || this.method.toUpperCase() === 'PUT') {
        config['body'] = data
      }
      // 执行桥接办法
      request(config)
        .then((response) => {
          // 处理呼应结果
          return response.result.response_data
        })
        .then((text) => {
          // 调用原始的 onreadystatechange 函数,并传入呼应结果
          this.status = 200
          this.statusText = JSON.stringify(text)
          this.response = text
          this.onreadystatechange && this.onreadystatechange()
          this.onload && this.onload()
        })
        .catch((error) => {
          // 调用原始的 onerror 函数,并传入错误信息
          this.onerror && this.onerror(error)
        })
    }
    return xhr
  }
  // 用新的 XMLHttpRequest 结构函数替换原始的 XMLHttpRequest 结构函数
  window.XMLHttpRequest = NewXHR
}
replaceXHR()

然后咱们在 WKWebView 中注入这段 JS 即可。

[userContentController addUserScript:[[WKUserScript alloc] initWithSource:[GDWebUtils injectJSForBundle:@"network-hook"] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]];

收效是收效了,但对咱们的 Web 页面却呈现了加载反常的状况,最终得知,咱们项目中已经对 XMLHttpRequest hook 过了,所以我这真的是魔改 …

总结:不能胡乱注入 JS 代码,简单翻车 …

邪道三:自界说 Scheme

为什么一开始会执着会执着去运用NSURLProtocol,而不运用WKURLSchemeHandler。原因是WKURLSchemeHandler最低支撑 iOS 11, 且在 iOS 11.3 以下 post body 相同会丢掉[手动狗头]。

那笔者奇思妙想下,咱们先阻拦加载的 HTML,然后再替换其间的资源加载途径呢?

做法大约描绘下:

运用WKURLSchemeHandler阻拦page://assets://两种自界说的 Scheme。

比方加载https://www.google.com时,其实是加载的 page://www.google.com来让WKURLSchemeHandler可以阻拦到页面加载了。

这样在 Response 中修正回来的资源加载途径,比方assets://xxxxx.css,这样也就可以让WKURLSchemeHandler阻拦到资源加载了。

理想很夸姣,关于页面来说的确阻拦加载成功了。

网络恳求跨域了 … 由于咱们当时域名是page://而网络恳求发出去的是https://

真要这么做,只能是服务端开放跨域限制,但这一点关于服务端是极不安全的。

定论:绕了半天然并卵。

正道:阻拦 Http / Https Scheme

尽管苹果不主张乃至不允许阻拦 Http / Https 协议的 Scheme,但这便是唯一一种办法了。

运用条件

  • 承认项目/功用只需支撑 iOS 11.3 以上版别。
  • 承认前端其间没有File 上传恳求,由于就算 iOS 11.3 以上版别,也会丢掉 blob 格局的 body 数据。真要做文件上传,请供应给前端相应的 Bridge 办法。
  • 切记处理 iOS 13 版别中的 post 恳求的崩溃问题(如下图)。

image.png

代码示意

让 Http / Https 恳求可被阻拦

黑魔法替换类办法完成

@implementation WKWebView (GDWebURLSchemeHandler)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getClassMethod(self, @selector(handlesURLScheme:));
        Method swizzledMethod = class_getClassMethod(self, @selector(gd_handlesURLScheme:));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}
+ (BOOL)gd_handlesURLScheme:(NSString *)urlScheme {
    if ([urlScheme isEqualToString:kGDWebHookURLScheme]) {
        return NO;
    } else {
        return [self handlesURLScheme:urlScheme];
    }
}
@end

阻拦步骤

结构阻拦类,完成WKURLSchemeHandler协议

image.png

在 WKWebView 中注册

[config setURLSchemeHandler:[GDWebURLSchemeHandler new] forURLScheme:@"https"];

阻拦资源完成

上图中的GDWebAssetStorageService服务便是做资源恳求阻拦及完成的。

内部完成也很简略,先判断本地是否存在资源,不存在就去下载后回来即可。

image.png

阻拦恳求完成

最终咱们仍是要处理阻拦到的网络恳求,WKURLSchemeHandler协议真的是很坑爹,阻拦掉的就无法调用默认完成了,需要自己结构。

但从原理来讲,苹果这样规划是合理的,究竟 WKWebView 不是跟 App 同一个进程的,这牵扯到跨进程通讯的问题,也是为什么苹果会在恳求中过滤掉 blob 数据格局。

简略的结构一个NSURLSession即可运用。

image.png

但这儿仍是有一个坑的,你会发现恳求重定向失效了,这儿咱们选用的是调用私有类来做一致的处理,私有类直接调用有审阅风险,简略的做一些代码混杂绕过去。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    // 调用 _didPerformRedirection:newRequest: 执行重定向
    NSArray *privateSelStrArr = @[@"st:", @"que", @"ewRe", @"n:n", @"ctio", @"dire", @"ormRe", @"_didPerf"];
    NSString *selName = [[[privateSelStrArr reverseObjectEnumerator] allObjects] componentsJoinedByString:@""];
    SEL sel = NSSelectorFromString(selName);
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.schemeTask performSelector:sel withObject:response withObject:request];
    #pragma clang diagnostic pop
    completionHandler(request);
}

定论

不想让前端代码做一些 App 个性化适配的条件下,想要提高秒开率,又不想开隐藏容器添加内存开支,那在 iOS 上只有这一种阻拦办法了。

尽管有着许多运用限制,但至少能满足现在的事务需求。

关于 blob

再简略的讲一下,关于前端 blob 数据传输给 App 的问题。

这个其实也走了许多歪门邪道,前期想着用 base64 的办法不如直接 blob 传输省性能,但的确是做不到的。

这儿也听了 GPT-3.5 的许多鬼话 … 要是用 GPT-4 的话,它会清晰告知你只有两种挑选:转 base64 或许 App 搭建本地服务器。AI 的差距真的非常明显[手动狗头]。


感谢阅览,假如对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif

本文正在参加「金石方案」