## 事发总有个原因...
同学的业务有个需求,文件需要断点续传,用到了IndexedDB来做状态保存,但前人写的代码复制过来在保存`file`对象上有点问题,就去帮忙了,顺便了解新知识。
<br/>
## IndexedDB
[IndexedDB](https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API)是运行在网页的数据库,可以直接将对象保存在其中。
### 初始化数据库
```javascript
// 打开IndexedDB数据库连接
async function openDatabase(dbName, version) {
// 先获取当前站点下创建的所有数据库
const databases = await window.indexedDB.databases();
let flag = false;
// 判断是否已经创建了想要的数据库,没有则更改标识,之后要做处理
if (databases.filter(database => database.name == dbName).length == 0) {
flag = true;
}
return new Promise((resolve, reject) => {
// 数据库打开请求
/** @type {IDBOpenDBRequest} */
const DBOpenRequest = window.indexedDB.open(dbName, version);
// 若数据库未创建,则需要额外做处理
if (flag) {
// 数据库版本更新时会触发该回调,初次创建时也会触发
DBOpenRequest.onupgradeneeded = (event) => {
// 数据库对象
/** @type {IDBDatabase} */
const d = event.target.result
// 创建表,表只能在数据库版本更新的回调中进行创建。
initDatabaseStruct(d)
// 测试下来,当数据库不存在时,创建的连接是不能用于存储数据的,需要关闭后重新获取连接
d.close();
// 再次执行当前函数,并resolve二次执行的结果
resolve(openDatabase(dbName, version));
}
DBOpenRequest.onerror = (event) => {
reject(event.error)
}
} else {
DBOpenRequest.onsuccess = (event) => {
resolve(event.target.result)
}
DBOpenRequest.onerror = (event) => {
reject(event.error)
}
}
});
}
// 在数据库版本升级时对数据库做初始化操作
function initDatabaseStruct(/** @type {IDBDatabase} */ db) {
db.createObjectStore('fileStore', {
autoIncrement: true
});
}
```
### 添加数据
```javascript
// 保存数据到指定的表中
function saveData(/** @type {IDBDatabase} */ db, /** @type {String} */ storeName, /** @type {String} */ key, /** @type {Object} */ data) {
return new Promise((resolve, reject) => {
if (!db.objectStoreNames.contains(storeName)) {
reject(`store '${storeName}' is missing`)
return;
}
const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
/// 比较奇怪的顺序,平时都是先key再data,但这里应该是因为key是可选项,所以data在前
store.put(data, key);
resolve();
});
}
```
### 读取数据
```javascript
// 从指定的表中读取数据
function getData(/** @type {IDBDatabase} */ db, /** @type {String} */ storeName, /** @type {String} */ key) {
return new Promise((resolve, reject) => {
if (!db.objectStoreNames.contains(storeName)) {
reject(`store '${storeName}' is missing`)
return;
}
// 本以为读取数据可以只给个read,但是报错了
const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
const request = store.get(key);
request.onerror = (event) => {
reject(event.error)
}
request.onsuccess = (event) => {
resolve(event.target.result)
}
});
}
```
<br/>
## [FileSystemFileHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle)
才知道原来除了`file`类型的`input`标签外,在window上也有能选取文件的方法: `showOpenFilePicker`,该方法选取完之后会返回一个`FileSystemFileHandle`类型的数组(因为可以设置为多选,所以才是数组吧)。
对该handle对象的原型上有`getFile`方法,可以用来获取对应的文件,该方法返回是一个Promise对象(不是添加回调好文明)。
<br/>
有趣的来了,IndexedDB可以直接保存对象,哪怕是File对象也行,包括handle对象,所以可以这么做把选中的文件存放到IndexedDB里面,文件没上传完下次打开网站也可以继续读取。
```javascript
// 选取文件
async function pickTheFile() {
const pickerOpts = {
types: [
{
description: 'Images',
accept: {
'image/*': ['.png', '.gif', '.jpeg', '.jpg'],
},
},
],
excludeAcceptAllOption: true,
multiple: true,
};
/** @type {Array<FileSystemFileHandle>} */
const [filehandle] = await window.showOpenFilePicker(pickerOpts);
const fileData = await filehandle.getFile();
const db = await openDatabase('test', 1);
await saveData(db, 'fileStore', 'img', fileData);
}
// 打开IndexedDB数据库连接
async function openDatabase(dbName, version) {
const databases = await window.indexedDB.databases();
let flag = false;
if (databases.filter(database => database.name == dbName).length == 0) {
flag = true;
}
return new Promise((resolve, reject) => {
/** @type {IDBOpenDBRequest} */
const DBOpenRequest = window.indexedDB.open(dbName, version);
if (flag) {
DBOpenRequest.onupgradeneeded = (event) => {
const d = event.target.result
initDatabaseStruct(d)
// 测试下来,当数据库不存在时,创建的连接是不能用于存储数据的,需要关闭后重新获取连接
d.close();
// 用callee重新执行当前函数,并resolve二次执行的结果
resolve(openDatabase(dbName, version));
}
DBOpenRequest.onerror = (event) => {
reject(event.error)
}
} else {
DBOpenRequest.onsuccess = (event) => {
resolve(event.target.result)
}
DBOpenRequest.onerror = (event) => {
reject(event.error)
}
}
});
}
// 在数据库版本升级后对数据库做初始化操作
function initDatabaseStruct(/** @type {IDBDatabase} */ db) {
db.createObjectStore('fileStore', {
autoIncrement: true
});
}
// 保存数据到指定的表中
function saveData(/** @type {IDBDatabase} */ db, /** @type {String} */ storeName, /** @type {String} */ key, /** @type {Object} */ data) {
return new Promise((resolve, reject) => {
if (!db.objectStoreNames.contains(storeName)) {
reject(`store '${storeName}' is missing`)
return;
}
const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
/// 比较奇怪的顺序,平时都是先key再data,但这里应该是因为key是可选项,所以data在前
store.put(data, key);
resolve();
});
}
```
<br/>
不过存File文件太大了,也可以选择将handle对象存入IndexedDB;但这种做法也存在一些问题。
1. handle的访问授权仅能在本次会话里存在,如果将网页关闭后(**Microsoft Edge 107版本在地址栏有提示,可以手动删除授权**),再将handle对象读取出来后,需要调用handle对象上的`requestPermission`方法再次申请访问授权,该方法会返回一个Promise对象,用户点击允许后则能继续使用`getFile`获取文件;如果不申请授权直接调用`getFile`则会报错,这个应该是出于安全考虑。
2. handle像是记录的文件路径,如果将磁盘上的文件删除,那么`getFile`就获取不到文件了。
感觉就像在用iptables来添加规则,重启/手动删除前一直生效,重启/删除后就要重新添加规则了。
<br/>
图片存储和读取展示示例:[IndexedDB测试](https://blog.yingye.site/htmlUtil/IndexedDB-Test.html),上面的代码片段都只复制了部分,可能有哪里忘了更改,建议Ctrl + S保存到本地文件,方便查看和预览。
[学习,记录,整理] IndexedDB和FileSystemFileHandle