CouchDB,开源 Cloud Firestore 的替代品?
注意:这篇文章最初发布在marmelab.com上。
在我们上一个客户项目中,我们使用了Google 的后端即服务Firebase作为后端。虽然我们对这个“全包式”套件的整体功能感到满意,但其专有性方面仍让我们感到失望。
这就是为什么我主动寻找 Firebase 的开源替代品,它可以满足我们所有的需求,而无需依赖第三方服务。
这一任务的第一步是找到用于网络的Cloud Firestore实时 NoSQL 数据库的替代品。
我们需要什么?
使用 Firestore 而非更传统的数据库并非易事。这通常是因为需要快速开发具有以下功能的应用程序:
- 离线首先,客户端写入与远程数据库同步的本地数据库
- 实时远程更改必须与我们的本地数据库同步
有一些解决方案可以满足这一需求,其中大多数基于NoSQL 数据库,例如MongoDB、Cassandra、RethinkDB、Gun或其他基于 MongoDB 的解决方案,例如Minimongo、turtleDB或tortoiseDB。
在我们的例子中,我们将尝试CouchDB(以及用于前端的PouchDB),因为从我们的角度来看,它是更强大和最知名的解决方案。
CouchDB 和 PouchDB
CouchDB 是一款开源/跨平台的面向文档的数据库软件。它基于面向并发的 Erlang语言开发,因此具有很高的可扩展性。它使用JSON 格式存储数据,并通过HTTP API进行访问。
CouchDB 诞生于 2005 年。自 2008 年起,CouchDB 成为Apache 软件基金会项目,这使其受益于大量支持和庞大的社区。
以下是 CouchDB 的主要功能:
- 多版本并发控制(让您轻松构建离线优先解决方案)
- 具有复制功能的分布式架构
- 文件存储
- HTTP/REST API
由于CouchDB 在服务器上运行,许多客户端库允许通过它提供的 HTTP 接口与其进行通信。
最著名的 Web CouchDB 客户端库是 PouchDB。PouchDB是一个开源的 JavaScript 数据库,旨在在浏览器中运行。这样,它允许离线本地存储数据,并在用户重新上线时将其与远程 CouchDB 服务器同步。
CouchDB 和 PouchDB 实践
介绍够了,让我们开始实践吧!在本节中,我将逐步描述如何使用 CouchDB 和 PouchDB 作为数据库系统来开发一个ReactJS应用程序。同时,我会尽可能地将 CouchDB 的实现与 Firestore 的实现进行比较。
此外,我还将向您介绍我在 Javascript 库方面的最新爱好:Final-Form、ElasticUI和Indicative。
在这个项目中,我将创建一个啤酒登记处,让用户可以跟踪他们的啤酒库存。
项目设置
为了使本教程尽可能简单,我将使用create-react-app创建一个 ReactJS 应用程序。
create-react-app reactive-beers && cd reactive-beers
npm install -S pouchdb
应用程序骨架如下所示:
julien@julien-P553UA:~/Projets/marmelab/reactive-beers$ tree -L 1
.
├── node_modules
├── package.json
├── package-lock.json
├── public
├── README.md
└── src
然后,由于我不想直接在我的计算机上安装 CouchDB,我将使用Docker。因此,第一步是配置docker-compose.yml文件及其相关内容Makefile,以改善开发人员体验。
// ./docker-compose.yml
version: "2.1"
services:
  couchdb:
    image: couchdb:2.3.0
    ports:
      - "5984:5984"
  node:
    image: node:10
    command: npm start
    working_dir: "/app"
    volumes:
      - ".:/app"
    ports:
      - "4242:3000"
    depends_on:
      - couchdb
# ./Makefile
USER_ID = $(shell id -u)
GROUP_ID = $(shell id -g)
export UID = $(USER_ID)
export GID = $(GROUP_ID)
DOCKER_COMPOSE_DEV = docker-compose -p reactive-beers
help: ## Display available commands
    @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
