浏览器离线存储

176 阅读6分钟

浏览器组成

  • 用户界面
  • 浏览器引擎
  • 渲染引擎
  • 网络
  • js解释器
  • 用户界面后端
  • 数据存储(data storage)

浏览器离线存储的方式如下

  • Cookie
  • Web Storage
  • WebSQL
  • IndexedDB
  • File System

WebSQL

关系型数据库,大多数浏览器都支持,但是现在已经不再维护了,慎用。 WebSQL的三个核心方法:

  1. openDatabase:使用现有数据库或创建一个新的数据库对象
  2. transaction:这个方法让我们执行一个事务,以及基于这种情况执行提交或回滚。
  3. executeSql:这个方法用于执行实际的SQL查询。

打开数据库

打开一个名为mysql的数据库,因为第一次不存在此库,所以会创建该数据库,版本号为1.0,大小为2M。

var db = openDatabase('mysql','1.0','tetst db',2*1024*1024);

执行操作

创建logs表,包含id和log字段,id是唯一的。

 db.transaction(function(tx){ 
     tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique,log)');
 });

执行sql

var name 
var age = 15;
var db = openDatabase('mysql', 'test db', 1.0, 2 * 1024 * 1024);

// 插入操作
db.transaction(function (tx) {
    // tx 是一个SQLTransaction对象,后面操作都基于tx
    tx.executeSql('CREATE TABLE IF NOT EXISTS STU(id unique,name,age)');
    tx.executeSql('INSERT INTO STU (id,name,age) VALUES (1,"ming",12)');
    tx.executeSql('INSERT INTO STU (id,name,age) VALUES (2,"hong",13)');
    tx.executeSql('INSERT INTO STU (id,name,age) VALUES (3,?,?)',[name,age]);
});

// 读取操作
db.transaction(function(tx){
    tx.executeSql('SELECT * FROM STU',[],function(tx,result){
        var len = result.rows.length;
        for(let i=0;i<len;i++){
            console.log(result.rows[i]);
        }
    },null);
});

// 更新操作
db.transaction(function(tx){
    tx.executeSql('UPDATE STU SET name="王羲之" where id=3');
});


// 删除操作
var id = 1;
db.transaction(function(tx){
    tx.executeSql('DELETE FROM STU where id=?',[id]);
});

IndexDB

简介

  • 诞生的背景 浏览器的功能越来越强大,就开始考虑是否可以将大量数据存储在客户端,减少从服务端获取数据,直接从本地获取。 目前所有的数据存储方案都不适合存储大量数据,Cookie大大小不超过4KB,且每次请求都会发送回服务器;LocalStorage在2.5MB到10MB之间(各个浏览器不同),而且不提供搜索功能,不能建立自定义索引。所以提出来一种新的方案,就是IndexDB。

  • 什么是IndexDB IndexDB是一种底层API,用于在客户端存储大量的结构化数据(也包括文件、二进制大型对象blobs)。该API使用索引实现对数据的高性能搜索。虽然Web Storage在存储较少量的数据很有用,但是对于存储更大量的结构化数据来说力不从心。而IndexDB提供了这种场景的解决方案。

  • 通俗理解 IndexDB就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexDB允许存储大量数据,提供查找接口,还能建立索引。这些都是LocalStorage所不具备的。就数据库类型而言,IndexDB不属于关系型数据库(不支持SQL查询语句),更接近NoSQL数据库。

  • 几种常见客户端存储方式的对比:

属性会话期Cookie持久性CookieSessionStorageLocalStorageIndexDBWebSQL
存储大小4KB4KB2.5-10MB2.5-10MB>250mb已废弃
失效时间浏览器关闭自动清除设置过期时间,到期自动清除浏览器关闭后清除永久保存-只能手动清除手动更新或删除已废弃

