聊聊前端本地缓存

1,151 阅读9分钟

前言

前端本地储存方案有很多,我们常用的存储方式主要两类:cookie、webStorage(localStorage 和 sessionStorage)

localStorage、sessionStorage 是在 HTML5 中新增(HTML5 是 2008 年正式发布的),在前端的“上古时代”里,我们前端想要存储数据,只有一种方式,那就是 cookie。

前端本地存储三剑客异同点

分类生命周期存储容量存储位置使用场景访问权限
cookie默认保存在内存中,随浏览器关闭失效(如果设置过期时间,在到过期时间后失效)4KB保存在客户端,每次请求时都会带上会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息),浏览器行为跟踪(如跟踪分析用户行为等)任意窗口
localStorage理论上永久有效的,除非主动清除。5MB(不同浏览器情况不同,safari 2.49M)保存在客户端,不与服务端交互。节省网络流量适合长期保存在本地的数据,比如页面的默认偏好配置、长期登录任意窗口
sessionStorage仅在当前网页会话下有效,关闭页面或浏览器后会被清除。5MB(部分浏览器没有限制)同上敏感账号一次性登录当前页面窗口

cookie 和 session

Cookie 和 Session 都是用来跟踪浏览器用户身份的会话方式。Cookie 一般用来保存用户信息,Session 主要作用是通过服务端记录用户的状态。

区别:

  • 作用范围不同:cookie 存放在浏览器中,而 session 存放在服务器。

  • 隐私策略不同: cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗,所以一般存放一些不敏感信息;而 session 更安全。

  • 存储大小不同:cookie 一般4kb大,很多浏览器都限制一个域名最多保存 50 个 cookie,而 session 并没有大小的要求。

  • 存取方式的不同: cookie 中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。

  • 有效期不同:Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。

Cookie的缺陷:

  1. Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
  2. 由于在HTTP请求中的Cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
  3. Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。

一些人反对Cookies在网络中的应用,他们的理由如下:

  1. 识别不精确
  2. 识别有时候会发生错误
  3. 隐私,安全和广告

Cookies的替代品

  1. Brownie方案,是一项开放源代码工程,由SourceForge发起。Brownie曾被用以共享在不同域中的接入,而Cookies则被构想成单一域中的接入。这项方案已经停止开发
  2. P3P(个人隐私安全平台项目),由全球资讯联盟网所开发,让用户获得更多控制个人隐私权利的协议。
  3. 在与服务器传输数据时,通过在地址后面添加唯一查询串,让服务器识别是否合法用户,也可以避免使用Cookie

indexedDB

随着web应用程序的不断发展,5M的存储大小对于一些大型的web应用程序来说有些不够,web Storage只能存储string类型的数据。对于Object类型的数据只能先用JSON.stringify()转换一下在存储。前端社区又提出了浏览器数据库存储这个概念。而Web SQL DatabaseindexedDB(索引数据库)是对这个概念的实现。其中Web SQL Database在目前来说基本已经被放弃(下文有介绍)。

indexedDB 介绍

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。

IndexedDB 是一种 NoSQL 数据库 (非关系型数据库,使用键值对存储数据,且结构不固定,非常类似JavaScript中的纯对象),和关系型数据库不同的是,IndexedDB是面向对象的,它存储的是Javascript对象,类似 mangoDB的存储方式。

使用场景

  • 缓存大文件: 音视频 Blod 格式、图片文件流、 Base64资源。

  • WebGL、3D模型资源: 用来存储大量的 hdr、glb、gltf 等文件。

  • 即时聊天工具(IM),大量消息需要存在本地。

  • 固定的键值对: 数据字典、存储国标数据。

  • 离线 Web 应用程序。

  • 笔记软件:如我常用的Notion这种类型的软件可能会使用到indexedDB这项技术,当用户没有网络时离线储存数据,并且当有网络时再将离线储存的数据上传至服务器。

  • 网站上的富文本编辑器:数据量大,且需要返回上一步,草稿等功能。这类功能讲数据放在服务端会出现各种问题是不现实的,所以就需要接入客户端数据库来解决。

  • 股票金融类的统计图数据:这些数据都需要实时展示且过去数据基本不会改变,可以把已加载过的数据缓存在客户端来减少服务端的压力。

