浏览器本地存储的方式有哪些?区别及应用场景?
一、方式
javaScript
本地缓存的方法我们主要讲述以下四种:
- cookie
- sessionStorage
- localStorage
- indexedDB
cookie
Cookie
,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP
无状态导致的问题
作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie
有效期、安全性、使用范围的可选属性组成
但是cookie
在每次请求中都会被发送,如果不使用 HTTPS
并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie
保持登录态的网站上,如果 cookie
被窃取,他人很容易利用你的 cookie
来假扮成你登录网站
关于cookie
常用的属性如下:
- Expires 用于设置 Cookie 的过期时间
Expires=Wed, 21 Oct 2015 07:28:00 GMT
- Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比
Expires
高)
Max-Age=604800
Domain
指定了Cookie
可以送达的主机名Path
指定了一个URL
路径,这个路径必须出现在要请求的资源的路径中才可以发送Cookie
首部
Path=/docs # /docs/Web/ 下的资源会带 Cookie 首部
- 标记为
Secure
的Cookie
只应通过被HTTPS
协议加密过的请求发送给服务端
通过上述,我们可以看到cookie
又开始的作用并不是为了缓存而设计出来,只是借用了cookie
的特性实现缓存
关于cookie
的使用如下:
document.cookie = '名字=值';
关于cookie
的修改,首先要确定domain
和path
属性都是相同的才可以,其中有一个不同得时候都会创建出一个新的cookie
Set-Cookie:name=aa; domain=aa.net; path=/ # 服务端设置
document.cookie =name=bb; domain=aa.net; path=/ # 客户端设置
最后cookie
的删除,最常用的方法就是给cookie
设置一个过期的事件,这样cookie
过期后会被浏览器删除
第三方 Cookie
之所以有第三方 Cookie 这个称呼,是因为 Cookie 执行同源策略,a.com
和 b.com
各自只能访问自己的 Cookie,无法访问对方或者任何不属于自己的 Cookie
如果在访问 a.com
时,设置了一个 b.com
的 Cookie(比如内嵌 b.com
的页面),那么这个 Cookie 相对于 a.com
而言就是第三方 Cookie
值得一提的是,是同一个 host 下的不同端口倒是可以互相访问 Cookie
这里提一下对第三方 Cookie 而言非常重要的一个特性:Cookie 可以被服务端设置
服务器可以通过 response 的请求头来要求浏览器设置 Cookie
Set-Cookie: userId=123;
浏览器在检测到返回请求的 header 里有 Set-Cookie 请求头后,就会自动设置 Cookie,不需要开发者用 JS 去做额外的操作
这样带来的好处是,当 abc.com
和 xyz.com
想在自己的网页上内嵌淘宝广告时,只需要把淘宝提供的组件放进 HTML 即可,不需要写额外的 JS,也能让淘宝进行跨站定位用户
<img src="taobao.com/some-ads" />
(这个组件纯属虚构,仅为方便理解)
它是如何工作的呢?
- 当用户处于
abc.com
时,浏览器会向taobao.com/some-ads
发起一个 HTTP 请求 - 当淘宝服务器返回广告内容时,会顺带一个
Set-Cookie
的 HTTP 请求头,告诉浏览器设置一个源为taobao.com
的 Cookie,里面存上当前用户的 ID 等信息 - 这个 Cookie 相对于
abc.com
而言就是第三方 Cookie,因为它属于taobao.com
- 而当用户访问
xyz.com
时,由于xyz.com
上也嵌入了淘宝的广告,因此用户的浏览器也会向taobao.com/some-ads
发起请求 - 有意思的来了,发请求时,浏览器发现本地已有
taobao.com
的 Cookie(此前访问abc.com
时设置的),因此,浏览器会将这个 Cookie 发送过去 - 淘宝服务器根据发过来的 Cookie,发现当前访问
xyz.com
的用户和之前访问abc.com
的用户是同一个,因此会返回相同的广告
广告商用第三方 Cookie 来跨站定位用户大概就是这么个过程,实际肯定要复杂许多,但基本原理是一致的
总之,关键就是利用了 Cookie 的两个特点
- Cookie 可以被服务器设置
- 浏览器每次请求会自动带上 Cookie
正因为这两个特点,即使 Cookie 在今天看来缺点一大堆,但仍然在部分领域有不可替代的价值
但也是因为这两个特点,导致 Cookie 的安全性相对不高,总之 Cookie 的这个设计放在今天来看,就是一把双刃剑
localStorage
HTML5
新方法,IE8及以上浏览器都兼容
特点
- 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
- 存储的信息在同一域中是共享的
- 当本页操作(新增、修改、删除)了
localStorage
的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件。 - 大小:5M(跟浏览器厂商有关系)
localStorage
本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡- 受同源策略的限制
下面再看看关于localStorage
的使用
设置
localStorage.setItem('username','cfangxu');
获取
localStorage.getItem('username')
获取键名
localStorage.key(0) //获取第一个键名
删除
localStorage.removeItem('username')
一次性清除所有存储
localStorage.clear()
localStorage
也不是完美的,它有两个缺点:
- 无法像
Cookie
一样设置过期时间 - 只能存入字符串,无法直接存对象
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'
sessionStorage
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage
将会删除数据
扩展的前端存储方式
indexedDB
是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索
虽然 Web Storage
对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB
提供了一个解决方案
优点:
- 储存量理论上没有上限
- 所有操作都是异步的,相比
LocalStorage
同步操作性能更高,尤其是数据量较大时 - 原生支持储存
JS
的对象 - 是个正经的数据库,意味着数据库能干的事它都能干
缺点:
- 操作非常繁琐
- 本身有一定门槛
关于indexedDB
的使用基本使用步骤如下:
打开数据库并且开始一个事务
创建一个
object store
构建一个请求来执行一些数据库操作,像增加或提取数据等。
通过监听正确类型的
DOM
事件以等待操作完成。在操作结果上进行一些操作(可以在
request
对象中找到)
关于使用indexdb
的使用会比较繁琐,大家可以通过使用Godb.js
库进行缓存,最大化的降低操作难度
二、区别
关于cookie
、sessionStorage
、localStorage
三者的区别主要如下:
存储大小:
cookie
数据大小不能超过4k
,sessionStorage
和localStorage
虽然也有存储大小的限制,但比cookie
大得多,可以达到5M或更大有效时间:
localStorage
存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage
数据在当前浏览器窗口关闭后自动删除;cookie
设置的cookie
过期时间之前一直有效,即使窗口或浏览器关闭数据与服务器之间的交互方式,
cookie
的数据会自动的传递到服务器,服务器端也可以写cookie
到客户端;sessionStorage
和localStorage
不会自动把数据发给服务器,仅在本地保存
三、应用场景
在了解了上述的前端的缓存方式后,我们可以看看针对不对场景的使用选择:
- 标记用户与跟踪用户行为的情况,推荐使用
cookie
- 适合长期保存在本地的数据(令牌),推荐使用
localStorage
- 敏感账号一次性登录,推荐使用
sessionStorage
- 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用
indexedDB
扩展
IndexedDB
IndexedDB 的全称是 Indexed Database,从名字中就可以看出,它是一个数据库
IndexedDB 早在 2009 年就有了第一次提案,但其实它和 Web Storage 几乎是同一时间普及到各大浏览器的(没错,就是 2015 年那会,es6 也是那时候)
IndexedDB 是一个正经的数据库,它在问世后替代了原来不正经的 Web SQL 方案,成为了当今唯一运行在浏览器里的数据库
在我看来,IndexedDB 其实更适合当作终极前端本地数据储存方案
相比于 LocalStorage,IndexedDB 的优点是
储存量理论上没有上限
- Chrome 对 IndexedDB 储存空间限制的定义是:硬盘可用空间的三分之一
所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时
原生支持储存 JS 的对象
是个正经的数据库,意味着数据库能干的事它都能干
但是缺点也比较致命:
- 操作非常繁琐
- 本身有一定门槛(需要你懂数据库的概念)
由于提案较早,IndexedDB 的 API 设计其实是比较糟糕的,对于新手而言,光是想连上数据库,并往里面加东西,都需要折腾半天
对于简单的数据储存而言,IndexedDB 的 API 显得太复杂了,再加上其 API 全是异步的,会带来额外的心智负担,远没有 LocalStorage 简单两行代码搞定数据存取来的快
因此,IndexedDB 在今天的使用规模相比 LocalStorage 差远了,即使 IndexedDB 本身的设计其实更适合用来在浏览器上储存数据
总之,如果不考虑 IndexedDB 的操作难度,其作为一个前端本地储存方案其实是接近完美的
简单理解数据库
在使用 IndexedDB 前,你首先需要懂基本的数据库概念
这里用 Excel 类比,简单介绍数据库的基本概念,不做太深入的讨论
需要了解四个基本概念,以关系型数据库为例
- 数据库 Database
- 数据表 Table(IndexedDB 中叫 ObjectStore)
- 字段 Field
- 事务 Transaction
(虽然 IndexedDB 算不上关系型数据库,但概念都是相通的)
假设清华和北大各自需要建一个数据库,用来存各自学生与教工的信息,假设命名为
- 清华:
thu
- 北大:
pku
这样,清北之间的数据就可以相互独立
然后,我们再到数据库里建表
student
表,储存学生信息stuff
表,储存教工信息
数据表(Table)是什么?说白了,就是一个类似于 Excel 表一样的东西
比如 student
表,可以长这样:
image-20210204032958192
上面的 学号、姓名、年龄、专业 就是数据表的字段
当我们想往 student
表添加数据时,就需要按照规定的格式,往表里加数据(关系型数据库的特点,而 IndexedDB 允许不遵守格式)
数据库也给我们提供了方法,当我们知道一个学生的学号(id),就可以在非常短的时间内,在表里成千上万个学生中,快速找到这个学生,并返回他的完整信息
也可以根据 id 定位,对该学生的数据进行修改,或者删除
id 这种每条数据唯一的值,就可以被用来做主键(primary key),主键在表内独一无二,无法添加相同主键的数据
而主键一般会被建立索引,所谓对字段建立索引,就是可以根据这个字段的值,在表里非常快速的找到对应的数据(通常不高于 O(logN)),如果没有索引,那可能就需要遍历整个表(O(N))
增、删、改、查这些操作,都需要通过事务 Transaction 来完成
- 如果事务中任何一个操作没有成功,整个事务都会回滚
- 在事务完成之前,操作不会影响数据库
- 不同事务之间不能互相影响
举个例子,当你发起一个事务,想利用这个事务添加两个学生,如果第一个学生添加成功,但是第二个学生添加失败,事务就会回滚,第一个学生将根本不会在数据库中出现过
事务在银行转账这种场景非常有用:如果转账中任何一步失败了,整个转账操作就和没发生过一样,不会造成任何影响
在同一个 Excel 文件(数据库)中,我们除了 student
表,还可以有 stuff
表(同一个数据库中有了两个不同的数据表):
image-20210204033839030
然后,清华和北大各自分一个 Excel 文件,就相当于分了两个数据库
image-20210204034441432
总而言之,不扯数据库各种难理解的概念,我们其实完全可以用 Excel 来类比数据库
- 一个 Excel 文件就是一个 Database
- 一个 Excel(Database)里可以有很多不同表格(数据表 Table)
- 表格的列的名称其实就是字段
上述类比最接近 MySQL 这种关系型数据库,但放在其它一些比较特殊的数据库上可能就不太妥当(比如图数据库)
如果你是新手,用 Excel 类比理解数据库完全没问题,足以使用 IndexedDB 了
虽然说 IndexedDB 使用 key-value 的模式储存数据,但你也完全可以用数据表 Table 的模式来看待它
IndexedDB 的使用
使用 IndexedDB 的第一步是打开数据库:
const request = window.indexedDB.open('pku');
上面这个操作打开了名为 pku
的数据库,如果不存在,浏览器会自动创建
然后 request
上有三个事件:
var db; // 全局 IndexedDB 数据库实例
request.onupgradeneeded = function (event) {
db = event.target.result;
console.log('version change');
};
request.onsuccess = function (event) {
db = request.result;
console.log('db connected')l;
};
request.onblocked = function (event) {
console.log('db request blocked!')
}
request.onerror = function (event) {
console.log('error!');
};
IndexedDB 有一个版本(version)的概念,连接数据库时就可以指定版本
const version = 1;
const request = window.indexedDB.open('pku', version);
版本主要用来控制数据库的结构,当数据库结构(表结构)发生变化时,版本也会变化
如上,request
上有四个事件:
onupgradeneeded
在版本改变时触发- 注意首次连接数据库时,版本从 0 变成 1,因此也会触发,且先于
onsuccess
- 注意首次连接数据库时,版本从 0 变成 1,因此也会触发,且先于
onsuccess
在连接成功后触发onerror
在连接失败时触发onblocked
在连接被阻止的时候触发,比如打开版本低于当前存在的版本
注意这四个事件都是异步的,意味着在连接 IndexedDB 的请求发出去后,需要过一段时间才能连上数据库,并进行操作
开发者对数据库的所有操作,都得放在异步连上数据库之后,这有的时候会带来很大的不便
而开发者如果想创建数据表(在 IndexedDB 里面叫做 ObjectStore),只能将其放到 onupgradeneeded
事件中(官方的定义是需要一个 IDBVersionChange
的事件)
request.onupgradeneeded = function (event) {
db = event.target.result;
if (!db.objectStoreNames.contains('student')) {
db.createObjectStore('student', {
keyPath: 'id', // 主键
autoIncrement: true // 自增
});
}
}
上面这段代码,在数据库初始化时,创建了一个 student
的表,并且以 id
为自增主键(每加一条数据,主键会自动增长,无需开发者指定)
在这一切做好以后,终于,我们可以连接数据库,然后添加数据了
const adding = db.transaction('student', 'readwrite') // 创建事务
.objectStore('student') // 指定 student 表
.add({ name: 'luke', age: 22 });
adding.onsuccess = function (event) {
console.log('write success');
};
adding.onerror = function (event) {
console.log('write failed');
}
用同样的方法再加一条数据
db.transaction('student', 'readwrite')
.objectStore('student')
.add({ name: 'elaine', age: 23 });
然后,打开浏览器的开发者工具,我们就能看到添加的数据:
这里可以看到 IndexedDB 的 key-value 储存特性,key 就是主键(这里指定主键为 id
),value 就是剩下的字段和对应的数据
这个 key-value 结构对应的 Table 结构如下:
image-20210204050515354
如果要获取数据,需要一个 readonly
的 Transaction
const request = db.transaction('student', 'readonly')
.objectStore(this.name)
.get(2); // 获取 id 为 2 的数据
request.onsuccess = function (event) {
console.log(event.target.result) // { id: 2, name: 'elaine', age: 23 }
}
综上,哪怕只是想简单的往 IndexedDB 里增加和查询数据,都需要写一大堆代码,操作非常繁琐,一不小心还容易掉坑里
那么,有没有什么办法,能更优雅的使用 IndexedDB,在代码量减少的情况下,还能更好的发挥其实力呢?
GoDB.js
GoDB.js
是一个基于 IndexedDB 实现前端本地储存的类库
帮你做到代码更简洁的同时,更好的发挥 IndexedDB 的实力
首先安装:
npm install godb
对 IndexedDB 的增删改查,一行代码就可以搞定!
import GoDB from 'godb';
const testDB = new GoDB('testDB'); // 连接数据库
const user = testDB.table('user'); // 获取数据表
const data = { name: 'luke', age: 22 }; // 随便定义一个对象
user.add(data) // 增
.then(luke => user.get(luke.id)) // 查
.then(luke => user.put({ ...luke, age: 23 })) // 改
.then(luke => user.delete(luke.id)); // 删
或者,一次性添加许多数据,然后看看效果:
const arr = [
{ name: 'luke', age: 22 },
{ name: 'elaine', age: 23 }
];
user.addMany(arr)
.then(() => user.consoleTable());
上面这段代码,会在添加数据后,在控制台中展示出 user
表的内容:
add-many
回到之前 LocalStorage 出问题的那个例子,用 GoDB
就可以实现正常储存:
import GoDB from 'godb';
const testDB = new GoDB('testDB'); // 连接数据库
const store = testDB.table('store'); // 获取数据表
const obj = {
a: undefined,
b: /abc/,
c: new Date()
};
store.add(obj)
.then(item => store.get(item.id)) // 获取存进去的实例
.then(res => console.log(res));
// {
// id: 1,
// a: undefined,
// b: /abc/,
// c: new Date()
// }
并且,循环引用的对象也能使用 GoDB
进行储存
const a = { key: 'value' };
a['a'] = a;
store.add(a)
.then(item => store.get(item.id)) // 获取存进去的实例
.then(result => console.log(result));
// 打印出来的对象比 a 多了个 id,其它完全一致
关于 GoDB
更详细的用法,可以参考 GoDB
的项目官网(不断完善中):
总之,GoDB
可以
帮你在背后处理好 IndexedDB 各种繁琐操作
帮你在背后维护好数据库、数据表和字段
- 以及字段的索引,各种属性(比如
unique
)
- 以及字段的索引,各种属性(比如
帮你规范化 IndexedDB 的使用,使你的项目更易维护
最终,开放几个简单易用的 API 给你,让你用简洁的代码玩转 IndexedDB