IndexDB特点:

  • 键值对存储,IndexDB内部采用对象仓库存放数据。所有类型都可存入。如js对象等。主键是唯一的。
  • 异步的,indexdb操作时不会锁死浏览器,用户依然可以进行其他操作。这与localStorage形成鲜明对比。后者是同步的,异步是为了放置大量数据的读写拖慢网页的表现。
  • 支持事务,一系列的操作步骤之中,如果有一个操作失败,则整个事务就取消,并且数据库回滚到事务发生之前。和MySQL类似。
  • 同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能跨域访问数据库。
  • 存储空间大,大于250MB,没有上限。
  • 支持二进制存储,可以存储字符串,也能存储二进制数据。
  • 数据可视化界面,大量数据,每次请求会消耗很大性能。
  • 即时聊天工具,大量消息需要存在本地
  • 其他存储方式存出来不足时,不得已使用

重要概念

IndexDB是一个比较复杂的API,包含:

  • 数据库:IDBDatabase对象 就是一系列数据的容器,一个域名下可以创建多个(协议+域名+端口)

  • 对象仓库:IDBObjectStore对象 每个数据库包含多个对象仓库,类似于关系型数据库的table。

  • 索引:IDBIndex对象 不同的属性建立索引,可以加快数据检索。

  • 事务:IDBTransaction对象

  • 操作请求:IDBRequest对象

  • 指针:IDBCursor对象

  • 主键集合:IDBKeyRange对象

