如今主流的存储方案:
- cookie
- web storage
- indexDB
这三个浏览器兼容性最高的三种前端储存方案
1、cookie
它的出现是为了解决 HTTP 协议无状态特性的问题,简单来说就是想要得到上次http请求的数据是办不到的,只有再次从新请求。我们见得最多的应该就是登录态的长久保持。
虽然 Cookie 在部分领域仍有不可替代的价值,但其已经不再适合被做为一个前端本地储存方案去使用:
- Cookie 在每次请求中都会被发送,如果不使用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险
- cookie在每次请求时都会自动写入到请求头内,会增加带宽占用,但其实放在今天的网络环境来看,这点占用基本可以忽略不计
- Cookie 只允许储存 4kb 的数据
假设 abc.com 和 xyz.com 都内嵌了淘宝的广告,你会发现即使 abc.com 和 xyz.com 所有者不一致,两个网站上淘宝广告推荐的商品也出奇的一致,这背后是因为淘宝知道是同一个人,分别在 abc.com 和 xyz.com 访问淘宝的广告
这是如何实现的呢?答案是第三方 Cookie
它是如何工作的呢?
- 当用户处于 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
Set-Cookie响应头字段(Response header)是服务器发送到浏览器或者其他客户端的一些信息,一般用于登陆成功的情况下返回给客户端的凭证信息,然后下次请求时会带上这个cookie,这样服务器端就能知道是来自哪个用户的请求了。
Cookie请求头字段是客户端发送请求到服务器端时发送的信息(满足一定条件下浏览器自动完成,无需前端代码辅助)。
拿一个Http POST请求来说 http://aaa.www.com/xxxxx/list
如果满足下面几个条件:
1、浏览器端某个Cookie的domain字段等于aaa.www.com或者www.com
2、都是http或者https,或者不同的情况下Secure属性为false
3、要发送请求的路径,即上面的xxxxx跟浏览器端Cookie的path属性必须一致,或者是浏览器端Cookie的path的子目录,比如浏览器端Cookie的path为/test,那么xxxxxxx必须为/test或者/test/xxxx等子目录才可以
上面3个条件必须同时满足,否则该Post请求就不能自动带上浏览器端已存在的Cookie
HttpOnly 则用来禁止使用 JS 访问 cookie,很好的可以防止xss攻击。
2、Web Storage
HTML5 标准中,新增了一个 Web Storage 的本地储存方案,其包括
- LocalStorage
- SessionStorag
两者用法一致,相比于cookie,存储容量增加到了5M之多;区别点:
- localstorage是持久化存储,除非自己手动删除,关闭浏览器后依然存在;而sessionstorage关闭浏览器后自动删除。
- localstorage作用范围是整个浏览器,简单来说只要是用同一个浏览器打开的不同窗口还是不同网页之间都能够共享数据;而sessionstorage只能在同一个窗口下共享数据。
这里主要以 LocalStorage 为例进行介绍
主要特点:
- 使用 Key-Value 形式储存
- 使用很方便
- Key 和 Value 以字符串形式储存
使用方法:
localStorage.setItem("键","值")//存值
localStorage.getItem("键") //取值
localStorage.removeItem("键")//删除某个值
localStorage.clear()//清空所有
缺点:
只能存入字符串数据类型,无法直接存对象......
举个🌰
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'localStorage.setItem('key', 1);
console.log(localStorage.getItem('key')); // '1'
你会发现,存进去的如果是对象,拿出来就变成了字串‘[Object,object]’数据丢失了!
存进去的如果是 number,拿出来也变成了 string
要解决这个问题,一般是使用 JSON.stringify() 配合 JSON.parse()。
但是,这么做有一个缺点,那就是 JSON.stringify() 本身是存在一些问题的
🌰
const a = JSON.stringify({a: undefined,b: function(){},c: /abc/,d: new Date()
});
console.log(a) // "{"c":{},"d":"2022-02-02T19:40:12.346Z"}"
console.log(JSON.parse(a)) // {c: {}, d: "2022-02-02T19:40:12.346Z"}
如上,JSON.stringify() 无法正确转换 JS 的部分属性
- undefiend
- Function
- RegExp(正则表达式,转换后变成了空对象)
- Date(转换后变成了字符串,而非 Date 类的对象)
其实还有个 Symbol 也无法被转换,但由于 Symbol 本身定义(全局唯一性)就决定了,它不应该被转换,否则即使转换回来,也不会是原来那个 Symbol
Function 也比较特殊,不过要兼容的话,可以先调用 .toString() 转换为字符串储存,需要的时候再 eval 转回来
3、indexedDB
IndexedDB 的全称是 Indexed Database,从名字中就可以看出,它是一个数据库
IndexedDB 早在 2009 年就有了第一次提案,但其实它和 Web Storage 几乎是同一时间普及到各大浏览器的。IndexedDB 是一个正经的数据库,它在问世后替代了原来不正经的 Web SQL 方案,成为了当今唯一运行在浏览器里的数据库
在我看来,IndexedDB 其实更适合当作终极前端本地数据储存方案
相比于 LocalStorage,IndexedDB 的优点是
- 储存量理论上没有上限
-
- Chrome 对 IndexedDB 储存空间限制的定义是:硬盘可用空间的三分之一
- 所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时
- 原生支持储存 JS 的对象
- 是个正经的数据库,意味着数据库能干的事它都能干
但是缺点也比较致命:
- 操作非常繁琐
- 本身有一定门槛(需要你懂数据库的概念)
对于简单的数据储存而言,IndexedDB 的 API 显得太复杂了,再加上其 API 全是异步的,会带来额外的心智负担,远没有 LocalStorage 简单两行代码搞定数据存取来的快
因此,IndexedDB 在今天的使用规模相比 LocalStorage 差远了,即使 IndexedDB 本身的设计其实更适合用来在浏览器上储存数据
总之,如果不考虑 IndexedDB 的操作难度,其作为一个前端本地储存方案其实是接近完美的
indexedDB的使用
详细文档 IndexedDB API - JavaScript 教程 - 网道
使用 IndexedDB 的第一步是打开数据库:
const version = 1;
const request = window.indexedDB.open('studentdb',version);
上面这个操作打开了名为 studentdb 的数据库,如果不存在,浏览器会自动创建。IndexedDB 有一个版本(version)的概念,连接数据库时就可以指定版本。
然后 request 上有四个事件:
var db; // 全局 IndexedDB 数据库实例request.onupgradeneeded = function (event) {db = event.target.result;//得到实例console.log('version change');
};request.onsuccess = function (event) {db = request..target.result;console.log('db connected');
};request.onblocked = function (event) {console.log('db request blocked!')
}request.onerror = function (event) {console.log('error!');
};
触发事件时机:
- onupgradeneeded 在版本改变时触发
-
- 注意首次连接数据库时,版本从 0 变成 1,因此也会触发,且先于 onsuccess
- onsuccess 在连接成功后触发
- onerror 在连接失败时触发
- onblocked 在连接被阻止的时候触发,比如打开版本低于当前存在的版本
注意这四个事件都是异步的,意味着在连接 IndexedDB 的请求发出去后,需要过一段时间才能连上数据库,并进行操作
开发者对数据库的所有操作,都得放在异步连上数据库之后,这有的时候会带来很大的不便。
新建表
新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。
通常,新建数据库以后,第一件事是新建对象仓库 ObjectStore(即新建表)。
request.onupgradeneeded = function(event) {db = event.target.result;var objectStore = db.createObjectStore('person', { keyPath: 'id',autoIncrement: true // 自增整数 });
}
其中person为表名,id为你设置的表的主键。
更好的写法是先判断一下,这张表格是否存在,如果不存在再新建。
if (!db.objectStoreNames.contains('person')) {objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
新建对象仓库以后,下一步可以新建索引。
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 });
}
上面代码中,IDBObject.createIndex()的三个参数分别为索引名称、索引所在的属性、配置对象(说明该属性是否包含重复的值)。
添加数据
db.transaction('person', 'readwrite').objectStore('person').add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });
写入数据需要新建一个事务。新建时必须指定表格名称和操作模式(“只读”或“读写”)。新建事务以后,通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,再通过表格对象的add()方法,向表格写入一条记录。
数据形式如图所示:
直接添加数据,未添加索引
添加了索引
获取数据
如果要获取数据,需要一个 readonly 的 Transaction
const request = db.transaction('person', 'readonly').objectStore(person).get(2);
objectStore.get()方法用于读取数据,参数是主键的值。
读写操作是一个异步操作,通过监听事物对象的success事件和error事件,了解是否写入成功。
request.onsuccess = function (event) {console.log('数据写入/读取成功');
};request.onerror = function (event) {console.log('数据写入/读取失败');
}
遍历数据表
遍历数据表格的所有记录,要使用指针对象 IDBCursor。
objectStore.openCursor().onsuccess = function (event) {var cursor = event.target.result;if (cursor) {console.log('Id: ' + cursor.key);console.log('Name: ' + cursor.value.name);cursor.continue();} else {console.log('没有更多数据了!');}
};
新建指针对象的openCursor()方法是一个异步操作,所以要监听success事件。
更新和删除数据
var request = db.transaction(['person'], 'readwrite')//更新
request.objectStore('person').put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });
//删除
request.objectStore('person').delete(1);
使用索引
索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。
即查找person表中索引name中名为张三的数据
var request = db.transaction(['person'], 'readwrite').objectStore('person').index('name').get('张三')
以上就是indexedDB的基本操作,更多说明查看文档。
综上,哪怕只是想简单的往 IndexedDB 里增加和查询数据,都需要写一大堆代码,操作非常繁琐,一不小心还容易掉坑里
4、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)); // 删
可以参考 GoDB 的项目官网:https://godb-js.github.io/
5、localForage(推荐)
这是目前对indexedDB封装中最火热的类库,目前在github上的 star已经23.3k了。
它不仅极大的降低了用户心智负担,还具有良好的兼容性。而且他还有一个优雅降级策略,若浏览器不支持 IndexedDB 则使用 WebSQL ,如果不支持 WebSQL 则使用 localStorage,在所有主流浏览器中都可用。
另外能存储多种类型的数据,支持es6的 Promises API,而且支持添加回调函数。
localForage的使用
下载
npm install localforage
创建一个indexedDB
const myIndexedDB = localforage.createInstance({name: 'myIndexedDB',})
数据存取
由于indexedDB的存取都是异步的,建议使用 promise.then() 或 async/await 去读值
//存值
myIndexedDB.setItem(key, value)//取值
myIndexedDB.getItem('somekey').then(function (value) {// we got our value
}).catch(function (err) {// we got an error
});
orconst value = await myIndexedDB.getItem('somekey');
其他常用方法:
- removeItem:删除key对应的值。
- clear:删除所有的key,并且重置数据库。
- length:获取仓库中key的长度。
- keys:获取数据仓库中所有的key。
- iterate:迭代数据库中所有的键值对,如果有一个value是undefined,就会推出,并且将 该键传入成功回调内。
配置相关API
- setDriver:用来选择驱动或者配置数据库,应该放在第一个调用数据API之前调用。
-
- 默认情况下,localForage会按照以下的顺序选择数据仓库的后后端驱动。1. IndexedDB2. WebSQL3. localStorage
- config:见词思意,用来配置localForage的API,在调用localForage之前必须先调用,此方法设置的任何值都将保留,属性较多,案例详解。
localforage.config({// 将数据库从 “localforage” 重命名为 “Hipster PDA App”name: 'Hipster PDA App'// 配置不同的驱动优先级driver: [localforage.WEBSQL,localforage.INDEXEDDB,localforage.LOCALSTORAGE],//size:数据库的大小(以字节为单位)。现在只用于WebSQL。 默认值:4980736 size:'100000',//version:将来用来升级version:'1.0'
});