MDN文档

developer.mozilla.org/zh-CN/docs/…

兼容

image.png

特点

  • 非关系型数据库(NoSql):我们都知道MySQL等数据库都是关系型数据库,它们的主要特点就是数据都以一张二维表的形式存储,而Indexed DB是非关系型数据库(非关系型数据库,使用键值对存储数据,且结构不固定,非常类似JavaScript中的纯对象)。

  • 键值对储存:IndexedDB 内部采用对象仓库存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

  • 持久化存储:IndexedDB存储的数据,会永远存在,除非手动删除数据库。

  • 异步操作:IndexedDB 执行的操作是异步执行的,以免阻塞应用程序 (IndexedDB 最初包括同步和异步 API,有需要可以重新引入同步 API)。用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

  • 支持事务:IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源策略:IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

  • 储存空间大:IndexedDB 的储存空间非常大,具体取决本地磁盘与浏览器限制,Chrome67 之前的版本是50%的硬盘空间,因此,如果硬盘驱动器是 500GB,那么浏览器的总存储容量为 250GB。

  • 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)

优劣:

  • 优势

    • 可以存储大量结构化数据,容量较大。
    • 提供异步 API,不会阻塞主线程。
  • 劣势

    • API 相对复杂,使用起来可能较为复杂,学习成本较高。
    • 不支持所有浏览器,需要考虑兼容性问题。

使用

  • 打开数据库。
  • 在数据库中创建一个对象存储(object store)。
  • 启动事务,并发送一个请求来执行一些数据库操作,如添加或获取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 对结果进行一些操作(可以在 request 对象中找到)。

什么是事务

一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的

  1. 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  2. 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
const dbName = 'myDatabase';
const storeName = 'myObjectStore';

// 打开或创建数据库
const request = indexedDB.open(dbName, 1);
// 该事件仅在最新的浏览器中实现,仅在新数据库或新版本号时触发,onupgradeneeded是唯一可以修改数据库结构的地方!
request.onupgradeneeded = event => {
  const db = event.target.result;

  // 创建对象存储空间
  const objectStore = db.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true });

  // 创建索引
  objectStore.createIndex('nameIndex', 'name', { unique: false });
};

request.onsuccess = event => {
  const db = event.target.result;

  // 添加数据
  function addData(name, age) {
		// 打开事务
    const transaction = db.transaction(storeName, 'readwrite');
		// 指定store
    const objectStore = transaction.objectStore(storeName);

    const newItem = { name, age };
    const addRequest = objectStore.add(newItem);

    addRequest.onsuccess = () => {
      console.log('【ADD】Data added successfully');
    };
  }

  // 删除数据
  function deleteData(id) {
    const transaction = db.transaction(storeName, 'readwrite');
    const objectStore = transaction.objectStore(storeName);

    const deleteRequest = objectStore.delete(id);

    deleteRequest.onsuccess = () => {
      console.log('【DELETE】Data deleted successfully');
    };
  }

  // 更新数据
  function updateData(id, newData) {
    const transaction = db.transaction(storeName, 'readwrite');
    const objectStore = transaction.objectStore(storeName);

    const getRequest = objectStore.get(id);

    getRequest.onsuccess = () => {
      const data = getRequest.result;

      if (data) {
        const updatedData = { ...data, ...newData };
        const updateRequest = objectStore.put(updatedData);

        updateRequest.onsuccess = () => {
          console.log('【UPDATE】Data updated successfully');
        };
      }
    };
  }

  // 查询数据
  function fetchData() {
    const transaction = db.transaction(storeName, 'readonly');
    const objectStore = transaction.objectStore(storeName);

    const data = [];
    const cursorRequest = objectStore.openCursor();

    cursorRequest.onsuccess = event => {
      const cursor = event.target.result;
      if (cursor) {
        data.push(cursor.value);
        cursor.continue();
      } else {
        console.log('Fetched data:', data);
      }
    };
  }

  // 添加示例数据
  addData('Alice', 25);
  addData('Bob', 30);

  // 删除示例数据
  deleteData(1);

  // 更新示例数据
  updateData(2, { age: 31 });

  // 查询所有数据
  fetchData();

  // 关闭数据库连接
  db.close();
};

