利用IndexDB实现网页版断点续传,刷新可继续上传

1,062 阅读4分钟

首先介绍一下整体流程

未命名文件 .png

IndexDB的基本使用

  1. 创建(打开)数据库
  let that = this;
  const dbName = "databaseName"; //数据库名称
  const tablename = "tableName"; //表名
  const dbVersion = 1.0; //数据库版本
  //实例化IndexDB数据上下文,这边根据浏览器类型来做选择
  let indexedDB =
    window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
  if ("webkitIndexedDB" in window) {
    window.IDBTransaction = window.webkitIDBTransaction;
    window.IDBKeyRange = window.webkitIDBKeyRange;
  }
  H5AppDB.indexedDB = {};
  H5AppDB.indexedDB.db = null;
  //错误信息,打印日志
  H5AppDB.indexedDB.onerror = function(e) {
    console.log("错误信息:", e);
  };
 H5AppDB.indexedDB.open = function(version) {
        //初始IndexDB
        var request = indexedDB.open(dbName, version || dbVersion);
        request.onsuccess = function(e) {
          console.log("成功打开数据库: " + dbName);
          H5AppDB.indexedDB.db = e.target.result;
          //这边可以做一下打开数据库后的操作
        };
        // 如果版本不一致,执行版本升级的操作
        request.onupgradeneeded = function(e) {
          console.log("开始升级数据库。");
          H5AppDB.indexedDB.db = e.target.result;
          var db = H5AppDB.indexedDB.db;
          if (db.objectStoreNames.contains(tableName)) {
            db.deleteObjectStore(tableName);
          }
          let store = db.createObjectStore(tableName, {
            keyPath: "md5"
          }); //NoSQL类型数据库中必须的主键,唯一性
          store.createIndex("fileId", "fileId", { unique: false }); //建立查找索引
           store.createIndex("fileId", ["fileId","index"], { unique: false }); //多条件索引
        };
        request.onfailure = H5AppDB.indexedDB.onerror;
      };
  1. 插入/更新数据--插入数据的key如果数据表已存在则更新
H5AppDB.indexedDB.addTodo = function(data, name) {
        var db = H5AppDB.indexedDB.db;
        var trans = db.transaction([name || tablename], "readwrite");
        var store = trans.objectStore(name || tablename);
        //数据以对象形式保存,体现NoSQL类型数据库的灵活性
        var request = store.put(data); //保存数据
        request.onsuccess = function() {
          console.log("添加成功");
        };
        request.onerror = function(e) {
          console.log("添加出错: ", e);
        };
      };

3.删除数据

H5AppDB.indexedDB.deleteTodo = function(id, name) {
        var db = H5AppDB.indexedDB.db;
        var trans = db.transaction([name || tablename], "readwrite");
        var store = trans.objectStore(name || tablename);
        var request = store.delete(id); //根据主键来删除
        request.onsuccess = function() {
          console.log("删除成功");
        };
        request.onerror = function(e) {
          console.log("删除出错: ", e);
        };
      };
      H5AppDB.indexedDB.open(1.0);
    },

4.查找数据

H5AppDB.indexedDB.getAllTodoItems = function(
        name,
        key,
        callback,
        ...value
      ) {
        //let todos = "";
        var db = H5AppDB.indexedDB.db;
        var trans = db.transaction([name || tablename], "readwrite"); //通过事物开启对象
        var store = trans.objectStore(name || tablename); //获取到对象的值
        var index = store.index(key);
        // Get everything in the store;
        var keyRange = IDBKeyRange.only(value.length > 1 ? value : value[0]);//如果是单条件查询需要传入具体数据值,如果是多条件则传入数据值数组
        var cursorRequest = index.openCursor(keyRange); //开启索引为0的表
        let res = [];
        cursorRequest.onsuccess = function(e) {
          let result = e.target.result;
          if (!!result === false) return;
          res.push(result.value);
          result.continue(); //这边执行轮询读取
        };
        cursorRequest.onerror = H5AppDB.indexedDB.onerror;
        trans.oncomplete = () => {//事物执行完成后执行
          callback(res);//由于indexDB是异步执行的,为了确保查询完数据再对数据进行进一步操作,可以传入回调函数在这边执行
        };
      };

断点续传

文件分片

这边未采用动态分片,动态分片可根据前面上传分片所花费的时间确定下一分片的大小,暂未实现;

 // 文件开始分片,push分片到fileChunkedList数组中
 const chunkSize=2 * 1024 * 1024;//分片大小
  for (let i = 0; i < optionFile.size; i = i + chunkSize) {
    const tmp = optionFile.slice(
      i,
      Math.min(i + chunkSize, optionFile.size)
    );
    fileChunkedList.push(tmp);
  }

