关于indexeddb:indexedDB学习资料

<template> <div class="container"> <el-button type="primary" @click="closeDB">敞开此数据库</el-button> <el-button type="primary" @click="deleteDB">删除此数据库</el-button> <el-button type="primary" @click="insertOneData">减少单条数据</el-button> <el-button type="primary" @click="insertMoreData">减少多条数据</el-button> <el-button type="primary" @click="queryData">查问数据(游标)</el-button> <el-button type="primary" @click="updateData">批改数据</el-button> <el-button type="primary" @click="deleteOneData">删除一条数据</el-button> <el-button type="primary" @click="queryAllData">查问某张表的所有数据</el-button> <el-button type="primary" @click="queryByPrimaryKey">依据主键查问某条数据</el-button> <el-button type="primary" @click="queryByIndex">依据索引查问某条数据</el-button> <el-button type="primary" @click="clearTable">清空某张表的数据</el-button> <el-button type="primary" @click="deleteByPrimaryKey">通过主键删除某条数据</el-button> <el-button type="primary" @click="updateByPrimaryKey">通过主键更改某条数据</el-button> </div></template><script>/** * 形容 * @date 2021-11-22 * @des 参考:https://www.npmjs.com/package/idb-js?activeTab=dependencies * @装置 npm install idb-js --save */import Idb from "idb-js";import db_student_config from "../common/db_common_config";export default { data() { return { student_db: null, }; }, mounted() { Idb(db_student_config).then( (student_db) => { this.student_db = student_db; }, (err) => { console.log(err); } ); }, methods: { closeDB() { this.student_db.close_db(); }, deleteDB() { this.student_db.delete_db(); }, insertOneData() { this.student_db.insert({ tableName: "test1", data: { id: 3, score: 100, name: "小刚", }, success: () => console.log("增加胜利"), }); }, insertMoreData() { this.student_db.insert({ tableName: "test1", data: [ { id: 1, score: 98, name: "小明", }, { id: 2, score: 99, name: "小方", }, ], success: () => console.log("增加胜利"), }); }, queryData() { this.student_db.query({ tableName: "test1", condition: (item) => item.score == 99, success: (r) => { console.log(r); }, }); }, updateData() { this.student_db.update({ tableName: "test1", condition: (item) => item.name == "小明", handle: (r) => { r.score = 80; }, success: (r) => { console.log("批改胜利", r); }, }); }, deleteOneData() { this.student_db.delete({ tableName: "test1", condition: (item) => item.name == "刚", success: (res) => { console.log("删除胜利"); }, }); }, queryAllData() { this.student_db.queryAll({ tableName: "test1", success: (res) => { console.log(res); }, }); }, queryByPrimaryKey() { this.student_db.query_by_index({ tableName: "test1", indexName: "name", target: "小明", success: (res) => { console.log(res); }, }); }, queryByIndex() { this.student_db.query_by_primaryKey({ tableName: "test1", target: 1, success: (res) => { console.log(res); }, }); }, clearTable() { this.student_db.clear_table({ tableName: "test1", }); }, deleteByPrimaryKey() { this.student_db.delete_by_primaryKey({ tableName: "test1", target: 1, success: () => console.log("删除胜利"), }); }, updateByPrimaryKey() { this.student_db.update_by_primaryKey({ tableName: "test1", target: 1, handle: (val) => (val.score = 101), success: (res) => { console.log(res); }, }); }, },};</script><style scoped lang="less">.container{ padding: 50px;}.el-button { margin-bottom: 20px;}</style>db_common_config.jsexport default { dbName: "test-DB", // *数据库名称 version: 1, // 数据库版本号(默认为以后工夫戳) tables: [ // *数据库的表,即ObjectStore { tableName: "test1", // *表名 option: { keyPath: "id" }, // 表配置,即ObjectStore配置,此处指明主键为id indexs: [ // 数据库索引(倡议加上索引) { key: "id", // *索引名 option: { // 索引配置,此处示意该字段不容许反复 unique: true } }, { key: "name" }, { key: "score" } ] }, { tableName: "info", // *表名 另外一张表,同理 option: { keyPath: "id" }, indexs: [ { key: "id", option: { unique: true } }, { key: "name" }, { key: "age" }, { key: "sex" } ] } ]};

February 16, 2022 · 2 min · jiezi

关于indexeddb:IndexedDB数据库

IndexedDB 数据库特点如下 键值对存储 每一条记录有对应的主键 主键是举世无双的异步 这与localStorage造成鲜明对比反对事务 反对回滚操作同源限度 不能拜访跨源数据库贮存空间大 个别不少于250MB反对二进制存储 例如ArrayBuffer 和 Blob对象数据库对象 IDBDatabase 仓库 IDBObjectStore 索引 IDBIndex 事务 IDBTransaction 操作申请 IDBRequest 指针 IDBCursor 主键汇合 IDBKeyRange 操作流程关上数据库`var request = window.indexedDB.open(databaseName, version);`数据库关上返回函数1.error事件 request.onerror = function (event) { console.log('数据库关上报错');};2.success事件 var db;request.onsuccess = function (event) { db = request.result; console.log('数据库关上胜利');};3.upgradeneeded 事件 数据库降级事件 var db;request.onupgradeneeded = function (event) { db = event.target.result;}新建数据库新建数据库并创立person表 主键为id request.onupgradeneeded = function(event) { db = event.target.result; var objectStore; if (!db.objectStoreNames.contains('person')) { objectStore = db.createObjectStore('person', { keyPath: 'id' }); }}主动生成主键 ...

October 19, 2020 · 2 min · jiezi

浏览器里的本地数据库IndexedDB

本文首发于政采云前端团队博客:浏览器里的本地数据库:IndexedDBIndexedDB 是什么在现代浏览器的本地存储方案中,indexedDB 是一项重要的能力组成, 它是可以在浏览器端使用的本地数据库,可以存储大量数据,提供接口来查询,还可以建立索引,这些都是其他存储方案 Cookie 或者 LocalStorage 无法提供的能力。单从数据库类型来看,IndexedDB 是一个非关系型数据库(不支持通过 SQL 语句操作)。 IndexedDB 的主要概念IndexedDB 是一个比较复杂的 API 组合,学习它的过程就相当于学习它的各个对象 API 接口,包括以下这些( IDB 指当前操作的数据库实例 ): 数据库:IDBDatabase 对象仓库对象: IDBObjectStore 对象索引:IDBIndex 对象事务:IDBTransaction 对象操作请求:IDBRequest 对象指针:IDBCursor 对象主键:IDBKeyRange 对象在这些 API 中包含一些主要概念: 数据库:数据库是所有相关数据的基本容器。在同源策略( 协议 + 域名 + 端口 )的前提下,每个域名下可以新建任意多的数据库。IndexedDB 中有版本概念,这就规定了同一时刻下只有一个版本的数据库存在。对象仓库:对象仓库 ObjectStore 在 IndexedDB 中对应的是 MYSQL 中的表 Table。数据:对象仓库中记录的是若干条数据,数据只有主键和数据体两个部分,主键不能重复,可以为自增的整数编号或者数据中指定的一个属性。数据体可以是任意数据类型,不限于对象。索引:为不同的属性建立索引可以加快数据的检索。事务:数据的 CURD (增删查改) 都要通过事务来完成。通过简单的对比图来理解 IndexedDB 的概念: 快速起步 IndexedDB在介绍了 IndexedDB 的主要概念之后,可以通过一个简单实用的 CURD 例子来学习在日常开发中我们是怎么使用 IndexedDB 的,各个 API 细节日后可以慢慢深入学习。 必不可少的浏览器支持检查: if(!('indexedDB' in window)){ console.log('当前浏览器支持 IndexedDB'); return;} else { console.log('您的浏览器不支持 IndexedDB') // todo 建议升级或者更换其他浏览器}连接数据库 ...

October 13, 2019 · 2 min · jiezi

Web-存储技术

一、背景介绍第一个Web存储的技术叫做Cookie,它是网站的身份证。是网站为了辨别用户身份,进行session(服务端的session)跟踪而存储在用户本地终端上的数据,也就是说它是存在电脑硬盘上的,一个很小的txt类型的文件。Cookie每次都会跟随http请求发送到服务端,也就是说每一个http请求都会带上我们的cookie数据,因此它存在一个安全性的问题。 cookie本身也是有很大的局限性的,首先它很小,主流的浏览器最大支持 4096 字节,除了最大字节的限制,每个网站的cookie个数(也就是每一个first每一个域)也是有限制的,一般浏览器是20个。除此之外,cookie还会默认跟随所有http请求发送,即使不需要使用这个cookie来鉴别用户但是它也是会跟随http请求发送的,这样就会造成一个网络资源的浪费。然后部分的浏览器还限制了总的cookie个数300个。 在cookie的诸多局限性下,Web Storage应运而生。Web Storage 解决了很多问题: 比如它支持存储大量数据,支持复杂的本地数据库,而且也不会默认跟随http请求。Web Storage主要是有四个: SessionStorageLocalStorageWebSQLindexedDB二、Cookie的简单介绍 Cookie是HTML4的一个标准,它一般不需要考虑兼容。它是网站的一个身份证,服务器可以针对不同用户,做出不同的响应。cookie存储在用户的机器上是一个纯文本,就是一个txt文件并不是一个脚本,它不能执行东西只负责记录。浏览器每次请求都会带上当前网站的cookie。 Cookie分为两种类型,一种呢是会话cookie,也就是临时性的cookie,退出浏览器或者是关闭即删除; 另一种叫持久cookie,它会一直存在,存在的时间由特定的过期时间或者是有效期来决定。 Cookie的域 Domain决定了当前的一个cookie的权限,哪一个域可以使用这个cookie。 Cookie的路径 Path,下面一个简单的例子: www.baidu.com id="123456" domain="www.baidu.com"www.baidu.com/user id="123456" user="eric" domain="www.baidu.com" path="/user/"www.baidu.com/search id="123456";www.baidu.com/user/search id="123456" user="eric";如上www.baidu.com设置了一个id等于123456,domain是www.baidu.com,然后另外一个跟第一个一样多设置了一个user,id相同,但是多了一个user=“Eric”,它的domain设置成了www.baidu.com,path就到了user下面。这两者设置完成之后,当我们访问www.baidu.com/search时百度只能拿到id,因为user="Eric"是属于user这个域下面的,也就是说在search下面是获取不到的,但是在www.baidu.com/user/search这个时候我们就可以获取到名叫Eric的user。Path也是一种权限的控制只是相较于域domain是低一级的。 Cookie的安全secure,如果这个属性为TRUE,那么网站只有在https的请求下面才会携带当前的cookie。 Cookie的HttpOnly这个属性如果为TRUE,那么就不允许JavaScript操作cookie。 因为cookie是存储在客户端一个独立的文件,因此服务器是无法分辨用户和攻击者的。关于cookie的目的分为两种:一种是跨站点脚本攻击,一种是跨站请求伪造。 三、SessionStorage key-value的键值对,是HTML5新增的一个会话存储对象。 SessionStorage是临时保存在同一窗口,也就是同一标签页的数据。如果当前标签页关闭了,那么SessionStorage也就失效了。这也是SessionStorage最显著的一个特点:单页标签限制。 除此之外,它还有的一些特点有: 同源策略,也就是在同一协议,同一主机名和同一端口下的同一tab只在本地存储,不会跟随http请求发送到服务器存储方式采用key-value键值对,这里面的value只能存字符串类型,如果存其他的会自动转换成字符串。存储上线限制达到了5MB,如果当前存储超出上限新的内容会把旧的内容覆盖但不会报错。属性: sessionStorage.length - 键值对数量sessionStorage.key(int index) -> nullsessionStorage.getItem(string key) -> nullsessionStorage[string key]sessionStorage.setItem(string key, string value)sessionStorage.removeItem(string key)sessionStorage.clear()Json对象 JSON.stringify()JSON.parse()四、LocalStorage LocalStorage也是在浏览器的Application下面有一个Local Storage,它和SessionStorage是十分相似的,同样是key-value键值对,也是HTML5的新增存储对象,它与SessionStorage的特点不同之处在于没有标签页的限制和在浏览器的无痕模式下LocalStorage是不允许读取的,永久性的存储,然后SessionStorage超出限制是覆盖不会报错而LocalStorage超出会报错。 特点: 同源策略,也就是在同一协议,同一主机名和同一端口下的同一tab没有标签页的限制只在本地存储,不会跟随http请求发送到服务器存储方式采用key-value键值对,这里面的value只能存字符串类型,如果存其他的会自动转换成字符串。存储上线限制达到了5MB,如果当前存储超出上限会报错。无痕模式下不可读取永久性存储属性: sessionStorage.length - 键值对数量sessionStorage.key(int index) -> nullsessionStorage.getItem(string key) -> nullsessionStorage[string key]sessionStorage.setItem(string key, string value)sessionStorage.removeItem(string key)sessionStorage.clear()注意事项:LocalStorage和SessionStorage在web view是不可靠的,web view指的是在开发混合APP的时候使用了浏览器来实现我们的APP,这个时候是不可靠的,因为在浏览器崩溃的情况下数据可能没有存进去。 另外一个在IOS浏览器中不可重复setItem,如果重复会报错,然后这个时候我们需要先removeItem再添加item。 监听storage的变化监听storage包括SessionStorage和LocalStorage。然后这里需要提到两个概念:同源和监听同源网页。 同源:协议、域名、端口三者相同,同源的情况下我们可以共享SessionStorage和LocalStorage。同源策略还禁止不同源执行任何脚本。 http://localhost:63342/simpleApp/app/index.html#/(协议) (域名) (端口) (路径)监听同源网页,但是同一网页是无效的 ...

July 8, 2019 · 3 min · jiezi

IndexedDB-应用探索

IndexedDB 诞生背景在开始之前,我们先简单梳理一下浏览器存储的几种方式(详见????浏览器存储方式) 会话期 Cookie 持久性 Cookie sessionStorage localStorage indexedDB WebSQL 存储大小 4kb 4kb 2.5~10 MB 2.5~10 MB >250MB 已废弃 失效时间 浏览器关闭自动清除 设置过期时间,到期后清除 浏览器关闭后清除 永久保存(除非手动清除) >手动更新或删除 已废弃 与服务端交互 有 有 无 无 无 已废弃 访问策略 符合同源策略可以访问 符合同源策略可以访问 符合同源策略可以访问 即使同源也不可相互访问 符合同源策略可以访问 已废弃 cookie:存储大小有限(4kb)、与服务端有交互,安全性较低、原生接口不友好,需要自己封装、能设置过期时间webStorage:存储空间较大,但有上限(2.5~10MB,各家浏览器不同)、与服务端无交互,安全性高、原生接口友好,数据操作比 cookie 方便2.1 localStorage:持久化本地存储,关闭浏览器重新打开数据依然存在(除非手动删除数据)2.2 sessionStorage:浏览器窗口关闭后销毁数据cookie 和 webStorage 存储数据格式仅支持 String,存储时需要借助 JSON.stringify() 将 JSON 对象转化为字符串,读取时需要借助 JSON.parse() 将字符串转化为 JSON 对象 一般来说,我们更推荐使用 webStroage,但其存储大小有限、数据存储仅支持 String 格式、不提供搜索功能,不能建立自定义的索引。因此,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。 一、什么是 IndexedDB ?通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。从 DB(Data Base) 可以看出它是一个数据库。常用的数据库有两种类型: ...

May 10, 2019 · 5 min · jiezi

PWA 系列(三)—ndexedDB

IDB 操作的基本步骤是open 方法打开数据库 ????然后是创建数据库 store 对象仓库 ????需要注意更新数据库版本应先调用 close 方法关闭旧版数据库需要注意创建 store 一定要在新版本数据库的 upgradeneeded 事件处理函数中创建,因为本质上他是修改数据库结构如果对数据库进行数据操作那么需要通过事务来执行 ????。打开数据库window.indexedDB 是一个 IDBFactory 对象,调用对象 open 方法返回的是一个 IDBOpenDBRequest 请求,监听 success 事件,e.target.result 指向一个名为 IDBDatabase 的对象,该对象就是连接到数据库的唯一 API。需要注意的是 IDBDatabase 对象有 close、createObjectStore、deleteObjectStore、transaction 方法和 name、version 等常用属性:下面是个例子:function getDB(dbName) { // 1️ return new Promise((resolve, reject) => { const request = window.indexedDB.open(dbName) // 2️ request.onerror = reject request.onsuccess = function (e) { // 3️ let db = e.target.result console.log(db ${db.name}::${db.version} open success) resolve(db) } })}步骤:第一步,需要先定义一个数据库这里通过自定义的 dbName 来创建这个新的数据库第二部,通过 open 方法打开数据库第三部,监听 onsuccess 方法,打开成功数据库对象就在这个 request 的 result 对象里面使用方法:; (async function () {+ let db = await getDB(‘DEMO’)+ db.close()})()打开 Application 面板就能够看到这个数据库和存储空间了:但目前没什么卵用 ???? 因为没有定义数据库数据结构调用还不能存储东西,这个时候我们就要更新版本号并创建数据存储的结构了升级版本号function getUpgradedDB(db) { // 1️ return new Promise((resolve, reject) => { console.log(current ${db.name}::${db.version}) let request = window.indexedDB.open(db.name, ++db.version) // 2️ request.onerror = reject request.onupgradeneeded = function (e) { // 3️ let db = e.target.result console.log(‘db upgraded’) resolve(db) } })}步骤:第一步,首先获取 db 对象,(可选,方便累加现有的数据库版本号)第二步,然后通过 open 方法传入新版本号来创建新的版本数据库( ⚠️ 注意版本号始终应当为整型数值)第三步,监听 upgradeneeded 事件,将 request 的 result 对象返回,这样我们就获得了新版本的数据库使用方法:; (async function () { let db = await getDB(‘DEMO’)+ db.close()+ db = await getUpgradedDB(db)})()⚠️️ 再次提醒如果传入 db 升级数据库,一定要先 close 方法关闭旧版本数据库现在我们数据库的版本就升级一了,这个时候就应该在升级的同时创建数据库的结构:创建存储空间假定我们需要存储这样的数据:{ name: ‘oli’, age: 12, email: ’example@example.com’ }需要注意的是:name 为 name 键的索引 不唯一age 为 age 键的索引 不唯一email 为主键 唯一不重复创建存储空间需要调用 db 数据库对象的 createObjectStore 方法;该方法返回的是一个 IDBObjectStore 对象。存储空间有以下方法和属性:看名字基本都能明白代表的含义,详细见文档:https://developer.mozilla.org…function initDBStructor(db, storeName, opts, indexArr) { // 1️ return new Promise((resolve, reject) => { let store = db.createObjectStore(storeName, opts) // 2️ indexArr.forEach(indexObj => { // 3️ store.createIndex(indexObj.name, indexObj.name, { unique: indexObj.unique }) }) store.transaction.onerror = reject store.transaction.oncomplete = function (e) { // 4️ console.log(‘db initiated’) resolve(e.target.db) } })}步骤首先,我们接收数据库作为参数,storeName 代表存储空间的名称,opts 参数是创建的存储空间的选项(比如设置主键等),indexArr 则代表数据库需要创建的索引(这里假定所有数据都需要创建索引)然后通过调用数据库的 createObjectStore 方法创建存储空间根据传入的数据结构使用 store 的 createIndex 创建索引监听 store 事务的 complete 方法,数据库初始化完成使用方法:; (async function () { let db = await getDB(‘DEMO’) db.close() db = await getUpgradedDB(db)+ db = await initDBStructor(db, ‘users’, { keyPath: ’email’ }, [{ name: ’name’, unique: false }, { name: ‘age’, unique: false }, { name: ’email’, unique: true }])})()根据数据,我们设置了主键为 email 字段,然后 name 和 age 字段 unique 为 false,email 字段 unique 属性为 true,设置完毕再去调试面板:然后我们就可以对数据仓库进行操作了:数据的增查改删增加数据function setItem(db, storeName, data) { // 1️ return new Promise((resolve, reject) => { let request = db.transaction(storeName, ‘readwrite’).objectStore(storeName).add(data) // 2️ request.onerror = reject request.onsuccess = function (e) { // 3️ console.log(‘add data success’) resolve(e.target.result) } })}步骤:这个函数需要获取三个参数,一个是数据库对象 db,一个是 store 的名称,另外一个则是需要插入的数据然后调用 db 的 transaction,传入 store 名称并设置操作为读写权限,然后打开存储空间并调用 add 方法插入数据最后监听 request 的 success 事件使用方法:; (async function () { let db = await getDB(‘DEMO’)- db.close()- db = await getUpgradedDB(db)+ let res = await setItem(db, ‘users’, { name: ‘oli’, age: 11, email: ‘hello@example.com’ })})()我们增加了一条数据,调试工具打开看下现在的数据库:成功插入数据!值得庆祝 ????查询数据function getItem(db, storeName, key) { // 1️ return new Promise((resolve, reject) => { let request = db.transaction(storeName, ‘readonly’).objectStore(storeName).get(key) // 2️ request.onerror = reject request.onsuccess = function (e) { // 3️ console.log(‘get data success’) resolve(e.target.result) } })}步骤:第一步,获取到 db 数据库对象,接收 store 名称以及数据 key 我们要根据 key 来做查询操作获取 value第二步,调用数据库对象的 transaction 传入 store 名称和只读权限,然后获取存储空间并调用 get 方法=第三步,监听 success 事件使用方法:; (async function () { let db = await getDB(‘DEMO’)- let res = await setItem(db, ‘users’, { name: ‘oli’, age: 11, email: ‘hello@example.com’ })+ let res = await getItem(db, ‘users’, ‘hello@example.com’)})()返回的数据如下:修改数据function modifyItem(db, storeName, key, data) { // 1️ return new Promise(async (resolve, reject) => { let request = db.transaction(storeName, ‘readonly’).objectStore(storeName).get(key) // 2️ request.onerror = reject request.onsuccess = function (e) { // 3️ if (e.target.result) { let requestUpdate = db.transaction(storeName, ‘readwrite’).objectStore(storeName).put({ …e.target.result, …data }) // 4️ requestUpdate.onerror = reject requestUpdate.onsuccess = function (e) { // 6️ console.log(‘modify item success’) resolve(e.target.result) } } else { reject(’not match key’) } } })}步骤:首先接收 db 数据库对象、store 存储空间名称以及 key 和修改后的 data然后根据 key 进行查询操作监听 request 的 success 事件然后调用 put 方法将查询的数据和修改后的数据 merge 并插入到存储空间(为了方便演示,直接合并对象)最后监听 requestUpdate 的 success 事件数据修改成功:; (async function () { let db = await getDB(‘DEMO’)- let res = await getItem(db, ‘users’, ‘hello@example.com’)+ let res = await modifyItem(db, ‘users’, ‘hello@example.com’, { name: ‘woooh’ })})()效果如下:删除数据最后再来个删除数据:function deleteItem(db, storeName, key) { // 1️ return new Promise((resolve, reject) => { let request = db.transaction(storeName, ‘readwrite’).objectStore(storeName).delete(key) // 2️ request.onerror = reject request.onsuccess = function (e) { // 3️ console.log(‘remove item success’) resolve(e.target.result) } })}步骤:接收参数 db 数据库对象,store 仓库名称,key然后创建 readwrite 事务,调用存储空间的 delete 方法监听 request 的 success 事件删除成功:; (async function () { let db = await getDB(‘DEMO’)- let res = await getItem(db, ‘users’, ‘hello@example.com’)+ let res = await deleteItem(db, ‘users’, ‘hello@example.com’)})()看看效果:empty! ????游标使用游标 openCursor 返回的是一个 IDBCursor 对象,监听 success 返回的 cursor 则是含有 value 的 IDBCursorWithValue 对象,两者区别:在使用游标之前,我们先插入几条假数据:然后实现一个通过游标获取所有数据的函数:function getAllValue(db, storeName) { // 1️ return new Promise((resolve, reject) => { let request = db.transaction(storeName).objectStore(storeName).openCursor() // 2️ let res = [] request.onerror = reject request.onsuccess = function (e) { // 3️ let cursor = e.target.result if (cursor) { // 4️ res.push(cursor.value) cursor.continue() } else { resolve(res) console.log(‘get all value finish’) } } })}步骤:首先函数接收两个参数,一个是 db 对象,一个是存储空间名称然后创建一个 request 调用存储空间的 openCursor 方法然后监听 request 的 success 事件检测是否存在 cursor,如果是则 push 数据并调用 continue 方法继续监听 openCursor 的 success 事件遍历,否则返回所有结果使用方法:; (async function () {+ let db = await getDB(‘DEMO’)+ let res = await getAllValue(db, ‘users’)})()效果如下:另外获得所有数据的数组还可以使用 getAll 方法:function getAllData(db, storeName) { return new Promise((resolve, reject) => { let request = db.transaction(storeName).objectStore(storeName).getAll() // 1️ request.onerror = reject request.onsuccess = function (e) { // 2️ let data = e.target.result console.log(‘get all value finish’) resolve(data) } })}步骤:使用存储空间上的 getAll 方法监听 success 事件游标范围另外还可以给游标 openCursor 方法传入 keyRange 来指定游标的范围,这种 keyRange 有多种类型,分别是 IDBKeyRange 的几个内置方法:only 指定仅匹配lowerBound 在…之下 另外接收一个 Boolean 指定是否包括当前 keyupperBound 在…之上 另外接收一个 Boolean 指定是否包括当前 keybound 在…之间 另外接收两个 Boolean 指定是否包括当前两个 key详细见下表:function getDataByCursor(db, storeName, indexName, type, …args) { // 1️ return new Promise((resolve, reject) => { let keyRange switch (type) { // 2️ case ‘only’: keyRange = IDBKeyRange.only(args[0]) break case ’lowerBound’: keyRange = IDBKeyRange.lowerBound(args[0], args[1]) break case ‘upperBound’: keyRange = IDBKeyRange.upperBound(args[0], args[1]) break case ‘bound’: keyRange = IDBKeyRange.bound(…args) break default: break } let request = db.transaction(storeName).objectStore(storeName).index(indexName).openCursor(keyRange) // 3️ let res = [] request.onerror = reject request.onsuccess = function (e) { // 4️ let cursor = e.target.result if (cursor) { res.push(cursor.value) cursor.continue() } else { resolve(res) console.log(‘get all value finished’) } } })}步骤:首先接收几个参数 db 数据库对象,store 存储空间名称,index 索引名称以及 范围类型名称和选项然后根据不同名称对应不同游标范围调用 openCursor 方法并传入游标范围参数监听 success 事件获取数据使用方法:; (async function () { let db = await getDB(‘DEMO’) let res+ res = await getDataByCursor(db, ‘users’, ’name’, ‘only’, ‘oli’)+ res = await getDataByCursor(db, ‘users’, ’name’, ’lowerBound’, ‘oli’)+ res = await getDataByCursor(db, ‘users’, ’name’, ’lowerBound’, ‘oli’, true)+ res = await getDataByCursor(db, ‘users’, ’name’, ‘bound’, ‘oli’, ’troy’, false, false)})()另外还可以指定游标的方向,给 openCursor 方法传入第二个参数 prev 来指定使用倒序;如果想要过滤重复的记录,那么传入 IDBCursor.nextunique 或 IDBCursor.prevunique 即可;其他参数见文档:https://developer.mozilla.org…使用索引搜索index 方法返回一个名为 IDBIndex 的对象IDBIndex 这个对象的属性方法见文档:https://developer.mozilla.org…可通过索引搜索已经建立索引的条目:function getDataByIndex(db, storeName, indexName, key) { // 1️ return new Promise((resolve, reject) => { let request = db.transaction(storeName).objectStore(storeName).index(indexName).get(key) // 2️ request.onerror = reject request.onsuccess = function(e) { // 3️ let data = e.target.result console.log(‘get value’) resolve(data) } })}首先接收参数 db 对象、store 存储空间名称、index 索引名称和搜索关键词然后调用存储空间的 index 方法的 get 方法搜索关键词然后监听 success request 的 result 值即为搜索到的数据; (async function () { let db = await getDB(‘DEMO’)- let res = await getAllData(db, ‘users’)+ let res = await getDataByIndex(db, ‘users’, ’name’, ’troy’)})()另外还有在索引上调用的两种游标使用方法,见 MDN 文档:https://developer.mozilla.org…事务和请求之间的关系最后来聊一聊 IDB 数据库的事务和请求之间的关系:首先,请求就是所有针对 IDB 的异步操作都会返回请求,我们监听 success 才能在请求成功之后通过 result 对象获取到的结果然后是事务,一个请求可能有事务也有可能没有事务,比方说只连接上数据库但没有操作数据,那么就存在请求,但不存在事务The transaction for the request. This property can be null for certain requests, such as for request returned from IDBFactory.open (You’re just connecting to a database, so there is no transaction to return).举个例子:; (async function () { let getIDBRequest = window.indexedDB.open(‘DEMO’) let db getIDBRequest.onsuccess = e => { db = getIDBRequest.result debugger }})()打开数据库操作就是一个请求:这里面就没有事务一旦操作数据,那么你就必须首先创建一个事务并设置模式,然后通过存储空间来发起请求; (async function () { let getIDBRequest = window.indexedDB.open(‘DEMO’) let db getIDBRequest.onsuccess = e => { db = getIDBRequest.result let transaction = db.transaction(‘users’, ‘readonly’) let store = transaction.objectStore(‘users’) let request = store.count() debugger }})()那么这个请求也就会包含事务了:PWA 系列第三章 IDB 的简要介绍就到这里,这个系列的下篇文章将介绍 Service Worker ???? ...

March 26, 2019 · 6 min · jiezi

使用IndexedDB做前端日志持久化

问题页面如果表现不符合预期,前端工程师在没有 javascript 日志的情况下,很难 debug。所以就需要针对必要的步骤记录日志,并上传。但是每记录一条日志就上传并不是一个合适的选择,譬如如果生成日志的操作比较密集,会频繁产生上传日志请求的情况。那么我们可以在页面做一次日志的缓存,把日志先存在本地,当缓存达到一定数量的时候一次批量上传,即节约了网络资源,对服务器也不会带来过重的负担。选型页面存储方案悉数下大概有这些:cookie、localStorage/sessionStorage、IndexedDB、WebSQL、FileSystem。cookie 存储量有限,显然不适合。localStorage/sessionStorage 必须自己设计及维护存储结构。WebSQL 已经是一种淘汰的标准,因为和 IndexedDB 功能重复了。FileSystem 也是比较边缘不太推荐的标准。那么 IndexedDB 容量合适,且能按条存储,不用自己维护存储结构,相较其他方案是我这次打算的选型。实现主要流程这里只介绍持久化所需要的基本操作,大而全的 API 操作见MDN文档第一、新建数据库及“表”IndexedDB 几乎所有的 API 都设计成异步的形式:const DATABASE_NAME = ‘alita’;let db = null;let request = window.indexedDB.open( DATABASE_NAME );request.onerror = function(event) { alert( ‘打开数据库失败’ + event.target.error );};request.onsuccess = function( event ) { // 如果打开成功,把数据库对象保存下来,以后增删改查都需要用到。 db = event.target.result;}如果数据库已经存在,indexedDB.open 会打开数据库,如果数据库不存在,indexedDB.open 会新建并打开。IndexedDB 也有类似于表的概念,在 IndexedDB 中叫 object store。并且新建 object store 还只能在特殊的场景下进行,先看下代码再解释:const DATABASE_NAME = ‘alita’;const OBJECT_STORE_NAME = ‘battleangel’;let db = null;let request = window.indexedDB.open( DATABASE_NAME );// 省略代码。// request.onerror = …// request.onsuccess = …request.onupgradeneeded = function(event) { let db = event.target.result; // 新建 object store let os = db.createObjectStore( OBJECT_STORE_NAME, {autoIncrement: true} ); // 如果想在新建完 object store 后初始化数据可以写在下面。 let initDataArray = […]; initDataArray.forEach( function(data){ os.add( data ); } );};db.createObjectStore 只能在 onupgradeneeded 回调函数中被调用。onupgradeneeded 什么时候触发呢?只有在你 indexedDB.open() 的数据库是新的,没有建立过的时候才会被触发。所以新建数据库和新建 object store 并不是随时随地都可以的(还有一种场景会触发,等会下面会说到)。createObjectStore 的第二个参数 {autoIncrement: true} 表示你以后添加进数据库的数据存储策略采用自增 key 的形式。第二、添加日志数据打开数据库后我们就可以添加数据了,我们来看下:let transaction = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ); // db 就是上面第一步保存下来的数据库对象。transaction.oncomplete = function(event) { alert( ‘事物关闭’ );};transaction.onerror = function(event) { // Don’t forget to handle errors!};let os = transaction.objectStore( OBJECT_STORE_NAME );let request = os.add( { // 日志对象。} );request.onsuccess = function(event) { alert( ‘添加成功’ )};request.onerror = function(event) { alert( ‘添加失败’ + event.target.error );};第三、读取所有日志数据在我们的场景中,添加完日志后,并不需要单独查询,只需要保存到一定数量后一次获取全部日志上传就可以了。获取表中所有数据也有新老 API 之分,先看新的 objectStore.getAll,chrome48及以上支持。let os = db.transaction( OBJECT_STORE_NAME, ‘read’ ).objectStore( OBJECT_STORE_NAME );let request = os.getAll();request.onsuccess = function(event) { let logObjectArray = event.target.result;};如果你用户的浏览器是不支持 getAll 方法,你还可以通过游标轮询的方式来迭代出所有的数据:let os = db.transaction( OBJECT_STORE_NAME, ‘read’ ).objectStore( OBJECT_STORE_NAME );let logObjectArray = [];let request = os.openCursor();request.onsuccess = function(event){ let cursor = event.target.result; if ( cursor ) { logObjectArray.push( cursor.value ); cursor.continue(); }};当 cursor.continue() 被调用后,onsuccess 会被反复触发,当 event.target.result 返回的 cursor 为空时,表示没有更多的数据了。我们的场景有点特殊,当日志存储到一定数量时,我们除了要读出所有的数据上传外,还要把已经上传的数据删除掉,这样就不至于越存越多,把 IndexedDB 存爆掉的情况,所以我们修改代码如下(请注意 db.transaction 的第二个参数这次不同了,因为我们要删数据,所以不能是只读):let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME );let logObjectArray = [];if ( os.getAll ) { let request = os.getAll(); request.onsuccess = function(event) { logObjectArray = event.target.result; // 删除所有数据 let clearRequest = os.clear(); // clearRequest.onsuccess = … // clearRequest.onerror = … // 上传日志 upload( logObjectArray ); };} else { let request = os.openCursor(); request.onsuccess = function(event){ let cursor = event.target.result; if ( cursor ) { logObjectArray.push( cursor.value ); cursor.continue(); } else { // 删除所有数据 let clearRequest = os.clear(); // clearRequest.onsuccess = … // clearRequest.onerror = … // 上传日志 upload( logObjectArray ); } };}以上的操作能完成我们的日志持久化的主流程了:存日志 - 获取已存日志 - 上传。问题及解决方案如果只有上述代码自然是没有办法完成一个健壮的持久化方案,还需要考虑如下几个点:当存和删除冲突怎么办我们看到代码了 IndexedDB 的操作都是异步,当我们正在获取所有日志时,又有写日志的调用怎么办?会不会在获取到所有日志和删除所有日志中间,新日志被添加进去了呢?这样新日志就会在没有被上传前就丢失了。这其实就是并发导致的问题,IndexedDB 有没有锁机制?规范中规定 ‘readwrite’ 模式的 transaction 同时只能有一个在处理 request,其他 ‘readwrite’ 模式的 transaction 即使生成了 request 也会被锁住不会触发 onsuccess。let request1 = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ).add({})let request2 = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ).add({})let request3 = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ).add({})// request1 没有处理完,request2 和 request3 就处于 pending 状态当前一个 transaction 完成后,后一个 transaction 才能响应,所以我们无需写额外的代码,IndexedDB 内部帮我们实现了锁机制。那么你要问了,什么时候 transaction 完成呢?没有看到你上面显式调用代码结束 transaction 呀?transaction 自动完成的条件有两个:必须有至少有一个和 transaction 关联的 request。也就是说如果你生成了一个 transaction 而没有生成对应的 request,那么这个 transaction 就成了孤儿事物,其他 transaction 没有办法继续操作数据库了,形成死锁。当 transaction 一个关联的 request 的 onsuccess/onerror 被调用,并且同时没有其他关联的 request 时,transaction 自动 commit。用代码举个例子:let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME );let request = os.getAll(); request.onsuccess = function(event) { logObjectArray = event.target.result; // 删除所有数据 let clearRequest = os.clear(); };上述代码中 os.clear() 之所以能被成功调用,是因为 os.getAll() 生成的 request 的 onsuccess 还没有执行完,os.clear() 就又生成了一个 request。所以当前 transaction 在 os.getAll().onsuccess 时并没有结束。但是如下代码中的 os.clear() 调用就会抛异常:let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME );let request = os.getAll(); request.onsuccess = function(event) { logObjectArray = event.target.result; // 删除所有数据 setTimeout( function(){ let clearRequest = os.clear(); // 这里会抛异常说 os 对应的 transaction 已经被关闭了。 }, 10 ); };怎么来判断数据库中存了多少数据我们解决了并发问题,那么我们如何来判断什么时候该上传日志了呢?有两个方案:1 基于数据库所存数据条数;2 基于数据库所存数据的大小。因为每条日志的数据或多或少都不一样,用条数来判断会出现同样30条数据,这次数据只占10k,下次可能有30k。所以相对理想的,我们应该以所存数据大小并设定一个阈值。这样每次上传量比较稳定。不过告诉大家一个悲伤的消息,IndexedDB 提供了查询条数的 API:objectStore.count,但是并没有提供查询容量的 API。所以我们采取了预估的方式先把查出来的所有数据转成 string,然后按 utf-8 的编码规则,逐个 char 累加,大致的代码如下:/** * UTF-8 是一种可变长度的 Unicode 编码格式,使用一至四个字节为每个字符编码 * * 000000 - 00007F(128个代码) 0zzzzzzz(00-7F) 一个字节 * 000080 - 0007FF(1920个代码) 110yyyyy(C0-DF) 10zzzzzz(80-BF) 两个字节 * 000800 - 00D7FF 00E000 - 00FFFF(61440个代码) 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz 三个字节 * 010000 - 10FFFF(1048576个代码) 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz 四个字节 */function sizeOf( str ) { let size = 0; if ( typeof str===‘string’ ) { let len = str.length; for( let i = 0; i < len; i++ ) { let charCode = str.charCodeAt( i ); if ( charCode<=0x007f ) { size += 1; } else if ( charCode<= 0x07ff ) { size += 2; } else if ( charCode<=0xffff ) { size += 3; } else { size += 4; } } } return size;}所以我们添加日志的代码可以进一步完善成如下:function writeLog( logObj ) { let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ); let request = os.getAll(); request.onsuccess = function(event) { let logObjectArray = event.target.result; logObjectArray.push( logObj ); let allDataStr = logObjectArray.map( l=>JSON.string(l) ).join( 分隔符 ); let allDataSize = sizeOf( allDataStr ); // 如果已存日志加上此次要添加的日志数据总和超过阈值,则上传并清空数据库 if ( allDataSize > 预设阈值 ) { os.clear(); upload( allDataStr ); } else { // 如果还没有达到阈值,则把日志添加进数据库 os.add( logObj ); } }}隐式问题:自增 key到上面为止正常的日志持久化方案已经较为完整了,上线也能够跑了(当然我示例代码里面省略了异常处理的代码)。但是这其中有一个隐形的问题存在,我们新建 object store 的时候存储结构使用的是自增 key。每个 object store 的自增 key 会随着新加入的数据不断的增加,删除和 clear 数据也不会重置这个 key。key 的最大值是2的53次方(9007199254740992)。当达到这个数值时,再 add 就会 add 不进数据了。此时 request.onerror 会得到一个 ConstraintError。我们可以通过显式得把 key 设置成最大的来模拟下:let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME );let request = os.add( {}, 9007199254740992 );setTimeout( function(){ let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ); let request = os.add( {} ); request.onerror = function(event) { console.log( event.target.error.name ); // ConstraintError }}, 2000 );这里有个一个问题,ConstraintError 并不是一个特定的 error 表示数据库“写满”了,其他场景也会触发抛出 ConstraintError,譬如添加 index 时候重复了。规范中也没有特定的 error 给到这种场景,所以这里要特别注意下。当然这个最大值是很大的,我们5秒钟写一次日志也需要14亿年写满。不过我比较任性,为了代码完备性,我给理论上兜个底。那么怎么才能重置 key 呢?很直接,就是删了当前的 object store,再建一个。这个时候坑爹的事又出现了。就像上面提到的 db.createObjectStore 只能在 onupgradeneeded 回调函数中被调用一样。db.deleteObjectStore 也只能在 onupgradeneeded 回调函数中被调用。那么我们上面提到了只有在新建的 db 的时候才能触发这个回调,怎么办?这个时候轮到 window.indexedDB.open 的第二个参数出场了。我们如果需要更新当前 db,那么就可以在第二个参数上传入一个比当前版本高的版本,就会触发 upgradeneeded 事件(第一次不传默认新建数据库的 version 就是1),代码如下:let nextVersion = 1;if ( db ) { nextVersion = db.version + 1; db.close(); // 这里一定要注意,一定要关闭当前 db 再做 open,要不然代码往下执行在 chrome 上根本不 work(其他浏览器没有测)。 db = null;}let request = window.indexedDB.open( DATABASE_NAME, nextVersion );request.onerror = function() { // 处理异常};request.onsuccess = ( event )=>{ db = event.target.result;};// 利用open version+1 的 db 重建 object store,因为 deleteObjectStore 只能在 onupgradeneeded 中调用。request.onupgradeneeded = function(event) { let currentDB = event.target.result; currentDB.deleteObjectStore( OBJECT_STORE_NAME ); currentDB.createObjectStore( OBJECT_STORE_NAME, { autoIncrement: true } );}所以添加日志的代码最终形态是:function recreateObjectStore( success ) { let nextVersion = 1; if ( db ) { nextVersion = db.version + 1; db.close(); // 这里一定要注意,一定要关闭当前 db 再做 open,要不然代码往下执行在 chrome 上根本不 work(其他浏览器没有测)。 db = null; } let request = self.indexedDB.open( DATABASE_NAME, nextVersion ); request.onerror = function() { // 处理异常 }; request.onsuccess = ( event )=>{ db = event.target.result; success && success(); }; // 利用open version+1 的 db 重建 object store,因为 deleteObjectStore 只能在 onupgradeneeded 中调用。 request.onupgradeneeded = function(event) { let currentDB = event.target.result; currentDB.deleteObjectStore( OBJECT_STORE_NAME ); currentDB.createObjectStore( OBJECT_STORE_NAME, { autoIncrement: true } ); }}let recreating = false; // 标志位,为了在没有重新建立 object store 前不要重复触发 recreate function writeLog( logObj ) { let os = db.transaction( OBJECT_STORE_NAME, ‘readwrite’ ).objectStore( OBJECT_STORE_NAME ); let request = os.getAll(); request.onsuccess = function(event) { let logObjectArray = event.target.result; logObjectArray.push( logObj ); let allDataStr = logObjectArray.map( l=>JSON.string(l) ).join( 分隔符 ); let allDataSize = sizeOf( allDataStr ); // 如果已存日志加上此次要添加的日志数据总和超过阈值,则上传并清空数据库 if ( allDataSize > 预设阈值 ) { os.clear(); upload( allDataStr ); } else { // 如果还没有达到阈值,则把日志添加进数据库 let addRequest = os.add( logObj ); addRequest.onerror = function(e) { // 如果添加新数据失败了 if ( error.name===‘ConstraintError’ ) { // 1.先把已有数据上传 uploadAllDbDate(); // 2. 看看是否已经在重置了 if ( !recreating ) { recreating = true; // 3. 如果没有重置,就重置 object store recreateObjectStore( function(){ // 4. 重置完成,再添加一遍数据 recreating = false; writeLog( logObj ); } ) } } } } }}好了到现在为止,整个日志持久化方案的流程就闭环了,当然实际代码肯定要更精细,结构更好。因为并发锁问题,数据大小问题,重置 object store 问题都不是很容易查到解决方案,网上大多数只有一些基本操作,所以这里记录下,方便有需要的人。参考文档:Using IndexedDB.Locking model for IndexedDB?.How do you keep an indexeddb transaction alive?. ...

March 8, 2019 · 6 min · jiezi