【HTTP缓存】原理讲解和实战演练

134 阅读12分钟

| 前言

我们都知道当我们在浏览器中打开一个页面时,浏览器会根据你输入的URL到对应的服务器上请求你想要的数据资源。但这个过程中可能页面可能需要等待一段时间(白屏时间)才能渲染到你的页面中。

当你想要提高用户体验时,那就不得不提各种缓存技术了,例如:DNS缓存、CDN缓存。浏览器缓存、页面本地缓存等等,有一个良好的缓存策略可以减低重复资源的请求,降低服务器的开销,提高用户页面的加载速度。

而本文主要想通过引入以下的几个问题来帮大家更好的了解HTTP缓存实践和应用

该文章想解决的几个问题在于:

  1. 强缓存和协商缓存的缓存策略
  2. 缓存存放在什么地方?
  3. 在webpack和nginx中我们如何应用这一个缓存来优化我们的网站体验

| 缓存原理解析

HTTP 缓存作为客户端缓存的一种是 Web 性能优化的重要手段,了解其原理,对于前端应用开发维护和面试都有重要的意义。

| 缓存类型

HTTP 缓存分为两种:强缓存和协商缓存。两种缓存可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时且命中缓存,则直接使用缓存数据,不在进行缓存协商。当多种缓存方案同时存在时,新标准(HTTP1.1)比旧标准(HTTP1.0)优先。

强缓存

在浏览器加载资源的时候,首先会根据请求头的expirescache-control判断是否命中强缓存策略,判断是否向远程服务器请求资源还是去本地获取缓存资源。 image.png 在浏览器中,强缓存分为Expires(http1.0规范)、cache-control(http1.1规范)两种。

Expires

Expireshttp1.0的规范,用于表示资源的过期时间的请求头字段,值是一个绝对时间,是由服务器端返回的。

在浏览器第一个请求资源时,服务器端的响应头会附上Expires这个响应字段,当浏览器在下一次请求这个资源时会根据上次的expires字段是否使用缓存资源(当请求时间小于服务端返回的到期时间,直接使用缓存数据)

expires是根据本地时间来判断的,假设客户端和服务器时间不同,会导致缓存命中误差

Cache-control

上面我们提到了Expires有个缺点,当客户端本地时间被修改时浏览器会直接向服务器请求新的资源,为了解决这个问题,在http1.1规范中,提出了cache-control字段,且这个字段优先级高于上面提到的Expires,值是相对时间。

cache-control中有常见的几个响应属性值,它们分别是

属性值备注
max-age3600例如值为3600,表示(当前时间+3600秒)内不与服务器请求新的数据资源
s-maxage和max-age一样,但这个是设定代理服务器的缓存时间
private内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
public所有内容都将被缓存(客户端和代理服务器都可缓存)
no-store不缓存任何数据
no-cache储存在本地缓存区中,只是在与原始服务器进行新鲜度再验证之前,暂不能将其提供给客户端使用

协商缓存

上面提到的强缓存都是由本地浏览器确定是否使用缓存,当浏览器没有命中强缓存时就会向服务器发送请求,并且在请求头中设置了If-Modified-Since (对应 last-modified)或者 If-None-Match(对应Etag) 的时候,会将这两个属性值发到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。

  • Last-modified表示本地文件最后修改时间,由服务器返回

  • if-modified-since是浏览器在请求数据时返回的,值是上次浏览器返回的Last-modified

  • ETag是一个文件的唯一标识符,当资源发生变化时这个ETag就会发生变化。弥补了上面last-modified可能出现文件内容没有变化但是last-modified发生了变化出现重新向服务器请求资源情况。这个值也是由服务器返回的

  • if-none-match是浏览器请求数据时带上的字段,值是上次服务器返回的ETag 详情可看本图: image.png

| 缓存存储位置

在强缓存的场景下,我们可以经常看到如下的 code 和 size的响应状态。

http codesize
200from memory cache
200from disk cache

image.png 这就引起了我们对这两种状态的判断逻辑的思考了,

  1. 什么是 内存缓存,什么是硬盘缓存,他们的差异点在哪里
  2. 什么时候会存到内存缓存中, 什么时候会存到硬盘缓存中
  3. 这一种行为会和文件有比较大的关系的嘛

接下来就为大家解答一下上述的这几个问题🤔

内存缓存(from memory cache) :内存缓存具有两个特点,分别是快速读取时效性

1、快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。

2、时效性:一旦该进程关闭,则该进程的内存则会清空。

硬盘缓存(from disk cache) :硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。

在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

| 设置缓存实战

通过上面的了解,我们也对「强缓存」和「协商缓存」的运作机制有了一个大概的明确,那么在工程中,我们是如何去运用这一个缓存方法来达到更快的首屏打开速度更轻的网络请求带宽压力更及时和准确的更新上做平衡呢? 首先我们先梳理一下我们打开一个网页所需要的所有资源。

  • html和静态资源:通常html不设置缓存,因为其它资源的入口都是html文件;静态资源(js,css,图片等)会设置缓存
  • 浏览器和服务端:服务端需要决定使用哪种缓存策略并在响应头返回;前端不需要设置,是浏览器本身机制。

| 代码的动态更新缓存机制

在上述的过程中对于资源的类型「js,css,图片,视频」
对于像「图片,视频」这一种固定路径的资源可以直接设置强缓存,不需要有二次更新的过程,但是对于js,css而言,几乎每一次发布都会去修改这一部分代码。