实操

  <script>
        // 1. 创建并连接数据库
        function openDB(dbname, version = 1) {
            return new Promise((resolve, reject) => {
                var db; // 存储创建的数据库
                // 打开数据库,若没有则会创建
                const request = indexedDB.open(dbname, version);

                // 数据库打开成功回调
                request.onsuccess = function (event) {
                    db = event.target.result; // 存储数据库对象
                    console.log('数据库打开成功');
                    resolve(db);
                };

                // 数据库打开失败回调
                request.onerror = function (event) {
                    console.log('数据库打开失败');
                    // reject();
                }

                // 数据库有更新时的回调:1. 版本号更新 2. 添加或删除表(对象仓库)
                // 第一次调用时会触发这个事件
                // 初始化仓库对象-数据表
                request.onupgradeneeded = function (event) {
                    // 数据库创建或升级时会触发
                    console.log('onupgradeneeded')
                    db = event.target.result;
                    // 创建仓库对象-数据表 
                    var objectStore = db.createObjectStore('stu', {
                        keyPath: 'stuId', //主键
                        autoIncrement: true, // 实现自增
                    })
                    // 创建索引,有了索引,查询速度会加快
                    objectStore.createIndex('stuId', 'stuId', { unique: true });
                    objectStore.createIndex('stuName', 'stuName', { unique: false });
                    objectStore.createIndex('stuAge', 'stuAge', { unique: false });
                }
            });
        }
        // 关闭数据库
        function closeDB(db) {
            db.close();
            console.log('关闭数据库');
        }

        // 删除数据库
        function deleteDB(dbname) {
            let deleteRequest = window.indexedDB.deleteDatabase(dbname);
            deleteRequest.onerror = function (event) {
                console.log("删除数据库失败");
            }
            deleteRequest.onsuccess = function (event) {
                console.log("删除数据库成功");
            };
        }

        // 插入数据
        function addData(db, storeName, data) {
            // 数据库示例,仓库对象-表,数据
            // 创建读写事务
            var request = db.transaction(storeName, 'readwrite')
                .objectStore(storeName)
                .add(data);
            request.onsuccess = function () {
                console.log('数据写入成功');
            }
            request.onerror = function () {
                console.log('数据写入失败');
            }
        }

        // 更新数据
        function updateData(db, storeName, data) {
            return new Promise((resolve, reject) => {
                var request = db
                    .transaction(storeName, 'readwrite') // 事务对象,默认是只读readonly
                    .objectStore(storeName) // 仓库对象
                    .put(data);
                request.onsuccess = function (e) {
                    resolve({
                        status: true,
                        message: '数据更新成功'
                    })
                }
                request.onerror = function (e) {
                    console.log("数据更新失败!")
                }
            });
        }

        // 删除数据
        function deleteData(db, storeName, key) {
            return new Promise((resolve, reject) => {
                var request = db
                    .transaction(storeName, 'readwrite') // 事务对象
                    .objectStore(storeName) // 仓库对象
                    .delete(key)
                request.onsuccess = function (e) {
                    resolve({
                        status: true,
                        message: '数据删除成功'
                    })
                }
                request.onerror = function (e) {
                    console.log("数据删除失败!")
                }
            });
        }

        function deleteDataByCursor(db, storeName, indexName, indexValue) {
            return new Promise((resolve, reject) => {
                var store = db
                    .transaction(storeName, 'readwrite') // 事务对象
                    .objectStore(storeName); // 仓库对象

                var request = store.index(indexName) // 索引对象
                    .openCursor(IDBKeyRange.only(indexValue));  // 指针对新

                request.onsuccess = function (e) {
                    var cursor = e.target.result;
                    var deleteRequest;
                    if (cursor) {
                        deleteRequest = cursor.delete(); // 请求删除当前项
                        deleteRequest.onsuccess = function () {
                            console.log("游标删除记录成功");
                            resolve({
                                status: true,
                                message: '游标删除记录成功'
                            })
                        }
                        deleteRequest.onerror = function () {
                            console.log("游标删除记录失败");
                            resolve({
                                status: false,
                                message: '游标删除记录失败'
                            })
                        }
                        cursor.continue();
                    }
                    resolve({
                        status: true,
                        message: '数据删除成功'
                    })
                }
                request.onerror = function (e) {
                    console.log("数据删除失败!")
                }
            });
        }

        // 查询数据
        function getDataByKey(db, storeName, key) {
            return new Promise((resolve, reject) => {
                var request = db.transaction([storeName])
                    .objectStore(storeName)
                    .get(key);

                request.onsuccess = function () {
                    console.log('查询数据成功');
                    resolve(request.result);
                }

                request.onerror = function () {
                    console.log('查询数据失败');
                }
            })
        }
        // 查询所有数据
        function getAllData(db, storeName) {
            return new Promise((resolve, reject) => {
                var request = db.transaction([storeName])
                    .objectStore(storeName)
                    .getAll();

                request.onsuccess = function () {
                    console.log('查询数据成功');
                    resolve(request.result);
                }

                request.onerror = function () {
                    console.log('查询数据失败');
                }
            })
        }
        // 查询所有数据:通过索引和游标
        function getAllDataByCursor(db, storeName) {
            return new Promise((resolve, reject) => {
                var list = [];
                var request = db.transaction([storeName], 'readwrite')
                    .objectStore(storeName)
                    .openCursor();// 创建一个指针(游标)

                request.onsuccess = function (event) { // 游标移动一次,读一条
                    console.log('查询数据成功');
                    var cursor = event.target.result;
                    console.log(cursor);
                    if (cursor) {
                        list.push(cursor);
                        cursor.continue(); // 游标移到到下一条数据
                    } else {
                        resolve(list);
                    }
                }
                request.onerror = function () {
                    console.log('查询数据失败');
                }
            })
        }
        // 查询所有数据:分页查询
        function getDataByPage(db, storeName, indexName, indexValue, page, pageSize) {
            return new Promise((resolve, reject) => {
                var list = [];
                var counter = 0; // 计数器
                var advanced = true; // 是否跳过多少条查询
                var store = db.transaction([storeName], 'readwrite').objectStore(storeName); // 仓库对象

                var request = store
                    // .index(indexName) // 索引对象
                    // .openCursor(IDBKeyRange.only(indexValue)) // 按照指定值分页查询
                    .openCursor();// 创建游标对象,目前指向第一条

                request.onsuccess = function (event) { // 游标移动一次,读一条
                    var cursor = event.target.result;
                    if (page > 1 && advanced) {
                        advanced = true;
                        cursor.advance((page - 1) * pageSize); // 跳过多少条
                        return;
                    }
                    if (cursor) {
                        list.push(cursor.value);
                        counter++;
                        if (counter < pageSize) {
                            cursor.continue(); // 游标移到到下一条数据
                        } else {
                            cursor = null;
                            resolve(list);
                        }
                    } else {
                        resolve(list);
                    }
                }
                request.onerror = function () {
                    console.log('分页查询数据失败');
                }
            })
        }
        // 根据索引来查询数据
        function getDataByIndex(db, storeName, indexName, indexValue) {
            return new Promise((resolve, reject) => {
                var list = [];
                var request = db.transaction([storeName], 'readwrite')
                    .objectStore(storeName)
                    .index(indexName)
                    // .get(indexValue)
                    .openCursor(IDBKeyRange.only(indexValue));

                // IDBKeyRange.only()--指定只包含一个值,all keys = indexValue
                // IDBKeyRange.lowerBound(val,false)--指定下限 all keys >= indexValue
                // IDBKeyRange.upperBound(val,false)--指定上限 all keys <  indexValue
                // IDBKeyRange.bound(val1,val2,false)---指定上下限 all keys <= val2 && keys >=val1

                request.onsuccess = function (event) {

                    var cursor = event.target.result;
                    console.log(cursor)
                    if (cursor) {
                        list.push(event.target.result);
                        cursor.continue();
                    } else {
                        resolve(list);
                    }
                }
                request.onerror = function (event) {
                    console.log('根据索引和游标查询失败');
                }
            })
        }
        // 1. 创建stu数据库
        openDB('stuDB', 1).then((db) => {
            // 1. 添加数据
            // addData(db, 'stu', { 'stuId': 1, 'stuName': '张三', 'stuAge': 12 });
            // addData(db, 'stu', { 'stuId': 2, 'stuName': '李四', 'stuAge': 15 });
            // addData(db, 'stu', { 'stuId': 3, 'stuName': '王五', 'stuAge': 11 });
            // addData(db, 'stu', { 'stuId': 4, 'stuName': '张三', 'stuAge': 12 });
            // addData(db, 'stu', { 'stuId': 5, 'stuName': '李五', 'stuAge': 15 });
            // addData(db, 'stu', { 'stuId': 6, 'stuName': '王六', 'stuAge': 11 });

            // 2. 根据主键查询数据
            // return getDataByKey(db, 'stu', 2);

            // 3. 查询仓库对象中的所有数据
            // return getAllData(db, 'stu');

            // 4. 通过游标读取数据
            // return getAllDataByCursor(db, 'stu');

            // 5. 通过索引查询:返回满足条件的第一条数据
            // return getDataByIndex(db, 'stu', 'stuName', '张三');

            // 6. 分页查询
            return getDataByPage(db, 'stu', '', '', 1, 2);

            // 7. 更新数据
            // return updateData(db, 'stu', { 'stuId': 1, 'stuName': '张三222', 'stuAge': 122 })

            // 8. 删除数据
            // return deleteData(db, 'stu', 6);

            // 9. 通过索引删除
            // return deleteDataByCursor(db, 'stu', 'stuName', '张三');
        })
            .then((stuInfo) => {
                // console.log('用户id=2的信息', stuInfo);
                console.log(stuInfo);
            });
    </script>

