TagDown 扩展程序开发——前端数据持久化之 IndexedDB 基础
TagDown 是一款开源的书签管理插件, 您可以使用扩展程序浏览、新增、修改书签,它也支持以不同方式导出书签。
除了常见的书签管理功能,还具有以下特点:
支持 ???? 新增书签,并附加额外的信息,例如
tags
、groups
等支持 ???? 导出任意书签为
json
文档以 ???? 树图的形式浏览层级结构的书签数据
一键打开多个书签,支持在 ???? 标签组内打开书签
参考:
《现代 JavaScript 教程》IndexedDB 一章
A closer look at IndexedDB
前端数据持久化有多种方式实现,其中较常用的是通过浏览器自带的 IndexedDB 数据库实现。
???? 由于扩展程序的后台 service worker 由于 Service workers 无法访问 DOM 和相关的 API,且环境中没有 window
这个变量对象,所以无法使用浏览器提供的 localStorage
和 sessionStorage
进行数据存储。Chrome 为扩展程序提供一个数据存储 API chrome.storeage
,该 API 可以实现 类似 localStorage 的功能,用于存储少量的数据(一般是存储扩展程序的设置参数)。对于 sync 同步存储的数据,允许总大小为 100KB;对于 local 本地存储的数据,允许总大小为 5MB(类似于 localstorage 的存储限制)。
IndexedDB 具有以下特点:
通过支持多种类型的键,来存储几乎可以是任何类型的值
支撑事务,有良好的可靠性(事务是数据库通用术语,是指一组操作要么全部成功,要么全部失败,不存在中间结果从而导致数据冲突或不完整)
支持键的范围查询,也支持为数据添加索引
⚠️ IndexedDB 遵循同源策略限制,即每个数据库都是绑定到源(域/协议/端口)的,不同的网站不能相互访问对方的数据库。
基本概念
IndexedDB 数据有几个基本的概念,和 SQL、NoSQL 等常用数据库概念类似:
(同源)网页可以创建一个专属的数据库 Database(遵循同源策略的限制)
每一个数据库可以创建多个对象库 ObjectStore,和 SQL 的表格概念类似
在对象库中就是以键值对的形式存储着的一条条数据
使用 IndexedDB 的基本流程如下:
创建/打开一个数据库
定义数据结构,创建对象库,指定数据项的键 key(还可以创建索引 index)
启动事务对对数据进行操作
监听相应的事件
获取操作结果
数据库
使用方法 open()
(连接)一个数据库,第一个参数 name
是数据库名称,数据库可以有许多不同的名称;第二个参数是一个正整数,表示数据库版本,默认为 1
。
let openRequest = indexedDB.open(name, version); 复制代码
该方法返回 openRequest
对象,我们需要监听该对象上的事件(因为对 IndexedDB APIs 一般都是异步操作,待事件触发后才可以在回调函数中执行后续操作):
success
:数据库准备就绪时触发的事件。然后在openRequest.result
中有了一个数据库对象 Database Object,使用它对数据库进行进一步的调用error
:打开失败时触发的事件upgradeneeded
:数据库已准备就绪,但其版本已过时触发的事件。可以根据需要比较版本,并升级数据结构。升级操作顺利完成后,onsuccess
事件被触发,数据库才算是成功打开了。
???? 如果数据库还不存在时(从此时数据库的版本是 0
),打开数据库操作就会触发 upgradeneeded
事件,此时可以执行初始化(如创建对象库,指定数据项的键 key,定义数据项的索引 index 等)
⚠️ 只有在 upgradeneeded
事件的回调函数中,才可以更新升级数据库的结构(例如对象库的创建,以及修改对象库的索引属性)
let openRequest = indexedDB.open("tagdown", 1); // 如果浏览器中没有该数据库(或已存在的数据库版本,与打开的版本不符),则会触发 upgradeneeded 事件 openRequest.onupgradeneeded = function() { // 执行初始化 let bookmark = db.createObjectStore('bookmark', { keyPath: "id" }); let index = bookmark.createIndex('tags', 'tags', { multiEntry: true}) // 参数依此表示:索引名称为 tags,对应以数据的哪一个属性作为索引值,由于该属性值为数组,且将数组的每个元素作为索引时需要配置 multiEntry 为 true }; // 如果浏览器中有该数据库,则会触发 onsuccess 事件 openRequest.onsuccess = function() { let db = openRequest.result; // 继续使用已有的 db 对象处理数据库 }; openRequest.onerror = function() { console.error("Error", openRequest.error); }; 复制代码
???? 如果想删除整个数据库(而不是单一条数据),可以使用方法 deleteDatabase(databaseName)
事务
接着,在 success
事件的回调函数中,使用方法 transaction
启动事务,对数据进行操作。
使用事务操作数据的基本流程如下:
使用
db.transaction(store)
创建一个事务,表明要访问的对象库使用
transaction.objectStore(name)
获取存储对象发起请求,操作数据
监听请求的成功/错误事件,并执行相应的操作
方法 transaction
创建一个事务,它接收的第一个参数是事务要访问的对象库名称(如果我们要访问多个对象库,则该参数值是抑恶数组);(可选)第二个参数是事务类型:
readonly
只读,默认值readwrite
可读取和写入数据(但不能创建/删除/更改的对象库,这类操作只能在upgradeneeded
事件的回调函数中进行)
db.transaction(store[, type]); 复制代码
???? 需要两种事务类型,是因为两种类型的事物操作「性能」是不同的。许多 readonly
事务能够同时访问同一存储区;但 readwrite
事务不能。因为 readwrite
事务会「锁定」存储区进行写操作,下一个事务必须等待前一个事务完成,才能访问相同的存储区。
创建事务后,使用事务实例的方法 objectStore(name)
获取相应的对象库,然后就可以对里面的数据项进行操作。对象库支持两种存储值的方法:
方法
put(value, [key])
将value
添加到存储区。如果已经存在相同键的数据,则将替换该值。方法
add(value, [key])
与方法put
类似,但是如果已经存在相同键的数据,则请求失败,并生成一个名为"ConstraInterror"
的错误。
let transaction = db.transaction("bookmark", "readwrite"); // 创建一个事务,表明要访问的对象库是 bookmark,事务类型是读写 // 获取对象库进行操作 let bookmark = transaction.objectStore("bookmark"); // 数据项 let node = { id: '123', title: 'Google' url: 'www.google.com' tags: ['search', 'tool'] }; let request = bookmark.add(node); // 向对象库中添加数据项 // 监听请求成功事件 request.onsuccess = function() { console.log("Bookmark added to the store", request.result); }; // 监听请求失败事件 request.onerror = function() { console.log("Error", request.error); }; 复制代码
删除特定的数据需要指定查询条件,使用方法 delete(query)
// 删除键值满足 id='123' 的数据 bookmark.delete('123'); 复制代码
搜索数据
数据库另一个重要功能是搜索数据,IndexedDB 支持两种主要的搜索类型:
基于一个键或范围进行搜索
基于一个索引或范围进行搜索
两者的区别是:由于每个数据的键 key 是特殊唯一的,如果基于单一值搜索,最多获得一个数据项;而索引值可以复用,如果基于单一索引值搜索,可以获得多个数据项。
使用方法 get(query)
按键 key 搜索数据,返回满足条件的数据项;使用方法 getKey(query)
按键 key 搜索数据,但是返回的是满足条件的数据项的键值
bookmark.get('123') // 返回数据项 bookmark.getKey('123') // 返回 123 复制代码
???? 使用方法 getAll([query], [count])
或 getAllKeys([query], [count])
获取一个范围的数据或键值,返回数组,其中参数 query
表示一个范围,需要通过调用相应的函数创建。
???? 只返回键值的方法效率更高,该操作不需要解析读取完整的数据
通过索引 index 进行查询,也是使用相同的方法,但是通过索引对象调用这些方法。
let tagsIndex = bookmark.index("tags"); // 索引对象 tagsIndex.getAll('tool'); // 获取所有标记有 tool 的数据项
作者:Benbinbin
链接:https://juejin.cn/post/7018820211188957192