前端工程师必备之缓存问题

缓存是个老生长谈的问题,对于前端工程师来讲更是我们的必修课。或许很多人会说我的项目并没有问题,根本不需要聊什么缓存。如果真的是这样,只能证明你前端道路才刚刚开始。

背景

小郭今天分享缓存的原因在于:公司的一个核心APP中嵌入了SPA,而且应用核心都分布在SPA中,功能复杂且重。问题出现了:应用核心页面打开一直处于加载状态,排除掉弱网环境的原因,重点就在于没有缓存,每次进入页面都需要重载DOM和数据,拖慢页面打开速度。

那应该处理缓存问题呢?接下来小郭从三个方向来讲解。

浏览器缓存策略

在了解浏览器缓存前,我们需要先了解一下相关的概念:cache-control,expires,last-Modified,ETag。

浏览器通过请求头实现缓存,关键的请求头有cache-control,expires,last-Modified,ETag等。我们从时间和空间两个角度来看浏览器缓存。

时间

浏览器发送第一次请求:不缓存,服务端根据设定的缓存策略返回相应的header,如:cache-control,expires,last-Modified,ETag。

浏览器发送第二次请求:

  • 强缓存策略:不需要和服务端通信就决定是否使用缓存,cache-control优先级大于expires① 有cache-control且不过期,返回本地磁盘缓存,状态值200;② 有expires且不过期,返回本地磁盘缓存,状态值200。
  • 协商缓存策略:需要和服务端通信决定是否用缓存,Etag优先级大于last-Modified。① 有Etag,请求头添加If-None-Match,值就是上次返回的Etag值,然后发送给服务端。服务端对比If-None-Match与现有的Etag值是否一样;一样的话只返回header,状态码304,浏览器从本地磁盘获取缓存信息;不一样走正常流程,返回header+body,状态码200;② 有last-Modified,添加请求头If-Modified-Since,值是上次返回的last-Modified,然后发送给服务端。服务端对比If-Modified-Since与现有的是否一样;一样的话返回只返回header,状态码304,浏览器从本地磁盘获取缓存信息;不一样走正常流程,返回header+body,状态码200
  • 无缓存

空间

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

部署时缓存的问题

如果缓存就按理论上设置,那就太简单了。在实际应用有个严重的问题,我们不仅要缓存代码,还需要更新代码。如果静态资源名字不变,怎么让浏览器即能缓存又能在有新代码时更新。最简单的解决方式就是静态资源路径添加一个版本值,版本不变就走缓存策略,版本变了就加载新资源。如下:

<script src="xx/xx.js?v=24334452"></script>

然而这种处理方式在部署时有问题。

解决方法:静态资源和页面是分开部署

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

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

那如何实现呢?

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

webpack持久化缓存

一、webpack给文件名添加hash值是很简单的,但hash/chunkhash/contenthash要用哪个呢?

官方定义

hash: unique hash generated for every build

chunkhash: hashes based on each chunks' content

contenthash: hashes generated for extracted content

根据分析,contenthash才是我们需要的,内容有更新,hash值才会更新。

二、webpack会打包业务代码、第三方库及运行时代码,为保证缓存互不干扰,应该将它们提取出来。

第三方库提取方式是设置optimization的splitChunks的cacheGroups。splitChunks能提取模块,cacheGroups能缓存模块,并且cacheGroups的配置会覆盖splitChunks相同配置,既能提取又能缓存,故只需设置cacheGroups。

运行时代码的提取方式为配置runtimeChunk,默认为false,表示运行时代码嵌入到不同的chunk文件中;现在将运行时代码提取出来,并命名为manifest。

module.exports = {  entry: {    index: "./src/index.js",    bar: "./src/bar.js"  },  output: {    filename: "[name].[contenthash].js"  },  optimization: {    splitChunks: {      cacheGroups: {        vendor: {          test:/[\\/]node_modules[\\/]/,          name: "vendors",          chunks: "all"        }      }    },    runtimeChunk: {      name: "manifest"    }  }};

三、 moduleName 和 chunkName 对文件的影响

module:就是js模块

chunk:webpack编译过程中由多个module组成的文件

bundle:bundle是chunk文件的最终状态,是webpack编译后的结果

一个文件被分离为3个文件,文件间怎么相互依赖的,会影响彼此打包,解决方法是将moduleId和chunkId改成按照文件路径生成。

optimization: {  moduleIds: 'hashed',  namedModules: true,  namedChunks: true}

这样子moduleId在编译后的文件是文件目录的hash值,更加安全。这也是namedChunks在production默认为false的原因,不想依赖的文件路径在编译后的文件直接展示,但是为了持久性缓存,这里也只能打开。

四、CSS文件缓存

当css代码提取成单独文件,当我们改变css时,怎么保证不影响引用它的js文件呢?配置如下:

plugins: [  new MiniCssExtractPlugin({    filename: "[contenthash].css"  })]

webpack持久化缓存目标是当且仅当该文件内容变动才改变该文件名字的hash值

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {   
  output: {     
    filename: [name].[contenthash].js, // 让hash值只在内容变动时更新    
    chunkFilename: [name].[contenthash].js // 动态引入的模块命名,同上  
  },   
  module: {     
    rules: [{       
      test: /\.css$/,       
      use: [         
        "loader: MiniCssExtractPlugin.loader", // 提取出来css "css-loader"       
      ]     
     }]   
  },   
  optimization: {     
    moduleIds: "hashed", // 混淆文件路径名     
    runtimeChunk: { name: 'manifest' }, // 提取runtime代码命名为manifest
    namedModules: true, // 让模块id根据路径设置,避免每增加新模块,所有id都改变,造成缓存失效的情况    
    namedChunks: true, // 避免增加entrypoint,其他文件都缓存失效
    cacheGroups: {       
      vendor: { // 提取第三方库文件         
        test: /[\\/]node_modules[\\/]/,         
        name: 'vendors', chunks: 'all',       
      },    
   },  
}   
plugins: [    
  new webpack.HashedModuleIdsPlugin(), // 与namedModules: true作用一样  
  new MiniCssExtractPlugin({       
    filename: "[contenthash].css", // css文件也是按contenthash命名      
    chunkFilename: "[contenthash].css", // 动态引入的css命名,同上     
})   
], 
}

总结

浏览器有其缓存机制,想要既能缓存又能在部署时没有问题,需要给静态文件名添加hash值。在webpack中,有些配置能让我们实现持久化缓存。感兴趣的同学可以自行去测试哦!

有任何问题可以在下方留言,想了解更多前端知识欢迎关注公众号“一郭鲜”,文章也将同步于公众号,前端学习不迷路

一郭鲜

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容