缓存一直以来都是用来提高性能的一项必不可少的技术 , 利用这项技术可以很好地提高 web 的性能。 缓存可以很有效地降低网络的时延,同时也会减少大量请求对于服务器的压力。 接下来这篇文章将会详细地介绍在 web 领域中缓存的一些知识点和应用。
由于整个网络服务都是基于 http 协议 的,因此先来介绍一下 HTTP 协议当中定义的缓存机制。HTTP 协议主要是通过请求头当中的一些字段来和服务器进行通信,从而采用不同的缓存策略。
一般来说,对于一个完整的 HTTP GET 请求缓存过程会包含七个主要的步骤:①从接收网络请求开始,②客户端会读取请求报文并且对报文进行解析, 进而提取 URL 和各种首部,③然后将会查询是否在本地有副本,如果本地没有副本就会从服务器上获取一份副本并且保存在本地。④接着会进行查看副本是否足够新鲜(新鲜度检测), 如果缓存已经失效就会询问服务器是否有任何更新,⑤服务器就会用新的首部和已缓存的主体来构建一条响应报文,⑥最后发送给客户端。⑦根据服务器的不同,会可选地选择创建日志记录该过程。
具体的流程可以看下面这张图 (该图来自 HTTP 权威指南):
根据缓存处理方式的不同,接着又会分为两类:强缓存和协商缓存。
强缓存主要是采用响应头中的 Cache-Control 和 Expires 两个字段进行控制的。其中 Expires 是 HTTP 1.0 中定义的,它指定了一个绝对的过期时期。而 Cache-Control 是 HTTP 1.1 时出现的缓存控制字段。Cache-Control:max-age 定义了一个最大使用期,就是从第一次生成文档到缓存不再生效的合法生存日期。由于 Expires 是 HTTP1.0 时代的产物,因此设计之初就存在着一些缺陷,如果本地时间和服务器时间相差太大,就会导致缓存错乱。这两个字段同时使用的时候 Cache-Control 的优先级给更高一点。 这两个字段的效果是类似的,客户端都会通过对比本地时间和服务器生存时间来检测缓存是否可用。如果缓存没有超出它的生存时间内,客户端就会直接采用本地的缓存。如果生存日期已经过了,这个缓存也就宣告失效。接着客户端将再次与服务器进行通信来验证这个缓存是否需要更新。
强缓存机制如果检测到缓存失效,就需要进行服务器再验证。这种缓存机制也称作协商缓存。浏览器在第一次获取请求的时候,就会在响应头中携带上资源的上次服务器修改日期 (Last-Modified) 或者资源的标签(Etag)。后续的请求服务器会根据请求头上的 If-Modified-Since(对应 Last-Modified)和(If-None-Match)字段来判断资源是否失效,一旦资源过期,则服务器会重新发送新的资源到客户端上,从而保证资源的有效性。
其中 Last-Modified 字段对应的是资源最后修改时间,例如:`Last-Modified:
Sat, 30 Dec 2017 20:18:56 GMT` ,当客户端再次请求该资源的时候,会在其请求头上附带上 If-Modified-Since 字段,值就是之前返回的 Last-Modified 值。如果资源未过期,命中缓存,服务器就直接返回 304 状态码,客户端直接使用本地的资源。否则,服务器重新发送响应资源。
另外一种协商缓存的校验方式的通过校验码而不是时间,这样就保证了在文件内容不变的情况下不会重复占用网络资源。响应头中 Etag 字段是服务器给资源打上的一个标记,利用这个标记就可以实现缓存的更新。后续发起的请求,会在请求头上附带上 If-None-Match 字段,其值就是这个标记的值。
需要注意的是当响应头中同时存在 Etag 和 Last-Modified 的时候,会先对 Etag 进行比对,随后才是 Last-Modified。
上面介绍了网络协议层面的缓存方案,接下来从前端的角度来看一下浏览器中几种常用的缓存技术。
本来 HTTP 协议的缓存方案很美好了,不过当用户主动触发页面刷新内容,如:F5 等,就会使浏览器的强缓存失效,进而转变成协商缓存。而利用 LocalStorage 可以无视用户主动刷新行为,并且可以存储较大体积的资源(2M 以上)。
localStorage 的使用也较为简单:
- const key = 'scq000';
- const value = 'hello world';
- // 存
- localStorage.setItem(key, value);
- // 取
- localStorage.getItem(key);
虽说 localStorage 一般是用来存储应用数据的,但是也可以利用其存储 js 和 CSS 等静态资源。
- <
- script
- id
- =
- "testJs"
- src
- =
- "example.js"
- >
- </script>
- // 以js为例
- var lsKey = 'loadJSv1.0'; // 作为localStorage存取的key;
- // 第一次运行的时候缓存
- function cacheJs(url) {
- // 获取代码内容
- getScriptContent(url,
- function(codeStr) {
- console.log(codeStr);
- localStorage.setItem(lsKey, codeStr);
- });
- }
- // 后续访问的时候读取缓存
- function loadJs(url) {
- var cacheStr = localStorage.getItem(lsKey);
- if (cacheStr) {
- // 插入浏览器中,或者也可以直接使用eval执行
- var script = document.createElement('script');
- script.innerhtml = cacheStr;
- console.log("使用缓存成功");
- } else {
- }
- }
- // 获取要缓存或者执行的源码内容
- function getScriptContent(url, callback) {
- var httpRequest = new XMLHttpRequest();
- httpRequest.onreadystatechange = function() {
- if (httpRequest.readyState === 4) {
- if (httpRequest.status === 200) {
- // 获取代码内容
- var codeStr = httpRequest.responseText;
- callback && callback(codeStr);
- }
- }
- };
- httpRequest.open('GET', url);
- httpRequest.send();
- }
上面只是一个简单的 demo,如果真的要使用这种方案,还需要考虑到更新处理问题。
作为一种性能优化的方案,这种方法也曾被大量应用于移动端的网页中。不过缺点也很明显,由于 localStorage 是保存在本地中的,所以很容易导致 xss 注入攻击。如果要使用这种方案,一定要做好对应的安全措施。在这里推荐一篇文章: 使用 SRI 增强 localStorage 代码安全 。
HTML5 曾经提供了一个应用程序缓存机制, 使得基于 web 的应用程序可以离线运行。这就是 App Cache(采用 mainfest 文件进行缓存), 由于方案目前正在从 web 标准中删除,所以在这里只做简单的介绍。
- <!DOCTYPE html>
- <html manifest="index.appcache">
- <head>
- <title></title>
- </head>
- <body>
- </body>
- </html>
- CACHE MANIFEST
- # v1 - 2017-11-11
- # 缓存版本号
- # 指定需要被缓存的文件
- CACHE:
- index.html
- script.js
- # 指定需要和服务器连接的白名单,将不进行缓存
- NETWORK:
- style.css
- # 回退页面,当资源无法访问,浏览器将采用该页面
- FALLBACK:
- index_bak.html
这个方案一个比较不好的地方,是需要和服务器进行配合,mainfest 文件的版本更新也是一个问题,同时资源还不支持部分更新。如果你想了解更多,可以访问 Using the application cache
作为 AppCache 的替代方案,Service Worker 是一个相对来说比较新的技术,其目的也主要是为了提高 web app 的用户体验,可以实现离线应用消息推送等等一系列的功能, 从而进一步缩小与原生应用的差距。 Service Worker 可以看做是一个独立于浏览器的 Javascript 代理脚本, 通过 JS 的控制,能够使应用先获取本地缓存资源(Offline First),从而在离线状态下也能提供基本的功能。 出于安全性的考虑,Service Worker 只能在 https 协议下使用,不过为了便于开发,现在的浏览器默认支持 localhost 使用 Service Worker。
Service Worker 整个的使用过程包括了注册,安装,激活,睡眠销毁等等一系列的状态。
首先需要在页面中注册一个 Service Worker。需要写在入口文件中:
- if ('serviceWorker ' in navigator) {
- navigator.serviceWorker.register('. / testSW.js ', {scope: ' / src '}).then(reg => {
- console.log('service worker is working ', reg);
- }).catch(e => console.log('register service worker failed '));
- }'
由于兼容性的问题,需要在代码开始做浏览器特性检测处理。注册时候,scope 参数是可选的,用来限制 SW 的工作范围的。
- // 用来标记缓存
- const CACHE_FILE = 'my-sw-demo-v1';
- let filesToCache = [
- '/',
- '/index.html',
- '/scripts/main.js',
- '/styles/main.css'
- ];
- // 安装
- self.addEventListener('install', event => {
- event.waitUntil(
- caches.open(CACHE_FILE)
- .then(cache => cache.addAll(filesToCache));
- );
- });
- // 添加fetch事件监听
- self.addEventListener('fetch', event => {
- event.responseWith(
- caches.match(event.request)
- .then(response => response)
- .catch(() => fetch(event.request));
- );
- });
当用户首次访问页面的时候,会触发 SW 的安装事件,addAll 方法接收需要被缓存文件的 url 列表,并会自动获取这些文件存入缓存中。 接下来注册的 fetch 事件监听器会在每次 SW 被控制的资源请求时触发,拦截请求并在缓存中匹配对应资源。如果缓存命中,则直接返回资源,否则去发起 fetch 请求。 当然,如果你想更进一步,可以在缓存没有命中的时候,获取资源然后将获取到资源加入缓存中。另外,在网络不可用的时候,提供一种回退方案。上面的代码可以改写成这样:
- // 添加fetch事件监听
- self.addEventListener('fetch', event = >{
- event.respondWith(caches.match(event.request).
- catch(() = >{
- return fetch(event.request).then(response = >{
- return caches.open('v1').then(cache = >{
- cache.put(event.request, response.clone());
- return response;
- });
- });
- }).
- catch(() = >{
- // 回退资源
- return caches.match('/fallback.html');
- }));
- });
如果应用中 SW 已经安装,但是刷新的时候检测到有新版保持可用,就会自动安装。但是需要注意的是,只有当不再有任何已加载页面在使用旧版 SW 的时候,新版本的 SW 才会被激活。
咱们把上面的版本号更改一下:
- const CACHE_FILE = 'my-sw-demo-v2';
此时刷新页面,当 install 事件发生的时候,前一个版本(my-sw-demo-v1) 如果还在被其它页面使用,则这个新版本不会被激活,当所有页面都不再使用 v1 的时候,v2 就会激活并开始响应请求。
作为缓存的完整生命周期来说,提供删除功能必不可少。我们有时候需要手动删除旧版本的缓存,以便释放有限的浏览器缓存空间。此时,可以利用 activate 事件和 waitUntil 这样一个方法来清理缓存。
- // 清理缓存操作
- self.addEventListener('activate', event => {
- // 设置白名单,不需要删除的缓存key
- const cacheWhiteList = ['v2'];
- event.waitUntil(
- cache.keys().then(keyList => {
- return Promise.all(keyList.map(key => {
- if (!cacheWhiteList.includes(key)) {
- // 如果不在白名单里面,就删除该缓存
- return cache.delete(key);
- }
- }));
- });
- )
- });
调试的时候,可以在谷歌浏览器中输入
查看各个页面 SW 脚本的工作情况。也可以在开发者工具中查看当前页的 SW 脚本情况:
- chrome://serviceworker-internals/
SW 目前还是一个草案,在 PC 端上各个浏览器的支持度并不是很高,但是在手机端已大部分能够实现支持了。作为 PWA 的一种核心技术,谷歌对 SW 提供很多很有用的工具,如: Sw-precache , Sw-toolbox , 感兴趣的可以去研究一番。 下面收集了一些比较有用的工具和参考文章,如果需要深入学习,可以一阅: serviceworker-webpack-plugin
https://www.npmjs.com/package/workbox-webpack-plugin
http://air.ghost.io/using-workbox-webpack-to-precache-with-service-worker/
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers
https://ivweb.io/topic/5876d4ee441a881744b0d6d6
https://x5.tencent.com/tbs/guide/serviceworker.html
https://foio.github.io/
https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/
最后,作为 2018 年的开篇之作,希望各位读者在新的一年里都能工作顺利,生活快乐!
来源: https://juejin.im/post/5a482d976fb9a044fc451456