介绍
浏览器本地存储API有很多,例如:Cookie
、IndexDB
、WebStorage
他们都有各自的缺点,我们希望设计一套通用的本地存储API去解决这个问题。
首先我们要知道每个本地存储API的缺点:
Cookie
:容量小(大约4KB),只能存储字符串类型的数据,发送请求时浏览器会自动携带对应的Cookie
无法干预此操作。IndexDB
:容量大(当前可用磁盘空间的50%),可以存储任意类型的数据,存储又可以分为临时存储(默认)和持久存储,但是老版本浏览器不兼容。- 临时存储:默认行为,超过存储容量会采用
LRU
算法自动清空最近最少使用的源。 - 持久存储:尚在实验阶段,通过
navigator.storage.persist()
请求本地数据存储的权限返回Promise
对象状态值为布尔值表示开启成功或失败。 - 全局存储限制:当前可用磁盘空间的 50%。
- 组存储限制:为全局限制的 20%,但它至少有 10 MB,最大为 2GB。相同 eTLD+1 的域被视为一个组,每个具体的子域或域被视为一个源。
- 临时存储:默认行为,超过存储容量会采用
WebStorage
:包含两种类型:localStorage
和sessionStorage
,存储容量(大约5MB)。localStorage
:只能存储字符串类型的数据,浏览器兼容性好。sessionStorage
:只能存储字符串类型的数据,只在当前会话中有效,页面关闭数据清除。
eTLD+1域:
e
表示可用的,TLD
表示顶级域名,TLD+1
表示顶级域名和直接上级的域名。组:所有具有相同
eTLD+1
的域被视为一个组。在这个组中,所有源共享一个存储配额,这个配额是全局存储限制的20%。源:每个具体的子域或域被视为一个源。例如:
mozilla.org
、www.mozilla.org
和joe.blogs.mozilla.org
都是不同的源但是他们具有相同的eTLD+1域
所以共享一个存储配额。
对于 indexDB 中 域 、组 、eTLD+1 概念不清楚可查阅官方文档:indexDB浏览器存储限制和清理标准
indexDB基本使用
const request = window.indexedDB.open("MyTestDatabase", 3); // 开启数据库。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 为数据库创建对象存储(objectStore)
const objectStore = db.createObjectStore("form", { keyPath: "myKey" });
// 创建索引,方便后续查找。
objectStore.createIndex("name", "name", { unique: false });
};
// 后续封装 inexdDB 时也需要在 onsucess 回调中拿到 event.target.result 在执行其他操作。
request.onsucess = (event) => {
const db = event.target.result;
// 想要操作数据必须开启事务。
const transaction = db.transaction(["customers"], "readwrite");
const objectStore = transaction.objectStore('form'); // 拿到对应的对象存储。
// 在 onupgradeneeded 设置了索引之后才可以直接删除,否则需要使用游标查找对应的项之后在执行删除操作。
const request = objectStore.delete(key); // 执行删除操作。
request.onsuccess = (event) => {
// event.target.result 是删除的结果。
});
// 使用游标查找。
const request$1 = objectStore.openCursor();
request$1.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
// 这里进行筛查要对哪个数据进行增删改操作。
if(shouldStopProcessing(cursor)){ // 找到对应的数据。
cursor.delete(); // 执行删除操作。
} else {
cursor.continue(); // 不断取出下一项。
}
} else {
console.wran('已经没有更多数据了');
}
}
}
// 保存 IDBDatabase 接口。
request.onerror = (event) => {
console.error(`数据库错误:${event.target.errorCode}`);
};
预期的结果:
基于以上的问题能够使用的API只有 indexDB
和 localStorage
,我们希望可以在现代浏览器中使用 indexDB
在老版本浏览器中使用 localStorage
进行存储数据,
遇到的问题:
它们的存储方式是不同的,例如:indexDB
需要调用 open
方法开启数据库后才开始存储操作并且所有的操作都是基于异步的,而 localStorage
只需要使用 getItem
、setItem
等方法直接同步存储。
如何解决:
我们可以通过模板模式设置一个基类内部包含 getItem
、setItem
、removeItem
、clear
、getAll
等方法并且要求所有操作都是异步的,让 indexDB
和 localStorage
都继承该基类去解决问题。
代码实现
设置模板:
class Template {
getItem(key){
throw new Error('必须实现 getItem 方法');
},
setItem(key,value){
throw new Error('必须实现 setItem 方法');
},
removeItem(key){
throw new Error('必须实现 removeItem 方法');
},
clear(){
throw new Error('必须实现 clear 方法');
},
getAll(){
throw new Error('必须实现 getAll 方法');
}
}
实现LocalStorage类:
class LocalStorage {
getItem(key) {
return new Promise((resolve, reject) => {
const result = localStorage.getItem(key);
try {
resolve({
status: true,
message: '获取数据成功',
data: JSON.parse(result)
});
} catch (e) {
resolve({
status: true,
message: '获取数据成功',
data: result
});
}
});
}
setItem(key, value) {
return new Promise((resolve, reject) => {
try {
localStorage.setItem(key, value);
resolve({
status: true,
message: '新增数据成功',
data: null
});
} catch (e) {
reject(e);
}
});
}
removeItem(key) {
return new Promise((resolve, reject) => {
try {
localStorage.removeItem(key);
resolve({
status: true,
message: '删除数据成功',
data: null
});
} catch (e) {
reject(e);
}
});
}
clear() {
return new Promise((resolve, reject) => {
try {
localStorage.clear();
resolve({
status: true,
message: '清除所有数据成功',
data: null
});
} catch (e) {
reject(e);
}
})
}
getAll() {
return new Promise((resolve, reject) => {
try {
const len = localStorage.length;
const items = [];
for (let i = 0; i < len; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
items.push({key: value});
}
resolve({
status: true,
message: '获取所有数据成功',
data: items
})
} catch (e) {
reject(e);
}
})
}
}
实现IndexDB类
// 这个方法用于打开 indexDB 数据库并返回 Promise 对象。
function openDatabase(databaseName, version, storeName = 'store_personal') {
return new Promise((resolve, reject) => {
const request = indexedDB.open(databaseName, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 为数据库创建对象存储(objectStore)
const objectStore = db.createObjectStore(storeName, {
keyPath: "key",
autoIncrement: true
});
// 创建一个索引
objectStore.createIndex("key", "key", {unique: false});
};
request.onsuccess = (event) => {
resolve(event);
};
request.onerror = (event) => {
console.error(`数据库错误:${event.target.errorCode}`);
reject(event.target.errorCode);
};
});
}
class IndexDB {
constructor(databaseName,version,storeName) {
this.IDBDatabase = openDatabase(databaseName,version,storeName);
this.storeName = storeName;
}
getItem(key) {
return new Promise(async (resolve) => {
const IDBDatabase = await this.IDBDatabase; // 等待外界拿到indexDB数据库实例。
const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readonly');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(key);
request.onsuccess = (event) => {
resolve({
status: true,
message: '查找成功',
data: event.target.result || request.result
});
};
request.onerror = (event) => {
resolve({
status: false,
message: '查找失败',
data: event.target.error || null
});
}
});
}
setItem(key, value) {
return new Promise(async (resolve, reject) => {
const IDBDatabase = await this.IDBDatabase;
const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
// put方法如果之前存在则修改,否则新增。如果想要put修改生效则需要设置主键让数据库知道哪些数据是相同的。
const request = objectStore.put({key, value});
request.onsuccess = () => {
resolve({
status: true,
message: '新增成功',
data: {key, value}
})
}
request.onerror = (event) => {
resolve({
status: false,
message: '新增失败',
data: event.target.error || null
});
}
});
}
deleteItem(key) {
return new Promise(async (resolve) => {
const IDBDatabase = await this.IDBDatabase;
const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.delete(key);
request.onsuccess = (event) => {
resolve({
status: true,
message: '删除成功',
data: event.target.result || request.result
});
}
request.onerror = (event) => {
resolve({
status: false,
message: '删除失败',
data: event.target.error || null
});
}
});
}
clear() {
return new Promise(async (resolve) => {
const IDBDatabase = await this.IDBDatabase;
const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.clear();
request.onsuccess = (event) => {
resolve({
status: true,
message: '数据清除成功',
data: null
});
}
request.onerror = (event) => {
resolve({
status: false,
message: '数据清除失败',
data: event.target.error || null
});
}
});
}
getAll() {
return new Promise(async (resolve) => {
const IDBDatabase = await this.IDBDatabase;
const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.getAll();
request.onsuccess = (event) => {
resolve({
status: true,
message: '获取所有数据成功',
data: event.target.result || request.result
});
}
request.onerror = (event) => {
resolve({
status: false,
message: '获取所有数据失败',
data: event.target.error || null
});
}
});
}
}
根据浏览器支持情况选择对应的类:
function useStore() {
const _indexDB = new IndexDB('store', 1, 'store_personal');
const _localStorage = new LocalStorage();
const strategy = window.indexedDB !== undefined ? _indexDB : _localStorage;
return {
getItem(key) {
return strategy.getItem(key);
},
setItem(key,value) {
return strategy.setItem(key,value);
},
removeItem(key) {
return strategy.removeItem(key);
},
clear(){
return strategy.clear();
},
getAll(){
return strategy.getAll();
}
}
}