Http缓存、本地缓存策略

在这里插入图片描述

Http缓存 & 本地缓存

缓存可以划分为资源缓存数据缓存两大类

资源缓存:用于静态资源按照我们所期望的规则存储在本地,用户访问页面时如果相关资源未发生改变,可以直接从本地拿取资源渲染网页。缓存策略就是确定资源是否发生了更新。良好的缓存策略可以减少资源重复加载,进而提高网页的整体加载速度, 减少服务器负载。

数据缓存:用于将常用数据存储在本地,如:用户登录态信息、不常变动且不涉及数据安全问题的数据等。数据缓存的方案有:cookie、localstorage、indexedDB等。

资源缓存

浏览器缓存策略通常分为:强缓存协商缓存,当然还包括service worker

强缓存
  • 强缓存的相关字段有expires,cache-control

    • expires是 HTTP1.0 提出的,它描述的是一个绝对时间,由服务端返回给客户端。会受到本地时间的影响。(通常表示为: Expires: Sun, 07 Apr 2024 08:22:17 GMT

    • cache-control是 HTTP1.1 提出的,它描述的是一个相对时间,单位(秒/s),由服务端返回。相对的是服务端的时间,所以不会受到本地时间的影响。(通常表示为:Cache-Control:max-age=86400

      指令
      max-age指定缓存的最大有效时间,单位为秒
      no-cache表示缓存需要重新验证,即每次都需要向服务器发送请求,与服务器进行新鲜度校验
      no-store不缓存资源到本地,每次都需要向服务器发送请求
      pubilc可被所有用户缓存,多用户进行共享,包括终端或CDN等中间代理服务器。
      private只能被浏览器客户端缓存,不允许CDN等中间代理服务器对其进行缓存。
  • 如果cache-controlexpires同时存在的话,cache-control的优先级高于expires

以下 index.js文件会命中强缓存,10秒内刷新页面会从本地读取文件,不会发生资源请求:

const http = require("http");
const fs = require("fs");
const path = require("path");

// 定义路由表
const routes = {
  "/": function html(req, res) {
    res.end("hello");
  },
  "/index.html": function html(req, res) {
    const data = fs.readFileSync(path.join(__dirname, "public", "/index.html"));
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end(data);
  },
  "/index.css": function css(req, res) {
    fs.readFile(path.join(__dirname, "public", "/index.css"), (err, data) => {
      res.writeHead(200, { "Content-Type": "text/css" });
      res.end(data);
    });
  },
  "/index.js": function js(req, res) {
    fs.readFile(path.join(__dirname, "public", "/index.js"), (err, data) => {
      res.writeHead(200, {
        "Content-Type": "text/javascript",
        // 设置响应头 Cache-Control 字段,表示浏览器缓存文件 10 秒
        "Cache-Control": "max-age=10",
        // 设置响应头 Expires 字段,表示浏览器缓存文件 10 秒
        Expires: new Date(Date.now() + 10 * 1000).toUTCString(),
      });
      res.end(data);
    });
  },
};

const server = http.createServer((req, res) => {
  console.log(req.url);
  return routes[req.url]?.(req, res);
});

server.listen(8080, () => {
  console.log("server start !");
});

在这里插入图片描述

协商缓存
  • 浏览器加载资源时,若强缓存未命中,将发送资源请求至服务器。若协商缓存命中,请求响应返回 304 状态码。

  • 协商缓存主要使用到两对请求响应头字段:

    • Last-ModifiedIf-Modified-Since
    • EtagIf-None-Match
  • Last-ModifiedIf-Modified-Since : Last-Modified表示资源的最后修改时间,服务器在响应请求时会将该参数返回给客户端 。 客户端在下一次请求时会将该参数作为If-Modified-Since的值发送给服务器。

    Last-Modified 由上一次请求的响应头返回,且该值会在本次请求中,通过请求头 If-Modified-Since 传递给服务端,服务端通过 If-Modified-Since 与资源的修改时间进行对比,若相同,则资源没有更新,请求响应返回 **304 ** 状态码,使用本地缓存资源。若不同,则在此日期后资源有更新,则将新的资源发送给客户端。

    Last-Modified:Tue, 26 Mar 2024 03:58:58 GMT

不过,通过文件的修改时间来判断资源是否更新是不明智的,因为很多时候文件更新时间变了,但文件内容未发生更改。这样一来,就出现了 ETagIf-None-Match

  • EtagIf-None-MatchETag表示资源的唯一标识符,服务器在响应请求时会将该参数返回给客户端。 客户端在下一次请求时会将该参数作为If-None-Match的值发送给服务器。

    不同于 Last-Modified,Etag 通过计算文件指纹,与请求传递过来的 If-None-Match 进行对比,若值不等,则将新的资源发送给客户端。

    Etag:W/“105805c9f6fd45ca2f5065b90e067a0a”

    ETag 的优先级比 Last-Modified 高

以下 index.css为协商缓存策略:根据文件是否更新来判断是否使用缓存,Etag同理

const http = require("http");
const fs = require("fs");
const path = require("path");
const etag = require("etag");
const fresh = require("fresh");

// 定义路由表
const routes = {
  "/": function html(req, res) {
    res.end("hello");
  },
  "/index.html": function html(req, res) {
    const data = fs.readFileSync(path.join(__dirname, "public", "/index.html"));
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end(data);
  },
  "/index.css": function css(req, res) {
    const cssFilePath = path.join(__dirname, "public", "/index.css");
    const css = fs.readFileSync(cssFilePath);

    // 读取文件描述信息,用于计算etag及设置Last-Modified
    const stat = fs.statSync(cssFilePath);
    console.log(stat);
    // 获取文件最后修改时间
    const Last_Modified = stat.mtime.toUTCString();
    const fileEtag = etag(stat);
    // 获取请求头中是否有 if-modufied-since 字段
    const ifModifiedSince = req.headers["if-modified-since"];
    // 协商判断
    const isModified = !ifModifiedSince || ifModifiedSince !== Last_Modified;

    res.setHeader("Last-Modified", Last_Modified);
    res.setHeader("ETag",fileEtag);

    // 根据请求头判断缓存是否是最新的
        let isFresh = fresh(req.headers, {
          etag: fileEtag,
          "last-modified": lastModified,
        });
      
    // 判断文件是否更改,更改则返回200,重新请求数据; 未更改则返回304
    res.writeHead(isFresh? 200 : 304, { "Content-Type": "text/css" });
    res.end(css);
  },
  "/index.js": function js(req, res) {
    fs.readFile(path.join(__dirname, "public", "/index.js"), (err, data) => {
      res.writeHead(200, {
        "Content-Type": "text/javascript",
        // 设置响应头 Cache-Control 字段,表示浏览器缓存文件 10 秒
        "Cache-Control": "max-age=10",
        // 设置响应头 Expires 字段,表示浏览器缓存文件 10 秒
        Expires: new Date(Date.now() + 10 * 1000).toUTCString(),
      });
      res.end(data);
    });
  },
};

const server = http.createServer((req, res) => {
  console.log(req.url);
  return routes[req.url]?.(req, res);
});

server.listen(8080, () => {
  console.log("server start !");
});

在这里插入图片描述

缓存策略
  1. 浏览器第一次发起请求时,本地无缓存,浏览器会向服务器发送请求,浏览器响应请求,在请求头中设置expirescache-control
  2. 刷新页面,浏览器会先获取资源缓存的响应头信息,根据expirescache-control判断资源是否过期。若没过期,则直接从缓存中获取资源信息, 所以此次请求不会和服务器进行通信
  3. 如果已过期,浏览器会向服务器发送请求,会携带第一次请求返回的有关缓存的响应头字段信息,客户端会通过If-None-Match头将先前服务器端发送过来的Etag数据发送给服务器,服务器会对比这个客户端发过来的Etag是否与服务器相同,若相同,就将If-None-Match的值设置为false,返回状态304,客户端继续使用本地缓存;若不相同就将If-None-Match的值设为true,返回状态200,客户端重新接收服务器端返回的数据;
Service Worker(策略缓存)

Service Worker 是专门的 JavaScript 资源,充当网络浏览器和 Web 服务器之间的代理。它们旨在通过提供离线访问来提高可靠性,以及提升网页性能。

缓存策略: 仅限缓存、仅限网络、缓存优先,回退到网络、网络优先,回退到缓存、重新验证时过时

详情请查阅:ServiceWorker

  • 仅限缓存

当 Service Worker 控制着页面时,匹配的请求将只会进入缓存。这意味着,任何缓存的资源都需要进行预缓存,以使模式正常工作,并且在 Service Worker 更新之前,绝不会在缓存中更新这些资源。

在这里插入图片描述

  • 仅限网络

与“仅缓存”相反,“仅限网络”是指请求通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。这是确保内容新鲜度的好策略(比如使用标记),但需要权衡的是,用户离线时自定义设置将永远不会起作用。

在这里插入图片描述

  • 缓存优先,回退到网络

在此策略中,需要更深入地发挥作用。对于匹配请求,流程如下:

  1. 请求到达缓存。如果资源位于缓存中,请从缓存中提供。
  2. 如果请求不在缓存中,请转到网络。
  3. 网络请求完成后,将其添加到缓存中,然后从网络返回响应。

在这里插入图片描述

  • 网络优先,回退到缓存
  1. 您先前往网络请求一个请求,然后将响应放入缓存中。
  2. 如果您日后处于离线状态,则会回退到缓存中该响应的最新版本。

此策略非常适合 HTML 或 API 请求,当您想在线获取资源的最新版本,同时希望离线访问最新的可用版本时。

在这里插入图片描述

  • 重新验证时过时
  1. 在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应。
  2. 对于后续请求,首先从缓存提供资源,然后“在后台”从网络重新请求该资源,并更新资源的缓存条目。
  3. 对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。

对于很重要、但并非很重要的事情,这是一种很好的策略。 可以将内容想象成社交媒体网站的头像。 它们会在用户执行相应操作时进行更新,但并不是每个请求都绝对有必要使用最新版本。

在这里插入图片描述

详情请查阅:缓存策略

状态码
  • 200:强缓存 Expires / Cache-Control 失效时,返回新的资源文件
  • 200(from disk cache) Expires / Cache-Control 两者都存在且有效,Cache-Control 优先 Expires ,浏览器从本地获取资源成功。
  • 200(from memory cache)
  • 304(Not Modified)协商缓存 Last-modified / Etag 有效,则服务端返回该状态码。

缓存技术方案实践

静态资源优化方案与思考
  • 配置超长时间的本地缓存 —— 节省带宽,提高性能
  • 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  • 静态资源 CDN 部署 —— 优化网络请求
  • 更资源发布路径实现非覆盖式发布 —— 平滑升级
充分利用浏览器缓存机制
  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
  • 对于频繁变动的资源(比如经常需要刷新的首页,资讯论坛新闻类),可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。
  • 静态资源文件通过 Service Worker 进行缓存控制和离线化加载

数据缓存

  • cookie 4K,可手动设置失效实践
  • localStorage 5M,需要手动清除,否则一直存在
  • sessionStorage 5M,仅限同标签访问,页面关闭就会清理
  • indexedDB 无限容量,浏览器端数据库,需手动清除,否则一直存在
cookie

​ cookie 实际是一小段文本信息。客户端请求服务端,如果服务器需要记录该用户的登录状态,就需要使用在响应时向客户端返回一个 cookie。客户端浏览器会将 cookie 保存。客户端再次请求该网站时,会携带 cookie 一同提交到服务端。此时服务端检查该 cookie 来确定用户登录状态。服务器还可以根据需要修改 cookie 内容。

cookie 包含以下属性:

  • Expires :cookie 过期时间,绝对时间;
  • Max-Age:cookie 失效时间,相对时间;
  • Domain:指定 cookie 可以送达的主机名。
  • Path:指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部
  • Secure:一个带有安全属性的 cookie 只有在请求使用 SSL 和 HTTPS 协议的时候才会被发送到服务器。
  • HttpOnly: 设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。
localStorage、sessionStorage

​ 容量通常不超过 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();
// 获取索引的key
localStorage.key('index');
sessionStorage.key('index');

// 事件监听
window.addEventListener('storage', function(e) {
  console.log(e.key, e.oldValue, e.newValue);
});

借助 localfroge 库做数据本地缓存

localfroge 中文文档

indexedDB

可用于存储非结构化数据,该数据库属于非关系型数据库,便于查询存储。

一个示例演示 indexedDB 的使用方式,如下:

const DB_NAME = "Netease";
const DB_VERSION = 1;
const OB_NAMES = {
  UseKeyPath: "UseKeyPath",
  UseKeyGenerator: "UseKeyGenerator",
};

// 1.打开数据库
function openIndexedDB() {
  return new Promise((resolve, reject) => {
    /**
     * NOTE:
     * 1. 第一次打开可能会提示用户获取 indexDB 的权限
     * 2. 浏览器隐身模式不会存在本地,只会存储在内存中
     */
    // 创建连接池
    const pool = indexedDB.open(DB_NAME, DB_VERSION);
    pool.onerror = function (event) {
      console.error(event.target.error);
    };
    pool.onsuccess = function (event) {
      let 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);
    };
    pool.onupgradeneeded = function (event) {
      /**
       * NOTE:
       * 1. 创建新的 objectStore
       * 2. 删除旧的不需要的 objectStore
       * 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建
       */
      // The IDBDatabase interface
      console.log("onupgradeneeded", event);
      var db = event.target.result; // Create an objectStore for this database
      obUseKeypath(db);
      obUseKeyGenerator(db);
      db.transaction.oncomplete = function (e) {
        console.log("obj create success", e);
      };
    };
  });
}

// 2.创建对象仓库
function obUseKeypath(db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: "time",
  });
  objectStore.createIndex("name", "name", {
    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,
  });
}

