iOS离线静态资源包技术方案分析

方案1: Scheme 拦截

1. 方案是什么?

Scheme拦截是一种网络请求拦截策略,主要通过定义自定义的URL Scheme和相应的处理方式,使得可以在网络请求时拦截并处理特定的URL请求。在iOS中,我们可以使用WKURLSchemeHandler来实现这个功能。苹果为WKWebView提供了一个自定义scheme的功能,即WKURLSchemeHandler,可以拦截自定义scheme的请求,然后返回我们自定义的数据。比如我们可以拦截所有"myScheme://"的请求,然后返回我们的离线资源。

2. 这个方案是为了解决什么问题而设计的?

URL Scheme原本是被设计出来用于在Web和Native应用之间进行交互的。在iOS平台上,我们可以为一个应用定义自己的URL Scheme,然后其他应用或者Web页面就可以通过这个URL Scheme来启动该应用并传递参数。对于WKWebView来说,iOS 11开始引入了WKURLSchemeHandler这个接口,让我们可以拦截WKWebView中的自定义URL Scheme请求,从而让我们有机会自定义这些请求的处理方式,比如从本地加载一个资源文件,或者做一些特殊的处理。

3. 这种方案怎么展开工作,解决方法是什么?

具体的实现步骤如下:

定义自定义的URL Scheme和相应的处理方式:在iOS中,可以通过实现WKURLSchemeHandler协议来定义如何处理自定义的URL Scheme。

将自定义的URL Scheme与WebView关联:使用WKWebViewConfiguration的setURLSchemeHandler:forURLScheme:方法,将自定义的URL Scheme和对应的处理方式关联起来。

在网络请求时拦截特定的URL:当WebView遇到自定义的URL Scheme时,会调用相应的WKURLSchemeHandler进行处理。

处理拦截的URL:在WKURLSchemeHandler的方法中,我们可以决定如何处理拦截的URL。例如,可以从本地缓存中获取相应的资源,并将其返回给WebView。

4. 这个方案存在什么问题, 如何解决这些问题?

存在的问题包括:

WKWebView的scheme拦截只能拦截页面内部链接的资源,对于浏览器自动加载的资源(如CSS背景图像)或JavaScript动态加载的资源,无法进行拦截。解决方案是尝试修改前端代码,使得所有资源都通过HTML链接加载,或者使用Service Workers或其他拦截技术进行拦截。

尽管我们可以拦截并提供缓存的资源,但是何时将这些资源渲染给浏览器还是一个问题。解决方案是,在页面的onload事件后进行资源的渲染。

案例:

首先,我们需要在HTML中使用自定义的scheme,例如:

<img src="myScheme://image.png" />

然后,在iOS端,我们需要实现一个遵循WKURLSchemeHandler协议的类:

class MySchemeHandler: NSObject, WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        // 根据urlSchemeTask.request来判断要返回什么资源
        if let url = urlSchemeTask.request.url,
           let path = url.path,
           let resourcePath = Bundle.main.path(forResource: path, ofType: nil),
           let data = try? Data(contentsOf: URL(fileURLWithPath: resourcePath)) {
            let response = URLResponse(url: url, mimeType: "image/png", expectedContentLength: data.count, textEncodingName: nil)
            urlSchemeTask.didReceive(response)
            urlSchemeTask.didReceive(data)
            urlSchemeTask.didFinish()
        } else {
            urlSchemeTask.didFailWithError(NSError(domain: "Domain", code: 0, userInfo: nil))
        }
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
        // 停止加载
    }
}

然后在创建WKWebView的时候,注册这个scheme handler:

let configuration = WKWebViewConfiguration()
let schemeHandler = MySchemeHandler()
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: "myScheme")
let webView = WKWebView(frame: .zero, configuration: configuration)

这样,当WKWebView在加载一个使用"myScheme://"的资源时,就会调用我们的MySchemeHandler来获取数据。

方案2: 前端打包本地静态文件

1. 方案是什么?

这个方案中,我们将所有前端资源(HTML,CSS,JavaScript,图片等)打包到一个本地文件或者目录中(比如一个ZIP文件),然后直接在WebView中加载这个本地文件或者目录。

2. 这个方案是为了解决什么问题而设计的?

在网络不稳定或者没有网络的环境下,通过本地加载网页资源,我们可以保证用户可以正常使用我们的Web应用,而不受网络环境的影响。

3. 这种方案怎么展开工作,解决方法是什么?

前端打包本地静态文件的一般步骤如下:

使用工具打包前端资源:可以使用如Webpack这样的工具将所有前端资源打包到一个ZIP文件中。

