详解前端中的缓存

494 阅读12分钟

缓存是系统快速响应中的一种关键技术,是一组被保存起来以备将来使用的东西。

缓存的优点

  • 获得更快的读写能力:相比数据库I/O操作磁盘,缓存I/O操作内存速度更快
  • 降低数据库压力: 把常用的数据放在缓存中,请求直接读取缓存,可以减轻数据库的负担。
  • 减少冗余数据传输:很多的静态资源,基本上很少有改动,有了缓存之后,我们只用在有文件改动的时候,再重新从服务器拉取新的资源即可。
  • 节省流量:有了缓存,对于那些非常大的静态资源,我们无需每次进行请求,可以节省大量的流量,流量就是钱,利用缓存为我们服务的公司节省一笔不小的开支。
  • 降低时延: 对于已经缓存好的数据,无需再发请求给服务器,这样就节省下很多HTTP请求的时间,降低时延。

缓存的分类

CDN缓存

CDN(内容分发网络)是通过在多个节点部署来减少请求时间的,这样我们不需要每次都回源到源站服务器进行请求

CDN我们可以类比京东物流。大家都知道京东的物流可以说是国内最快的物流了,那么它是如何做到的呢?其实很简单,它在全国各地都设有物流仓库。当我们下单的时候,物流系统判断我们的位置距离哪个物流点最近,找出最近的点,然后从这个物流点给我们发货,这就大大提高了速度,相应的用户体验也是一流的。其实CDN的原理也是这样,通过在各个地方部署相应的服务器,形成CDN集群,从而提高访问速度。

CDN对于常见的HTTP请求都是支持的,但是并不是对所有请求方式都会进行缓存,进行缓存的只有GET请求,对于其它请求均不作缓存,仅仅起到转发作用,相当于proxy。

关于CDN站点部署最好使用动静分离的形式,将动态请求和静态请求的内容独立成两个站点,而 CDN 仅仅加速静态站点中的资源,现在的CDN服务商大多数也都支持这个功能。

数据库缓存

数据库缓存就是我们把一些经常会被访问到资源直接放到内存当中,当数据没有变化我们并不会去让直接读写数据库,只有数据发生变化的时候我们才会去操作数据库。

浏览器缓存

浏览器缓存是根据一套与服务端约定好的规则来进行工作的。工作规则很简单,检查以确保副本是最新的,通常只要一次会话。浏览器会在硬盘上专门开辟一个空间来存储资源副本作为缓存。在用户触发"后退"操作或点击一个之前看过的链接的时候,浏览器缓存会很管用。同样,如果访问系统中的同一张图片,该图片可以从浏览器缓存中调出并几乎立即显现出来。

本地缓存

本地存储主要有以下几种:LocalStorage、SessionStorage和Cookie和IndexDB,其中IndexDB主要用在前端有大容量存储需求的页面上,例如在线编辑浏览器或者网页邮箱。

LocalStorage

LocalStorage是HTML5新引入的特性,可能有的同学会说我们不是有Cookie吗,但是Cookie非常致命的一个缺陷就是Cookie 的大小区间是[0KB—4KB],可以看到Cookie的大小上限是4KB,一旦我们的业务需要存储超过4KB的信息,Cookie这个时候就无法满足我们的需求,所以LocalStorage便应运而生了。

优势

  • 大小方面相比,LocalStorage突破了4KB大小体积限制,一般是5MB(不同的浏览器大小有所区别),这相当于一个5MB大小的数据库提供给我们来使用,我们可以存储更多信息,这方面我们可以有更多的想象;
  • LocalStorage是持久存储,它并不会随着页面的关闭而消失,除非我们主动去清理,不然它会一直在本地,不会过期;
  • 仅仅存储于本地,不会像Cookie那样,每次的HTTP请求都会携带

劣势

  • 如果浏览器设置为隐私模式,那么我们无法读取LocalStorage;
  • LocalStorage受同源策略的限制,即协议、端口、主机地址有任何一个不同,则无法访问

SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage和LocalStorage相比,SessionStorage只在当前会话下才会起作用,一旦我们关闭当前的Tab,SessionStorage也就失效了。因此,SessionStorage是一个有时效性的存储方案

由于SessionStorage具有时效性,常用的业务场景比如网站常见的游客登录,就可以存储在SessionStorage当中,还有网站的一些临时浏览记录都可以使用SessionStorage来进行记录。

Cookie

