前端Web存储的几种方式

419 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

什么是前端Web 存储?

简单来说,就是使用HTML5可以在本地存储用户的数据。 早些时候,本地存储使用的是 cookie。但是Web 存储需要更加的安全与快速,这些数据不会被保存在服务器上,但是这些数据可用于用户请求网站的数据。它也可以存储大量的数据,而不影响网站的性能。
我总结了前端Web存储的5种方式,但是实际上可能会有很多种方式,比如使用一些第三方库来实现的前端存储,但是他们底层也是使用了这几种方式。

1.cookies

在HTML5标准前,本地储存的主要方式,优点是兼容性好,请求头自带cookie方便,缺点是大小只有4k(不知道现在有没有变化),自动请求头加入cookie浪费流量,每个domain限制20个cookie,使用起来麻烦需要自行封装,还有跨域的问题,因为之前讲过,在跨域的情况下,浏览器默认是不携带cookie信息的,我的之前文章里有讲过,需要理解的小伙伴们可以去查看。
这种方式现在已经很少使用了,只是作为了解就可以了。

2. localStorage

localStorage,是HTML5加⼊的以键值对(Key-Value)为标准的方式,键值和值只能是字符串,优点是操作⽅便,永久性储存(除非手动删除),大小为5M。使用起来也很简单,只有几个简单的API,在实际项目中使用的还是很多的。

3. sessionStorage:

与localStorage基本类似,区别是sessionStorage当页面关闭后会被清理,而且与cookie、localStorage不同,他不能在所有同源窗中共享,是会话级别的储存方式,这也是它和localStorage的区别。在实际项目中使用也是比较多的。
localStorage和localStorage使用起来都比较简单,所以这里就不在准备代码例子了。

4. Web SQL

2010年被W3C废弃的本地数据库数据存储方案,但是主流浏览器(火狐除外)都已经有了相关的实现,web SQL类似于SQLite,是真正意义上的关系型数据库,用sql进行操作,当我们使用时要进行转换,比较繁琐。关系型数据库中的表,列比较固定,而前端数据比较灵活,读取数据后还需要进行转换,所以前端的数据其实并不适合存储在关系型数据库中。
Web SQL 数据库 API 并不是 HTML5 规范的一部分,但是它是一个独立的规范,引入了一组使用 SQL 操作客户端数据库的 APIs。
虽然它不是H5的标准,但是曾经出现过,而且大部分主流浏览器已经实现了,尽管我们在工作中可能不会去使用它,但是我觉得还是有必要了解一下。先来看一下它的几个核心API吧。

1. 核心API

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

2. 打开数据库

var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);

openDatabase() 方法对应的五个参数说明:

  1. 数据库名称
  2. 版本号
  3. 描述文本
  4. 数据库大小
  5. 创建回调

第五个参数,创建回调会在创建数据库后被调用。

3. 执行查询

var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);

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

4. 插入数据

var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);
db.transaction(function (tx) {  
  tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
  tx.executeSql('INSERT INTO LOGS (id,log) VALUES (?, ?)', [e_id, e_log]);
});

5. 读取数据

<!DOCTYPE HTML>
<html>
   <head>
      <meta charset="UTF-8">
      <title>我的教程</title> 
      <script type="text/javascript">
		
         var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);
         var msg;
			
         db.transaction(function (tx) {
            tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
            tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "Vue教程")');
            tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "React教程")');
            msg = '<p>数据表已创建,且插入了两条数据。</p>';
            document.querySelector('#status').innerHTML =  msg;
         });

         db.transaction(function (tx) {
            tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
               var len = results.rows.length, i;
               msg = "<p>查询记录条数: " + len + "</p>";
               document.querySelector('#status').innerHTML +=  msg;
					
               for (i = 0; i < len; i++){
                  msg = "<p><b>" + results.rows.item(i).log + "</b></p>";
                  document.querySelector('#status').innerHTML +=  msg;
               }
            }, null);
         });
			
      </script>
		
   </head>
	
   <body>
      <div id="status" name="status">状态信息</div>
   </body>
	
</html>

当然还可以更新和删除!

db.transaction(function (tx) {
  tx.executeSql('DELETE FROM LOGS  WHERE id=1'); //删除
  tx.executeSql('DELETE FROM LOGS WHERE id=?', [id]);
  tx.executeSql('UPDATE LOGS SET log='Angular' WHERE id=2'); //更新
  tx.executeSql('UPDATE LOGS SET log='算法' WHERE id=?', [id]);
});