install: ## Install docker stack
    $(DOCKER_COMPOSE_DEV) run --rm node bash -ci 'npm install'
start: ## Start all the stack
    $(DOCKER_COMPOSE_DEV) up -d
stop: ## Stop all the containers
    $(DOCKER_COMPOSE_DEV) down
log: ## Show logs
    $(DOCKER_COMPOSE_DEV) logs -f node
因此,我们现在准备开始使用我们的完整堆栈make install start。
julien@julien-P553UA:~/Projets/marmelab/reactive-beers$ docker ps
CONTAINER ID        IMAGE            COMMAND                  CREATED       STATUS       PORTS                                        NAMES
6884f92c5341        node:10          "npm start"              3 hours ago   Up 3 hours   0.0.0.0:4242->3000/tcp                       reactive-beers_node_1
21897f166ce4        couchdb:2.3.0    "tini -- /docker-ent…"   3 hours ago   Up 3 hours   4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp   reactive-beers_couchdb_1
一切已启动。您可能已经注意到,5984我们的文件中暴露了端口docker-compose.yml,它是 CouchDB API。然后,如果您localhost:5984在浏览器中打开它,您将看到类似以下内容。
{
    "couchdb": "Welcome",
    "version": "2.3.0",
    "git_sha": "07ea0c7",
    "uuid": "49f4e7520f0e110687dcbc8fbbb5409c",
    "features": ["pluggable-storage-engines", "scheduler"],
    "vendor": {
        "name": "The Apache Software Foundation"
    }
}
访问文档存储
好的,我们的服务器已启动并运行。但是,是否有像 Firestore 一样的界面来可视化/监控CouchDB 呢?答案是肯定的!CouchDB 已经包含一个名为 的管理界面Fauxton。我们可以在 上浏览它http://localhost:5984/_utils/。
该Fauxton界面允许访问数据库、设置节点和集群、配置复制、设置权限等。虽然实用,但最好还是使用专用脚本自动执行这些管理任务。
React 开始发挥作用
现在,我们可以开始开发我们的第一个 PouchDB 驱动的界面了。下面是我们的App.js入口点和Home.js启动屏幕。
// ./src/App.js
import React from 'react';
import { Home } from './screens/Home';
const App = () => <Home />;
export default App;
该App.js文件目前没什么用。将来我们需要添加更多路线和屏幕时,它肯定会派上用场。
// ./src/screens/Home.js
import React, { useState, useEffect } from 'react';
import { addBeer, getBeers, onBeersChange } from '../api/beers';
export const Home = () => {
  const [beers, setBeers] = useState([]);
  const refreshBeers = () => getBeers().then(setBeers);
  useEffect(() => {
    // We fetch beers the first time (at mounting)
    refreshBeers();
    // Each change in our beers database will call refreshBeers
    const observer = onBeersChange(refreshBeers);
    return () => {
        // Don't forget to unsubscribe our listener at unmounting
        observer.cancel();
    };
  }, []);
  return (
    <div>
      <button onClick={() => addBeer({ title: 'Beer X' })}>Add a beer</button>
      <ul>
        {/* beer._id is an unique id generated by CouchDB */}
        {beers.map(beer => <li key={beer._id}>{beer.title}</li>)}
      </ul>
    </div>
  );
};
CouchDB 需要比 Firestore 更多的请求
如您所见,在这个例子中,我们结合使用监听器(onBeersChange)和查询(getBeers)来获取初始啤酒列表,并在数据库发生更改时刷新它。
与 Firestore 提供的操作相比,此操作并非最佳。事实上,虽然Pouchdb 无法同时返回更改和数据,但 Firestore 凭借系统机制能够做到这一点QuerySnapshot,从而减少了服务器之间的往返。您可以参考以下 Firestore 示例自行验证:
  db.collection("anything")
    .onSnapshot(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
          // This forEach loop is executed at first execution
          // And executed each time the query result changes
        });
    });
