رد + IndexDb + التحديث التلقائي = AsyncRedux تقريبًا

في هذه المقالة ، سأخبرك خطوة بخطوة عن كيفية إعداد IndexDB (قاعدة بيانات مضمّنة في أي مستعرض حديث) لاستخدامها في المشاريع المكتوبة في ReactJS. نتيجة لذلك ، يمكنك استخدام البيانات من IndexDB بسهولة كما لو كانت موجودة في Redux Store في التطبيق الخاص بك.

IndexDB هو DBMS موجه للوثائق ، وهو أداة ملائمة للتخزين المؤقت لكمية صغيرة نسبيا (وحدات وعشرات ميغابايت) من البيانات المهيكلة على جانب المتصفح. تتضمن المهمة القياسية التي يجب عليّ استخدامها لـ IndexDB تخزين البيانات المخزنة مؤقتًا لأدلة الأعمال من جانب العميل (أسماء البلدان والمدن والعملات حسب الكود وما إلى ذلك). بعد نسخها إلى جانب العميل ، يمكنك فقط تنزيل التحديثات من هذه الدلائل من الخادم (أو كلها - صغيرة) ولا تقوم بذلك في كل مرة تفتح فيها نافذة المتصفح.

هناك طرق غير قياسية ومثيرة للجدل للغاية ، ولكن تعمل لاستخدام IndexDB:

  • تخزين البيانات المخزنة مؤقتًا حول جميع كائنات الأعمال حتى يتسنى على جانب المتصفح استخدام إمكانات الفرز والتصفية الشاملة
  • تخزين حالة التطبيق في IndexDB بدلاً من Redux Store

هناك ثلاثة اختلافات رئيسية بين IndexDB و Redux Store مهمة بالنسبة لنا:

  1. IndexDB هي وحدة تخزين خارجية لا يتم مسحها عند مغادرة الصفحة. بالإضافة إلى ذلك ، هو نفسه بالنسبة لعدة علامات تبويب مفتوحة (مما يؤدي أحيانًا إلى سلوك غير متوقع إلى حد ما)
  2. IndexDB هو DBMS غير متزامن تماما. جميع العمليات - الافتتاح ، والقراءة ، والكتابة ، والبحث - غير متزامن.
  3. لا يمكن تخزين IndexDB (بطريقة تافهة) في JSON واستخدام تقنيات الخداع الذهني من Redux لإنشاء لقطات ، وسهولة تصحيح الأخطاء ، ورحلة إلى الماضي.

الخطوة 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>; } } 
( كود جيثب المصدر )

حتى الآن ، جميع العمليات مع المهام متزامنة. إذا استغرقت إضافة مهمة 3 ثوانٍ ، فسيتم تجميد المتصفح لمدة 3 ثوانٍ فقط. بالطبع ، بينما نحتفظ بكل شيء في ذاكرتنا ، لا يمكننا التفكير في الأمر. عندما نقوم بتضمين المعالجة باستخدام خادم أو مع قاعدة بيانات محلية ، فسيتعين علينا الاهتمام بالمعالجة الجميلة للتزامن. على سبيل المثال ، حظر العمل باستخدام جدول (أو عناصر فردية) أثناء إضافة عناصر أو إزالتها.

من أجل عدم تكرار وصف واجهة المستخدم في المستقبل ، سنضعها في مكون 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} /> </>; } } 
( كود جيثب المصدر )

