Создание автономного приложения с помощью React и CouchDB

Около трех лет назад я опубликовал статью на (теперь уже несуществующем) блоге Manifold о создании автономного приложения с использованием React и CouchDB. Помимо того, что статья больше недоступна, она также была очень устаревшей, поскольку была построена на очень старой версии React. Тем не менее, я считаю, что тема статьи актуальна и сегодня.

Многие приложения требуют от своих пользователей постоянного подключения к сети, чтобы избежать потери работы. Существуют различные стратегии, одни лучше других, чтобы убедиться, что пользователи могут продолжать работать, даже находясь в автономном режиме, путем синхронизации их работы после возвращения в сеть. За три года технология значительно улучшилась, и я по-прежнему считаю, что CouchDB — это инструмент, который стоит рассмотреть при создании приложения, ориентированного на работу в автономном режиме.

Присоединяйтесь ко мне, чтобы снова изучить CouchDB и ее возможности, пока мы создаем список для чтения, который определенно не является замаскированным списком дел.

Что такое CouchDB?

CouchDB — это база данных NoSQL, созданная для синхронизации. Движок CouchDB может поддерживать несколько реплик (представьте себе сервер базы данных) для одной и той же базы данных и синхронизировать их в режиме реального времени с помощью процесса, не отличающегося от git. Это позволяет нам распространять наши приложения по всему миру без того, чтобы база данных была ограничивающим фактором. Эти реплики также не ограничиваются серверами. Совместимые с CouchDB базы данных, такие как PouchDB, позволяют синхронизировать базы данных в браузере или на мобильных устройствах. Это позволяет создавать действительно автономные приложения, пользователи работают с собственной локальной базой данных, которая синхронизируется с сервером, когда это возможно и необходимо. Синхронизация зависит от выбранного протокола репликации и может быть запущена вручную. В PouchDB это происходит, когда любые изменения вызывают синхронизацию. Конечно, для синхронизации необходимо, чтобы сервер работал! Репликация будет приостановлена, если реплика находится в автономном режиме, что обеспечивает конечную согласованность, о которой мы поговорим ниже.

Когда вы создаете документ в CouchDB, он создает ревизию для легкого слияния и обнаружения конфликтов со своими копиями. Когда база данных синхронизируется, CouchDB сравнивает ревизии и историю изменений, пытается объединить документы и вызывает конфликт слияния, если это не удается.