// 3.添加数据
/**
 * 添加数据
 * @param {array} docs 要添加数据
 * @param {string} objName 仓库名称
 */
function addData(docs, objName) {
  if (!(docs && docs.length)) {
    throw new Error("docs must be a array!");
  }
  return openIndexedDB().then((db) => {
    // 事务
    const tx = db.transaction([objName], "readwrite");

    tx.oncomplete = function () {
      return Promise.resolve(docs);
    };
    tx.onerror = function (event) {
      e.stopPropagation();
      return Promise.reject(event.target.error);
    };
    tx.onabort = (e) => {
      console.warn("tx:addData abort", e.target);
      return Promise.reject(e.target.error);
    };

    // 提交事务
    const ob = tx.objectStore(objName);
    docs.forEach((doc) => {
      ob.add(doc);
    });
  });
}
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)
);

以上是我们完全使用 indexedDB 原生 api 实现,通常我们为了简化开发,会选用例如Dexie 实现。

<script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>

var db = new Dexie("FriendDatabase");
db.version(1).stores({
  friends: "++id,name,age"
});


db.friends.add({name: "Josephine", age: 21}).then(function() {
    return db.friends.where("age").below(25).toArray();
}).then(function (youngFriends) {
    alert ("My young friends: " + JSON.stringify(youngFriends));
}).catch(function (e) {
    alert ("Error: " + (e.stack || e));
});
indexedDB 的使用场景
  • 游戏
  • 前端数据缓存
  • 离线应用数据存储

end
在这里插入图片描述

  • 41
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值