request.onerror = event => {
  console.error('Database error:', event.target.error);
};

第三方库

localForage

  • 离线存储,改进。使用简单但功能强大的API封装IndexedDB、WebSQL或localStorage
  • 23.5k stars
  • github.com/localForag…
localforage.setItem('key', 'value', function (err) {
  // if err is non-null, we got an error
  localforage.getItem('key', function (err, value) {
    // if err is non-null, we got an error. otherwise, value is the value
  });
});

Dexie.js

  const db = new Dexie('MyDatabase');

    // Declare tables, IDs and indexes
    db.version(1).stores({
        friends: '++id, name, age'
    });

    // Find some old friends
    await db.friends
        .where('age')
        .above(75)
        .toArray();

    // or make a new one
    await db.friends.add({
        name: 'Camilla',
        age: 25,
        street: 'East 13:th Street',
        picture: await getBlob('camilla.png')
    });

zangodb

  • 给HTML5 IndexedDB 封装类似 mongodb 类似接口, 如果你熟悉 mongodb , 那可以使用zangodb
  • 1.1k star
  • github.com/erikolson1…
let db = new zango.Db('mydb', { people: ['age'] });
let people = db.collection('people');

let docs = [
    { name: 'Frank', age: 20 },
    { name: 'Thomas', age: 33 },
    { name: 'Todd', age: 33 },
    { name: 'John', age: 28 },
    { name: 'Peter', age: 33 },
    { name: 'George', age: 28 }
];

people.insert(docs).then(() => {
    return people.find({
        name: { $ne: 'John' },
        age: { $gt: 20 }
    }).group({
        _id: { age: '$age' },
        count: { $sum: 1 }
    }).project({
        _id: 0,
        age: '$_id.age'
    }).sort({
        age: -1
    }).forEach(doc => console.log('doc:', doc));
}).catch(error => console.error(error));

JsStore

  var value = {
    column1: value1,
    column2: value2,
    column3: value3,
    ...
    columnN: valueN
};

connection.insert({
    into: "TABLE_NAME",
    values: [Value], //you can insert multiple values at a time

}).then(function(rowsAffected) {
    if (rowsAffected > 0) {
        alert('Successfully Added');
    }
}).catch(function(error) {
    alert(error.message);
});

localForage

localForage 的出现几乎抹平 indexedDB 缺陷,让我们轻松无负担的在浏览器中使用 indexedDBlocalForage 是 一个简单的 Polyfill,提供了简单的客户端数据存储的值语法。它在后台使用 IndexedDB,并在不支持 IndexedDB 的浏览器中回退到 WebSQL 或 localStorage。

什么是Polyfill?

polyfill (polyfiller),指的是一个代码块。这个代码块向开发者提供了一种技术, 这种技术可以让浏览器提供原生支持,抹平不同浏览器对API兼容性的差异。 --《介绍HTML5》

localForage 的使用

  1. 安装
npm i localforage
  1. 引用
import localforage from 'localforage'
  1. 创建一个 indexedDB
const myIndexedDB = localforage.createInstance({
  name: 'myIndexedDB',
})
  1. 存值
myIndexedDB.setItem(key, value)
  1. 取值

由于indexedDB的存取都是异步的,建议使用 promise.then() 或 async/await 去读值

myIndexedDB.getItem('somekey').then(function (value) {
  // we got our value
}).catch(function (err) {
  // we got an error
});

or

try {
  const value = await myIndexedDB.getItem('somekey');
  // This code runs once the value has been loaded
  // from the offline store.
  console.log(value);
} catch (err) {
  // This code runs if there were any errors.
  console.log(err);
}
  1. 删除某项
myIndexedDB.removeItem('somekey')
  1. 重置数据库
myIndexedDB.clear()