تطبيق مكون يعرض قائمة من المهام ويحتوي على نموذج لإضافة عنصر جديد
 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 { //   ,     await this.props.onAdd( this.state.newTaskText ); this.setState( { newTaskText: '' } ); } finally { this.setState( { newTaskAdding: false } ); } }; this.handleDeleteF = idToDelete => async() => await this.props.onDelete( 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.props.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 disabled={this.state.newTaskAdding} 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>; } } 
( كود جيثب المصدر )

بالفعل في نموذج التعليمة البرمجية ، يمكنك رؤية الكلمات الرئيسية متزامنة / تنتظر. يمكن للبنيات Async / انتظار تقليل حجم التعليمات البرمجية التي تعمل مع Promises بشكل ملحوظ. تتيح لك الكلمة الرئيسية التي تنتظر الانتظار انتظار استجابة من وظيفة ترجع الوعد ، كما لو كانت وظيفة عادية (بدلاً من انتظار نتيجة في ذلك الحين ()). بطبيعة الحال ، لا تتحول الوظيفة غير المتزامنة بطريقة سحرية إلى وظيفة متزامنة ، على سبيل المثال ، سيتم مقاطعة سلسلة التنفيذ عند الانتظار. ولكن بعد ذلك ، يصبح الرمز أكثر إيجازًا ومفهومًا ، ويمكن استخدامه في الحلقات وفي عمليات الإنشاء / التجريب / في النهاية.

على سبيل المثال ، لا يستدعي TaskList this.props.onAdd معالج this.props.onAdd فقط ، ولكنه يقوم باستخدام الكلمة المفتاحية " await . في هذه الحالة ، إذا كانت المعالج هو وظيفة عادية لا تُرجع شيئًا ، أو تُرجع أي قيمة أخرى غير Promise ، TaskList مكون TaskList ببساطة أسلوب handleAdd بالطريقة المعتادة. ولكن إذا قامت المعالج بإرجاع Promise (بما في ذلك إذا تم إعلان المعالج كدالة غير TaskList ) ، TaskList حتى ينتهي المعالج من التنفيذ ، وعندها فقط سوف يعيد تعيين قيم newTaskText و newTaskText .

الخطوة 1: إضافة IndexDB إلى React Component


لتبسيط عملنا ، سنقوم أولاً بكتابة مكون بسيط يقوم بتنفيذ طرق الوعد من أجل:

  • فتح قاعدة بيانات مع معالجة الأخطاء التافهة
  • البحث عن العناصر الموجودة في قاعدة البيانات
  • إضافة عناصر إلى قاعدة البيانات

الأول هو الأكثر "غير تافهة" - ما يصل إلى 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 - مجمع يدعو IndexDb في الوعد
 //    ObjectStore,   IDBRequest //     Promise function wrap( methodName ) { return function() { const [ objectStore, ...etc ] = arguments; return new Promise( ( resolve, reject ) => { const request = objectStore[ methodName ]( ...etc ); request.onsuccess = () => resolve( request.result ); request.onerror = reject; } ); }; } const deletePromise = wrap( 'delete' ); const getAllPromise = wrap( 'getAll' ); const getPromise = wrap( 'get' ); const putPromise = wrap( 'put' ); } 

وضعه معًا في صف واحد مفهرسإيداع

IndexedDbRepository - المجمع حول IDBDatabase
 const DB_NAME = 'objectStore'; const OBJECT_STORE_NAME = 'objectStore'; /* ... */ export default class IndexedDbRepository { /* ... */ constructor( keyPath ) { this.error = null; this.keyPath = keyPath; //     async //      this.openDatabasePromise = this._openDatabase(); } async _openDatabase( keyPath ) { try { this.dbConnection = await openDatabasePromise( keyPath ); } catch ( error ) { this.error = error; throw error; } } async _tx( txMode, callback ) { await this.openDatabasePromise; // await db connection const transaction = this.dbConnection.transaction( [ OBJECT_STORE_NAME ], txMode ); const objectStore = transaction.objectStore( OBJECT_STORE_NAME ); return await callback( objectStore ); } async findAll() { return this._tx( 'readonly', objectStore => getAllPromise( objectStore ) ); } async findById( key ) { return this._tx( 'readonly', objectStore => getPromise( objectStore, key ) ); } async deleteById( key ) { return this._tx( 'readwrite', objectStore => deletePromise( objectStore, key ) ); } async save( item ) { return this._tx( 'readwrite', objectStore => putPromise( objectStore, item ) ); } } 
( كود جيثب المصدر )

يمكنك الآن الوصول إلى IndexDB من الكود:

  const db = new IndexedDbRepository( 'id' ); // ,     await db.save( { id: 42, text: 'Task text' } ); const item = await db.findById( 42 ); const items = await db.findAll(); 

قم بتوصيل "المستودع" بمكوننا. وفقًا لقواعد الرد ، يجب أن تكون الدعوة إلى الخادم في طريقة componentDidMount ():

 import IndexedDbRepository from '../common/IndexedDbRepository'; /*...*/ componentDidMount() { this.repository = new IndexedDbRepository( 'id' ); //     this.repository.findAll().then( tasks => this.setState( { tasks } ) ); } 

من الناحية النظرية ، يمكن تعريف الدالة componentDidMount() على أنها غير متزامنة ، ثم يمكن استخدام بنيات متزامن / انتظار بدلاً من ذلك (). ولكن لا يزال componentDidMount() ليس وظيفة "لنا" ، ولكن يسمى React. من يدري كيف ستتصرف مكتبة رد الفعل 17.x استجابةً لمحاولة إرجاع Promise بدلاً من undefined ؟

الآن في المنشئ ، بدلاً من ملء صفيف فارغ (أو صفيف ببيانات الاختبار) ، سنملأها خالية. وفي التقديم ، ستقوم بمعالجة هذه القيمة الفارغة كضرورة لانتظار معالجة البيانات. يمكن لأولئك الذين يرغبون ، من حيث المبدأ ، وضع هذا في أعلام منفصلة ، ولكن لماذا تنتج كيانات؟

  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></>; /* ... */ } 

يبقى تنفيذ handleDelete / 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 () ستذهب واحدة تلو الأخرى. لكن الكلمة الرئيسية التي تنتظر الانتظار في الأسطر الأخيرة من المعالجات ستتسبب في حدوث مكالمة setState () الثانية فقط بعد حل Promise () التي تم الحصول عليها من طريقة findAll ().

الخطوة 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 ); } } 

