针对indexedDB的简易封装
连接数据库
我们首先创建一个DBManager类,通过这个类new出来的对象管理一个数据库
具体关于indexedDB的相关内容可以看我的这篇博客
indexedDB
class DBManager{}
我们首先需要打开数据库,打开数据库需要数据库名和该数据库的版本
constructor(dbName, version) {this.dbName = dbName;this.version = version;this.db = null
}
在constructor中我们先初始化数据库相关信息,dbName为该对象管理的数据库的数据库名,version为该数据库的版本,db为该数据库的IDBDatabase对象
现在我们开始实现openDB方法
openDB() {return new Promise((resolve, reject) => {const cmd = indexedDB.open(this.dbName, this.version)cmd.onsuccess = (event) => {console.log('数据库打开成功')this.db = event.target.resultresolve(this.db)}cmd.onerror = (event) => {console.log('数据库打开失败')reject(event.target.error)}})
}
因为打开数据库涉及i/o操作,所以是异步的,所以我们需要返回一个Promise
关闭数据库
当数据库使用完毕,为了节省资源,我们可以选择断开数据库的连接
closeDB() {if (this.db) {console.log('关闭数据库')this.db.close()this.db = null}
}
删除数据库
如果数据库某一天不在使用,我们可以选择删除这个数据库来节省资源
deleteDB() {return new Promise((resolve, reject) => {const cmd = indexedDB.deleteDatabase(this.dbName)cmd.onsuccess = (event) => {console.log('数据库删除成功')resolve()}cmd.onerror = (event) => {console.log('数据库删除失败')reject(event.target.error)}})
}
同样,删除数据库是异步的,我们需要返回一个Promise
我们接下来来测试一下
(async function () {const db = new DBManager("student", 1)await db.openDB()await db.closeDB()await db.deleteDB()
})()
需要注意的是,我们在删除数据库之前必须先断开数据库连接
创建对象仓库
我们接下来需要实现创建对象的方法
createStore(storeName, keyPath, keys) {return new Promise((resolve, reject) => {if (this.db) {console.log('添加存储仓库', storeName)const store = this.db.createObjectStore(storeName, { keyPath: keyPath, autoIncrement: true })if (keys) {keys.forEach(key => {store.createIndex(key, key, { unique: key === keyPath ? true : false })})}resolve(this.db)} else {reject('数据库未打开')}})
}
但是如果我们直接通过调用createStore来创建对象仓库的话浏览器会报错
这是因为针对对象仓库的操作是需要放在db.onupgradeneeded的回调中,所以我们不能直接这么写
数据库的更新
我们可以用一个更新方法来手动触发onupgradeneeded这个事件
updateDB(callback) {return new Promise(async (resolve, reject) => {console.log('数据库升级')if (this.db) {this.closeDB()this.version += 1await this.openDB(callback)resolve(this.db)}else {reject('数据库未打开')}})
}
openDB(callback) {return new Promise((resolve, reject) => {const cmd = indexedDB.open(this.dbName, this.version)cmd.onsuccess = (event) => {console.log('数据库打开成功')this.db = event.target.resultresolve(this.db)}cmd.onerror = (event) => {console.log('数据库打开失败')reject(event.target.error)}if (callback) {cmd.onupgradeneeded = (event) => {this.db = event.target.resultcallback(event)}}})
}
update方法通过调用close和open方法更新数据库,同时将对对象仓库的操作封装成函数传入update方法中,再将这个函数放入open方法中,open方法中通过判断是否传入参数来判断是否需要监听onupgradeneeded事件,因为当用户第一次创建数据库的时候会触发这个事件,而第一次的时候我们是不需要监听的
接下来我们重新处理下createStore里的逻辑
createStore(storeName, keyPath, keys) {return new Promise(async (resolve, reject) => {if (this.db) {await this.updateDB((event) => {console.log('添加存储仓库', storeName)const store = this.db.createObjectStore(storeName, { keyPath: keyPath, autoIncrement: true })if (keys) {keys.forEach(key => {store.createIndex(key, key, { unique: key === keyPath ? true : false })})}})resolve(this.db)} else {reject('数据库未打开')}})
}
接下来我们再来测试一下
(async function () {const db = new DBManager("student", 1)await db.openDB()await db.createStore("student", "id", ['id', 'name', 'age', 'score'])await db.closeDB()await db.deleteDB()
})()
为什么是先打印添加存储仓库,后打印数据库打开?因为当IDBDatabase对象同时出发onsuccess和onupgradeneeded事件时,会先执行onupgradeneeded的回调,然后执行onsuccess中的回调
数据记录的操作
我们接下来实现关于数据记录的方法
增加数据
增加数据记录的逻辑较为简单,调用indexedDB提供的add方法就行
insert(storeName, data) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.add(data)cmd.onsuccess = (event) => {console.log('插入数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('插入数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})
}
我们来测试一下
(async function () {const db = new DBManager("student", 1)await db.openDB()await db.createStore("student", "id", ['id', 'name', 'age', 'score'])await db.insert("student", { id: 1, name: "张三", age: 18, score: 90 })await db.insert("student", { id: 2, name: "李四", age: 20, score: 56 })await db.closeDB()await db.deleteDB()
})()
查询数据
查询数据我们需要根据不同的查询方式来实现不同的方法
-
通过key查询
queryByKey(storeName, value) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.get(value)cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}}) }
-
查询全部数据记录
queryAll(storeName) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.getAll()cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}}) }
-
通过游标查询
queryByCursor(storeName, range, direction = "next") {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cursor = range ? store.openCursor(range, direction) : store.openCursor()const result = []cursor.onsuccess = (event) => {const cursor = event.target.resultif (cursor) {result.push(cursor.value)cursor.continue()} else {console.log('查询数据成功')resolve(result)}}cursor.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}}) }
-
通过指定key-value查询
queryByIndex(storeName, indexName, value) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.index(indexName).get(value)cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}}) }
我们现在来测试一下
(async function () {const db = new DBManager("student", 1)await db.openDB()await db.createStore("student", "id", ['id', 'name', 'age', 'score'])await db.insert("student", { id: 1, name: "张三", age: 18, score: 90 })await db.insert("student", { id: 2, name: "李四", age: 20, score: 56 })await db.insert("student", { id: 3, name: "王五", age: 19, score: 80 })await db.insert("student", { id: 4, name: "赵六", score: 70 })const result = await db.queryByIndex("student", "age", 18)console.log(result)const result2 = await db.queryByKey("student", 3)console.log(result2)const result3 = await db.queryByCursor("student")console.log(result3)const result4 = await db.queryByCursor("student", IDBKeyRange.only(4))console.log(result4)const result5 = await db.queryAll("student")console.log(result5)await db.closeDB()await db.deleteDB()
})()
更新数据
更新数据记录也是通过调用indexedDB中的put方法来实现
update(storeName, key, data) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.put(data)cmd.onsuccess = (event) => {console.log('更新数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('更新数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})
}
删除数据
更新数据记录也是通过调用indexedDB中的delete方法来实现
delete(storeName, key) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.delete(key)cmd.onsuccess = (event) => {console.log('删除数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('删除数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})
}
完整代码
最后我们来看一下完整代码
class DBManager {constructor(dbName, version) {this.dbName = dbName;this.version = version;this.db = null}openDB(callback) {return new Promise((resolve, reject) => {const cmd = indexedDB.open(this.dbName, this.version)cmd.onsuccess = (event) => {console.log('数据库打开成功')this.db = event.target.resultresolve(this.db)}cmd.onerror = (event) => {console.log('数据库打开失败')reject(event.target.error)}if (callback) {cmd.onupgradeneeded = (event) => {this.db = event.target.resultcallback(event)}}})}closeDB() {if (this.db) {console.log('关闭数据库')this.db.close()this.db = null}}deleteDB() {return new Promise((resolve, reject) => {const cmd = indexedDB.deleteDatabase(this.dbName)cmd.onsuccess = (event) => {console.log('数据库删除成功')resolve()}cmd.onerror = (event) => {console.log('数据库删除失败')reject(event.target.error)}})}updateDB(callback) {return new Promise(async (resolve, reject) => {console.log('数据库升级')if (this.db) {this.closeDB()this.version += 1await this.openDB(callback)resolve(this.db)}else {reject('数据库未打开')}})}createStore(storeName, keyPath, keys) {return new Promise(async (resolve, reject) => {if (this.db) {await this.updateDB((event) => {console.log('添加存储仓库', storeName)const store = this.db.createObjectStore(storeName, { keyPath: keyPath, autoIncrement: true })if (keys) {keys.forEach(key => {store.createIndex(key, key, { unique: key === keyPath ? true : false })})}})resolve(this.db)} else {reject('数据库未打开')}})}deleteStore(storeName) {return new Promise(async (resolve, reject) => {if (this.db) {await this.updateDB((event) => {console.log('删除存储仓库', storeName)const store = this.db.deleteObjectStore(storeName)})resolve(this.db)} else {reject('数据库未打开')}})}insert(storeName, data) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.add(data)cmd.onsuccess = (event) => {console.log('插入数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('插入数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}update(storeName, key, data) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.put(data)cmd.onsuccess = (event) => {console.log('更新数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('更新数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}delete(storeName, key) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readwrite')const store = transaction.objectStore(storeName)const cmd = store.delete(key)cmd.onsuccess = (event) => {console.log('删除数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('删除数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}queryByKey(storeName, value) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.get(value)cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}queryAll(storeName) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.getAll()cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}queryByIndex(storeName, indexName, value) {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cmd = store.index(indexName).get(value)cmd.onsuccess = (event) => {console.log('查询数据成功')resolve(event.target.result)}cmd.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}queryByCursor(storeName, range, direction = "next") {return new Promise((resolve, reject) => {if (this.db) {const transaction = this.db.transaction(storeName, 'readonly')const store = transaction.objectStore(storeName)const cursor = range ? store.openCursor(range, direction) : store.openCursor()const result = []cursor.onsuccess = (event) => {const cursor = event.target.resultif (cursor) {result.push(cursor.value)cursor.continue()} else {console.log('查询数据成功')resolve(result)}}cursor.onerror = (event) => {console.log('查询数据失败')reject(event.target.error)}} else {reject('数据库未打开')}})}
}
相关文章:

针对indexedDB的简易封装
连接数据库 我们首先创建一个DBManager类,通过这个类new出来的对象管理一个数据库 具体关于indexedDB的相关内容可以看我的这篇博客 indexedDB class DBManager{}我们首先需要打开数据库,打开数据库需要数据库名和该数据库的版本 constructor(dbName,…...

网络编程--网络理论基础(二)
这里写目录标题 网络通信流程mac地址、ip地址arp协议交换机路由器简介子网划分网关 路由总结 为什么ip相同的主机在与同一个互联网服务通信时不冲突公网ip对于同一个路由器下的不同设备,虽然ip不冲突,但是因为都是由路由器的公网ip转发通信,接…...

Python MongoDB 基本操作
本文内容主要为使用Python 对Mongodb数据库的一些基本操作整理。 目录 安装类库 操作实例 引用类库 连接服务器 连接数据库 添加文档 添加单条 批量添加 查询文档 查询所有文档 查询部分文档 使用id查询 统计查询 排序 分页查询 更新文档 update_one方法 upd…...
Node.js 入门:
Node.js 是一个开源、跨平台的 JavaScript 运行时环境,它允许开发者在浏览器之外编写命令行工具和服务器端脚本。以下是一些关于 Node.js 的基础教程: 1. **Node.js 入门**: - 了解 Node.js 的基本概念,包括它是一个基于 Chro…...
java8 List的Stream流操作 (实用篇 三)
目录 java8 List的Stream流操作 (实用篇 三) 初始数据 1、Stream过滤: 过滤-常用方法 1.1 筛选单元素--年龄等于18 1.2 筛选单元素--年龄大于18 1.3 筛选范围--年龄大于18 and 年龄小于40 1.4 多条件筛选--年龄大于18 or 年龄小于40 and sex男 1.5 多条件筛…...

机器学习python实践——数据“相关性“的一些补充性个人思考
在上一篇“数据白化”的文章中,说到了数据“相关性”的概念,但是在统计学中,不仅存在“相关性”还存在“独立性”等等,所以,本文主要对数据“相关性”进行一些补充。当然,如果这篇文章还能入得了各位“看官…...

MySQL——触发器(trigger)基本结构
1、修改分隔符符号 delimiter $$ $$可以修改 2、创建触发器函数名称 create trigger 函数名 3、什么样在操作触发,操作哪个表 after :……之后触发 before :……之后触发 insert :……之后触发 update :……之后触…...

数字孪生定义及应用介绍
数字孪生定义及应用介绍 1 数字孪生(Digital Twin, DT)概述1.1 定义1.2 功能1.3 使用场景1.4 数字孪生三步走1.4.1 数字模型1.4.2 数字影子1.4.3 数字孪生 数字孪生地球平台Earth-2 参考 1 数字孪生(Digital Twin, DT)概述 数字孪…...
数据赋能(122)——体系:数据清洗——技术方法、主要工具
技术方法 数据清洗标准模型是将数据输入到数据清洗处理器,通过一系列步骤“清理”数据,然后以期望的格式输出清理过的数据。数据清洗从数据的准确性、完整性、一致性、惟一性、适时性、有效性几个方面来处理数据的丢失值、越界值、不一致代码、重复数据…...

【SCAU数据挖掘】数据挖掘期末总复习题库简答题及解析——中
1. 某学校对入学的新生进行性格问卷调查(没有心理学家的参与),根据学生对问题的回答,把学生的性格分成了8个类别。请说明该数据挖掘任务是属于分类任务还是聚类任务?为什么?并利用该例说明聚类分析和分类分析的异同点。 解答: (a)该数据…...

2024年注册安全工程师报名常见问题汇总!
注册安全工程师报名 24年注册安全工程师报名已正式拉开序幕,报名时间为6月18日—7月10日,考试时间为10月26日—10月27日。 目前经有12个地区公布了2024年注册安全工程师报名时间: 注册安全工程师报名信息完善 根据注安报名系统提示&am…...

JRebel-JVMTI [FATAL] Couldn‘t write to C:\Users\中文用户名-完美解决
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 热部署下载参考博客解决第一步第二步第三步:第四步: 热部署下载 下载后启动报错:JRebel-JVMTI [FATAL] Couldn’t write to C:\…...
STM32基于DMA数据转运和AD多通道
文章目录 1. DMA数据转运 1.1 初始化DMA步骤 1.2 DMA的库函数 1.3 设置当前数据寄存器 1.4 DMA获取当前数据寄存器 2. DMA数据转运 2.1 DMA.C 2.2 DMA.H 2.3 MAIN.C 3. DMAAD多通道 3.1 AD.C 3.2 AD.H 3.3 MAIN.C 1. DMA数据转运 对于DMA的详细解析可以看下面这篇…...
安卓应用开发——Android Studio中通过id进行约束布局
在Android开发中,布局通常使用XML文件来描述,而约束(如相对位置、大小等)可以通过多种方式实现,但直接使用ID进行约束并不直接对应于Android的传统布局系统(如LinearLayout、RelativeLayout等)。…...

Elasticsearch过滤器(filter):原理及使用
Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 💥💥个人主页:奋斗的小羊 💥💥所属专栏:C语言 🚀本系列文章为个人学习…...
Docker配置与使用详解
一、引言 随着云计算和微服务的兴起,Docker作为一种轻量级的容器化技术,越来越受到开发者和运维人员的青睐。Docker通过容器化的方式,将应用程序及其依赖项打包成一个可移植的镜像,从而实现了应用程序的快速部署和扩展。本文将详…...

触控MCU芯片(1):英飞凌PSoC第6代第7代
前言: 说到触摸MCU芯片,这个历史也是很久了,比如日常经常接触到的洗衣机、电冰箱、小家电,隔着一层玻璃,轻轻一按就能识别按键,感觉比过去纯机械式的按键更高级更美观,不仅白电,现在很多汽车也都在进行触摸按键的改版,不再使用笨重的机械按键,比如空调调温按键、档位…...
git pull报错:unable to pull from remote repository due to conflicting tag(s)
背景 我在vscode里正常拉取代码,突然就报了如题所示的错误。 原因 因为vscode的拉取按钮执行的实际命令是:git pull --tags origin branch-name,该命令的实际含义是从远程仓库拉取指定的分支和该远程仓库上的所有标签。 在拉取标签时本地的…...

Python将字符串用特定字符分割并前面加序号
Python将字符串用特定字符分割并前面加序号 Python将字符串用特定字符分割并前面加序号,今天项目中就遇到,看着不难,得花点时间搞出来急用啊,在网上找了一圈,没发现有完整流程的文章。所以就搞出来并写了这个文章。仅…...

【第16章】Vue实战篇之跨域解决
文章目录 前言一、浏览器跨域二、配置代理1.公共请求2.代理配置 总结 前言 前后端项目分离衍生出浏览器跨域问题,开发之前我们通过配置代理解决这个问题。 一、浏览器跨域 浏览器的跨域问题主要是由于浏览器的同源策略导致的。同源策略是浏览器的一个安全功能&…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
C++.OpenGL (20/64)混合(Blending)
混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...