我们可以将缓存划分为资源缓存与数据缓存两大类。
资源缓存:用于将静态资源按照我们所期望的规则存储在本地,用户访问网页时如果相关资源未发生改变,则可以直接从本地拿取资源渲染网页。所以资源缓存的策略其实就是用于确定资源是否已经发生了更新。
数据缓存:用于将常使用数据存储在本地,例如用户登录态信息、不常变动且不涉及数据安全问题的数据等。数据缓存的方案有很多,例如:cookie、localstorage、indexedDB 等。
资源缓存用于缓存静态资源,上面已经提到。良好的缓存策略可以减少资源重复加载进而提高网页的整体加载速度。
通常浏览器缓存策略分为两种:强缓存和协商缓存,当然还包括 service worker。
expires
和 cache-control
值来判断是否命中强缓存,命中则直接从本地缓存中读取资源,这一过程不需请求服务器;last-modified
和 etag
值来验证资源是否命中协商缓存,若命中,则服务器会将这个请求返回,但是不会返回这个资源的数据,浏览器接收到该请求响应后依然从本地缓存中读取资源;强缓存和策略缓存如果命中,都是直接从客户端缓存加载对应资源。但不同点是:强缓存自比较开始至缓存命中不会请求服务端,而策略缓存的是否使用本地缓存这一决定是需要服务端参与的,换言之策略缓存需要请求服务端来完成的。
强缓存通过 Expires
和 Cache-Control
响应头实现。两者详细说明如下:
中文释义为:到期,表示缓存的过期时间。expire 是 HTTP 1.0 提出的,它描述的是一个绝对时间,该时间由服务端返回。因为 expire 值是一个固定时间,因此会受本地时间的影响,如果在缓存期间我们修改了本地时间,可能会导致缓存失效。
通常表示如下:
Expires: Wed, 11 May 2018 07:20:00 GMT
中文释义为:缓存控制。cache-control 是 HTTP 1.1 提出的,它描述的是一个相对时间,该相对时间由服务端返回。
表示如下:
Cache-Control: max-age=315360000
该属性还包括访问性及缓存方式设置,列举如下:
缓存与使用缓存流程说明如下:
浏览器加载资源时,若强缓存未命中,将发送资源请求至服务器。若协商缓存命中,请求响应返回 304 状态码。
协商缓存主要使用到两对请求响应头字段,分别是:
Last-Modified 由上一次请求的响应头返回,且该值会在本次请求中,通过请求头 If-Modified-Since 传递给服务端,服务端通过 If-Modified-Since 与资源的修改时间进行对比,若在此日期后资源有更新,则将新的资源发送给客户端。
不过,通过文件的修改时间来判断资源是否更新是不明智的,因为很多时候文件更新时间变了,但文件内容未发生更改。
这样一来,就出现了 ETag 与 If-None-Match。
不同于 Last-Modified,Etag 通过计算文件指纹,与请求传递过来的 If-None-Match 进行对比,若值不等,则将新的资源发送给客户端。
值得一提的是,通常为了减轻服务器压力,并不会完整计算文件 hash 值作为 Etag,并且有些时候 Etag 的表现会退化为 Last-Modified (当指纹计算为文件更新时间时)。那为什么我们通常还是要选用 Etag 呢?原因有一下几点:
💡 ETag 的优先级比 Last-Modified 更高!
缓存启用的顺序可列举如下:
需要注意的是协商缓存需要配合强缓存使用,如果不启用强缓存那么协商缓存就失去了意义。大部分 web 服务器都默认开启了协商缓存,而且是同时启用(Last-Modified、If-Modified-Since)和(ETag、If-None-Match)。但当我们的系统选用分布式部署时,则需要注意以下问题:
为了更灵活配置缓存策略,引入了 service worker 技术,关于该技术的应用可见文章 workbox 应用
cookie 实际是一小段文本信息。客户端请求服务端,如果服务器需要记录该用户的登录状态,就需要使用在响应时向客户端返回一个 cookie。客户端浏览器会将 cookie 保存。客户端再次请求该网站时,会携带 cookie 一同提交到服务端。此时服务端检查该 cookie 来确定用户登录状态。服务器还可以根据需要修改 cookie 内容。
cookie 包含以下属性:
容量通常不超过 5M,存储内容格式为字符串,可以格式化为字符串的资源均可存储在其中。localstorage 中数据在同域下可共享,而 sessionstorage 只在会话生命周期中有效。
// 保存数据localStorage.setItem('key', 'value');sessionStorage.setItem('key', 'value');// 读取数据localStorage.getItem('key');sessionStorage.getItem('key');// 删除单个数据localStorage.removeItem('key');sessionStorage.removeItem('key');// 删除全部数据localStorage.clear();sessionStorage.clear();// 获取索引的keylocalStorage.key('index');sessionStorage.key('index');
window.addEventListener('storage', function(e) {console.log(e.key, e.oldValue, e.newValue);});
可用于存储非结构化数据,该数据库属于非关系型数据库,便于查询存储。
一个示例演示 indexedDB 的使用方式,如下:
const DB_NAME = 'Netease';const DB_VERSION = 1;const OB_NAMES = {UseKeyPath: 'UseKeyPath',UseKeyGenerator: 'UseKeyGenerator',};function openIndexDB() {// The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.return new Promise((resolve, reject) => {/*** NOTE:* 1. 第一次打开可能会提示用户获取 indexDB 的权限* 2. 浏览器隐身模式不会存在本地,只会存储在内存中*/const request = window.indexedDB.open(DB_NAME, DB_VERSION);request.onerror = function(event) {// Do something with request.errorCode!console.log('open request failed', event);console.error(event.target.error);};request.onsuccess = function(event) {// Do something with request.result!// console.log('open request success', event)var db = event.target.result;db.onerror = function(e) {console.error('Database error: ', e.target.error);reject(e.target.error);};db.onclose = e => {console.error('Database close:', e.target.error);reject(e.target.error);};resolve(db);};request.onupgradeneeded = function(event) {/*** NOTE:* 1. 创建新的 objectStore* 2. 删除旧的不需要的 objectStore* 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建*/// The IDBDatabase interfaceconsole.log('onupgradeneeded', event);var db = event.target.result; // Create an objectStore for this databaseobUseKeypath(db);obUseKeyGenerator(db);/*** NOTE:* transaction* 三个事件:* 1. error* 2. abort* 3. complete* 两个方法:* 1. abort* Rolls back all the changes to objects in the database associated with this transaction. If this transaction has been aborted or completed, then this method throws an error event.* 2. objectStore* Returns an IDBObjectStore object representing an object store that is part of the scope of this transaction.*/db.transaction.oncomplete = function(e) {console.log('obj create success', e);};};});}function obUseKeypath(db) {const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {keyPath: 'time',});objectStore.createIndex('errorCode', 'errorCode', {unique: false,});objectStore.createIndex('level', 'level', {unique: false,});}function obUseKeyGenerator(db) {const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {autoIncrement: true,});objectStore.createIndex('errorCode', 'errorCode', {unique: false,});objectStore.createIndex('time', 'time', {unique: true,});objectStore.createIndex('level', 'level', {unique: false,});}/*** 添加数据* @param {array} docs 要添加数据* @param {string} objName 仓库名称*/function addData(docs, objName) {if (!(docs && docs.length)) {throw new Error('docs must be a array!');}return openIndexDB().then(db => {const tx = db.transaction([objName], 'readwrite');tx.oncomplete = e => {console.log('tx:addData onsuccess', e);return Promise.resolve(docs);};tx.onerror = e => {e.stopPropagation();console.error('tx:addData onerror', e.target.error);return Promise.reject(e.target.error);};tx.onabort = e => {console.warn('tx:addData abort', e.target);return Promise.reject(e.target.error);};const obj = tx.objectStore(objName);docs.forEach(doc => {const req = obj.add(doc);req.onerror = e => {console.error('obj:addData onerror', e.target.error);};});});}const TestData = [{event: 'NE-TEST1',level: 'warning',errorCode: 200,url: 'http://www.example.com',time: '2017/11/8 下午4:53:039',isUploaded: false,},];addData(TestData, OB_NAMES.UseKeyGenerator).then(() =>addData(TestData, OB_NAMES.UseKeyPath),);