( كود جيثب المصدر )

سنقوم بتمرير ختم إلى المعالجات ، والذي سيتغير مع كل مكالمة إلى onChange (). ونقوم بتعديل طريقة _tx بحيث onChange() استدعاء onChange() لكل مكالمة في معاملة ذات وضع readwrite :

  async _tx( txMode, callback ) { await this.openDatabasePromise; // await db connection try { const transaction = this.dbConnection.transaction( [ OBJECT_STORE_NAME ], txMode ); const objectStore = transaction.objectStore( OBJECT_STORE_NAME ); return await callback( objectStore ); } finally { if ( txMode === 'readwrite' ) this.onChange(); // notify listeners } } 

( كود جيثب المصدر )

إذا كنا لا نزال نستخدم then() / catch() في then() للعمل مع Promise ، فسنضطر إما إلى تكرار الدعوة إلى onChange() ، أو استخدام polyfills الخاصة لـ Promise () التي تدعم final() . لحسن الحظ ، يتيح لك المزامنة / الانتظار القيام بذلك ببساطة ودون رمز غير ضروري.

المكون 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; } } 
( كود جيثب المصدر )

الآن ، سنقوم بتضمين معالجة تغييرات المخزون في handleAdd handleDelete مبدأ DRY ، سنقوم بإزالة الكود المقابل من handleDelete / 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(); // initial load } 

( كود جيثب المصدر )

ونضيف دعوة للتعامل مع RepositoryChanged من RepositoryListener المتصل:

  render() { /* ... */ return <RepositoryListener onChange={this.handleRepoChanged} repository={this.repository}> <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={this.state.tasks} /> </RepositoryListener>; } 

( كود جيثب المصدر )

الخطوة 3. اخراج تحميل البيانات وتحديثها في مكون منفصل


لقد كتبنا مكونًا يمكنه تلقي البيانات من المستودع ، ويمكن تغيير البيانات في المستودع. ولكن إذا كنت تتخيل مشروعًا كبيرًا يحتوي على أكثر من 100 مكونًا ، فسيظهر أن كل مكون يعرض بيانات من المستودع سيضطر إلى:

  • تأكد من أن المستودع متصل بشكل صحيح من نقطة واحدة
  • توفير تحميل البيانات الأولي في طريقة componentDidMount()
  • قم بتوصيل مكون RepositoryListener ، والذي يوفر استدعاء معالج لإعادة تحميل التغييرات

