览器缓存对于前端一点都不陌生,最常见的就是,新版本上线了,测试却说这怎么还没有变化呢?使用 ctr + F5 强制刷新之后,立马就好了。或者清除浏览器缓存,按住ctr+shift+delete,弹出如图:
我们会发现目前浏览器缓存的图片和文件的大小。或者进入chrome://chrome-urls/找到chrome://cache/ 就可以看到所有缓存的地址列表。对于浏览器缓存,前端对它是又爱又恨,有时想保留,有时想禁掉,所以看看浏览器缓存到底是怎样的?
浏览器缓存就是浏览器根据 url 第一次访问网站之后,将网站的 html、css、js、图片等文件复制一份保留到浏览器中,当你二次访问这个 url 的网站时,如果网站没有明确表示有更新时,浏览器直接在缓存中查找内容,不会再次请求网页内容,只有网页明确表示有更新时,浏览器才会向服务器发起网路请求,再次下载网页。
如上图,百度首页就是使用了缓存机制,首次访问之后 web资源被缓存,在后面重复请求中,资源直接在缓存中读取,而不是向服务器请求资源。
2.1、为什么很多网站二次打开速度很快?
网页二次打开很快,主要原因是第一次加载页面过程中,缓存了部分耗时数据,这一现象,对于单页面应用开发非常明显。
上一篇文章《浏览器工作原理》中,浏览器工作流程介绍,输入网址回车以后浏览器向服务器发起服务之前,会现在浏览器缓存中查询是否有需要的文件?如果有则直接在缓存中获取文件,避免向服务器请求和下载文件,所以节省了一部分时间。
2.2、浏览器缓存优点
1、减少网络带宽消耗
对于网站运营者或者访问网页的用户,带宽就代表着 money ,过多的消耗带宽,我们服务器配置就得升级,使用浏览器缓存之后,就会减少网络流量,降低运营成本。
2、降低服务器压力
使用浏览器缓存之后,除第一次访问需要向服务器请求网站全部资源,后续访问可以重复使用浏览器本地缓存,减少对服务器的请求,间接降低服务器的压力,同时,搜索引擎的爬虫也会根据缓存过期机制降低抓取的频率,也可以降低服务器压力。
3、减少网络延迟,加快网页加载
浏览器缓存 web资源后,减少网络请求,可以更快速地获取到服务器返回数据,同时使用浏览器缓存内的文件比服务器获取快很多,所以网页加载速度明显快很多。
对于浏览器端的缓存来讲,这些规则是在 http 协议和 meta 标签中定义的。分别从两个维度:新鲜度和校验值,规定浏览器是否可以直接使用缓存中的副本,还是直接从服务器获取最新资源。
3.1、新鲜度(过期):浏览器缓存的有效期,缓存必须满足以下两个条件,浏览器才会认为是最新的,可以直接使用。
3.2、校验值(验证):服务器返回资源的时候,会在响应头信息中带上资源实体标签 Entity Tag,可以用来作为浏览器再次请求过程的校验标识,如果发现校验标识不匹配,说明资源已经被修改过或过期,浏览器需要重新请求资源。
缓存规则可以设置在html的meta标签,也可以设置在http协议头内。
4.1、前端 html 中 meta 标签
在 html 页面中加入缓存设置,代码如下:
<meta http-equiv="Pragma" content="no-cache" />
<!-- Pragma是http1.0版本中给客户端设定缓存方式之一 -->
上边代码,禁止浏览器缓存,浏览器每次访问网页都要去服务器请求。事实这种禁用缓存形式作用有限:
4.2、HTTP协议头
http请求和响应头中,与缓存相关的常见类型:
规则 | 消息报头 | 值/示例 | 类型 | 作用 |
新鲜度 | Pragma | no-cache | 响应 | 告诉浏览器忽略资源的缓存副本,每次访问都需要去服务器拉取【http1.0中存在的字段,在http1.1已被抛弃,使用Cache-Control替代,但为了做http协议的向下兼容,很多网站依旧会带上这个字段】 |
Expires | Mon, 15 Aug 2016 03:56:47 GMT | 响应 | 启用缓存和定义缓存时间。告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求【http1.0中存在的字段,该字段所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代】 | |
Cache-Control | no-cache | 响应 | 告诉浏览器忽略资源的缓存副本,强制每次请求直接发送给服务器,拉取资源,但不是“不缓存” | |
no-store | 响应 | 强制缓存在任何情况下都不要保留任何副本 | ||
max-age=[秒] | 响应 | 指明缓存副本的有效时长,从请求时间开始到过期时间之间的秒数 | ||
public | 响应 | 任何路径的缓存者(本地缓存、代理服务器),可以无条件的缓存该资源 | ||
private | 响应 | 只针对单个用户或者实体(不同用户、窗口)缓存资源 | ||
Last-Modified | Mon, 15 Aug 2016 03:56:47 GMT | 响应 | 告诉浏览器这个资源最后的修改时间。服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端【只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间】 | |
If-Modified-Since | Mon, 15 Aug 2016 03:56:47 GMT | 请求 | 其值为上次响应头的Last-Modified值,再次向web服务器请求时带上头If-Modified-Since。web服务器收到请求后发现有头If-Modified-Since则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),包括更新Last-Modified的值,HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304(无需请求,节省浏览),告知浏览器继续使用所保存的cache | |
校验值 | ETag | "fd56273325a2114818df4f29a628226d" | 响应 | 告诉浏览器当前资源在服务器的唯一标识符(生成规则由服务器决定) |
If-None-Match | "fd56273325a2114818df4f29a628226d" | 请求 | 当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match(Etag的值)。web服务器收到请求后发现有头If-None-Match则与被请求资源的相应校验串进行比对,决定返回200或304 |
各种类型之间的关系和区别:
并不是所有的请求都能被缓存,无法被缓存的有:
存是个老生长谈的问题,对于前端工程师来讲更是我们的必修课。或许很多人会说我的项目并没有问题,根本不需要聊什么缓存。如果真的是这样,只能证明你前端道路才刚刚开始。
小郭今天分享缓存的原因在于:公司的一个核心APP中嵌入了SPA,而且应用核心都分布在SPA中,功能复杂且重。问题出现了:应用核心页面打开一直处于加载状态,排除掉弱网环境的原因,重点就在于没有缓存,每次进入页面都需要重载DOM和数据,拖慢页面打开速度。
那应该处理缓存问题呢?接下来小郭从三个方向来讲解。
在了解浏览器缓存前,我们需要先了解一下相关的概念:cache-control,expires,last-Modified,ETag。
浏览器通过请求头实现缓存,关键的请求头有cache-control,expires,last-Modified,ETag等。我们从时间和空间两个角度来看浏览器缓存。
时间
浏览器发送第一次请求:不缓存,服务端根据设定的缓存策略返回相应的header,如:cache-control,expires,last-Modified,ETag。
浏览器发送第二次请求:
空间
如果缓存就按理论上设置,那就太简单了。在实际应用有个严重的问题,我们不仅要缓存代码,还需要更新代码。如果静态资源名字不变,怎么让浏览器即能缓存又能在有新代码时更新。最简单的解决方式就是静态资源路径添加一个版本值,版本不变就走缓存策略,版本变了就加载新资源。如下:
<script src="xx/xx.js?v=24334452"></script>
然而这种处理方式在部署时有问题。
解决方法:静态资源和页面是分开部署
这些问题的本质是以上的部署方式是“覆盖式发布”,解决方式是“非覆盖式发布”。即用静态资源的文件摘要信息给文件命名,这样每次更新资源不会覆盖原来的资源,先将资源发布上去。这时候存在两种资源,用户用旧页面访问旧资源,然后再更新页面,用户变成新页面访问新资源,就能做到无缝切换。简单来说就是给静态文件名加hash值。
那如何实现呢?
现在前端代码都用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中,有些配置能让我们实现持久化缓存。感兴趣的同学可以自行去测试哦!
有任何问题可以在下方留言,想了解更多前端知识欢迎关注公众号“一郭鲜”,文章也将同步于公众号,前端学习不迷路
一、是什么
函数缓存,就是将函数运算过的结果进行缓存
本质上就是用空间(缓存存储)换时间(计算过程)
常用于缓存数据计算结果和缓存对象
二、如何实现
实现函数缓存主要依靠闭包、柯里化、高阶函数,这里再简单复习下
1-闭包
- 闭包可以理解成,函数 + 函数体内可访问的变量总和
- add函数本身,以及其内部可访问的变量,即 a = 1,这两个组合在⼀起就形成了闭包
(function() {
var a = 1;
function add() {
const b = 2
let sum = b + a
console.log(sum); // 3
}
add()
})()
2-柯里化
- 将一个二元函数拆分成两个一元函数
// 非函数柯里化
var add = function (x,y) {
return x+y;
}
add(3,4) //7
// 函数柯里化
var add2 = function (x) {
//**返回函数**
return function (y) {
return x+y;
}
}
add2(3)(4) //7
3-高阶函数
- 通过接收其他函数作为参数或返回其他函数的函数
function foo(){
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2
- 函数 foo 如何返回另一个函数 bar,baz 现在持有对 foo 中定义的bar 函数的引用。由于闭包特性,a的值能够得到
三、应用场景
虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存
以下几种情况下,适合使用缓存:
对于昂贵的函数调用,执行复杂计算的函数
对于具有有限且高度重复输入范围的函数
对于具有重复输入值的递归函数
对于纯函数,即每次使用特定输入调用时返回相同输出的函数
*请认真填写需求信息,我们会在24小时内与您取得联系。