
在本文中,我将逐步告诉您如何准备IndexDB(任何现代浏览器中内置的数据库)以用于用ReactJS编写的项目中。 结果,您可以像使用应用程序的
Redux存储一样方便地使用IndexDB中的数据。
IndexDB是
面向文档
的 DBMS,这是一种方便的工具,用于在浏览器端临时存储相对少量(单位和数
十兆字节)的结构化数据。 我必须使用IndexDB的标准任务包括在客户端缓存业务目录的数据(国家/地区名称,城市名称,按代码显示的货币等)。 将它们复制到客户端后,您只能偶尔从服务器(或整个目录-很小)从这些目录下载更新,而不是每次打开浏览器窗口时都执行此操作。
有一些非标准的,很有争议的但使用IndexDB的可行方法:
- 缓存有关所有业务对象的数据,以便在浏览器端使用广泛的排序和过滤功能
- 将应用程序状态存储在IndexDB中而不是Redux Store中
IndexDB和Redux Store之间的三个主要区别对我们很重要:
- IndexDB是一个外部存储,离开页面时不会清除。 此外,多个打开的标签页也是一样的(有时会导致某些意外情况)
- IndexDB是一个完全异步的DBMS。 所有操作-打开,读取,写入,搜索-异步。
- IndexDB不能(以简单的方式)存储在JSON中,并且不能使用Redux的脑力激荡技术来创建Snapshots,调试方便以及过去的旅程。
步骤0:任务列表
已经是带有任务列表的经典示例。 在当前和唯一组件的状态下具有状态存储的变体
任务列表组件的实现,该任务列表组件以组件状态存储列表import React, { PureComponent } from 'react'; import Button from 'react-bootstrap/Button'; import counter from 'common/counter'; import Form from 'react-bootstrap/Form'; import Table from 'react-bootstrap/Table'; export default class Step0 extends PureComponent { constructor() { super( ...arguments ); this.state = { newTaskText: '', tasks: [ { id: counter(), text: 'Sample task' }, ], }; this.handleAdd = () => { this.setState( state => ( { tasks: [ ...state.tasks, { id: counter(), text: state.newTaskText } ], newTaskText: '', } ) ); }; this.handleDeleteF = idToDelete => () => this.setState( state => ( { tasks: state.tasks.filter( ( { id } ) => id !== idToDelete ), } ) ); this.handleNewTaskTextChange = ( { target: { value } } ) => this.setState( { newTaskText: value || '', } ); } render() { return <Table bordered hover striped> <thead><tr> <th>#</th><th>Text</th><th /> </tr></thead> <tbody> { this.state.tasks.map( task => <tr key={task.id}> <td>{task.id}</td> <td>{task.text}</td> <td><Button onClick={this.handleDeleteF( task.id )} type="button" variant="danger"></Button></td> </tr> ) } <tr key="+1"> <td /> <td><Form.Control onChange={this.handleNewTaskTextChange} placeholder=" " type="text" value={this.state.newTaskText || ''} /></td> <td><Button onClick={this.handleAdd} type="button" variant="primary"></Button></td> </tr> </tbody> </Table>; } }
(
github源代码 )
到目前为止,所有带任务的操作都是同步的。 如果添加任务需要3秒钟,那么浏览器将冻结3秒钟。 当然,虽然我们将所有内容都保留在内存中,但我们无法考虑它。 当我们使用服务器或本地数据库进行处理时,我们还必须注意异步处理的优美性。 例如,在添加或删除元素时阻止使用表(或单个元素)。
为了以后不再重复对UI的描述,我们将其放置在单独的TaskList组件中,该组件的唯一任务将为任务列表生成HTML代码。 同时,我们将用引导程序按钮周围的特殊包装替换常规按钮,该包装将阻塞按钮,直到按钮处理程序完成其执行,即使该处理程序是异步函数也是如此。
实现以反应状态存储任务列表的组件 import React, { PureComponent } from 'react'; import counter from 'common/counter'; import TaskList from '../common/TaskList'; export default class Step01 extends PureComponent { constructor() { super( ...arguments ); this.state = { tasks: [ { id: counter(), text: 'Sample task' }, ] }; this.handleAdd = newTaskText => { this.setState( state => ( { tasks: [ ...state.tasks, { id: counter(), text: newTaskText } ], } ) ); }; this.handleDelete = idToDelete => this.setState( state => ( { tasks: state.tasks.filter( ( { id } ) => id !== idToDelete ), } ) ); } render() { return <> <h1> </h1> <h2> </h2> <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={this.state.tasks} /> </>; } }
(
github源代码 )
一个组件的实现,该组件显示任务列表并包含用于添加新任务的表单 import React, { PureComponent } from 'react'; import Button from './AutoDisableButtonWithSpinner'; import Form from 'react-bootstrap/Form'; import Table from 'react-bootstrap/Table'; export default class TaskList extends PureComponent { constructor() { super( ...arguments ); this.state = { newTaskAdding: false, newTaskText: '', }; this.handleAdd = async() => { this.setState( { newTaskAdding: true } ); try {
(
github源代码 )
在示例代码中,您已经可以看到关键字async / await。 异步/等待构造可以大大减少可用于Promises的代码量。 关键字await允许您等待返回Promise的函数的响应,就好像它是常规函数一样(而不是在then()中等待结果)。 当然,异步函数不会神奇地变成同步函数,例如,当使用await时,执行线程将被中断。 但是随后代码变得更加简洁易懂,并且可以在循环和try / catch / finally结构中使用await。
例如,
TaskList
调用 this.props.onAdd
处理程序,而且使用
await
关键字进行
调用 。 在这种情况下,如果处理程序是将不返回任何内容或返回除
Promise
之外的任何值的普通函数,则
TaskList
组件
TaskList
handleAdd
常规方式继续执行
handleAdd
方法。 但是,如果处理程序返回
Promise
(包括如果将处理程序声明为异步函数),则
TaskList
将等待处理程序完成执行,然后才重置
newTaskAdding
和
newTaskText
。
步骤1:将IndexDB添加到React组件
为了简化我们的工作,首先我们将编写一个简单的组件来实现Promise方法:
- 打开数据库以及简单的错误处理
- 在数据库中搜索项目
- 将项目添加到数据库
第一个是最“平凡的”-多达5个事件处理程序。 但是,没有火箭科学:
openDatabasePromise()-打开数据库 function openDatabasePromise( keyPath ) { return new Promise( ( resolve, reject ) => { const dbOpenRequest = window.indexedDB.open( DB_NAME, '1.0.0' ); dbOpenRequest.onblocked = () => { reject( ' , , ' + ' .' ); }; dbOpenRequest.onerror = err => { console.log( 'Unable to open indexedDB ' + DB_NAME ); console.log( err ); reject( ' , .' + ( err.message ? ' : ' + err.message : '' ) ); }; dbOpenRequest.onupgradeneeded = event => { const db = event.target.result; try { db.deleteObjectStore( OBJECT_STORE_NAME ); } catch ( err ) { console.log( err ); } db.createObjectStore( OBJECT_STORE_NAME, { keyPath } ); }; dbOpenRequest.onsuccess = () => { console.info( 'Successfully open indexedDB connection to ' + DB_NAME ); resolve( dbOpenRequest.result ); }; dbOpenRequest.onerror = reject; } ); }
getAllPromise / getPromise / putPromise-Promise中的包装器IndexDb调用 将所有内容放到一个IndexedDbRepository类中
IndexedDbRepository-IDBDatabase的包装器 const DB_NAME = 'objectStore'; const OBJECT_STORE_NAME = 'objectStore'; export default class IndexedDbRepository { constructor( keyPath ) { this.error = null; this.keyPath = keyPath;
(
github源代码 )
现在您可以从代码访问IndexDB:
const db = new IndexedDbRepository( 'id' );
将此“存储库”连接到我们的组件。 根据反应规则,对服务器的调用应在componentDidMount()方法中:
import IndexedDbRepository from '../common/IndexedDbRepository'; componentDidMount() { this.repository = new IndexedDbRepository( 'id' );
从理论上讲,可以将
componentDidMount()
函数声明为异步,然后可以使用async / await构造代替then()。 但是
componentDidMount()
仍然不是“我们的”函数,而是被React调用。 谁知道react 17.x库将如何响应尝试返回
Promise
而不是
undefined
?
现在在构造函数中,我们将用null填充它,而不是用空数组(或包含测试数据的数组)填充它。 在渲染中,它将根据需要等待数据处理的情况来处理此null。 原则上希望这样做的人可以将其放在单独的标记中,但是为什么要产生实体?
constructor() { super( ...arguments ); this.state = { tasks: null }; } render() { if ( this.state.tasks === null ) return <><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span> ...</span></>; /* ... */ }
它仍然需要实现
handleAdd
/
handleDelete
:
constructor() { this.handleAdd = async( newTaskText ) => { await this.repository.save( { id: counter(), text: newTaskText } ); this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; this.handleDelete = async( idToDelete ) => { await this.repository.deleteById( idToDelete ); this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; }
在这两个处理程序中,我们首先转到存储库以添加或删除项目,然后清除当前组件的状态,然后再次从存储库中请求新列表。 似乎对setState()的调用将一个接一个地进行。 但是,处理程序最后几行中的await关键字仅在从findAll()方法获得的Promise()被解析之后才导致第二个setState()调用发生。
步骤2.聆听变更
上面代码中的一个大缺陷是,首先,存储库连接在每个组件中。 其次,如果一个组件更改了存储库的内容,那么另一组件将不知道它,直到它由于任何用户操作而重新读取状态为止。 这很不方便。
为了解决这个问题,我们将引入新的RepositoryListener组件,并使其做两件事。 首先,该组件将能够订阅存储库中的更改。 其次,RepositoryListener将这些更改通知创建它的组件。
首先,添加了在IndexedDbRepository中注册处理程序的功能:
export default class IndexedDbRepository { constructor( keyPath ) { this.listeners = new Set(); this.stamp = 0; } addListener( listener ) { this.listeners.add( listener ); } onChange() { this.stamp++; this.listeners.forEach( listener => listener( this.stamp ) ); } removeListener( listener ) { this.listeners.delete( listener ); } }
(
github源代码 )
我们将向处理程序传递一个戳记,该戳记将在每次调用onChange()时更改。 然后,我们修改_tx方法,以便在具有
readwrite
模式的事务中为每个调用调用
onChange()
:
async _tx( txMode, callback ) { await this.openDatabasePromise;
(
github源代码 )
如果我们仍然使用
then()
/
catch()
来与Promise一起使用,我们将不得不复制对
onChange()
的调用,或者对Promise()使用支持
final()
特殊polyfills。 幸运的是,异步/等待允许您简单地执行此操作,而无需不必要的代码。
RepositoryListener组件本身在componentDidMount和componentWillUnmount方法中连接事件侦听器:
RepositoryListener代码 import IndexedDbRepository from './IndexedDbRepository'; import { PureComponent } from 'react'; export default class RepositoryListener extends PureComponent { constructor() { super( ...arguments ); this.prevRepository = null; this.repositoryListener = repositoryStamp => this.props.onChange( repositoryStamp ); } componentDidMount() { this.subscribe(); } componentDidUpdate() { this.subscribe(); } componentWillUnmount() { this.unsubscribe(); } subscribe() { const { repository } = this.props; if ( repository instanceof IndexedDbRepository && this.prevRepository !== repository ) { if ( this.prevRepository !== null ) { this.prevRepository.removeListener( this.repositoryListener ); } this.prevRepository = repository; repository.addListener( this.repositoryListener ); } } unsubscribe( ) { if ( this.prevRepository !== null ) { this.prevRepository.removeListener( this.repositoryListener ); this.prevRepository = null; } } render() { return this.props.children || null; } }
(
github源代码 )
现在,我们将在主组件中包括存储库更改的处理,并且
在DRY原则的指导
下 ,我们将从
handleAdd
/
handleDelete
删除相应的代码:
constructor() { super( ...arguments ); this.state = { tasks: null }; this.handleAdd = async( newTaskText ) => { await this.repository.save( { id: counter(), text: newTaskText } ); }; this.handleDelete = async( idToDelete ) => { await this.repository.deleteById( idToDelete ); }; this.handleRepositoryChanged = async() => { this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; } componentDidMount() { this.repository = new IndexedDbRepository( 'id' ); this.handleRepositoryChanged();
(
github源代码 )
然后,从连接的RepositoryListener中添加对handleRepositoryChanged的调用:
render() { return <RepositoryListener onChange={this.handleRepoChanged} repository={this.repository}> <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={this.state.tasks} /> </RepositoryListener>; }
(
github源代码 )
步骤3.在单独的组件中进行数据的加载和更新
我们编写了一个组件,该组件可以从存储库接收数据,可以更改存储库中的数据。 但是,如果您想象一个包含100多个组件的大型项目,那么事实证明,显示存储库中数据的
每个组件都将被迫执行以下操作:
- 确保从单个点正确连接存储库
- 在
componentDidMount()
方法中提供初始数据加载 - 连接
RepositoryListener
组件,该组件提供处理程序调用以重新加载更改
是否有太多重复的动作? 似乎并非如此。 如果忘记了什么? 迷糊复制粘贴?
确保以某种方式确保一旦编写了从存储库中获取任务列表的规则,这很妙,并且神奇地执行了这些方法,为我们提供了数据,处理了存储库中的更改,并且对于堆,它还可以将其连接起来资料库。
this.doFindAllTasks = ( repo ) => repo.findAll(); <DataProvider doCalc={ this.doFindAllTasks }> {(data) => <span>... -, data...</span>} </DataProvider>
该组件实现中唯一不平凡的时刻是doFindAllTasks()是Promise。 为了方便我们的工作,我们将创建一个单独的组件,等待Promise执行并调用具有计算值的后代:
PromiseComponent代码 import { PureComponent } from 'react'; export default class PromiseComponent extends PureComponent { constructor() { super( ...arguments ); this.state = { error: null, value: null, }; this.prevPromise = null; } componentDidMount() { this.subscribe(); } componentDidUpdate( ) { this.subscribe(); } componentWillUnmount() { this.unsubscribe(); } subscribe() { const { cleanOnPromiseChange, promise } = this.props; if ( promise instanceof Promise && this.prevPromise !== promise ) { if ( cleanOnPromiseChange ) this.setState( { error: null, value: null } ); this.prevPromise = promise; promise.then( value => { if ( this.prevPromise === promise ) { this.setState( { error: null, value } ); } } ) .catch( error => { if ( this.prevPromise === promise ) { this.setState( { error, value: null } ); } } ); } } unsubscribe( ) { if ( this.prevPromise !== null ) { this.prevPromise = null; } } render() { const { children, fallback } = this.props; const { error, value } = this.state; if ( error !== null ) { throw error; } if ( value === undefined || value === null ) { return fallback || null; } return children( value ); } }
(
github源代码 )
该组件的逻辑和内部结构与RepositoryListener非常相似。 因为彼此都必须“签名”,“监听”事件并以某种方式处理它们。 另外请记住,您需要听的事件可能会发生变化。
此外,到目前为止,非常神奇的组件DataProvider看起来非常简单:
import repository from './RepositoryHolder'; export default class DataProvider extends PureComponent { constructor() { super( ...arguments ); this.handleRepoChanged = () => this.forceUpdate(); } render() { return <RepositoryListener onChange={this.handleRepoChanged} repository={repository}> <PromiseComponent promise={this.props.doCalc( repository )}> {data => this.props.children( data )} </PromiseComponent> </RepositoryListener>; } }
(
github源代码 )
实际上,我们采用了名为doCalc的存储库(以及现在正在导入的单独的singlenton RepositoryHolder),这将使我们能够将任务数据传输到this.props.children,从而绘制任务列表。 Singlenton看起来也很简单:
const repository = new IndexedDbRepository( 'id' ); export default repository;
现在,将主组件中的数据库调用替换为DataProvider调用:
import repository from './RepositoryHolder'; export default class Step3 extends PureComponent { constructor() { super( ...arguments ); this.doFindAllTasks = repository => repository.findAll(); } render() { return <DataProvider doCalc={this.doFindAllTasks} fallback={<><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span> ...</span></>}> { tasks => <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={tasks} /> } </DataProvider>; } }
(
github源代码 )
这可能会停止。 结果非常好:我们描述了接收数据的规则,并且有一个单独的组件监视此数据的实际接收以及更新。 从字面上看,有几件事:
- 对于每个数据请求,您需要在代码中的某个地方(而不是
render()
方法中render()
,描述访问存储库的功能,然后将此功能传递给DataProvider - 对DataProvider的调用非常棒,完全符合React的精神,但是从JSX的角度来看非常难看。 如果您有多个组件,则不同的DataProvider的两个或三个嵌套级别将使您非常困惑。
- 令人遗憾的是,数据的接收是在一个组件(DataProvider)中完成的,而它们的更改是在另一个组件(主组件)中完成的。 我想用相同的机制来描述它。
步骤4.连接()
熟悉react-redux的标题已经猜到了标题。 我将对其余内容进行以下提示:如果DataProvider服务组件根据规则填充了组件的属性,而不是使用参数调用children(),那将是很好的。 并且,如果存储库中发生了某些更改,则只需使用标准React机制更改属性即可。
为此,我们将使用高阶组件。 实际上,这并不复杂,它只是一个将组件类作为参数并提供另一个更复杂的组件的函数。 因此,我们编写的函数将是:
对该函数的调用将如下所示:
const mapRepoToProps = repository => ( { tasks: repository.findAll(), } ); const mapRepoToActions = repository => ( { doAdd: ( newTaskText ) => repository.save( { id: counter(), text: newTaskText } ), doDelete: ( idToDelete ) => repository.deleteById( idToDelete ), } ); const Step4Connected = connect( mapRepoToProps, mapRepoToActions )( Step4 );
第一行
Step4
组件的属性名称和将从存储库加载的值之间
Step4
映射。 接下来是操作映射:如果从
Step4
组件中调用
this.props.doAdd(...)
或
this.props.doDelete(...)
,将会发生什么。 最后一行将所有内容组合在一起,并调用connect函数。 结果是一个新的组件(这就是为什么将此技术称为“高阶组件”的原因)。 而且我们将不再从文件中导出原始Step4组件,而是围绕它的包装器:
class Step4 extends PureComponent { } const Step4Connected = connect( )( Step4 ); export default Step4Connected;
现在,使用TaskList的组件看起来像一个简单的包装器:
class Step4 extends PureComponent { render() { return this.props.tasks === undefined || this.props.tasks === null ? <><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span> ...</span></> : <TaskList onAdd={this.props.doAdd} onDelete={this.props.doDelete} tasks={this.props.tasks} />; } }
(
github源代码 )
就是这样。 没有构造函数,没有其他处理程序-一切都由connect()函数放置在组件的props中。
剩下的要看connect()函数的外观了。
连接()代码 import repository from './RepositoryHolder'; class Connected extends PureComponent { constructor() { super( ...arguments ); this.handleRepoChanged = () => this.forceUpdate(); } render() { const { childClass, childProps, mapRepoToProps, mapRepoToActions } = this.props; const promises = mapRepoToProps( repository, childProps ); const actions = mapRepoToActions( repository, childProps ); return <RepositoryListener onChange={this.handleRepoChanged} repository={repository}> <PromisesComponent promises={promises}> { values => React.createElement( childClass, { ...childProps, ...values, ...actions, } )} </PromisesComponent> </RepositoryListener>; } } export default function connect( mapRepoToProps, mapRepoToActions ) { return childClass => props => <Connected childClass={childClass} childProps={props} mapRepoToActions={mapRepoToActions} mapRepoToProps={mapRepoToProps} />; }
(
github源代码 )
该代码似乎也不是很复杂...尽管如果您开始潜水,则会出现问题。 您需要从头开始阅读。 在那里定义了
connect()
函数。 它需要两个参数,然后返回一个函数,该函数又返回...一个函数? 不完全是 最后的
props => <Connected...
构造不仅返回一个函数,而且返回
一个React函数组件 。
因此,当我们将ConnectedStep4嵌入虚拟树中时,它将包括:- 父->匿名功能组件->连接-> RepositoryListener-> PromisesComponent-> Step4
多达4个中间类,但每个都执行其功能。未命名的组件采用传递给函数的参数connect()
,嵌套组件的类,在调用时传递给组件本身的属性(props),并将其已经传递给组件Connect
。该组件Connect
负责从传递的参数Promise
(具有行键和Promise值的字典对象)中获取集合。PromisesComponent
提供值的计算,并将其传递回Connect组件,Connect组件与原始传输的属性(props),计算的属性(值)和action属性(actions)一起,将它们传递给组件Step4
(通过call React.createElement(...)
)。好吧,组件RepositoryListener
更新组件,如果存储库中发生了某些更改,则强制重新计算有偿。结果,如果任何组件都想使用IndexDb中的存储库,那么他只要连接一个功能connect()
,定义属性与从存储库中获取属性的功能之间的映射,就可以使用它们而不会造成任何其他麻烦。最后,我们以库的形式安排所有这些内容,以便其他开发人员可以在其项目中使用它。此步骤的结果是:而不是得出结论:什么都遗忘了
上面的代码已经足够实用,可以在工业解决方案中使用。但是,请不要忘记这些限制:- 并非总是更改属性应该带来新的希望。在这里,您需要使用备忘录功能,但要考虑数据库更改标志。这是来自IndexedDbRepository的戳记派上用场的地方(对于某些这段代码来说似乎有些多余)。
- 即使在一个地方,通过导入连接存储库也是错误的。有必要考虑使用上下文。
- , IndexedDB .
- : . . IndexDB «» , — .
- , IndexDB Redux Storage. IndexDB , .
Online-