هل هناك الكثير من الإجراءات المكررة؟ لا يبدو كذلك. وإذا نسي شيء ما؟ تضيع مع نسخة لصق؟

سيكون من الرائع أن نتأكد بطريقة أو بأخرى من أننا نكتب بمجرد قاعدة الحصول على قائمة المهام من المستودع ، وأن شيئًا سحريًا ينفذ هذه الأساليب ، يعطينا البيانات ، يعالج التغييرات في المستودع ، ومن أجل الكومة يمكن أن يربط هذا أيضًا مستودع.

 this.doFindAllTasks = ( repo ) => repo.findAll(); /*...*/ <DataProvider doCalc={ this.doFindAllTasks }> {(data) => <span>...   -,   data...</span>} </DataProvider> 

اللحظة غير التوفيقية الوحيدة في تنفيذ هذا المكون هي أن doFindAllTasks () هو وعد. لتسهيل عملنا ، سننشئ مكونًا منفصلًا ينتظر تنفيذ الوعد ويستدعي سليلًا بقيمة محسوبة:

رمز 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 ); } } 

( كود جيثب المصدر )

يشبه هذا المكون في المنطق والهيكل الداخلي الخاص به 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>; } } 

( كود جيثب المصدر )

في الواقع ، أخذنا المستودع (والمنفرد المنفصل RepositoryHolder ، الذي هو الآن قيد الاستيراد) ، ودعا doCalc ، وهذا سوف يسمح لنا بنقل بيانات المهمة إلى 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>; } } 

( كود جيثب المصدر )

هذا يمكن أن يتوقف. اتضح بشكل جيد: نحن نصف قاعدة تلقي البيانات ، ويقوم مكون منفصل بمراقبة الاستلام الفعلي لهذه البيانات ، وكذلك التحديث. كان هناك حرفيا بضعة أشياء صغيرة:

  • لكل طلب بيانات ، في مكان ما في الكود (ولكن ليس في طريقة التقديم render() تحتاج إلى وصف وظيفة الوصول إلى المستودع ، ثم تمرير هذه الوظيفة إلى DataProvider
  • إن الدعوة إلى DataProvider رائعة ورائعة بروح React ، لكنها قبيحة للغاية من وجهة نظر JSX. إذا كان لديك العديد من المكونات ، فإن مستويين أو ثلاثة مستويات من التعشيش بين DataProviders المختلفة سوف تربكك كثيرًا.
  • من المحزن أن يتم استلام البيانات في مكون واحد (DataProvider) ، ويتم تغييرها في مكون آخر (المكون الرئيسي). أود أن أصفها بنفس الآلية.

الخطوة 4. الاتصال ()


دراية رد الفعل من قبل عنوان العنوان خمنت بالفعل. سأقدم التلميح التالي إلى الباقي: سيكون من الجيد أن يقوم مكون خدمة DataProvider ، بدلاً من الاتصال بالأطفال () بالمعلمات ، بملء خصائص المكون الخاص بنا وفقًا للقواعد. وإذا تغير شيء ما في المستودع ، فسيؤدي ذلك ببساطة إلى تغيير الخصائص باستخدام آلية React القياسية.

لهذا سوف نستخدم مكون الطلب العالي. في الواقع ، ليس شيئًا معقدًا ، إنها مجرد وظيفة تأخذ فئة مكون كمعلمة وتعطي مكونًا آخر أكثر تعقيدًا. لذلك ، فإن وظيفتنا التي نكتبها ستكون:

  • تأخذ كوسيطة فئة المكون حيث لتمرير المعلمات
  • اقبل مجموعة من القواعد ، وكيفية الحصول على البيانات من المستودع ، وما هي الخصائص لوضعها
  • في استخدامه ، سيكون مشابهًا لوظيفة connect () من رد فعل رد الفعل .

ستبدو الدعوة إلى هذه الوظيفة كما يلي:

 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(...) من داخل مكون Step4 . والخط الأخير يجمع كل شيء معًا ويدعو وظيفة الاتصال. والنتيجة هي مكون جديد (وهذا هو السبب في أن هذه التقنية تسمى مكون النظام العالي). ولن نقوم بعد ذلك بتصدير مكون 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} />; } } 