如何在浏览器中查看数据库数据?

6. 存储位置:

C:\Users\...\AppData\Local\Google\Chrome\User Data\Default\databases\

5. IndexedDB

背景

Cookie 的大小不超过4KB,且每次请求都会发送回服务器。localStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引,且只能保存字符串,使用起来需要转换,很不方便。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

它是被正式纳入HTML5标准的数据库存储方案,它是NoSQL(Not Only SQL)数据库,用键值对进行储存,可以进行快速读取操作,非常适合web场景,同时用JavaScript进行操作会非常方便。

IndexedDB是浏览器提供的本地数据库, 允许储存大量数据,提供查找接口,还能建立索引,功能非常强大,这些都是 localStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

IndexedDB的特点

键值对储存

IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

异步

IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 localStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。简单来说,就是读取数据的时候,是异步操作,而localStorage是同步的为什么没有问题呢?只是localStorage的数据量很小,耗时很短,设计成同步的也不会有大的影响,而且使用也简单。

支持事务

 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。支持事务这也是一个优秀数据库必备的基本技能哦。

同源限制

IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。为了安全性考虑,自然是只能访问自身域下的数据。

储存空间大

IndexedDB 的储存空间比 localStorage 大得多,一般来说不少于 250MB,甚至没有上限。

支持二进制储存

IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。IndexedDB竟如此的优秀。

操作

打开数据库

使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。

这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1

indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件errorsuccessupgradeneeded,处理打开数据库的操作结果。

var db;
var objectStore;
var request = window.indexedDB.open(databaseName, version);

request.onerror = function (event) {}

request.onsuccess = function (event) {
    db = request.result//可以拿到数据库对象
}

//如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded
request.onupgradeneeded = function (event) {
    db = event.target.result;
  
    if (!db.objectStoreNames.contains('person')) {//判断是否存在
        objectStore = db.createObjectStore('person', { keyPath: 'id' });
      //自动生成主键db.createObjectStore(
      //  'person',
      //  { autoIncrement: true }
      //);
    }
    //新建索引,参数索引名称、索引所在的属性、配置对象
    objectStore.createIndex('email', 'email', { unique: true });
}

新增数据

在以上操作的基础上,需要新建一个事务。新建时必须指定表格名称和操作模式("只读"或"读写")。新建事务以后,通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,再通过表格对象的add()方法,向表格写入一条记录。

写入操作是一个异步操作,通过监听连接对象的success事件和error事件,了解是否写入成功。

function add() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });

  request.onsuccess = function (event) {
    console.log('数据写入成功');
  };

  request.onerror = function (event) {
    console.log('数据写入失败');
  }
}

add();

读取数据

objectStore.get()方法用于读取数据,参数是主键的值。

function read() {
   var transaction = db.transaction(['person']);
   var objectStore = transaction.objectStore('person');
   var request = objectStore.get(1);

   request.onerror = function(event) {
     console.log('事务失败');
   };

   request.onsuccess = function( event) {
      if (request.result) {
        console.log('Name: ' + request.result.name);
        console.log('Age: ' + request.result.age);
        console.log('Email: ' + request.result.email);
      } else {
        console.log('未获得数据记录');
      }
   };
}

read();

遍历数据

遍历数据表格的所有记录,要使用指针对象 IDBCursor。openCursor()方法是一个异步操作,所以要监听success事件。

function readAll() {
  var objectStore = db.transaction('person').objectStore('person');

   objectStore.openCursor().onsuccess = function (event) {
     var cursor = event.target.result;

     if (cursor) {
       console.log('Id: ' + cursor.key);
       console.log('Name: ' + cursor.value.name);
       console.log('Age: ' + cursor.value.age);
       console.log('Email: ' + cursor.value.email);
       cursor.continue();
    } else {
      console.log('没有更多数据了!');
    }
  };
}

readAll();

数据更新

function update() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });

  request.onsuccess = function (event) {
    console.log('数据更新成功');
  };

  request.onerror = function (event) {
    console.log('数据更新失败');
  }
}

update();

数据删除

IDBObjectStore.delete()方法用于删除记录。

function remove() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .delete(1);

  request.onsuccess = function (event) {
    console.log('数据删除成功');
  };
}

