Loading...

文章背景图

[学习,记录,整理] IndexedDB和FileSystemFileHandle

2022-11-26
2669
-
- 分钟

事发总有个原因…

同学的业务有个需求,文件需要断点续传,用到了IndexedDB来做状态保存,但前人写的代码复制过来在保存file对象上有点问题,就去帮忙了,顺便了解新知识。


IndexedDB

IndexedDB是运行在网页的数据库,可以直接将对象保存在其中。

初始化数据库

// 打开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
  });
}

添加数据

// 保存数据到指定的表中
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();
  });
}

读取数据

// 从指定的表中读取数据
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)
    }
  });
}

FileSystemFileHandle

才知道原来除了file类型的input标签外,在window上也有能选取文件的方法: showOpenFilePicker,该方法选取完之后会返回一个FileSystemFileHandle类型的数组(因为可以设置为多选,所以才是数组吧)。

对该handle对象的原型上有getFile方法,可以用来获取对应的文件,该方法返回是一个Promise对象(不是添加回调好文明)。


有趣的来了,IndexedDB可以直接保存对象,哪怕是File对象也行,包括handle对象,所以可以这么做把选中的文件存放到IndexedDB里面,文件没上传完下次打开网站也可以继续读取。

  // 选取文件
  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();
    });
  }

不过存File文件太大了,也可以选择将handle对象存入IndexedDB;但这种做法也存在一些问题。

  1. handle的访问授权仅能在本次会话里存在,如果将网页关闭后(Microsoft Edge 107版本在地址栏有提示,可以手动删除授权),再将handle对象读取出来后,需要调用handle对象上的requestPermission方法再次申请访问授权,该方法会返回一个Promise对象,用户点击允许后则能继续使用getFile获取文件;如果不申请授权直接调用getFile则会报错,这个应该是出于安全考虑。
  2. handle像是记录的文件路径,如果将磁盘上的文件删除,那么getFile就获取不到文件了。

感觉就像在用iptables来添加规则,重启/手动删除前一直生效,重启/删除后就要重新添加规则了。


图片存储和读取展示示例:IndexedDB测试,上面的代码片段都只复制了部分,可能有哪里忘了更改,建议Ctrl + S保存到本地文件,方便查看和预览。

评论交流

文章目录