( كود جيثب المصدر )

وهذا كل شيء. لا يوجد مصممون ، ولا معالجات إضافية - يتم وضع كل شيء بواسطة وظيفة connect () في الدعائم الخاصة بالمكون.

يبقى أن نرى كيف يجب أن تبدو وظيفة 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} />; } 
( كود جيثب المصدر )

يبدو أن الكود ليس معقدًا جدًا ... على الرغم من أنك إذا بدأت الغوص ، فستظهر أسئلة. تحتاج إلى القراءة من النهاية. هناك يتم تعريف الدالة connect() . يستغرق معلمتين ، ثم ترجع الدالة التي ترجع ... مرة أخرى وظيفة؟ ليس حقا لا تؤدي الإنشاءات الأخيرة من props => <Connected... إرجاع وظيفة فحسب ، بل إلى مكون React وظيفي . وبالتالي ، عندما ندمج ConnectedStep4 في شجرة افتراضية ، فسيتضمن:

  • الأصل -> مكون وظيفي مجهول -> الاتصال -> RepositoryListener -> PromisesComponent -> Step4

ما يصل إلى 4 فصول وسيطة ، ولكن يؤدي كل منها وظيفته. يأخذ مكون غير مسمى المعلمات التي تم تمريرها إلى الوظيفة connect()، فئة المكون المتداخل ، والخصائص التي يتم نقلها إلى المكون نفسه عند استدعاء (props) وتمريرها بالفعل إلى المكون Connect. المكون Connectمسؤول عن الحصول على مجموعة من المعلمات التي تم تمريرها Promise(كائن القاموس مع مفاتيح الصفوف وقيم الوعد). PromisesComponentيوفر حساب القيم ، ويعيدها إلى مكون Connect ، والذي ، إلى جانب الخصائص الأصلية المنقولة (الدعائم) ، والخصائص المحسوبة (القيم) ، وخصائص الإجراءات (الإجراءات) ، ينقلها إلى المكون Step4(عبر مكالمة React.createElement(...)). حسنا ، المكونRepositoryListenerيقوم بتحديث المكون ، مما يؤدي إلى إعادة حساب promis'y إذا حدث شيء ما في المستودع.

نتيجة لذلك ، إذا أراد أي من المكونات استخدام المستودع من IndexDb ، فسيكون ذلك كافياً له للاتصال بوظيفة واحدة connect()وتحديد التعيين بين الخصائص ووظائف الحصول على الخصائص من المستودع واستخدامها دون أي صداع إضافي.

الخطوة 5. @ vlsergey / react-indexdb-repo


أخيرًا ، نرتب كل ذلك في شكل مكتبة بحيث يمكن للمطورين الآخرين استخدامها في مشاريعهم. كانت نتيجة هذه الخطوة:



بدلا من الاستنتاج: ما تبقى في البحر


الكود أعلاه عملي بالفعل بما يكفي لاستخدامه في الحلول الصناعية. ولكن لا يزال ، لا تنسى القيود:

  • لا يؤدي تغيير الخصائص دائمًا إلى وعود جديدة. تحتاج هنا إلى استخدام وظيفة الحفظ ، ولكن ضع في الاعتبار علامة تغيير قاعدة البيانات. هذا هو المكان الذي يأتي فيه الطابع من IndexedDbRepository في متناول يدي (ربما بدت هذه القطعة من التعليمات البرمجية زائدة عن الحاجة لشخص).
  • ربط المستودع من خلال الاستيراد ، حتى لو كان في مكان واحد ، خطأ. من الضروري التطلع نحو استخدام السياقات.
  • , IndexedDB .
  • : . . IndexDB «» , — .
  • , IndexDB Redux Storage. IndexDB , .



Online-

Source: https://habr.com/ru/post/ar472246/


All Articles