以上是比较常用的方式,细节及其他使用方式请参考官方中文文档

在 VUE 中推荐配合 Pinia 使用 localForage

如果你想使用多个数据库,建议通过 pinia 统一管理所有的数据库,这样数据的流向会更明晰,数据库相关的操作都写在 store 中,让你的数据库更规范化。

// store/modules/indexedDB.ts
import { defineStore } from 'pinia';
import localforage from 'localforage';

export const useIndexedDBStore = defineStore('indexedDB', {
  state: () => ({
    filesDB: localforage.createInstance({
      name: 'filesDB',
    }),
    usersDB: localforage.createInstance({
      name: 'usersDB',
    })
  }),
  actions: {
    // 写入
    async setFilesDB(key: string, value: any) {
      this.filesDB.setItem(key, value);
    },
    // 获取
    async getFilesDB(key: string) {
      return this.filesDB
        .getItem(key)
        .then(function (value) {
          console.log('数据获取成功: :>> ', value);
          return value;
        })
        .catch(function (err) {
          console.log('数据获取失败: :>> ', err);
          return err;
        });
    },
    // 移除
    async removeFilesDB(key: string) {
      this.filesDB.removeItem(key);
    },
    // 重置
    async clearFilesDB() {
      this.filesDB.clear();
    },
  },
});

使用的时候,就直接调用 store 中的方法

 import { useIndexedDBStore } from '/@/store/modules/indexedDB';
  const indexedDBStore = useIndexedDBStore();

  function storeWriteIn() {
    const file1 = { a: 'hello' };
    indexedDBStore.setFilesDB('file1', file1);
  }

  async function storeGain() {
    let file1 = await indexedDBStore.getFilesDB('file1');
    console.log('file1 :>> ', file1);
  }

  function storeDel() {
    indexedDBStore.removeFilesDB('file1');
  }

  function storeReset() {
    indexedDBStore.clearFilesDB();
  }

indexedDB存储和localStorage存储对比

  • indexedDB存储IE10+支持,localStorage存储IE8+支持,后者兼容性更好;

  • indexedDB存储比较适合键值对较多的数据,我之前不少项目需要存储多个字段,使用的是localStorage存储,结果每次写入和写出都要字符串化和对象化,很麻烦,如果使用indexedDB会轻松很多,因为无需数据转换。

  • indexedDB存储可以在workers中使用,localStorage貌似不可以。这就使得在进行PWA开发的时候,数据存储的技术选型落在了indexedDB存储上面。

和PWA技术的关系 PWA全称为“Progressive Web Apps”,渐进式网页应用。 (vue: 渐进式 JavaScript 框架)

渐进式是指 表现为缓慢的、持续的量的积累过程.

PWA的核心技术包括:

  • Web App Manifest – 在主屏幕添加app图标,定义手机标题栏颜色之类
  • Service Worker – 缓存,离线开发,以及地理位置信息处理等
  • App Shell – 先显示APP的主结构,再填充主数据,更快显示更好体验
  • Push Notification – 消息推送,之前有写过“简单了解HTML5中的Web Notification桌面通知”

Web SQL Database

WebSQL(已于 2010 年 9 月 18 日起弃用)

  • 优势

    • 提供 SQL 数据库的操作方式,适用于复杂的数据操作。
  • 劣势

    • 已被废弃,不被推荐使用,不支持所有浏览器。

indexedDB为何替代了Web SQL Database

cacheStorage

在网站上浏览页面时,浏览器会下载许多资源,例如 HTML、CSS、JavaScript 文件和图像。为了提高网页加载速度和降低网络流量,浏览器使用缓存来存储这些资源,以便在将来的页面访问中能够更快地获取它们。CacheStorage 是Service Worker API下的接口,截图如下:

image.png

其中,Cache直接和请求打交道,CacheStorage和Cache对象打交道,我们可以直接使用全局的caches属性访问CacheStorage,例如,虽然API上显示的是CacheStorage.open(),但我们实际使用的时候,直接caches.open()就可以了。