FileAPI

我们知道html的input表单控件可以用来上传文件

<input type="file" name="" id=""> 

但是选择文件后,只显示文件名,而不能在线查看,只能通过上传给服务器,服务器再返回来,才能查看。用户体验很差,我们无法在客户端对用户选择的文件进行验证,无法读取文件的大小,无法预览,无法判断类型。如果是多文件上传,就更复杂了。 所以H5提供的File API就能解决这些问题,改接口允许js读取本地文件,但并不能直接访问本地文件。需要依赖用户行为,比如用户在input控件上选择了某个文件或将文件拖拽到浏览器上。

<input type="file" name="" id="">
<script> 
var input = document.querySelector('input');   
input.onchange = function (e) {
       
    var files = e.target.files;
    console.log(files);
    console.log(files[0] instanceof File);//    true
    /*

    FileList
        0: 
            FilelastModified: 1640659856244
            lastModifiedDate: Tue Dec 28 2021 10:50:56 GMT+0800 (中国标准时间) {}
            name: "20211228.png"
            size: 97366
            type: "image/png"
            webkitRelativePath: ""
            [[Prototype]]: File
        length: 1
        [[Prototype]]: FileList
    */
}           
</script>

简介

浏览器提供了一个原生的File()构造函数,用来生成File实例对象。