因此,如果我们改用 Firestore,它看起来会是这样的:
  //...
  const [beers, setBeers] = useState([]);
  useEffect(() => {
    const unsubscribe =  db.collection("beers")
      .onSnapshot(function(querySnapshot) {
          const snapBeers = [];
          querySnapshot.forEach(function(doc) {
              snapBeers.push(doc.data());
          });
          setBeers(snapBeers);
      });
    return () => {
        unsubscribe();
    };
  }, []);
  // ...
也可以使用.map属性querySnapshot.docs以“非命令式”的方式检索所有文档。遗憾的是,官方文档中没有充分涵盖此功能。
模型
就像后端开发一样,我喜欢在前端应用中将模型逻辑与视图逻辑分离。以下是我们的 Beer 的 API 文件:
// ./src/api/beers.js
import PouchDB from 'pouchdb';
// We declare a PouchDB instance that is "remote only"
// There's no "offline" capability for the moment, everything is sync
export const beersDatabase = new PouchDB('http://localhost:5984/beers');
// If the beers database does not already exist
// => The database is automatically created when an object is added to it
export const addBeer = beer => beersDatabase.post(beer);
// Here, we list all the documents from our beers database
// A lot of options exists. Eg: we can paginate using "startKey", "endKey" or "limit"
export const getBeers = () =>
  beersDatabase
    .allDocs({
      include_docs: true,
      descending: true,
    })
    .then(doc => doc.rows.map(row => row.doc));
// We listen all the changes that happen since now
// We can also apply a "limit" option to this method
export const onBeersChange = callback => beersDatabase
    .changes({ since: 'now', live: true })
    .on('change', callback);