至于 Cache 和 CacheStorage 具体的增删改查API直接去这里一个一个找,Service Worker API的知识体量实在惊人,若想要系统学习,那可要做好充足的心理准备了。

image.png

使用场景

  1. 静态资源缓存: 将网站的静态资源(例如 CSS、JavaScript、图片等)缓存起来,以便在用户再次访问时能够更快地加载页面。
  2. 离线访问: 使用 Cache API 可以缓存应用程序的核心资源,使用户在离线状态下仍然能够访问内容,提供更好的离线体验。
  3. Service Worker 缓存: Service Worker 是一种运行在浏览器后台的脚本,可以拦截网络请求并将其缓存起来,从而实现更高级的缓存控制和离线功能。
  4. 数据预取和预加载: 在用户访问页面之前,提前缓存可能需要的资源,以减少页面加载时间。
  5. 动态内容缓存: 缓存 API 还可以用于缓存动态生成的内容,如 API 响应,以减轻服务器负担并提高响应速度。
  6. 减少网络请求: 缓存 API 可以减少重复的网络请求,从而降低服务器压力,提高性能。
  7. 图片懒加载: 缓存 API 可以用于懒加载图片,即只在用户需要时才加载图片资源,从而节省带宽和提高页面加载速度。
  8. 数据更新与刷新策略: 缓存 API 可以用于实现数据更新和刷新策略,确保用户始终获取最新的数据。
  9. 资源预加载: 提前缓存可能需要的资源,以便在用户访问相关页面时能够更快地加载内容。
  10. 响应速度优化: 缓存可以显著提高页面加载速度,从而改善用户体验和 SEO。

优劣:

  • 优势

    • 适用于缓存资源,如图片、样式等,提高性能。
    • 可以灵活控制缓存过程,支持更复杂的缓存策略。
  • 劣势

    • 主要用于缓存资源,不适合存储业务数据

本地存储进行跨标签页通信

跨标签页通信是指在浏览器中的不同标签页之间进行数据传递和通信的过程。在传统的Web开发中,每个标签页都是相互独立的,无法直接共享数据。然而,有时候我们需要在不同的标签页之间进行数据共享或者实现一些协同操作,这就需要使用跨标签页通信来实现。

Cookie、IndexedDB

CookieIndexedDB,可以用于在多个标签页之间共享数据。可以使用 setInterval 定时轮询来实现跨标签页通信,但相对于实时性较差。

localStorage、sessionStorage

当存储区域(localStorage 或 sessionStorage)被修改时,将触发 storage 事件,这是 MDN 上的解释)实际是:

  • 如果当前页面的 localStorage 值被修改,只会触发其他页面的 storage 事件,不会触发本页面的 storage 事件。
  • window.onstorage 事件只对 localStorage 的修改有效,sessionStorage 的修改不能触发。
  • localStorage 的值必须发生变化,如果设置成相同的值则不会触发。

window.onstorage 事件配合 localStorage 很完美,但是唯独对 sessionStorage 无效,目前没有发现一个很好且详细的解释。

由 window.onstorage 事件监听器发送给回调函数的事件对象有几个属性如下:

PropertyTypeDescription
target 只读[EventTarget]事件目标 (DOM 树中的最大目标)
type 只读[DOMString]事件的类型
bubbles 只读[Boolean]事件通常是否会出现冒泡
cancelable 只读[Boolean]事件是否可取消
key 只读[DOMString] (string)更改键
oldValue 只读[DOMString] (string)正在更改键的旧值
newValue 只读[DOMString] (string)正在更改键的新值
url 只读DOMString键更改的文档的地址
storageArea 只读[Storage]受影响的存储对象

例如,在一个标签页中修改LocalStorage的值:

  function saveLocalStorage() {
    let date = new Date().getTime();
    localStorage.setItem('change-event', String(date));
  }

在另一个标签页中监听LocalStorage的变化:

  window.onstorage = function (e) {
    console.log(e);
    console.log(`The ${e.key} key has been changed from ${e.oldValue} to ${e.newValue}.`);
  };

跨标签页通信除了通过本地通信还可以通过 Broadcast Channel APIWebsocket 等等。