new File(array,name[,options]) File()构造函数接受三个参数:

  • array:一个数组,成员可以是二进制对象,或是字符串,表示文件内容。
  • name:字符串,表示文件名,或文件路径
  • options:配置对象,设置实例的属性,可选。{type:字符串,表示实例对象的MIME类型,默认值为空字符串,lastModified:时间戳,最后一次修改时间,Date.now().}

File对象

var file = new File(['test test'], 'd://foo.txt');
console.log(file);
/*
lastModified: 1652452151978
lastModifiedDate: Fri May 13 2022 22:29:11 GMT+0800 (中国标准时间) {}
name: "d://foo.txt"
size: 9
type: ""
webkitRelativePath: ""
[[Prototype]]: File
*/

以上代码通过构造函数的方式创建了File对象,并打印了该文件的属性信息。因为File对象没有自己的实例方法,由于继承了Blob对象,所以可以调用Blob对象的方法slice()。

FileList对象

FileList是一个类似于数组的对象,代表一组选中的文件,没一个成员都是File实例。 使用场景:

  • input空间的files属性,返回FileList实例。
  • 拖拽一组文件时,目标区的DataTransfer.files属性,返回FileList实例。

FileReader对象

FileReader对象用于读取File对象或Blob对象所包含的文件内容。 浏览器原生提供了FileReader构造函数,用来生成FileReader实例。

var reader = new FileReader();

该对象有以下属性:

  • FileReader.error:读取文件时产生的错误对象
  • FileReader.readyState:表示读取文件时的当前状态,0-未加载任何数据,1-表示正在加载数据,2-表示加载完成
  • FileReader.result:读取完成后的文件内容,字符串或ArrayBuffer实例
  • FileReader.onabort:用户终止读取的监听函数
  • FileReader.onerror:读取错误的监听函数
  • FileReader.onload:读取完成的监听函数,通常在这里使用result属性拿到文件内容
  • FileReader.onloadstart:读取操作开始的监听函数
  • FileReader.onloadend:读取操作结束的监听函数
  • FileReader.onprogress:读取操作进行中的监听函数 该对象有以下实例方法:
  • FileReader.abort():读取终止操作,readyState直接变为2
  • FileReader.readAsArrayBuffer():返回ArrayBuffer实例。
  • FileReader.readAsBinaryString():返回二进制字符串。
  • FileReader.readAsDefaultURL():返回Data URL格式(Base64编码)的字符,不能对这个字符直接进行base64解码,需要把前缀data:image/png;base64, 从字符串去掉以后进行解码。
  • FileReader.readAsText():读取完成后,result属性将返回文件内容的文本字符串。第一个参数是文件的Blob实例,第二个参数是可选的,表示文本编码,默认为UTF-8。 示例:
var input = document.querySelector('input');
var img = document.querySelector('img');
input.onchange = function (e) {
    var files = e.target.files;         
    var firstFile = files[0];
    if (firstFile.type === "text/plain") {
        var reader = new FileReader();
        reader.readAsText(firstFile);//异步的通过监听函数拿到内容
        reader.onload = function (e) {
            console.log('文件内容', e.target.result);
        }
    }

    if (firstFile.type === "image/png") {
        var reader = new FileReader();
        reader.readAsDataURL(firstFile);
        reader.onload = function (e) {
            img.src = e.target.result;// 实现图片预览
        }
    } 
}

File System Access API

这里才跟离线存储有关,File Systen Access API 提供了比较稳妥的本地文件交互模式。既实用又安全。 目前只有Chrome支持。

可以去MDN研究下~ (以后有空再更新...)