这是我们第一个 CouchDB 应用程序的运行结果。如您所见,所有内容在多个窗口之间同步。
离线同步
遗憾的是,我们的实际版本仅在互联网连接正常且正常运行时有效。在其他情况下,例如网络拥堵或数据包丢失,由于“仅限远程”同步,啤酒永远不会(或缓慢地……)添加到啤酒列表中。
避免这个问题的正确方法是保持本地优先。这意味着我们必须在本地数据库上实现所有数据库操作,然后在恢复网络访问后再将其与远程数据库同步。
因此,第一步是声明一个新的 PouchDB 实例,并使用数据库名称而不是远程数据库 URL。这样,PouchDB 会自动检测到我们想要实例化本地数据库。
import PouchDB from 'pouchdb';
// Declare local database
const beersDatabase = new PouchDB('beers');
// Declare remote database
const remoteBeersDatabase = new PouchDB(`http://localhost:5984/beers`);
// Keep local and remote databases in sync
PouchDB.sync(beersDatabase, remoteBeersDatabase, {
  live: true, // replicate changes in live
  timeout: false, // disable timeout
  retry: true, // retry sync if fail
});
该PouchDB.sync指令相当于PouchDB.replicate本地和远程数据库之间的双向指令。
PouchDB.replicate(beersDatabase, remoteBeersDatabase);
PouchDB.replicate(remoteBeersDatabase, beersDatabase);
默认情况下,PouchDB 使用IndexedDB作为本地数据库(顺便说一下,就像 Firestore 一样)。现在设置已完成,我们可以使用 Chrome 控制台查看本地数据库。
如您所见,我们找到了已创建的完整啤酒列表。每个啤酒都由一个key和_idCouchDB_rev属性唯一标识。
{
  "_id": "0c2738a3-d363-405f-b9bb-0ab6f5ec9655",
  "_rev": "3-b90bd9d62fbe04e36fe262a267efbd42",
  "title": "Beer X"
}
表示_id一个唯一的文档,而_rev表示该文档的修订标识符。实际上,文档的每次修改都意味着一个新版本的出现,这使得管理冲突成为可能。
与 CouchDB 不同,Firestore 文档没有修订ID。因此,避免使用 Firestore 冲突的唯一方法是使用事务。
此外,由于 CouchDB 记录了每个提交的更改,因此可以再次返回或解决冲突,这对于避免丢失数据至关重要。
有关使用 PouchDB 进行冲突管理的更多信息,请查看PouchDB 冲突文档。
现在我们能够与本地和远程数据库通信,可以专注于业务逻辑和用户界面。此外,它还能让我们受益于乐观渲染,同时让我们的应用程序在处理网络问题时更加灵活。
表格与验证
在本节中,我们将实现一个表单来添加新的啤酒。为此,我将使用final-form(以及react-final-formReactJS 的一个适配器)。
npm install -S final-form react-final-form
因此,我们可以创建一个简单的表单来处理用户输入。
// ./src/components/BeerForm.js
import React from 'react';
import { Form, Field } from 'react-final-form';
export const BeerForm = ({ onSubmit }) => (
  <Form
    validate={() => ({})}
    onSubmit={onSubmit}
    render={({
      handleSubmit,
      hasValidationErrors,
      pristine,
      invalid,
      submitErrors,
      submitting,
      form,
    }) => (
        <form onSubmit={handleSubmit}>
         <div>
            <label>Title</label>
            <Field name="title" component="input" />
          </div>
          <div>
            <label>Description</label>
            <Field
              name="description"
              component="textarea"
              rows={2}
              placeholder="Tape your description here..."
            />
          <div/>
          <button type="submit" disabled={pristine || hasValidationErrors || submitting}>
            Submit
          </button>
          {submitErrors && submitErrors.global && (
            <p>{submitErrors.global}</p>
          )}
        </form>
      )
    }
  />
);
然后,我们可以用主屏幕中的表单替换操作按钮。
// ./src/screens/Home.js
import React, { useState, useEffect } from 'react';
import { addBeer, getBeers, onBeersChange } from '../api/beers';
export const Home = () => {
  const [beers, setBeers] = useState([]);
  /* ... */
  return (
    <div>
      <BeerForm onSubmit={beer => queries.addBeer(beer)} />
      <ul>
        {/* beer._id is an unique id generated by CouchDB */}
        {beers.map(beer => <li key={beer._id}>{beer.title}</li>)}
      </ul>
    </div>
  );
};
指示性数据验证
我们现在有了一个表单,但目前还没有数据验证。用户现在可以发送任何他们想要的内容。因此,我们要使用 (indicative我刚刚发现的一个库,我想尝试一下)来设置一个数据验证器。
npm install -S indicative
Indicative API 非常简单。它由一个Validator使用一组验证规则的对象和一个组成formatter。以下是一个使用示例:
import Validator from 'indicative/builds/validator';
import { Vanilla as VanillaFormatter } from 'indicative/builds/formatters';
import { required, email } from 'indicative/builds/validations';
const validator = Validator({ required, email }, VanillaFormatter);
const rules = {
  name: 'required',
  email: 'required|email',
};
const messages = {
  'required': '{{ field }} field is required', // This message works for all required rules
  'email.required': 'You must provide an email!', // This message is specific for required email
  'email.email': 'The email adress is invalid',
};
const values = {
  email: 'bad email',
};
// Validator.validate is async
validator
  .validate(values, rules, messages)
  .then(() => /* everything is ok! */)
  .catch((errors) => {
    /*
      [
        { field: 'name', message: 'name field is required!' },
        { field: 'email', message: 'The email adress is invalid' },
      ]
    */
  });
以下是我们的自定义实现BeerForm.js。
// ./src/components/BeerForm.js
import React from 'react';
import { Form, Field } from 'react-final-form';
import { Vanilla } from 'indicative/builds/formatters';
import Validator from 'indicative/builds/validator';
import { required } from 'indicative/builds/validations';
const validator = Validator({ required }, Vanilla);
const rules = {
  title: 'required',
  description: 'required',
};
const messages = {
  'title.required': 'Beer title is required',
  'description.required': 'Beer description is required',
};
const validate = async values =>
  validator
    .validate(values, rules, messages)
    .then(() => ({}))
    .catch(errors => {
      return errors.reduce((acc, error) => {
        acc[error.field] = error.message;
        return acc;
      }, {});
    });