Cookie是最早期被提出来的本地存储方式。在此之前,服务端是无法判断网络中的两个请求是否是同一个用户(浏览器)发起的。那么为了解决这个棘手的问题,Cookie也就出现在大家的视野当中。Cookie的大小最大只有4KB,而且它是纯文本的文件,我们每次发起HTTP请求都会携带Cookie。

浏览器的缓存

HTTP缓存策略不是前端单方面的事情,它需要服务端和我们共同配合才能完成,常见的服务器软件如:Apache、Nginx都可以为资源设置不同的HTTP缓存策略。在概念层面HTTP缓存策略又细分为强制缓存和协商缓存。

强制缓存

首先,不管是强制缓存还是协商缓存,主要针对的都是前端的静态资源,最完美的效果就是我们发起请求然后取到相应的静态资源。如果服务端的静态资源没有更新,我们就下次访问的时候直接从本地读取即可;如果服务端的静态资源已经更新,那么我们再次请求的时候就要到服务器请求最新的资源,然后进行拉取。

想要达到上述这种效果,需要强制缓存和协商缓存的共同配合才可以

强制缓存的配置参数有两个expires和Cache-Control。

expires

在首次发起请求的时候,服务端会在Response Header当中设置expires字段,比如设置的过期时间是Tue, 09 Jul 2022 06:16:29 GMT。那么如果在这个时间之前我们发起请求去请求资源,我们就不会发起新的请求,直接使用本地已经缓存好的资源,这样我们可以有效减少了不必要的HTTP请求,不仅提升了性能,而且节省了流量,减少网络资源的消耗。

expires作为最开始的强制缓存解决方案,看起来没什么问题,但它的时间和服务端的时间是保持一致的,可是我们最终比较的时候是用本地时间和expires设置的时间进行比较。如果服务端的时间和我们本地的时间存在误差,那么缓存这个时候很容易就失去了效果,这个时候功能更强大的Cache-Control出现了。

Cache-Control

Cache-Control同样也是强制缓存的关键字段。Cache-Control是HTTP1.1才有的字段,Cache-Control设置的是一个相对时间,可以更加精准地控制资源缓存。如下:

Cache-Control: max-age=315360000

Cache-Control可设置的字段值较多,下面我们一一来介绍:

  • no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;
  • no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
  • max-age=:设置缓存的最大有效期,单位为秒;

上面就是强制缓存的常用字段,实际开发当中expires和Cache-Control一般都要进行设置,这是为了兼容不支持HTTP1.1的环境。两者同时存在,Cache-Control的优先级要高于expires。

如果使用的是Nginx,那么打开Nginx的配置文件nginx.conf,具体配置方法如下:

// expires:给图片设置过期时间30天,这里也可以设置其它类型文件
location ~ \.(gif|jpg|jpeg|png)$ {
        root /var/www/img/;
        expires 30d;
}
// cache-control:给图片设置过期时间36秒,这里也可以设置其它类型文件
location ~ \.(gif|jpg|jpeg|png)$ {
        root /var/www/img/;
        add_header    Cache-Control  max-age=3600;
}

协商缓存

协商缓存的机制实现是根据2对字段实现的:

  • Last-Modifiled / If-Last-Modifiled 根据文件的修改时间判断资源是否过期

  • Etag / If-Node-Match 根据文件的修改内容生成唯一Key判断资源是否过期,与 Last-Modifiled 同时存在时,Etag 优先

// 处理加载图片资源
router.get(/\S*.(jpe?g|png)$/, async (ctx, next) => {
    const { path } = ctx
    ctx.type = mime.getType(path)

    const imagePath = Path.resolve(__dirname, `.${path}`)
    const imageStatus = await fs.stat(imagePath)
    const lastModified = imageStatus.mtime.toGMTString()
    const ifModifiedSince = ctx.request.headers['if-modified-since']

    // 如果命中,返回状态304
    if(ifModifiedSince === lastModified) {
        ctx.status = 304
    } else {
        const imageBuffer = await fs.readFile(imagePath)
        ctx.set('cache-control', 'no-cache')
        ctx.set('last-modified', lastModified)
        ctx.body = imageBuffer
    }

    await next()
})

设置协商缓存,一般来说也是要这2个字段同时存在的,因为Last-Modified/If-Modified-Since本身有一定的缺陷,加上Etag/If-None-Match之后,整个缓存系统更加稳定。

Last-Modified和Etag在Nginx和Apache当中都是默认启用的,所以无需我们手动进行相关配置。

