Cache-control 的小知识

一般的缓存策略都会根据 responsecache-control 来指定的。而这个 key 有以下几个选项:

  • public 表示数据内容都可以被储存起来,就连有密码保护的网页也储存,安全性很低
  • private 表示数据内容只能被储存到私有的cache,仅对某个用户有效,不能共享
  • no-cache 表示可以缓存,但是只有在跟WEB服务器验证了其有效后,才能返回给客户端,触发对比缓存
  • no-store 表示请求和响应都禁止被缓存,强制缓存,对比缓存都不会触发
  • max-age 表示返回数据的过期时间

对以上几项进行以下分类:首先 public & private 都是可以缓存内容的,但在安全性上有区别,而 no-cache 字面意思和实际上有点出入,并非不能缓存,而是可以缓存,但是每次访问都会先问一下服务器,这个文件是否变了。no-store 这个没什么好说的了,就是不能缓存,而 max-age 则和前面的格格不入,因为它的值是指过期时间。那么把前面几个分类的话,得到以下几个缓存策略

  • 强缓存:public & private 过期前都不会找服务器聊天
  • 对比缓存:no-cache 每次都会找服务器聊天,但如果发现没有更新的话,就不会重新下载。
  • 不缓存

由此可以看出,使用缓存最主要使用到 强缓存对比缓存,而什么时候使用 强缓存 ,什么时候 对比缓存,这个得从前端开发的基本流程说起。

前端开发流程

前端资源

前端开发的流程就目前主流来说一般会使用 webpack 等工具去打包,js , css , image 都能设置成每次打包根据内容是否替换去生成一个 hash 值的文件名。因此可以看出,在静态资源的把控上,前端已经能够做到了更新内容而每次更新文件名。因此在这些文件上,我们可以直接采用 强缓存

webpack

前端程序的入口

和所有程序都一样,前端工程入口一般就是一个 HTML 。 而这个 HTML 可以是一个 file 也可以是一个请求响应体。不管它是一个怎么的形态存在,responseheader 都会存在 content-type: text/html 这个设定。但当它是请求响应体的时候,我们是不会缓存它的。因此我们只讨论当它是一个存在于 CDN 中的一个 HTML 格式的 file 的时候。应该如何缓存。当然这个缓存策略还取决于客户端 webView 载入 URL 的方案。

  • 当一些页面,我们期望于前端在内容进行变化而非后端去操作才去变化的。比如说什么隐私公告,XX条款等,一般都会把访问 URL 写死在代码中。我们针对这种情况假设一个以下的场景

    https://www.XXX.com/index.html 是放置在 CDN 上的一个 HTML 文件,用于给客户端作为辅佐的内容展示页,因为产品会经常改动这个页面的内容。而 CDN 默认的 cache-control 一般只会设定 max-age 的。因此这个页面在打开的第一次就会缓存,过期前都不会再向服务器请求页面来看看是否更新了。当然最理想还是在 cache-control 加上 no-cache 从而做到 对比缓存,而且一般的 CDN 厂商提供的相关 API 去设置 cache-control 。但这样做的话不安全,如果有一次设置少了 no-cache 的话就直接变成了 强缓存 从而导致过期前无法再更新。因此最好客户端层面上最好做多一层保险。
  • 有一些页面,我们希望后端可以通过配置下发到客户端去打开,比如说开屏弹窗(webView)。

从以上两个场景我们可以看到,如果是后端下发的 URL 的话,我们可以通过修改入口名字来摆脱 强缓存 。但是如果是写死在客户端的 URL 的话,我们还是最好把它设置成 对比缓存 。但是为了开发的简便性,我还是比较推荐把所有的 html 类型的缓存都设置成 对比缓存 :毕竟有时候后端下发的配置可能就在 APP 打开的时候才去获取的,而有些用户能很久都不重启 APP

客户端的实现

由上面我们得到一个很不错的缓存策略:就是当 content-type: text/html 的时候,我们采用 对比缓存 。而反之采用 强缓存

安卓下的实现

安卓的缓存策略有:

  • LOAD_CACHE_ONLY:不使用网络,只读取本地缓存数据
  • LOAD_DEFAULT:(默认)根据 cache-control 决定是否从网络上取据。
  • LOAD_NO_CACHE:不使用缓存,只从网络获取数据.
  • LOAD_CACHE_ELSE_NETWORK:只要本地有,无论是否过期,或no-cache,都使用缓存中的数据。

需要做的方案很简单:禁用 webview 的缓存 setCacheMode(WebSettings.LOAD_NO_CACHE) 重写 shouldInterceptRequest 方法。使用 okhttp 的缓存替代 webview 的缓存

// SanYueWebView.java
public class SanYueWebView extends WebView {
  public SanYueWebView(Context context) {
    super(context, null,0,0);
    // 禁用缓存
    setCacheMode(WebSettings.LOAD_NO_CACHE);
    setWebViewClient(new WebViewClient(){
      @Nullable
      @Override
      public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        String url = String.valueOf(request.getUrl());
        // file 协议的不走自定义缓存
        if (url.startsWith("file")){
          return super.shouldInterceptRequest(view, request);
        }
        // 这里使用 OKHTTP,加入 NetworkInterceptor
        File cacheDirectory = new File(cacheDirectoryString);
        int cacheSize = 1024 * 1024 * 1024; // 1G
        Cache cache = new Cache(cacheDirectory, cacheSize);
        resourcesClient = new OkHttpClient.Builder()
                  // 这里如果希望能离线访问的话,可以加多一个 Interceptor 去操作没有网络的时候直接直接返回一个缓存内容
                  .addNetworkInterceptor(new SanYueNetCacheInterceptor(60*60*24*365))
                  .cache(cache).build();
        final Call call = resourcesClient.newCall(new Request.Builder().url(url).build());
        try{
          final Response response = call.execute();
          return new WebResourceResponse(
                  response.header("content-type", "text/plain"),
                  response.header("content-encoding", "utf-8"),
                  Objects.requireNonNull(response.body()).byteStream()
          );
        }
        catch (IOException e) {
          e.printStackTrace();
        }
        // 请求不成功就返回 空信息
        byte[] nullInputStream = {};
        return new WebResourceResponse("text/plain", "utf-8", new ByteArrayInputStream(nullInputStream));
      }      
    })
  }
}
// SanYueNetCacheInterceptor
public class SanYueNetCacheInterceptor implements Interceptor {
  private int maxAge;
  public SanYueNetCacheInterceptor(int maxAge) { this.maxAge = maxAge; }
  @Override
  public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      Request.Builder builder = request.newBuilder();
      request = builder.build();
      Response response = chain.proceed(request);
      List<String> contentTypes = response.headers("Content-Type");
      Boolean flag = false;
      for (String value : contentTypes){  
        if (value.equals("text/html")){  
          flag = true; 
          break;
        }  
      }
      // Content-Type 带上 text/html 的设置成对比缓存
      if(flag){
        return response.newBuilder()
              .header("Cache-Control", "no-cache , max-age=" + maxAge)
              .build();
      }
      return response;
      
  }
}

IOS 下实现

待续


三月懒驴
522 声望14 粉丝