{  
   "_id":"SpaghettiWithMeatballs",
   "_rev":"1–917fa2381192822767f010b95b45325b",
   "_revisions":{  
      "ids":[  
         "917fa2381192822767f010b95b45325b"
      ],
      "start":1
   },
   "description":"An Italian-American delicious dish",
   "ingredients":[  
      "spaghetti",
      "tomato sauce",
      "meatballs"
   ],
   "name":"Spaghetti with meatballs"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Все это обрабатывается через встроенный REST API и веб-интерфейс. Веб-интерфейс можно использовать для управления всеми базами данных и их документами, а также учетными записями пользователей, аутентификацией и даже вложениями документов. Если при синхронизации базы данных возникает конфликт слияния, этот интерфейс дает вам возможность обрабатывать конфликты слияния вручную. В нем также есть движок JavaScript для работы с представлениями и проверки данных.

В 2019 году CouchDB использовалась для работы CouchApps. Короче говоря, вы могли построить весь свой бэкенд, используя CouchDB и его движок JavaScript. Я был большим поклонником CouchApps, но ограничения CouchDB — а также бэкендов, использующих только базу данных — делали CouchApps гораздо менее мощным, чем более традиционный сервер базы данных+приложений. По мере продвижения к версии 4 (на момент написания этой статьи), CouchDB стал ближе к альтернативе Firebase или Hasura, чем к альтернативе вашему бэкенду.

Итак, стоит ли мне переводить все на CouchDB?

Как и все в программной инженерии, это зависит от ситуации.

CouchDB творит чудеса для приложений, где согласованность данных не так важна, как согласованность в конечном итоге. CouchDB не может обещать, что все ваши экземпляры будут последовательно синхронизированы. Что он может обещать, так это то, что данные в конечном итоге будут согласованы, и что по крайней мере один экземпляр всегда будет доступен. Ее используют или использовали такие крупные компании, как IBM, United Airlines, NPM, BBC и ученые LHC в CERN (да, именно в CERN). Все места, где заботятся о доступности и отказоустойчивости.

CouchDB может работать против вас и во многих других случаях. Она не заботится о том, чтобы обеспечить согласованность данных между экземплярами вне синхронизации, поэтому разные пользователи могут видеть разные данные. Это также база данных NoSQL, со всеми вытекающими отсюда плюсами и минусами. Кроме того, сторонний хостинг несколько непоследователен; у вас есть Cloudant и Couchbase, но за их пределами вы предоставлены сами себе.

Перед выбором системы баз данных необходимо учесть множество моментов. Если вы чувствуете, что CouchDB идеально подходит для вас, то самое время пристегнуть ремень безопасности, потому что вас ждет потрясающая поездка.

Что насчет PouchDB?

PouchDB — это база данных JavaScript, используемая как в браузере, так и на сервере, в значительной степени вдохновленная CouchDB. Это мощная база данных уже благодаря отличному API, а возможность синхронизации с одной или несколькими базами данных делает ее просто незаменимой для приложений, работающих в автономном режиме. Позволив PouchDB синхронизироваться с CouchDB, мы можем сосредоточиться на записи данных непосредственно в PouchDB, а она в конечном итоге позаботится о синхронизации этих данных с CouchDB. Наши пользователи будут иметь доступ к своим данным независимо от того, работает ли база данных в режиме онлайн или нет.

Создание приложения, ориентированного на работу в автономном режиме

Теперь, когда мы знаем, что такое CouchDB, давайте построим offline-first приложение с помощью CouchDB, PouchDB и React. Когда я искал CouchDB + React для первоначальной статьи, я нашел много приложений для выполнения дел. Я подумал, что пошутил, что создаю приложение для чтения, при этом утверждая, что список книг для чтения совершенно отличается от списка задач для выполнения. Для последовательности давайте оставим эту шутку в силе. Кроме того, приложения для чтения полностью отличаются от приложений для выполнения дел.

Весь код для этого приложения доступен на GitHub здесь: https://github.com/SavoirBot/definitely-not-a-todo-list. Не стесняйтесь следовать за кодом.

Первое, что нам нужно, это JavaScript-проект для нашего приложения. Мы будем использовать Snowpack в качестве нашего бандлера. Откройте терминал в директории проекта и введите npx create-snowpack-app react-couchdb --template @snowpack/app-template-minimal. Snowpack создаст скелет для нашего React-приложения и установит все зависимости. Как только он закончит свою работу, введите cd react-couchdb, чтобы попасть в только что созданный каталог проекта. create-snowpack-app очень похож на create-react-app в том, как он устанавливает ваш проект, но он гораздо менее навязчив (вам даже не нужно использовать eject в любой момент).

Чтобы завершить настройку проекта, установите все зависимости с помощью следующей команды:

npm install react react-dom pouchdb-browser
Войти в полноэкранный режим Выйти из полноэкранного режима

С нашим проектом в руках нам теперь нужна база данных CouchDB. Для простоты давайте запустим ее в контейнере docker с помощью docker-compose, что позволит нам легко запускать и останавливать ее. Создайте файл docker-compose.yaml и скопируйте в него это содержимое:

# docker-compose.yaml
version: '3'
services:
  couchserver:
    image: couchdb
    ports:
      - "5984:5984"
    environment:
      - COUCHDB_USER=admin
      - COUCHDB_PASSWORD=secret
    volumes:
      - ./dbdata:/opt/couchdb/data
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот файл определяет сервер CouchDB с несколькими переменными для установки имени пользователя и пароля администратора. Мы также определяем том, который будет синхронизировать данные CouchDB из контейнера в локальную папку dbdata. Это поможет сохранить наши данные, когда мы закроем контейнер.

Введите docker compose up -d в терминале, открытом в той же папке, где вы начали этот проект. После этого контейнер запустится и сделает вашу базу данных CouchDB доступной по адресу http://localhost:5984. Доступ к этому URL в браузере или с помощью curl должен вернуть приветственное сообщение в формате JSON. Чтобы наше локальное приложение работало, мы должны настроить CORS на нашей базе данных. Зайдите на приборную панель CouchDB под http://localhost:5984/_utils в вашем браузере. Используйте настроенное имя пользователя и пароль администратора, затем перейдите на вкладку Settings, затем на вкладку CORS, затем нажмите на Enable CORS и выберите All domains ( * ).

Настройка PouchDB для нашего приложения

В этом проекте мы будем использовать несколько крючков для настройки PouchDB и получения элементов для чтения. Давайте начнем с настройки самого PouchDB. Создайте каталог hooks, а затем создайте файл usePouchDB.js в этом каталоге с таким кодом.

// hooks/usePouchDB.js
import { useMemo } from 'react';
import PouchDB from 'pouchdb-browser';

const remoteUrl = 'http://localhost:5984/reading_lists';

export const usePouchDB = () => {
    // Create the local and remote databases for syncing
    const [localDb, remoteDb] = useMemo(
        () => [new PouchDB('reading_lists'), new PouchDB(remoteUrl)],
        []
    );

    return {
        db: localDb,
    };
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот хук использует хук useMemo из React для создания двух новых экземпляров PouchDB. Первый экземпляр — это локальная база данных, установленная в браузере, под названием reading_lists. Второй экземпляр — это удаленный экземпляр, который вместо этого подключается к нашему контейнеру CouchDB. Поскольку в нашем приложении нам нужен только локальный экземпляр, мы возвращаем объект только с этой локальной базой данных.

Теперь давайте настроим синхронизацию для этих двух баз данных. Вернитесь к usePouchDB.js и обновите код с учетом этих изменений.

// hooks/usePouchDB.js
import { useMemo, useEffect } from 'react';
import PouchDB from 'pouchdb-browser';

const remoteUrl = 'http://localhost:5984/reading_lists';

export const usePouchDB = () => {
    // Previous code omitted for brevity
    const [localDb, remoteDb] = useMemo(...);

    // Start the sync in a separate effect, cancel on unmount
    useEffect(() => {
        const canceller = localDb
            .sync(remoteDb, {
                live: true,
                retry: true,
            });

        return () => {
            canceller.cancel();
        };
    }, [localDb, remoteDb]);

    return {
        db: localDb,
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

Мы добавили хук useEffect для запуска двусторонней синхронизации между локальной и удаленной базами данных. Синхронизация использует опции live и retry, что заставляет PouchDB оставаться на связи с удаленной базой данных, а не синхронизироваться только один раз, и повторять попытку, если синхронизация не удалась. Этот эффект возвращает функцию, которая отменит синхронизацию, если компонент размонтируется во время синхронизации.

Было бы неплохо показывать небольшое сообщение нашим пользователям всякий раз, когда база данных CouchDB отключена или недоступна. Синхронизация PouchDB предоставляет события, которые мы можем прослушивать, такие как paused и active, которые, как упоминается в документации, могут срабатывать, когда база данных недоступна. Однако эти хуки связаны только с актом синхронизации данных. Если ничего не нужно синхронизировать, синхронизация вызовет событие paused независимо от состояния удаленной базы данных, а затем проигнорирует состояние удаленной базы данных. Вместо этого необходимо регулярно использовать метод info на базе данных, чтобы проверить состояние удаленной базы данных.

// hooks/usePouchDB.js
import { useMemo, useEffect, useState } from 'react';
import PouchDB from 'pouchdb-browser';

const remoteUrl = 'http://localhost:5984/reading_lists';

export const usePouchDB = () => {
    const [alive, setAlive] = useState(false);

    // Previous code omitted for brevity
    const [localDb, remoteDb] = useMemo(...);
    useEffect(...);

    // Create an interval after checking the status of the database for the
    // first time
    useEffect(() => {
        const cancelInterval = setInterval(() => {
            remoteDb
                .info()
                .then(() => {
                    setAlive(true);
                })
                .catch(() => {
                    setAlive(false);
                });
            }, 1000)
        });

        return () => {
            clearTimeout(cancelInterval);
        };
    }, [remoteDb]);

    return {
        db: localDb,
        ready,
        alive,
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

Мы добавили хук state для переменной alive, которая будет отслеживать, доступна ли удаленная база данных. Затем мы добавили еще один хук useEffect для установки интервала, который будет вызывать метод info каждую секунду, чтобы проверить, жива ли еще база данных. Как и в предыдущем useEffect, мы должны убедиться, что отменили интервал, когда компонент размонтируется, чтобы избежать утечки памяти.

Выборка всех документов

С нашим хуком PouchDB мы готовы к созданию следующего хука для выборки всех прочитанных документов из локальной базы данных. Давайте создадим еще один файл в каталоге hooks под названием useReadingList.js для логики выборки документов.

// hooks/useReadingList.js
import { useEffect, useState } from 'react';

export const useReadingList = (db, isReady) => {
    const [loading, setLoading] = useState(true);
    const [documents, setDocuments] = useState([]);

    // Function to fetch the data from pouchDB with loading state
    const fetchData = () => {
        setLoading(true);

        db.allDocs({
            include_docs: true,
        }).then(result => {
            setLoading(false);
            setDocuments(result.rows.map(row => row.doc));
        });
    };

    // Fetch the data on the first mount, then listen for changes (Also listens to sync changes)
    useEffect(() => {
        fetchData();

        const canceler = db
            .changes({
                since: 'now',
                live: true,
            })
            .on('change', () => {
                fetchData();
            });

        return () => {
            canceler.cancel();
        };
    }, [db]);

    return [loading, documents];
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот хук делает несколько вещей. Во-первых, мы создаем некоторые переменные состояния для хранения состояния загрузки и наших извлеченных документов. Затем мы определяем функцию для получения документов из базы данных с помощью allDocs, а затем добавляем документы в наши переменные состояния после загрузки. Мы используем опцию include_docs для функции allDocs, чтобы убедиться, что мы получаем весь документ. По умолчанию allDocs возвращает только ID и ревизию. Функция include_docs гарантирует, что мы получим все данные.

Затем мы создаем хук useEffect, который запускает процесс выборки данных, а затем слушаем изменения из базы данных. Когда мы изменим что-то в приложении или синхронизация изменит данные в локальной базе данных, сработает событие change, и мы снова получим данные. Опция live гарантирует, что это будет происходить в течение всего жизненного цикла приложения или до тех пор, пока слушатель не будет отменен при размонтировании компонента.

Собираем все вместе

Когда наши хуки готовы, нам нужно собрать React-приложение. Сначала откройте файл index.html, созданный snowpack, и замените <h1>Welcome to Snowpack!</h1> на <div id="root"></div>. Далее переименуйте файл index.js, созданный snowpack, в index.jsx и замените содержимое этого файла этим кодом:

// index.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';

const App = () => null;

createRoot(document.getElementById('root')).render(<App />);
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь вы можете запустить приложение snowpack с помощью npm run start, это должно запустить приложение, дать вам URL для открытия в браузере и показать вам пустой экран (нормально, так как мы возвращаем null из нашего приложения!) Давайте начнем создавать наш компонент App.

// index.jsx
// rest of the code remove for brevity
import { usePouchDB } from '../hooks/usePouchDB';
import { useReadingList } from '../hooks/useReadingList';

const App = () => {
    const { db, ready, alive } = usePouchDB();
    const [loading, documents] = useReadingList(db);

    return (
        <div>
            <h1>Definitely not a todo list</h1>
            {!alive && (
                <div>
                    <h2>Warning</h2>
                    The connection with the database has been lost, you can
                    still work on your documents, we will sync everything once
                    the connection is re-established.
                </div>
            )}
            {loading && <div>loading...</div>}
            {documents.length ? (
                <ul>
                    {documents.map(doc => (
                        <li key={doc._id}>
                            {doc.name}
                        </li>
                    ))}
                </ul>
            ) : (
                <div>No books to read added, yet</div>
            )}
        </div>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Приложение загружает наш хук PouchDB, затем наш хук загружает все наши элементы для чтения. Затем мы вернем базовую HTML-структуру, которая может показать предупреждающее сообщение, если произойдет отключение базы данных, сообщение о загрузке, когда мы получаем документы, и, наконец, элементы для чтения из базы данных. Свойство _id — это внутренний уникальный идентификатор в CouchDB/PouchDB, который является идеальным key для элементов нашего списка.

Показывать все элементы довольно приятно, но чтобы иметь возможность показывать любые элементы, нам нужен способ добавления новых элементов для чтения в нашу базу данных. Давайте вернемся к нашему файлу index.jsx и добавим в него этот код.

// index.jsx
import React, { useState } from 'react';
// rest of the code remove for brevity

import { usePouchDB } from '../hooks/usePouchDB';
import { useReadingList } from '../hooks/useReadingList';

// Component to add new books with a controlled input
const AddReadingElement = ({ handleAddElement }) => {
    const [currentName, setCurrentName] = useState('');

    const addBook = () => {
        if (currentName) {
            // If the currentName has data, clear it and add a new element.
            handleAddElement(currentName);
            setCurrentName('');
        }
    };

    return (
        <div>
            <h2>Add a new book to read</h2>
            <label htmlFor="new_book">Book name</label>
            <input
                type="text"
                id="new_book"
                value={currentName}
                onChange={event => setCurrentName(event.target.value)}
            />
            <button onClick={addBook}>Add</button>
        </div>
    );
};

const App = () => {
    const { db, ready, alive } = usePouchDB();
    const [loading, documents] = useReadingList(db);

    const handleAddElement = name => {
        // post sends a document to the database and generates the unique ID for us
        db.post({
            name,
            read: false,
        });
    };

    return (
        <div>
            {/* rest of the code remove for brevity */}
            <AddReadingElement handleAddElement={handleAddElement} />
        </div>
    );
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы добавили в этот файл новый компонент для добавления новых книг для чтения. Отдельный компонент помогает сделать структуру немного понятнее, не стесняйтесь извлечь его в другой файл. Этот компонент использует хук состояния для управления вводом, а затем вызывает метод post на локальной базе данных, когда нажимается кнопка Add.

Вернитесь в браузер и попробуйте добавить несколько книг для чтения, они должны появиться в списке при нажатии на кнопку.

Наконец, было бы здорово иметь возможность установить книги как прочитанные или удалить некоторые книги, которые мы больше не хотим видеть в нашем списке. Снова откройте файл index.jsx и добавьте туда этот код.

// index.jsx
// rest of the code remove for brevity
const App = () => {
    const { db, ready, alive } = usePouchDB();
    const [loading, documents] = useReadingList(db);

    // rest of the code remove for brevity
    const handleAddElement = name => ...;

    // The remove method removes a document by _id and rev. The best way to send
    // both is to send the document to the remove method
    const handleRemoveElement = element => {
        db.remove(element);
    };

    // The remove method updates a document, replacing all fields from that document.
    // like _id and rev, it needs both to find the document.
    const handleToggleRead = element => {
        db.put({
            ...element,
            read: !element.read,
        });
    };

    return (
        <div>
            {/* rest of the code remove for brevity */}
            {documents.length ? (
                <ul>
                    {documents.map(doc => (
                        <li key={doc._id}>
                            <input
                                type="checkbox"
                                checked={doc.read}
                                onChange={() => handleToggleRead(doc)}
                                id={doc._id}
                            />
                            <label htmlFor={doc._id}>{doc.name}</label>
                            <button
                                onClick={() => handleRemoveElement(doc)}
                            >
                                Delete
                            </button>
                        </li>
                    ))}
                </ul>
            ) : (
                <div>No books to read added, yet</div>
            )}
            {/* rest of the code remove for brevity */}
        </div>
    );
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы добавили две функции в наш App. Метод update использует метод put для обновления документа. Метод post на локальной базе данных создает документ без уникального ID и генерирует его после вставки элемента. Метод put может как обновлять, так и вставлять, но он требует ID и ревизии для выбора документа для put. В нашем случае мы используем ее, используя существующий документ, переключив свойство read. Вторая функция использует метод remove с документом, что гарантирует, что PouchDB сможет найти документ и удалить его.

Наконец, мы заменили список документов, чтобы добавить флажок и кнопку. При переключении флажка сработает метод update и переключит свойство read. При нажатии на кнопку сработает метод remove для удаления элемента.

Вернитесь в браузер и попробуйте переключить флажки или удалить элементы. Все должно работать без проблем.

Тестирование возможностей автономной работы

Теперь пришло время протестировать приложение, пока база данных находится в автономном режиме. Откройте новый терминал, в котором находится ваш проект (чтобы не убить команду npm run start) и введите docker compose stop couchserver. Вы должны немедленно увидеть предупреждающее сообщение в приложении React. Тем не менее, вы все еще должны иметь возможность взаимодействовать с приложением и добавлять/изменять/удалять документы. Введите docker compose start couchserver для перезапуска базы данных и перезагрузите страницу, когда предупреждение исчезнет. Все изменения, которые вы внесли, должны остаться в приложении, и вы сможете увидеть изменения на приборной панели CouchDB.

Заключение

Теперь у нас есть функциональное приложение, ориентированное на работу в автономном режиме. Независимо от состояния базы данных, наши пользователи могут продолжать добавлять книги для чтения и устанавливать состояние прочтения. Сообщение — это дополнительный бонус, который помогает нашим пользователям понять, что не стоит очищать кэш, пока мы не синхронизируем приложение должным образом.

Конечно, работа с базой данных непосредственно из клиента может быть не лучшим решением для большинства приложений. Особенно если мы синхронизируем эти данные без какой-либо проверки из базы данных. Пожалуйста, дайте мне знать в комментариях ниже, если вы хотели бы получить второй пост в этой серии, посвященный реализации бэкенда для валидации и синхронизации данных в приложении, работающем в автономном режиме.


Я буду рад услышать ваши мысли — пожалуйста, прокомментируйте или поделитесь этим постом.

Мы развиваем Savoir, так что следите за возможностями и обновлениями на нашем сайте savoir.dev. Если вы хотите подписаться на обновления или бета-тестирование, напишите мне на info@savoir.dev!

Savoir — это французское слово, означающее «знание», произносится как sɑvwɑɹ.

Оцените статью
devanswers.ru
Добавить комментарий