我们如果要关闭nginx上的Last-Modified和Etag可以这样设置:

http {
    etag off;
    add_header Last-Modified '';
}

这样响应头就不会有etag和Last-Modified。

这里需要注意,在nginx上即使给index.html配置为强制缓存,但是nginx仍然会对index.html进行协商缓存,因为我们一般认为index.html是经常修改的,如下图所示。

![image.png](p9-juejin.byteimg.com/tos-cn-i-k3… d9f9b818dd4d94215~tplv-k3u1fbpfcp-watermark.image?)

Webpack打包时如何应对缓存

首先来想这样一个问题,如果我们的静态资源在缓存期内被修改,那么我们该如何通知浏览器从服务端拉取最新的资源呢?在没有Webpack之前,我们一般的处理方法就是对CSS、JavaScript、图片这些静态资源设置为强制缓存,而入口文件(index.html)使用协商缓存或者干脆强制不缓存。这样可以通过修改入口文件(index.html)中对强制缓存静态资源的引入 URL 来达到即时更新的目的

那么后来Webpack出现之后,我们有了新的解决方案,那么目前采用的主流方案是Webpack增量更新,增量更新的含义就是只更新发生了改动的静态文件,对于没有发生改动的静态文件,则继续使用缓存好的,无需进行修改。

Webpack增量更新使用文件的hash指纹来确定一个文件是否进行了更新。hash指纹其实和我们前面介绍的Etag是非常类似的,只要文件修改,hash指纹就会发生修改

Webpack一共有三种hash,分别是hash、chunkhash、contenthash。

  • hash(the hash of the module identifier)是跟整个项目的构建相关,默认长度为20,我们可以根据实际情况设置合适长度。hash根据入口文件打包出的文件都使用相同的hash。如果当中有引用图片,那么不同的图片有不同的hash值,不会和前面打包出的文件使用相同的hash。因此,一般的图片和字体文件都是用hash来固定其缓存

  • chunkhash(the hash of the chunk content)根据不同的入口文件进行依赖文件分析然后生成对应的chunkhash。chunkhash对应的是每个文件,所以每个入口文件打包后的chunkhash都是不同的,所以我们的JavaScript文件用chunkhash来固定缓存

  • contenthash(the hash of extracted content)顾名思义是固定提取文件的hash值的,我们项目当中的CSS文件一般都要单独抽取出来单独维护,抽离出来的CSS文件和原有的JavaScript文件是使用的是相同的chunkhash。这个时候,如果我们只是修改了CSS,并没有修改JavaScript,你会发现JavaScript的chunkhash还是跟着CSS的一起变化,这显然不是我们需要的结果。所以针对这个问题,单独抽取的CSS文件我们都使用contenthash来固定

Webpack具体配置信息如下:

module.exports = {
    entry: {
        index: './test/test.js',
        about: './test/about.js'
    },
    output: {
    	// 打包出来的JavaScript文件用chunkhash
        filename: '[name].[chunkhash:8].js',
        path: path.resolve(__dirname, 'dist')
    },
  	module: {
        rules: [
            {
              	test: /\.css$/,
               	use: [{loader: MiniCssExtractPlugin.loader},'css-loader']
            },
            {
              	 test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
       		 loader: 'file-loader',
                 query: {
                     // 图片文件使用hash
                     name: '[name].[ext]?[hash]',
                     outputPath: 'static/img/',
                     publicPath: '/dist/static/img/'
                 }
            }
        ]
    },
   plugins: [
        new CleanWebpackPlugin(),
     	new MiniCssExtractPlugin({
     	    // 抽离的CSS文件用contenthash
            filename: '[name].[contenthash:8].css',
	    chunkFilename: '[id].css'
        }),
    ]
}

三级缓存

  • 先去内存看,如果有,直接加载
  • 如果内存没有,择取硬盘获取,如果有直接加载
  • 如果硬盘也没有,那么就进行网络请求
  • 加载到的资源缓存到硬盘和内存

以图片为例浏览器如何进行三级缓存的

  • 访问-> 200 -> 退出浏览器 (因为退出浏览器,那么内存里面的数据就没有了)
  • 再进来-> 200(from disk cache, 因为内存没有,那么就去硬盘去找) -> 刷新 -> 200(from memory cache, 只是刷新,没有关闭页面,所以内存数据还存在)

流行的缓存方案

目前的项目大多使用这种缓存方案的:

  • HTML: 协商缓存;
  • css、js、图片:强缓存,文件名带上hash。