前端缓存策略与基于Webpack的静态资源版本管理

为什么要做HTTP缓存

自Web2.0开始,随着Web产品和服务花样不断增加,网站的体积也开始变得越来越大。今天,体积过M的网站早已屡见不鲜。像facebook、twitter、淘宝、京东这样的网站,首屏的体积甚至接近10M(其中包含不少的部分是懒惰加载的图片)。如果不做前端缓存,每次打开网站都去服务器拉取,一个是绝大部分为重复的静态文件,浪费服务器网络资源;二是响应时间往往要在十几秒甚至二十秒(亲测,4M带宽下,淘宝首屏加载HTML,css,js的响应时间为15~20s),用户体验极不友好。在前端这个性能要求苛刻的领域,HTTP缓存便成为了解决这一问题的关键所在。

本文基于我在2016下半年学雷锋项目教学辅助系统,描述如何实现利用浏览器强缓存(disk cache&memory cache),同时结合Webpack实现应对缓存的版本更新。

基本知识

强缓存利用HTTP Response Header中的Expires或Cache-Control字段来实现,它们都用来表示资源在客户端缓存的有效期。两个header可以只启用一个,也可同时启用。Cache-Control优先级高于Expires。

浏览器第一次跟服务器请求一个资源,服务器返回资源的响应头如上(Chrome)。浏览器解析后,会将这个资源连同header一起缓存起来。当下次浏览器再请求这个资源时,将先在缓存中寻找,找到后再根据Expires或Cache-Control来计算是否过期。若未过期,则缓存命中,直接加载缓存资源。若已过期,则直接从服务器加载资源,同时重新缓存新的资源包。

需要注意的是,Cache-Control:max-age=0时是不启用缓存。另外,目前所有浏览器直接输入网址跳转都是利用缓存的,强制刷新ctrl+F5均放弃缓存,而刷新F5动作则有细微区别(Chrome使用缓存,Edge、Firefox放弃缓存)。

协商缓存利用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】两对Header字段来管理。在此不做过多赘述,详细请阅读浏览器缓存知识小结

实现方案

HTTP缓存的实现方案主要分两种:

  1. 利用服务端语言直接在返回头中加上响应头字段。
  2. 修改服务器配置,全局设置或添加响应头字段。

下面是我在Apache服务器上的实现,做法是直接修改Apache的http.conf配置文件:


<IfModule mod_expires.c>
	ExpiresActive On

	#过期时间设为访问日期起1个月
	ExpiresByType text/css "access plus 1 months"
	ExpiresByType application/javascript "access plus 1 months"
	ExpiresByType image/jpeg "access plus 1 months"
	ExpiresByType image/bmp "access plus 1 months"
	ExpiresByType image/x-icon "access plus 1 months"
	ExpiresByType image/png "access plus 1 months"
</IfModule>

注意,需要开启mod_expires模块。可以看到,代码还是非常语义化的。

重启服务器后,先后两次打开页面(Chrome),在F12控制台的网络监控台下可以看到如下信息:

首屏:

二次加载:

HTTP包:

可以看到,资源都从本地缓存中加载,加载时间大大缩短(静态资源越多,优化效果越好)。

但与之而来的一个问题是:如果在缓存有效期更新了服务器资源,如何通知浏览器放弃本地缓存,从服务器拉取更新后的资源呢?

利用Webpack实现资源增量更新

一个比较好的解决方案是利用Webpack在编译阶段为输出文件加上hash指纹(根据模块内容计算得出),从而实现前端静态资源的增量更新。

例如之前的截图中,index.86191.js文件的86191即为一个hash指纹。当该文件内容或其依赖的模块的内容发生变化时,这个数字将发生变化。在生成带有新的hash指纹的资源文件后,我们只须要将它们放入入口Html(入口Html不被缓存),如下图:

浏览器解析新的Html后将去请求新的资源文件(index.53214.js),而非加载本地旧文件的缓存(index.86191.js)。

另外,可以利用HtmlWebpackPlugin把新生成的js自动加入到Html中。

下面是Webpack关于自动构建添加hash指纹与自动生成Html的配置代码主要部分:

module.exports={
    entry: {
        index:'./frontend/index',
        vendor:['vue','vuex','vue-router']
    },
    output:{
        path:'./build',
        filename:debug?'[name].min.js':'[name].[chunkhash:5].js',  //这里
        publicPath:debug?'/build/':'',
        chunkFilename:'[id].[chunkhash:5].chunk.js'     //这里
    },
    module:{
        loaders:[
            {
                test:/\.(png|jpe?g|svg|gif)(\?\S*)?$/,
                loader:'file-loader',
                query:{
                    name:'[name].[ext]?[hash]'      //还有这里
                }
            },
            {
                test:/\.vue$/,
                loader:'vue'
            }
        ]
    },
    resolve:{
        extensions:['','.js','.jsx'],
        alias:{
            'vue$':'vue/dist/vue.min.js'
        }
    },
    plugins:[
        new HtmlWebpackPlugin({               //自动生成Html
            template:'index.html',
            inject:'body'
        })
    ]
}

下面是截自学雷锋项目中,带有hash指纹的资源文件:

如此,我们便可以完成在强缓存下的资源增量更新。

相关阅读

浏览器缓存知识小结

使用Apache Httpd实现http缓存

关于Webpack的hash指纹

编辑于 2017-03-14

文章被以下专栏收录