remove();

索引

添加索引后可以使用索引查询数据。建立索引,其实就是为了提高查询的速度。

var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');

request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}

完整案例

基于Vue2我做了一个小例子,实现了添加,删除等基本操作。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IndexedDB示例</title>
    <script type="text/javascript" src="https://unpkg.com/vue"></script>
    <style>
        .person {
            height: 25px;
            padding: 5px;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script>
        // IndexedDB
        var db;
        var objectStore;
        var request = window.indexedDB.open('MyFirm', 1);

        request.onerror = function(event) {
            console.error('初始化数据库发生错误!');
        }

        request.onsuccess = function(event) {
            db = request.result //可以拿到数据库对象
        }

        //如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded
        request.onupgradeneeded = function(event) {
            db = event.target.result;

            if (!db.objectStoreNames.contains('person')) { //判断是否存在
                // 自动生成主键
                objectStore = db.createObjectStore(
                    'person', {
                        autoIncrement: true
                    }
                );
            }
        }

        const app = new Vue({
            el: "#app",
            data() {
                return {
                    name: '',
                    age: null,
                    selectedKey: 0,
                    persons: []
                }
            },
            template: `
            <div>
                <p>Hello IndexedDB!</p>
                <input v-model="name" type="text" placeholder="姓名"/>
                <input v-model="age" type="number" placeholder="年龄"/>
                <button @click="addPerson">添加</button>
                <button @click="delPerson">删除</button>
                <div class="person" v-for="p in persons" @click="selectedKey=p.key;" :style="{'background-color': selectedKey === p.key ? 'aqua' : 'transparent'}">
                    #{{p.key}} {{p.name}}: {{p.age}}
                </div>
                <p>总共有{{personCount}}人!</p>
            </div>
        `,
            mounted() {
                setTimeout(() => {
                    this.getAllPersons();
                }, 1000);
            },
            methods: {
                addPerson() {
                    let person = {
                        name: this.name
                    };
                    if (this.age) {
                        person.age = this.age;
                    }

                    var request = db.transaction(['person'], 'readwrite')
                        .objectStore('person')
                        .add(person);

                    request.onsuccess = (event) => {
                        console.log('添加成功!');
                        person.key = event.target.result;
                        this.persons.push(person);
                    };

                    request.onerror = function(event) {
                        console.log('添加失败!');
                    }
                },
                getAllPersons() {
                    var objectStore = db.transaction('person').objectStore('person');

                    objectStore.openCursor().onsuccess = (event) => {
                        var cursor = event.target.result;

                        if (cursor) {
                            let person = {};
                            person.key = cursor.key;
                            person.name = cursor.value.name;
                            person.age = cursor.value.age;
                            this.persons.push(person);

                            cursor.continue();
                        }
                    };
                },
                delPerson() {
                    if (!this.selectedKey) {
                        alert('请选中');
                        return;
                    }

                    var request = db.transaction(['person'], 'readwrite')
                        .objectStore('person')
                        .delete(this.selectedKey);

                    request.onsuccess = (event) => {
                        this.persons = this.persons.filter((p) => p.key !== this.selectedKey);
                    };
                }
            },
            computed: {
                personCount() {
                    return this.persons.length;
                }
            },
        });
    </script>
</body>

</html>

IndexDB是不能跨域共享的,每个源都有可以存储的空件限制。 IndexedDB存储的是对象,而不是数据表,因为IndexedDB不是关系型数据库。对象存储是通过定义键然后添加数据来创建。游标用于查询对象存储中特定数据,而索引可以针对特定属性实现更快的查询。

总结

Web SQL,IndexedDB VS localStorage

localStorage相比 Web SQL,和IndexedDB,容量受限,且只能存储stirng字符串,使用时需要来回转换,操作上也只能getItem,setItem等,而且是同步的。而Web SQL,和IndexedDB 存储数据不限于string字符串,可以是二进制流或Blob对象。

Web SQL VS IndexedDB

Web SQL,和IndexedDB都属于数据库,Web SQL是关系型数据库,IndexedDB是非关系型数据库。

尽管很多浏览器支持WebSQL,使用较广泛,但是WebSQL不是HTML5的标准,且已经被废弃,不建议使用。

IndexedDB是HTML5标准,由于是非关系型数据库,所以易于扩展,读取的数据无需做转换,非常适合Web的场景,建议使用。