将ZIP文件放到应用的本地目录:可以在应用安装时将ZIP文件放到应用的沙盒目录中,或者在应用运行时下载ZIP文件并解压到沙盒目录中。

在WebView中加载本地资源:使用file协议在WebView中加载沙盒目录中的资源。

下面是使用Webpack打包前端资源的一个基本配置示例:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
  ],
  mode: 'production'
};
4. 这个方案存在什么问题, 如何解决这些问题?

跨域问题:由于浏览器的安全策略,本地文件可能无法正常的访问网络资源。解决办法是使用CORS进行跨域资源共享,或者通过设置WebView的安全策略来允许跨域访问。

更新问题:当前端资源需要更新时,需要重新打包并下载新的ZIP文件。解决办法是在应用启动时检查资源的更新,并下载新的资源。

缓存问题:对于一些大的或者不常更新的资源,可能不需要每次都重新下载。解决办法是使用缓存策略,如HTTP缓存,或者使用Service Worker进行资源的缓存。

在iOS中,可以使用如下代码在WebView中加载本地资源:

let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")
let request = URLRequest(url: url!)
webView.load(request)

在这个示例中,我们将本地资源放到了www子目录下,并且使用index.html作为入口文件。我们使用Bundle.main.url(forResource:withExtension:subdirectory:)方法获取到资源的URL,然后创建一个URLRequest并加载到WebView中。

方案三:使用 WebServer

1. 方案描述

此方案主要是在移动端内置一个小型的 WebServer,通过这个 WebServer 来提供静态资源服务。这样可以让所有的资源请求都是同源的,避免了跨域问题。同时,也可以方便地管理和更新资源文件。

2. 问题描述

内置 WebServer 的方案主要的问题是移动设备的性能和资源限制,包括 CPU、内存和电量。另外,WebServer 需要在后台持续运行,这在一些操作系统中可能受到限制。最后,WebServer 的安全性也需要考虑,虽然这是在本地网络环境下,但仍然可能有一些潜在的安全风险。

3. 解决方法

使用 Swift 实现内置 WebServer 的示例代码:

import GCDWebServer

let webServer = GCDWebServer()

webServer?.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self, processBlock: {request in
    let filePath = self.documentPath.appending(request.path)
    let html = try! String(contentsOfFile: filePath, encoding: .utf8)
    return GCDWebServerDataResponse(html: html)
})

webServer?.start(withPort: 8080, bonjourName: nil)

以上代码使用了 GCDWebServer 库来创建一个 HTTP 服务器,监听 8080 端口。addDefaultHandler 函数用来处理所有的 GET 请求,返回请求路径下的文件内容。

4. 存在的问题及解决方案

对于性能和资源限制问题,需要在设计和实现时尽量优化性能,减少资源占用。另外,需要处理好应用切换到后台时的情况,可能需要停止 WebServer,或者根据操作系统的规则进行其他处理。对于安全性问题,可以通过一些安全机制,比如只允许本地访问,或者使用 HTTPS 等方式来提高安全性。

方案四:前端使用Service Worker或WPA的Cache API

1. 方案描述

该方案是指通过前端技术 Service Worker 或 WPA(Web Progressive Applications)的 Cache API,来拦截和处理浏览器的网络请求,达到缓存资源和离线访问的目的。

2. 问题描述

当前,HTML5 提供了丰富的离线存储机制,但如何有效、灵活地使用这些机制进行资源管理,确保网页在断网或者网络不佳的情况下也能正常访问,是本方案需要解决的主要问题。

3. 解决方法

前端需要利用 Service Worker 或 WPA 的 Cache API,进行以下操作:

注册 Service Worker 或 WPA,为应用安装 Cache API。 通过 Service Worker 或 WPA 拦截网络请求,判断是否命中缓存。如果命中缓存,则直接返回缓存的资源;否则,向服务器发出请求,获取资源并存入缓存。 缓存策略的制定和实施。可以根据需要,定制不同的缓存策略,如优先使用缓存、优先获取最新资源等。

4. 存在的问题及解决方案

在使用 Service Worker 或 WPA 的 Cache API 过程中,可能会遇到以下问题:

浏览器兼容性:不是所有的浏览器都完全支持 Service Worker 或 WPA 的 Cache API。对于不支持的浏览器,需要寻找替代方案,如使用其他的缓存技术(例如 localStorage、IndexedDB)或者退化到普通的网络请求。 缓存管理:如果不注意管理,缓存的资源可能会占用大量的存储空间。因此,需要在合适的时机清理过期的或者不再需要的缓存。同时,需要处理好版本更新时的缓存替换问题。

下面是使用Service Worker配合Cache API进行离线资源管理的基础代码示例:

首先,我们需要在主线程的JavaScript中注册Service Worker:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      console.log('Service Worker 注册成功,scope: ', registration.scope);
    }, function(err) {
      console.log('Service Worker 注册失败: ', err);
    });
  });
}
然后在sw.js中定义Service Worker的安装和激活行为:

javascript
Copy code
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['my-site-cache-v1', 'blog-posts-cache-v1'];
  
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

接下来,我们定义如何响应请求:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        return fetch(event.request).then(
          function(response) {
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            var responseToCache = response.clone();
            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
  );
});

这是基本的 Service Worker 注册和使用 Cache API 的流程,你可以根据自己的需求进行更复杂的策略定制。需要注意的是,使用 Service Worker 和 Cache API 需要 HTTPS 环境。

最终选定的方案2:

即网页静态化方案,我们需要前端、后端和iOS端各自协作才能完成。下面我详细介绍各端应该怎么做:

前端:

我们的目标是将编辑器模块的所有资源打包成一个单独的文件,这个过程需要通过Webpack这样的打包工具来完成。

1.1 分包:

为了使项目更加模块化和优化加载性能,我们可以在webpack中使用splitChunks进行代码分包。在webpack.config.js中添加以下配置:

optimization: {
  splitChunks: {
    chunks: 'all',
  },
}
1.2 资源的scheme定义:

对于需要打包的资源,我们可以通过修改Webpack配置文件,将资源文件的引用路径设置为自定义scheme。

对于Vue项目,你可以在vue.config.js中修改webpack的配置:


chainWebpack: config => {
    config.module
        .rule('images')
        .use('url-loader')
        .tap(options => Object.assign(options, { name: 'myscheme://[name].[ext]' }))
}
iOS端:

iOS端主要任务是监听自定义的scheme,并在收到请求时返回对应的本地资源。

2.1 监听scheme:

你可以通过WKWebView的WKURLSchemeHandler来监听自定义的scheme:

let webViewConfig = WKWebViewConfiguration()
let customSchemeHandler = MySchemeHandler()
webViewConfig.setURLSchemeHandler(customSchemeHandler, forURLScheme: "myscheme")
let webView = WKWebView(frame: .zero, configuration: webViewConfig)
2.2 处理scheme请求:

在你的SchemeHandler中,你需要实现WKURLSchemeHandler协议的两个方法:

class MySchemeHandler: NSObject, WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        let url = urlSchemeTask.request.url!
        let resource = url.lastPathComponent
        let resourcePath = Bundle.main.path(forResource: resource, ofType: nil)!
        let resourceUrl = URL(fileURLWithPath: resourcePath)
        let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: nil)!
        urlSchemeTask.didReceive(response)
        urlSchemeTask.didReceive(Data(contentsOf: resourceUrl))
        urlSchemeTask.didFinish()
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
    }
}
后端:

对于后端,主要的任务可能会是协助前端进行资源文件的打包和发布。具体的工作可能会根据项目的实际需求而定。可能的任务包括:

提供资源文件的服务器托管 协助实现资源文件的版本控制和更新策略 提供API接口供移动端查询最新的资源文件信息

将在方案2中添加跨域问题的解决方案:

1, 资源跨域:使用自定义scheme可以解决资源跨域的问题。如上所述,通过设置自定义scheme,将资源的引用路径重定向到应用本地,避免了跨域的问题。自定义scheme如"myscheme",在前端打包时,资源的引用路径将会变为"myscheme://[资源名]",然后在iOS端拦截这个scheme,返回本地的资源文件。

2, 网络请求跨域:网络请求的跨域问题可能需要后端协作解决,或者通过移动端进行JS交互实现。一种可能的方法是在后端设置CORS(跨源资源共享)策略,允许来自特定来源的请求。另一种方法是通过iOS端的JavaScriptCore或WKScriptMessageHandler与网页进行交互,iOS端接收到JS的请求后,自行进行网络请求,然后将结果返回给JS。

在iOS端,可以使用WKUserContentController的add方法来添加消息处理器,然后在WKScriptMessageHandler协议的userContentController:didReceiveScriptMessage:方法中处理JS的请求。例如:

let contentController = WKUserContentController()
contentController.add(self, name: "jsHandler")
let config = WKWebViewConfiguration()
config.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: config)

// WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "jsHandler" {
        // 处理JS的请求,发送网络请求,然后将结果通过evaluateJavaScript返回给JS
    }
}

这样,通过JS交互,可以解决网络请求的跨域问题,但需要注意的是,由于在iOS端进行网络请求,可能会需要处理一些额外的问题,如证书验证、Cookie管理等。

全部评论

相关推荐

评论
点赞
1
分享
牛客网
牛客企业服务