现代浏览器中提供了多种存储机制,打开浏览器的控制台(Mac 可以使用 Command + Option + J 快捷键,Windows 可以使用 Control + Shift + J 快捷键)。选择 Application 选项卡,可以在 Storage中 看到 Local Storage、Session Storage、IndexedDB、Web SQL、Cookies 等:
那数据存储在浏览器中有什么使用场景呢?在以下情况下,将数据存储在浏览器中成为更可行的选择:
- 在浏览器存储中保存应用状态,比如保持用户偏好(用户特定的设置,例如亮模式或暗模式、字体大小等);
- 创建离线工作的渐进式 Web 应用,除了初始下载和更新之外没有服务器端要求;
- 缓存静态应用资源,如 HTML、CSS、JS 和图像等;
- 保存上一个浏览会话中的数据,例如存储上一个会话中的购物车内容,待办事项列表中的项目,记住用户是否以前登录过等。
无论哪种方式,将这些信息保存在客户端可以减少额外且不必要的服务器调用,并帮助提供离线支持。不过,需要注意,由于实现差异,浏览器存储机制在不同浏览器中的行为可能会有所不同。除此之外,许多浏览器已删除对 Web SQL 的支持,建议将现有用法迁移到 IndexedDB。
所以下面我们将介绍 Local Storage、Session Storage、IndexedDB、Cookies 的使用方式、使用场景以及它们之间的区别。
2. Web Storage
HTML5 引入了 Web Storage,这使得在浏览器中存储和检索数据变得更加容易。Web Storage API 为客户端浏览器提供了安全存储和轻松访问键值对的机制。Web Storage 提供了两个 API 来获取和设置纯字符串的键值对:
-
localStorage
:用于存储持久数据,除非用户手动将其从浏览器中删除,否则数据将终身存储。即使用户关闭窗口或选项卡,它也不会过期; -
sessionStorage
:用于存储临时会话数据,页面重新加载后仍然存在,关闭浏览器选项卡时数据丢失。
Web Storage API 由 4 个方法 setItem()
、getItem()
、removeItem()
、clear()
、key()
和一个 length
属性组成,以 localStorage 为例:
setItem()
:用于存储数据,它有两个参数,即key
和value
。使用形式:localStorage.setItem(key, value)
;getItem()
:用于检索数据,它接受一个参数 key,即需要访问其值的键。使用形式:localStorage.getItem(key)
;removeItem()
:用于删除数据,它接受一个参数 key,即需要删除其值的键。使用形式:localStorage.removeItem(key)
;clear()
:用于清除其中存储的所有数据,使用形式:localStorage.clear()
;key()
:该方法用于获取 localStorage 中数据的所有key,它接受一个数字作为参数,该数字可以是 localStorage 项的索引位置。
javascript
console.log(typeof window.localStorage) // Object
// 存储数据
localStorage.setItem("colorMode", "dark")
localStorage.setItem("username", "zhangsan")
localStorage.setItem("favColor", "green")
console.log(localStorage.length) // 3
// 检索数据
console.log(localStorage.getItem("colorMode")) // dark
// 移除数据
localStorage.removeItem("colorMode")
console.log(localStorage.length) // 2
console.log(localStorage.getItem("colorMode")) // null
// 检索键名
window.localStorage.key(0); // favColor
// 清空本地存储
localStorage.clear()
console.log(localStorage.length) // 0
localStorage 和 sessionStorage 都非常适合缓存非敏感应用数据。可以在需要存储少量简单值并不经常访问它们是使用它们。它们本质上都是同步的,并且会阻塞主 UI 线程,所以应该谨慎使用。
我们可以在浏览器上监听 localStorage 和 sessionStorage 的存储变化。 storage 事件在创建、删除或更新项目时触发。侦听器函数在事件中传递,具有以下属性:
newValue
:当在存储中创建或更新项目时传递给 setItem() 的值。 当从存储中删除项目时,此值设置为 null。oldValue
:创建新项目时,如果该键存在于存储中,则该项目的先前的值。key
:正在更改的项目的键,如果调用 .clear(),则值为 null。url
:执行存储操作的 URL。storageArea
:执行操作的存储对象(localStorage 或 sessionStorage)。
通常,我们可以使用 window.addEventListener("storage", func)
或使用 onstorage
属性(如 window.onstorage = func
)来监听 storage
事件:
ini
window.addEventListener('storage', e => {
console.log(e.key);
console.log(e.oldValu);
console.log(e.newValue);
});
window.onstorage = e => {
console.log(e.key);
console.log(e.oldValu);
console.log(e.newValue);
});
注意,该功能不会在发生更改的同一浏览器选项卡上触发,而是由同一域的其他打开的选项卡或窗口触发。此功能用于同步同一域的所有浏览器选项卡/窗口上的数据。 因此,要对此进行测试,需要打开同一域的另一个选项卡。
localStorage 和 sessionStorage 只能存储 5 MB 的数据,因此需要确保存储的数据不会超过此限制。
javascript
localStorage.setItem('a', Array(1024 * 1024 * 5).join('a'))
localStorage.setItem('b', 'a')
// Uncaught DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of `a` exceeded the quota.
在上面的例子中,收到了一个错误,首先创建了一个5MB的大字符串,当再添加其他数据时就报错了。
另外,localStorage 和 sessionStorage 只接受字符串。可以通过 JSON.stringify
和 JSON.parse
来解决这个问题:
css
const user = {
name : "zhangsan",
age : 28,
gender : "male",
profession : "lawyer"
};
localStorage.setItem("user", JSON.stringify(user));
localStorage.getItem("user"); // '{"name":"zhangsan","age":28,"gender":"male","profession":"lawyer"}'
JSON.parse(localStorage.getItem("user")) // {name: 'zhangsan', age: 28, gender: 'male', profession: 'lawyer'}
如果我们直接将一个对象存储在 localStorage 中,那将会在存储之前进行隐式类型转换,将对象转换为字符串,再进行存储:
javascript
const user = {
name : "zhangsan",
age : 28,
gender : "male",
profession : "lawyer"
};
localStorage.setItem("user", user);
localStorage.getItem("user"); // '[object Object]'
Web Storage 使用了同源策略,也就是说,存储的数据只能在同一来源上可用。如果域和子域相同,则可以从不同的选项卡访问 localStorage 数据,而无法访问 sessionStorage 数据,即使它是完全相同的页面。
另外:
- 无法在 web worker 或 service worker 中访问 Web Storage;
- 如果浏览器设置为隐私模式,将无法读取到 Web Storage;
- Web Storage 很容易被 XSS 攻击,敏感信息不应存储在本地存储中;
- 它是同步的,这意味着所有操作都是一次一个。对于复杂应用,它会减慢应用的运行时间。
3. Cookie
Cookie 主要用于身份验证和用户数据持久性。Cookie 与请求一起发送到服务器,并在响应时发送到客户端;因此,cookies 数据在每次请求时都会与服务器交换。服务器可以使用 cookie 数据向用户发送个性化内容。严格来说,cookie 并不是客户端存储方式,因为服务器和浏览器都可以修改数据。它是唯一可以在一段时间后自动使数据过期的方式。
每个 HTTP 请求和响应都会发送 cookie 数据。存储过多的数据会使 HTTP 请求更加冗长,从而使应用比预期更慢:
- 浏览器限制 cookie 的大小最大为4kb,特定域允许的 cookie 数量为 20 个,并且只能包含字符串;
- cookie 的操作是同步的;
- 不能通过 web workers 来访问,但可以通过全局 window 对象访问。
Cookie 通常用于会话管理、个性化以及跨网站跟踪用户行为。我们可以通过服务端和客户端设置和访问 cookie。Cookie 还具有各种属性,这些属性决定了在何处以及如何访问和修改它们,
Cookie 分为两种类型:
-
会话 Cookie:没有指定 Expires 或 Max-Age 等属性,因此在关闭浏览器时会被删除;
-
持久性 Cookie:指定 Expires 或 Max-Age 属性。这些 cookie 在关闭浏览器时不会过期,但会在特定日期 (Expires) 或时间长度 (Max-Age) 后过期。
下面先来看看如何访问和操作客户端和服务器上的 cookie。
① 客户端(浏览器)
客户端 JavaScript 可以通过 document.cookie 来读取当前位置可访问的所有 cookie。它提供了一个字符串,其中包含一个以分号分隔的 cookie 列表,使用 key=value 格式。
ini
document.cookie;
可以看到,在语雀主页中获取 cookie,结果中包含了登录的 cookie、语言、当前主题等。
同样,可以使用 document.cookie 来设置 cookie 的值,设置cookie也是用key=value格式的字符串,属性用分号隔开:
ini
document.cookie = "hello=world; domain=example.com; Secure";
这里用到了两个属性 SameSite 和 Secure,下面会介绍。如果已经存在同名的 cookie 属性,就会更新已有的属性值,如果不存在,就会创建一个新的 key=value。
如果需要经常在客户端处理 Cookie,建议使用像 js-cookie 这样的库来处理客户端 cookie:
csharp
Cookies.set('hello', 'world', { domain: 'example.com', secure: true });
Cookies.get('hello'); // -> world
这样不仅为 cookie 上的 CRUD 操作提供了一个干净的 API,而且还支持 TypeScript,从而帮助避免属性的拼写错误。
② 服务端(Node.js)
服务端可以通过 HTTP 请求的请求头和响应头来访问和修改 cookie。每当浏览器向服务端发送 HTTP 请求时,它都会使用 cookie 头将所有相关 cookie 都附加到该站点。请求标头是一个分号分隔的字符串。
这样就可以从请求头中读取这些 cookie。如果在服务端使用 Node.js,可以像下面这样从请求对象中读取它们,将获得以分号分隔的 key=value 对:
ini
http.createServer(function (request, response) {
const cookies = request.headers.cookie;
// "cookie1=value1; cookie2=value2"
...
}).listen(8124);
如果想要设置 cookie,可以在响应头中添加 Set-Cookie 头,其中 cookie 采用 key=value 的格式,属性用分号分隔:
ini
response.writeHead(200, {
'Set-Cookie': 'mycookie=test; domain=example.com; Secure'
});
通常我们不会直接编写 Node.js,而是与 ExpressJS 这样的 Node.js 框架一起使用。使用 Express 可以更轻松地访问和修改 cookie。只需添加一个像 cookie-parser 这样的中间件,就可以通过 req.cookies 以 JavaScript 对象的形式获得所有的 cookie。 还可以使用 Express 内置的 res.cookie() 方法来设置 cookie:
javascript
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
app.use(cookieParser())
app.get('/', function (req, res) {
console.log('Cookies: ', req.cookies)
// Cookies: { cookie1: 'value1', cookie2: 'value2' }
res.cookie('name', 'tobi', { domain: 'example.com', secure: true })
})
app.listen(8080)
4. IndexedDB
IndexedDB 提供了一个类似 NoSQL 的 key/value 数据库,它可以存储大量结构化数据,甚至是文件和 blob。 每个域至少有 1GB 的可用空间,并且最多可以达到剩余磁盘空间的 60%。
IndexedDB 于 2011 年首次实现,并于 2015 年 1 月成为 W3C 标准,它具有良好的浏览器支持。
key/value 数据库意味着存储的所有数据都必须分配给一个 key。它将key 与 value 相关联,key 用作该值的唯一标识符,这意味着可以使用该 key 跟踪该值。如果应用需要不断获取数据,key/value 数据库使用非常高效且紧凑的索引结构来快速可靠地通过 key 定位值。使用该 key,不仅可以检索存储的值,还可以删除、更新和替换该值。
在说 IndexedDB 之前,先来看一些相关术语:
- 数据库: 一个域可以创建任意数量的 IndexedDB 数据库,只有同一域内的页面才能访问数据库。
- object store:相关数据项的 key/value 存储。它类似于 MongoDB 中的集合或关系数据库中的表。
- key:用于引用 object store ****中每条记录(值)的唯一名称。它可以使用自动增量数字生成,也可以设置为记录中的任何唯一值。
- index:在 object store 中组织数据的另一种方式。搜索查询只能检查 key 或 index。
- schema:object store、key 和 index 的定义。
- version:分配给 schema 的版本号(整数)。 IndexedDB 提供自动版本控制,因此可以将数据库更新到最新 schema。
- 操作:数据库活动,例如创建、读取、更新或删除记录。
indexedDB 特点如下:
-
可以将任何 JavaScript 类型的数据存储为键值对,例如对象(blob、文件)或数组等。
-
IndexedDB API 是异步的,不会在数据加载时停止页面的渲染。
-
可以存储结构化数据,例如 Date、视频、图像对象等。
-
支持数据库事务和版本控制。
-
可以存储大量数据。
-
可以在大量数据中快速定位/搜索数据。
-
数据库是域专用的,因此任何其他站点都无法访问其他网站的 IndexedDB 存储,这也称为同源策略。
In dexedDB 使用场景:
-
存储用户生成的内容: 例如表单,在填写表单的过程中,用户可以离开并稍后再回来完成表单,存储之后就不会丢失初始输入的数据。
-
存储应用状态: 当用户首次加载网站或应用时,可以使用 IndexedDB 存储这些初始状态。可以是登录身份验证、API 请求或呈现 UI 之前所需的任何其他状态。因此,当用户下次访问该站点时,加载速度会增加,因为应用已经存储了状态,这意味着它可以更快地呈现 UI。
-
对于离线工作的应用: 用户可以在应用离线时编辑和添加数据。当应用程序来连接时,IndexedDB 将处理并清空同步队列中的这些操作。
不同浏览器的 IndexedDB 可能使用不同的名称。可以使用以下方法检查 IndexedDB 支持:
javascript
const indexedDB =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
window.shimIndexedDB;
if (!indexedDB) {
console.log("不支持 IndexedDB");
}
可以使用 indexedDB.open()
来连接数据库:
ini
const dbOpen = indexedDB.open('performance', 1);
indexedDB.open
的第一个参数是数据库名称,第二个参数是可选的版本整数。
可以使用以下三个事件处理函数监听 indexedDB 的连接状态:
① onerror
在无法建立 IndexedDB 连接时,将触发该事件:
ini
// 连接失败
dbOpen.onerror = e => {
reject(`IndexedDB error: ${ e.target.errorCode }`);
};
如果在无痕模式、隐私模式下运行浏览器,可能不支持 IndexedDB,需要禁用这些模式。
② onupgradeneeded
一旦数据库连接打开,就会触发 onupgradeneeded 事件,该事件可用于创建 object store。
ini
dbOpen.onupgradeneeded = e => {
const db = dbOpen.result;
// 创建 object store
const store = db.createObjectStore("cars", { keyPath: "id" });
// 使用自动递增的id
// const store = db.createObjectStore('cars', { autoIncrement: true });
// 创建索引
store.createIndex("cars_colour", ["colour"], {
unique: true
});
// 创建复合索引
store.createIndex("colour_and_make", ["colour", "make"], {
unique: false,
});
};
IndexedDB 使用了 object store 的概念,其本质上是数据集合的名称。可以在单个数据库中创建任意数量的 object store。keyPath是 IndexedDB 将用来识别对象字段名称,通常是一个唯一的编号,也可以通过 autoIncrement: true
来自动为 store 设置唯一递增的 ID。除了普通的索引,还可以创建复合索引,使用多个关键词的组合进行查询。
③ onsuccess
在连接建立并且所有升级都完成时,将触发该事件。上面我们已经新建了 schema,接下来就可以在onsuccess 中添加、查询数据。
ini
// 连接成功
dbOpen.onsuccess = () => {
this.db = dbOpen.result;
//1
const transaction = db.transaction("cars", "readwrite");
//2
const store = transaction.objectStore("cars");
const colourIndex = store.index("cars_colour");
const makeModelIndex = store.index("colour_and_make");
//3
store.put({ id: 1, colour: "Red", make: "Toyota" });
store.put({ id: 2, colour: "Red", make: "Kia" });
store.put({ id: 3, colour: "Blue", make: "Honda" });
store.put({ id: 4, colour: "Silver", make: "Subaru" });
//4
const idQuery = store.get(4);
const colourQuery = colourIndex.getAll(["Red"]);
const colourMakeQuery = makeModelIndex.get(["Blue", "Honda"]);
// 5
idQuery.onsuccess = function () {
console.log('idQuery', idQuery.result);
};
colourQuery.onsuccess = function () {
console.log('colourQuery', colourQuery.result);
};
colourMakeQuery.onsuccess = function () {
console.log('colourMakeQuery', colourMakeQuery.result);
};
// 6
transaction.oncomplete = function () {
db.close();
};
};
这里总共有六部分:
- 为了对数据库执行操作,我们必须创建一个 schema,一个 schema 可以是单个操作,也可以是多个必须全部成功的操作,否则都不会成功;
- 这里用来获取 cars object store 的引用以及对应的索引;
- object store 上的 put 方法用于将数据添加到数据库中;
- 这里就是数据的查询,可以使用 keyPath 的值直接查询项目(第14行);第15行中的 getAll 方法将返回一个包含它找到的每个结果的数组,我们正在根据 cars_colour 索引来搜索 Red,应该会查找到两个结果。第16行根据复合索引查找颜色为Blue,并且品牌为 Honda 的结果。
- 搜索成功的事件处理函数,它们将在查询完成时触发。
- 最后,在事务完成时关闭与数据库连接。 无需使用 IndexedDB 手动触发事务,它会自行运行。
运行上面的代码,就会得到以下结果:
可以在 Chrome Devtools 中查看:
下面来看看如何更新和删除数据。
- 更新: 首先使用个 get 来获取需要更新的数据,然后使用 store 上的 put 方法更新现有数据。 put 是一种"插入或更新"方法,它要么覆盖现有数据,要么在新数据不存在时插入新数据。
ini
const subaru = store.get(4);
subaru.onsuccess= function () {
subaru.result.colour = "Green";
store.put(subaru.result);
}
这会将数据库中 Silver 色的 Subaru 的颜色更新为绿色。
- 删除:可以使用 delete API 来删除数据,最简单的方法是通过其 key 来删除:
ini
const deleteCar = store.delete(1);
deleteCar.onsuccess = function () {
console.log("Removed");
};
如果不知道 key 并且希望根据值来删除,可以这样:
ini
const redCarKey = colourIndex.getKey(["Red"]);
redCarKey.onsuccess = function () {
const deleteCar = store.delete(redCarKey.result);
deleteCar.onsuccess = function () {
console.log("Removed");
};
};
结果如下:
总结:
cookie 、localStorage 和 sessionStorage:
-
cookie: 其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享。
-
sessionStorage: html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。
-
localStorage: html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享。
上面三种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。
Web Storage 和 cookie 的区别总结如下:
- Web Storage是为了更大容量存储设计的。Cookie 的大小是受限的,并且每次你请求一个新的页面的时候 Cookie 都会被发送过去,这样无形中浪费了带宽;
- cookie 需要指定作用域,不可以跨域调用;
- Web Storage 拥有 setItem,getItem,removeItem,clear 等方法,不像 cookie 需要前端开发者自己封装 setCookie,getCookie;
- Cookie 也是不可以或缺的:Cookie 的作用是与服务器进行交互,作为 HTTP 规范的一部分而存在 ,而 Web Storage 仅仅是为了在本地"存储"数据而生。