浏览器本地存储随着技术的发展越来越强大, 主流的主要是:
- cookie
- sessionStorage
- localStorage
- indexedDB
- webSQL(逐渐放弃)
Cookie
cookie 是一种纯文本的数据存储
工作过程
当网页要发送 http 请求的时候, 浏览器会先检查是否有相应的 cookie, 有则自动添加到 request header 中的 cookie 字段中.
存储在 cookie 中的数据, 每次都会被自动添加到 http 请求中, 必然是增加网络开销的.
Cookie 特征
- 不同的浏览器存放 cookie 的位置不同
- cookie 的存储是以域名形式进行区分的, 不同域下的 cookie 是独立的
- 可以设置 cookie 生效的域
- 一个域下存放的 cookie 数量是有限的, 不同的浏览器存放的个数不同.
- 每个 cookie 存放的内容大小不同, 一般为 4kb
- cookie 默认会话结束自动销毁, 可以设置过期时间
cookie可以分为两种不同类型: 会话cookie和持久cookie.
- 如果cookie不包含到期日期, 就是会话cookie, 会话cookie存储的内存中, 不会写入磁盘, 浏览器关闭的时候, cookie就会被丢弃
- 如果cookie包含日期, 就是持久性cookie, 会在指定的日期后从磁盘删除
Cookie 操作
设置 Cookie
document.cookie = '名字=值';
document.cookie = 'username=cfangxu;domain=baike.baidu.com'; //并且设置了生效域
复制代码
服务端设置 cookie,
服务端返回的 response header 中有意向叫 set-cookie, 是服务端专门用来设置 cookie 的,
Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
复制代码
一个set-Cookie
字段只能设置一个cookie
,当你要想设置多个 cookie
,需要添加同样多的set-Cookie
字段。
服务端会通过请求头部的cookie
字段读取cookie相关的信息.
读取 Cookie
我们可以通过document.cookie
来获取当前网站下的 cookie 的时候, 就可以得到当前网站下的所有 cookie.
修改 Cookie
想要修改一个 Cookie, 只要重新复制就可以了, 旧的值会被新的值覆盖, 但是在设置新的 cookie 时, path/domain 这几个要保持一直, 否则就是添加一个新的 cookie 了.
删除 cookie
将 cookie 的过期时间设置为已经过去的时间, 同样的 path/domain 需要保持一致.
工具函数
创建 cookie
function setCookie(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
var expires = 'expires=' + d.toGMTString();
document.cookie = cname + '=' + cvalue + '; ' + expires;
}
复制代码
获取 cookie
function getCookie(cname) {
var name = cname + '=';
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
}
return '';
}
复制代码
Cookie 选项
- domain: 指定 cookie 将要被发送至哪个或哪些域中. 默认情况下, domain 会被设置为创建该 cookie 的页面所在的域名, 所以当给相同域名发送请求时该 cookie 会被发送给服务器.
- path: 指定目录或子目录下的网页才可以访问, 即 path 属性
- secure: 确保 cookie 在 https 或者其他安全协议中才能被发送至服务器
- httpOnly: 设置 cookie 是否能通过 js 去访问. 默认 httpOnly 选项为空, 客户端是可以通过 js 代码访问的.
- same-site: 用来防止CSRF攻击和用户追踪
same-site
SameSite属性用来限制第三方Cookie, 从而减少安全风险.
它可以设置三个值:
- strict: 严格, 完全静止第三方的cookie, 跨站点的时候, 任何情况下都不会发送cookie.
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
复制代码
- lax: 大部分情况下不发送第三方cookie, 但是导航到目标网址的get请求除外.
导航到目标网站的GET请求, 只包含三种情况: 链接, 预加载请求, GET请求:
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="https://juejin.cn/post/..."></a> |
发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="https://juejin.cn/post/..."/> |
发送 Cookie | 发送 Cookie |
GET | 表单 <form method="GET" action="..."> |
发送 Cookie 发送 | Cookie |
POST | 表单 <form method="POST" action="..."> |
发送 Cookie | 不发送 |
iframe | <iframe src="https://juejin.cn/post/..."></iframe> 发送 Cookie |
不发送 | |
AJAX | $.get("...") |
发送 Cookie | 不发送 |
Image | <img src="https://juejin.cn/post/..."> |
发送 Cookie | 不发送 |
- None: lax会变为默认的设置, 这时, 网站可以选择显示关闭
samesite
, 将其设置为None. 不过前提是必须同时设置secure
属性(Cookie只能通过HTTPS协议发送), 否则无效.
无效设置:
Set-Cookie: widget_session=abc123; SameSite=None
复制代码
有效设置:
Set-Cookie: widget_session=abc123; SameSite=None; Secure
复制代码
LocalStorage
HTML5 的新方法, 兼容 IE8 以上的浏览器
特点
- 生命周期: 持久化的本地存储, 需要手动删除数据, 否则不会过期
- 存储的信息在同一个域中是共享的
- 当本页操作了 LocalStorage, 本页面不会触发 storage 事件, 但是别的页面会触发 storage 事件
- 大小: 与浏览器有关, 一般为 5M, 值得说明的是,安卓上手 Q 、手机QQ浏览器、微信中则是 2.5M 的数量级,因此在移动端,本地存储的size更加珍贵
- 在非 IE 浏览中可以本地打开, IE 需要在服务器中打开
- LocalStorage 本事还是对字符串的读取, 如果存储内容多会消耗内存空间, 导致页面卡顿.
- localStorage 收到同源策略的限制
操作
设置:
localStorage.setItem('username', 'cfangxu');
复制代码
获取:
localStorage.getItem('username');
//获取第一个键名
localStorage.key(0);
复制代码
删除:
localStorage.removeItem('username');
//一次清除所有存储
localStorage.clear();
复制代码
Storage 事件
当 storage 发生改变的时候触发。
注意: 当前页面对 storage 的操作会触发其他页面的 storage 事件
事件的回调函数中有一个参数 event,是一个 StorageEvent 对象,提供了一些实用的属性,如下表:
Property | Type | Description |
---|---|---|
key | String | The named key that was added, removed, or moddified |
oldValue | Any | The previous value(now overwritten), or null if a new item was added |
newValue | Any | The new value, or null if an item was added |
url/uri | String | The page that called the method that triggered this change |
模拟实现一个 localStorage
if (!window.localStorage) {
window.localStorage = {
getItem: function(sKey) {
if (!sKey || !this.hasOwnProperty(sKey)) {
return null;
}
return unescape(
document.cookie.replace(
new RegExp(
'(?:^|.*;\\s*)' +
escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') +
'\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*'
),
'$1'
)
);
},
key: function(nKeyId) {
return unescape(
document.cookie.replace(/\s*\=(?:.(?!;))*$/, '').split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]
);
},
setItem: function(sKey, sValue) {
if (!sKey) {
return;
}
document.cookie = escape(sKey) + '=' + escape(sValue) + '; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/';
this.length = document.cookie.match(/\=/g).length;
},
length: 0,
removeItem: function(sKey) {
if (!sKey || !this.hasOwnProperty(sKey)) {
return;
}
document.cookie = escape(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
this.length--;
},
hasOwnProperty: function(sKey) {
return new RegExp('(?:^|;\\s*)' + escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=').test(
document.cookie
);
}
};
window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length;
}
复制代码
上面是用 cookie 实现 LocalStorage, 然后是的 localStorage 支持 expires:
(function() {
var getItem = localStorage.getItem.bind(localStorage);
var setItem = localStorage.setItem.bind(localStorage);
var removeItem = localStorage.removeItem.bind(localStorage);
localStorage.getItem = function(keyName) {
var expires = getItem(keyName + '_expires');
if (expires && new Date() > new Date(Number(expires))) {
removeItem(keyName);
removeItem(keyName + '_expires');
}
return getItem(keyName);
};
localStorage.setItem = function(keyName, keyValue, expires) {
if (typeof expires !== 'undefined') {
var expiresDate = new Date(expires).valueOf();
setItem(keyName + '_expires', expiresDate);
}
return setItem(keyName, keyValue);
};
})();
复制代码
使用如下:
localStorage.setItem('key', 'value', new Date() + 10000); // 10 秒钟后过期
localStorage.getItem('key');
复制代码
SessionStorage
类似于 localstorage, 用于临时保存同一窗口(或标签页)的数据,与 localstorage 不同的是, sessionstorage 会在关闭窗口或标签页之后将会删除这些数据。
操作
设置:
sessionStorage.setItem('key', 'value');
复制代码
读取:
var lastname = sessionStorage.getItem('key');
复制代码
删除:
sessionStorage.removeItem('key');
复制代码
清除:
sessionStorage.clear();
复制代码
IndexedDb
IndexedDB 就是浏览器提供的本地数据库, 可以被网页脚本创建和操作, 允许存储大量数据, 提供查询接口, 简历索引. 不属于不关系型数据库, 更接近 NoSql
IndexedDb 特点
- 键值对存储: 每一条数据都有对应的主键, 主键独一无二
- 异步: 数据库操作不会锁死浏览器, 与 localstorage 不同, 后者是同步的.
- 支持事务:在一些列操作步骤之中, 只要有异步失败, 整个事务就都取消, 数据库回滚到事务发生之前的状态.
- 同原限制: 每个数据库对应创建它的域名, 网页只能访问自身域名下的数据库, 而不能访问跨域的数据库
- 存储空间大: 比 LocalStorage 大的多, 一般不少于 250MB, 甚至没有上限
- 支持二进制存储: 不仅可以存储字符串, 还可以存储二进制数据(ArrayBuffer 对象和 Blob 对象)
IndexedDB 概念
- 数据库: 数据库是一系列相关数据的容器, 每个域名(协议+域名+端口)都可以新建任意多个数据库, IndexedDB 数据库有版本的概念, 同一个时刻只能有一个版本的数据库存在, 如果要修改数据库结构, 只能通过升级数据库版本完成.
- 对象仓库: 每个数据库包含若干个对象仓库(object store), 它类似于关系型数据库的表格
- 数据记录: 对象仓库保存的是数据记录, 每条记录类似于关系型数据库的行, 但是只有主键和数据体来两部分, 主键用来建立默认的索引, 必须是不同的, 否则会报错. 主键可以使数据记录里面的一个属性, 也可以指定为一个递增的整数编号.
- 索引: 为了加速数据的检索, 可以再对象仓库里面, 为不同的属性建立索引
- 事务: 数据记录的读写和删改, 都要通过事务完成, 事务对象提供
error
,about
和complate
三个事件, 用来监听操作结果.
操作
打开数据库
var request = window.indexedDB.open(databaseName, version);
复制代码
- dataBaseName: 字符串, 数据库
- version: 整数, 默认为 1
返回一个 IDBRequest 对象, 通过三种事件处理结果:
- error
request.onerror = function(event) {
console.log('数据库打开报错');
};
复制代码
- success
var db;
request.onsuccess = function(event) {
db = request.result;
console.log('数据库打开成功');
};
复制代码
- upgradeneeded
如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件 upgradeneeded。
var db;
request.onupgradeneeded = function(event) {
db = event.target.result;
};
复制代码
新建数据库
新建数据库与打开数据库是同一个操作, 如果指定的数据库不存在, 就会新建. 不同之处在于后续的操作主要在upgradeneeded
事件的监听函数中完成.
通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore = db.createObjectStore('person', { keyPath: 'id' });
};
复制代码
当然最好的创建之前判断一下, 不存在再新建:
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore;
if (!db.objectStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
};
复制代码
主键key
是默认建立索引的属性. 也可以让 IndexedDB 自动生成主键:
var objectStore = db.createObjectStore('person', { autoIncrement: true });
复制代码
建立对象以后, 下一步可以新建索引:
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore = db.createObjectStore('person', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
objectStore.createIndex('email', 'email', { unique: true });
};
复制代码
三个参数分别是索引名称, 索引所在的属性, 配置对象(说明该属性是否包含重复的值)
新增数据
新增数据指向对象仓库写入数据, 这需要通过事务完成:
function add() {
var request = db
.transaction(['person'], 'readwrite')
.objectStore('person')
.add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });
request.onsuccess = function(event) {
console.log('数据写入成功');
};
request.onerror = function(event) {
console.log('数据写入失败');
};
}
add();
复制代码
写入数据需要新建一个事务, 新建是必须制定表格名称和操作模式. 新建事务以后, 通过IDBTransaction.objectStore(name)
方法, 拿到IDBObjectStore
对象, 在通过表格对象add()
想表哥写入一条记录.
读取数据
读取数据也只能通过事务完成:
function read() {
var transaction = db.transaction(['person']);
var objectStore = transaction.objectStore('person');
var request = objectStore.get(1);
request.onerror = function(event) {
console.log('事务失败');
};
request.onsuccess = function(event) {
if (request.result) {
console.log('Name: ' + request.result.name);
console.log('Age: ' + request.result.age);
console.log('Email: ' + request.result.email);
} else {
console.log('未获得数据记录');
}
};
}
read();
复制代码
objectStore.get()
用于读取数据, 参数是主键的值
遍历数据
遍历数据表格的所有记录,要使用指针对象 IDBCursor
。
function readAll() {
var objectStore = db.transaction('person').objectStore('person');
objectStore.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
console.log('Id: ' + cursor.key);
console.log('Name: ' + cursor.value.name);
console.log('Age: ' + cursor.value.age);
console.log('Email: ' + cursor.value.email);
cursor.continue();
} else {
console.log('没有更多数据了!');
}
};
}
readAll();
复制代码
上面代码中,新建指针对象的openCursor()
方法是一个异步操作,所以要监听 success 事件。
更新数据
function update() {
var request = db
.transaction(['person'], 'readwrite')
.objectStore('person')
.put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });
request.onsuccess = function(event) {
console.log('数据更新成功');
};
request.onerror = function(event) {
console.log('数据更新失败');
};
}
update();
复制代码
删除数据
function remove() {
var request = db
.transaction(['person'], 'readwrite')
.objectStore('person')
.delete(1);
request.onsuccess = function(event) {
console.log('数据删除成功');
};
}
remove();
复制代码
使用索引
索引的意义在于, 可以让你搜索任意字段, 也就是说从任意字段拿到数据记录. 如果不建立索引, 默认只能搜索主键:
//建立索引
objectStore.createIndex('name', 'name', { unique: false });
//通过索引数据
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');
request.onsuccess = function(e) {
var result = e.target.result;
if (result) {
// ...
} else {
// ...
}
};
复制代码
其他: WebSQL
2010 年被 W3C 废弃的本地数据库数据存储方案,但是主流浏览器(火狐除外)都已经有了相关的实现,web sql 类似于 SQLite,是真正意义上的关系型数据库,用 sql 进行操作,当我们用 JavaScript 时要进行转换,较为繁琐。