处理文件分块信息

  fileChunkedList = fileChunkedList.map((item, index) => {
    const formData = new FormData();
    const ans = {};
    if (option.data) {
      Object.keys(option.data).forEach(key => {
        formData.append(key, option.data[key]);
      });
    }
    // 看后端需要哪些,就传哪些,也可以自己追加额外参数
    formData.append(
      "multipartFile",
      that.isPurse ? item.formData : item
    ); // 文件,isPurse===true说明文件是刷新后继续上传的
    formData.append("key", option.file.name); // 文件名
    ans.formData = formData;
    ans.index = index;
    return ans;
  });

分片进行MD5摘要签名

为了不阻塞上传进度,这里利用浏览器间歇requestIdleCallback方法进行MD5加密;

 //间歇计算分片md5
const calculateHashIdle = async chunks => {
  return new Promise(resolve => {
    const spark = new SparkMD5.ArrayBuffer();
    let count = 0;
    const appendToSpark = async file => {
      return new Promise(resolve => {
        const reader = new FileReader();
        reader.readAsArrayBuffer(file);
        reader.onload = e => {
          const md5 = spark.append(e.target.result);
          resolve(md5);
        };
      });
    };
    const workLoop = async deadline => {
      // 有任务,并且当前帧还没结束
      while (count < chunks.length && deadline.timeRemaining() > 1) {
        if (!chunks[count]) {
          return;
        }
        await appendToSpark(chunks[count]);
        chunks[count].md5 = spark.end();
        H5AppDB.indexedDB.addTodo({//因为分块数据表的key为分块MD5,这里执行插入可以确保该分块的MD5已生成
          ...chunks[count],
          fileId
          formData
        });
        count++;
        resolve(chunks[count]);
      }
      window.requestIdleCallback(workLoop);
    };
    window.requestIdleCallback(workLoop);
  });
};

控制并发上传

 function sendRequest(chunks, limit = 3) {
    return new Promise((resolve, reject) => {
      const len = chunks.length;
      let counter = 0;
      let isStop = false;//是否停止上传
      const start = async () => {
        if (isStop) {
          return;
        }
        const item = chunks.shift();
        if (!item) {
          return;
        }
        if (!item.md5) {//因为前面是采用requestIdleCallback执行MD5加密,所以有可能浏览器特别忙,当前块还未来得及加密;
          await calculateHashIdle([item]);
        }
        if (item)
          if (item) {
            let config = {
              method: "POST",
              url: url,
              data: item.formData,
              onUploadProgress: e => {
                percentage[item.index] = e.loaded;
                updataPercentage(e);//处理进度条
              },
              headers: {
                "Content-Type": "multipart/form-data",
                md5: item.md5
              },
              withCredentials: true
            };
            axios(config)
              .then(response => {
                if (response.data.code) {
                  if (response.data.code !== "0") {                
                      isStop = true;
                      reject(response);             
                  } else {
                    if (counter === len - 1) {//说明最后一个分块上传成功
                      resolve(response.data);
                      H5AppDB.indexedDB.deleteTodo(//删除file表
                        fileId,
                         "fileCollection"
                      );
                    } else {
                      counter++;
                      H5AppDB.indexedDB.addTodo(//更新file表,主要为了更新上传进度
                        {
                          ...data
                        },
                        "fileCollection"
                      );
                      start();
                    }
                    H5AppDB.indexedDB.deleteTodo(//删除已上传成功的分块
                      response.config.headers.md5
                    );
                  }
                }
              })
              .catch(err => {
                that.$message({
                  message: "网络请求发生错误!",
                  type: "error",
                  showClose: true
                });
                reject(err);
              });
          }
      };
      while (limit > 0) {//控制并发
        setTimeout(() => {
          start();
        }, Math.random() * 1000);
        limit -= 1;
      }
    });
  }

处理文件上传进度条

// 更新上传进度条百分比的方法
const updataPercentage = e => {
let loaded = (that.uploaded / 100) * optionFile.size; // 当前已经上传文件的总大小
console.log(loaded);
percentage.forEach(item => {
  loaded += item;
});
e.percent = (loaded / optionFile.size) * 100;
if (that.value >= parseFloat(Number(e.percent).toFixed(0))) return;
that.value = parseFloat(Number(e.percent).toFixed(0));
};

刷新后初始化界面信息

  const getDataCallback = res => {
    if (res.length) {
      this.fileOption = {
        file: {
          size: res[0].fileSize,//数据总大小
          name: res[0].fileName
        }
      };
      res.forEach(item => {
        that.fileList[item.fileType] = [//展示文件信息
          {
            name: item.fileName,
            type: item.fileType,
            fileId: item.fileId
          }
        ];
      });
      this.value = res[0].uploaded;//展示进度条进度
      this.uploaded = res[0].uploaded;//已上传的数据百分比
      this.isPurse = true;
    }
  };
  H5AppDB.indexedDB.getAllTodoItems(
    "fileCollection",//查询的数据表
    "union",//查询的索引
    getDataCallback,//查询完成后执行的毁掉函数
    data//查询条件,可传多个
  );