这样就有一个严重的问题,代码被缓存下来了,那么如何去更新呢。如果静态资源名字不变,怎么让浏览器即能缓存又能在有新代码时更新。最简单的解决方式就是静态资源路径添加一个版本值,版本不变就走缓存策略,版本变了就加载新资源。如下:

<script src="xx/xx.js?v=24334"></script>
复制代码

然而这种处理方式在部署时有问题,在张云龙这篇精彩的文章中,详细介绍了这些缘由,重点总结如下:

静态资源和页面是分开部署的:

  1. 先部署页面再部署静态资源,会出现用户访问到旧的资源
  2. 先部署静态资源再部署页面,会出现没有缓存用户加载到新资源而报错

这些问题的本质是以上的部署方式是“覆盖式发布”,解决方式是“非覆盖式发布”。即用静态资源的文件摘要信息给文件命名,这样每次更新资源不会覆盖原来的资源,先将资源发布上去。这时候存在两种资源,用户用旧页面访问旧资源,然后再更新页面,用户变成新页面访问新资源,就能做到无缝切换。简单来说就是给静态文件名加hash值

那如何实现呢?现在前端代码都用webpack之类的构建工具打包,那么我们看下结合webpack该怎么做,怎么才能做到持久化缓存?

webpack的设置

首先我们先来聊一下webpack中的hash方式
1. hash -- 编译产生

webpack在实例化的时候会在全局创建一个Tapable子类Compiler, 该实例掌握了初始化配置信息,运行编译入口函数,注册和调用插件等等。

每次编译的时候都会实例化一个对象compilation, 该对象掌控着从编译开始到编译结束文件,模块的加载,封闭,优化,分块,哈希,重建等等都是由其负责, 此时的hash是由compilation来创建的,也就是说每次编译都会创建一个新的hash值, 并且所有文件的hash都是一样的,因为是同一个compilation生成

2. chunkhash -- 构建chunk生成

首先, 什么是chunk?

我们知道,每个入口文件都是一个chunk,每个chunk是由入口文件与其依赖所构成,异步加载的文件也被视为是一个chunk, chunkhash是由每次编译模块,根据模块及其依赖模块构成chunk生成对应的chunkhash, 这也就表明了每个chunk的chunkhash值都不一样, 也就是说每个chunk都是独立开来的,互不影响,每个chunk的更新不会影响其他chunk的编译构建

3. contenthash(满足要求)

针对文件内容生成不同的hash, 只有当文件内容发生变化此hash才会重新生成,此时需要利用mini-css-extract-plugin插件取提取出每个chunk的css文件,将css与js隔离开,然后将css更改后

注意点

  1. 输出js必须也是contenthash结尾, 如果是chunkhash,由于css已经发生变化了,整个chunk在编译时还是会生成新的hash,即使打包出的js内容没有发生变化
  2. 如果入口文件没有依赖异步chunk,入口文件以contenthash输出不会重新编译,如果入口文件有依赖异步chunk, 那么不管入口文件以哪种hash形式输出,都会重新编译,因为css contenthash已经发生变化了, 通过异步插入的link标签所指定的css文件也发生了变化,代表入口文件的内容就发生了变化, 所以不管哪个hash都会重新编译, 但是异步chunk不会发生变化

操作:

首先我们创建一个空文件夹:webpack-demo 进入到空文件中安装一些webpack的配置用于css,js,html识别注入。详细功能可参考webpack文档

npm init -y 
npm i -D css-loader html-webpack-plugin mini-css-extract-plugin style-loader webpack webpack-cli webpack-dev-server
npm i lodash

接下来我们在空文件夹中创建如下的结构

image.png

创建webpack.config.js 中填入以下内容。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'js/[name].[contenthash].js',
    chunkFilename: 'js/[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [{
          loader: MiniCssExtractPlugin.loader,
        }, 'css-loader'],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack-demo',
    }),
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "css/[name].[contenthash].css",
      chunkFilename: "css/[name].[chunkhash].css"
    })
  ],
  devServer: {
    static: './dist',
  },
  optimization: {
    runtimeChunk: 'single',
    moduleIds: 'deterministic',
    runtimeChunk: { name: 'manifest' }, // 提取runtime代码命名为manifest
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};

在这里简单介绍一下每一个配置项代表的意思: image.png

在这里编写一些js操作和css样式。 image.png 使用npm run start 来查看页面是否可以展示正常: 一些package.json中的一些脚本 image.png 可以发现此处是可以正常注入数据和样式的,打包成功。 image.png

并使用 npm run build来编译生成打包文件

image.png

| 如何使用koa搭建静态服务器设置缓存请求头

image.png 使用 npm run server来查看下此处的设置效果。 第一次访问: 第一次访问的时候可以看到此时是没有任何的缓存的。 image.png 接下来我们看一下其中的一个js文件的请求头吧: 可以看到此处是被正常设置好强缓存的。 image.png 第二次访问的时候:可以看到此时文件是从缓存中去得的。 image.png

这样子通过些许实战可以让我们更加了解http缓存是什么以及如何与我们的工程项目相融合,本文是以koa-static来举例子设置请求头的,也可以通过nginx去设置请求头,详情可以参考这一篇文章

| 总结

本文到这里就算结束啦,希望写的这一篇文章能给大家带来一些在「八股」之外的深层思考和应用,不局限于面试八股,往实战和项目中去出发。有一句经典名言写的挺好的「not talk show me your code」。