Final Form 需要一个对象作为错误模型,因此我们使用 来格式化错误catch。reduce或者,也可以使用自定义指示性格式化程序。
所以,现在我们有了自定义验证函数,我们可以替换空的验证函数。
export const BeerForm = ({ onSubmit }) => (
  <Form
-  validate={() => ({})}
+  validate={validate}
好了!我们的验证表单已启动并运行,我们可以开始使用了。
让我们让它变得美丽!
总而言之,我们可以显示啤酒,可以添加啤酒,所有功能都可以离线运行,并与远程服务器同步。但是现在,它不太美观,我不敢把它展示给我岳母。那么,让它更漂亮一点怎么样?
在本节中,我将使用Elastic(开发 ElasticSearch 的公司)正在使用的Elastic UI框架(又名)。eui
我想我们都同意,必须移除这个讨厌的列表,用一个美观的网格来代替。幸运的是,Eui 轻松实现了这一点。
如您所见,我们借此机会添加了直接从网格编辑和删除啤酒的功能。我们还将表单放在页面右侧的滑动面板中。这样,我们可以直接从导航栏中的“+”按钮添加啤酒,或者直接从网格编辑啤酒,而无需更改页面。
处理图像附件
我不知道你是怎么想的,但看到这些灰色的啤酒罐,我的心都碎了。所以,是时候在表单中允许上传图片了。
// ./src/components/BeerForm.js
const handleIllustration = async files => {
  if (files.length === 0) {
    form.change('_image', undefined);
    return;
  }
  const file = files[0];
  form.change('_image', {
    data: file,
    type: file.type,
  });
};
<EuiFormRow label="Beer Illustration">
  <EuiFilePicker onChange={handleIllustration} />
</EuiFormRow>
我刚刚添加到 beer 对象的这个自定义_image属性随后由我们的 beer api 处理,并被视为PouchDB 附件。
// ./src/api/queries.js
const saveBeer = async ({ _image, ...beer }) =>
  store
    .collection('beers')
    .post(beer)
    .then(
      ({ id, rev }) =>
        // if an "_image" attribute is present, we put an attachement to the document
        _image &&
        store
          .collection('beers')
          .putAttachment(id, 'image', rev, _image.data, _image.type)
    );
const getBeers = () =>
  store
    .collection('beers')
    .allDocs({
      include_docs: true,
      descending: true,
      attachments: true, // We include images in the output, so we can display them
    })
    .then(doc => doc.rows.map(row => row.doc));
};
在 CouchDB 中,每个文件都可以直接作为 附加到其对应的文档attachement。Firestore 中不存在此概念。因此,最好使用Firebase 存储(Google Cloud Storage)通过其存储桶系统来存储文件,并在 Firestore 中存储路径。
结论
我的啤酒注册应用程序的最终成果可以在 GitHub 上找到,地址如下:github.com/marmelab/reactive-beers。欢迎大家提出意见和改进!
虽然我一开始对 CouchDB 的功能有所怀疑,但我很快就被它的稳定性和 API 的易用性所征服。
由于我尚未在生产环境中部署此类应用程序,因此无法评价此类数据库的维护难易程度。尽管如此,我还是建议首先使用Firestore 进行 POC 测试,并首先使用Couchbase或IBM Cloudant等第三方服务来处理关键应用程序。
虽然这次经历让我能够平衡每个数据库主要功能的优缺点,但我并没有能够达到我预期的程度。
确实,我没有时间涵盖许多关键点,例如文档访问安全性、权限管理、服务器端文档验证、数据分页或部署。但无论如何,我决心就这些主题撰写更多文章。
所以,请继续关注!
文章来源:https://dev.to/juliendemageon/couchdb-the-open-source-cloud-firestore-alternative-2gc0 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          







