前端数据库 本地离线存储工具 PouchDB 简单入门(以Node.js场景为例)

2,425 阅读6分钟

前言

最近在做一个基于 ElectronVue 的桌面应用程序的个人项目,需要在本地存储和展示本地音频文件的索引数据。过程中也是踩坑无数,跟大家分享一下这段经历。

离线存储工具的选择

离线存储,实现的方式有很多。最简单的便是我们熟悉的浏览器端离线存储,如 LocalStorageSessionStorage ,又或是 IndexedDBWeb SQL 。 以上四种离线存储在项目中,均可以通过 Electron 渲染进程中的 Chromium 内核来实现。一些轻量级的第三方库,如 ImmortalDBweb-storage-cache对浏览器离线存储做了一些扩展,但这些库的维护情况还是不太让人满意。不过,这其中的localForage也还是让人眼前一亮。

受限于浏览器内核的环境,以上方法不能很好满足我的需求,于是只能寻找 node.js 端的存储工具,这其中又分两类,一类是js本身实现(如NeDBPouchDB),另一类则是成熟的数据库软件(如 MySQLMongoDB)。

按功能和性能来讲,数据库绝对是最优的选择,但不是很符合我的 Electron 应用程序的定位,1、它们体积非常庞大,需要打包进用户的安装包(实现起来有难度,也没几个让普通的用户能够忍受一个小小软件在自己的计算机上装 MySQL);2、功能庞大,杀鸡焉用牛刀?

于是,不占空间、轻量又方便打包的 NeDBPouchDB 进入了决赛圈。考虑到 NeDB 是明文存储,功能单一,又年久失修。我就果断选择了隔壁 PouchDB

PouchDB 入门

我们先来看看 PouchDB 官网 是如何介绍这个产品的:

PouchDB is an open-source JavaScript database inspired by Apache CouchDB that is designed to run well within the browser.

PouchDB was created to help web developers build applications that work as well offline as they do online.

PouchDB 在浏览器和 Node.js 环境下都可以运行,轻量便携、上手简单等优点。PouchDB 在浏览器中默认使用 IndexedDB ,若当前环境不支持,则回退到 Web SQL。在 Node.js 环境中则会以特定格式的本地文件存储数据。官方文档的这个部分详细介绍了在不同环境下的工作形式👉Adapters

安装

官方文档中给出了两种方式,分别是 CDNnpm

<script src="//cdn.jsdelivr.net/npm/pouchdb@7.2.1/dist/pouchdb.min.js"></script>
npm install --save pouchdb

初始化

node 环境中引用 pouchdb,通过PouchDB这个构造函数来创建一个数据库实例:

const PouchDB = require('pouchdb')
// 创建在根目录下database文件夹下名为my_database的数据库
const db = new PouchDB('database/my_database')

new PouchDB([name], [options]) 方法接受数据库名称与配置项作为参数(具体配置项参见文档)。name这个参数除了接受名称外,还可以传入远程 CouchDB 数据库地址,比如 const db = new PouchDB('http://pouch-db.com/my-db')

不过,官方文档对 node 环境下数据库存储位置并未做足够的说明。 在这里指出,在 node 环境下name可以为相对路径绝对路径,如下:

// 在{root}/database/my_database目录下创建
const db = new PouchDB('database/my_database')
// 在C:/Code/test/test_pouchdb目录下创建
const db2 = new PouchDB('D:/Code/test/test_pouchdb')

基本操作

数据库最基本的无非就是又快又方便地做 CRUD 。我们来快速实现一下 PouchDB 的基本操作。所有相关操作均可以在其API文档中找到传送门

首先,我们需要定义要存储的数据格式:

const myData = {
  _id: String,
  title: String
}

其次,最好使用统一的函数去增添数据,因为在 PouchDB 中并没有严格的 schema 。我们使用 db.put(doc, [options], [callback]) 方法添加或修改数据。

需要指出, PouchDB 除了用传统的 callback ,还支持更主流的 Promiseasync

// * 使用函数包装 新增数据 的方法
function addData(title) {
  // 数据格式
  const data = {
    _id: new Date().toISOString(),
    title: text
  }
  // 增改方法
  db.put(data, function cb(err, result) {
    if (err) {
      console.error(err)
    } else {
      console.log(result)
    }
  })
}

我们使用 addData() 方法创建一个 title'helloPouchDB' 的数据,然后看一看该数据包含些什么:

addData('helloPouchDB')

回调函数中的 result 返回了新增的状态、数据的 idrev 。数据中除了 title 也有相应的 _id_rev

// result
{
  ok: true,
  id: '2020-12-04T12:54:13.638Z',
  rev: '1-a2436e9326998a520a759f2a274e166b'
}
// 保存的那个data
{
  title: 'helloPouchDB',
  _id: '2020-12-04T12:54:13.638Z',
  _rev: '2-565fec0bf70044cc51d24a8379139f8e'
}

需要注意的是,修改数据需要指定当前修改数据项的 _rev (创建的时候默认就有)。 比如这样:

db.put({
  title: 'Nice to meet you',
  _id: '2020-12-04T12:54:13.638Z',
  _rev: '2-565fec0bf70044cc51d24a8379139f8e'
})

从数据库中根据id查找(fetch)数据,需要用到 db.get(docId, [options], [callback]),如 db.get(_id)。配置项中可以指定一个或者多个 rev,其他的配置项参考文档

如果需要获取多条数据,则需要使用 db.allDocs([options], [callback]) (文档传送门)。在配置项中可以指定 skiplimit 来实现分页功能。

function showData() {
  db.allDocs({
    include_docs: true, // 返回的数据中默认只有id和rev,带上数据需传true
    descending: true, // 降序排列
    skip: 20// 第3页
    limit: 10 // 每页10条
  })
    .then((res) => {
      console.log(res)
    })
    .catch((err) => {
      console.error(err)
    })
}

返回的数据,包含了总条数 total_rows 、跳过的条数 offset 、与数据的数组 rows 。格式如下:

{
  total_rows: 1,
  offset: 0,
  rows: [
    {
      id: '2020-12-04T13:00:26.358Z',
      key: '2020-12-04T13:00:26.358Z',
      value: { rev: '1-6da27cff722c24cfe38fb09be78f8576' },
      doc: {
        title: 'helloPouchDB',
        _id: '2020-12-04T13:00:26.358Z',
        _rev: '1-6da27cff722c24cfe38fb09be78f8576'
      }
    }
  ]
}

接下来是最重要的一个查询方法 db.find(request [, callback]) 。这个方法允许通过数据的字段来查询数据 文档传送门需要注意,这个方法需要引入 pouchdb-find 插件 。

# 安装
npm install --save pouchdb-find
const PouchDB = require('pouchdb')
// 注入插件
PouchDB.plugin(require('pouchdb-find'))

db.find() 通过 selector 指定查询内容、 fields 指定返回的内容包含的字段。 selector 也可以使用 $lt$gt 等过滤器指定特殊的查询条件。与 db.allDocs() 一样,可以指定 skiplimit

db.find({
  selector: {
    title: 'helloPouchDB',
  },
  fields: ['_id', 'name'],
})

改、删

结合 新增查询 这两种方法, 我们变可以获取已有的数据并使用 db.put() 方法对其进行修改:

// 查询
db.find({
  selector: {
    title: 'helloPouchDB',
  },
})
  .then((res) => {
  // 获取该数据
    const result = res.docs[0]
    // 修改数据
    result.title = 'Nice to meet you'
    // 修改并写入
    db.put(result)
      .then((res) => {
        console.log('yes!')
      })
      .catch((err) => {
        console.error(err)
      })
  })
  .catch((err) => {
    console.error(err)
  })

删除数据 可以通过 db.remove(doc, [options], [callback]) 方法,该方法除了接受 doc 对象外,也可以通过 idrev 删除数据。

// 方式1 - 获取实例并删除
db.get(_id, function(err, doc) {
  if (err) { return console.log(err) }
  db.remove(doc, function(err, response) {
    if (err) { return console.log(err) }
  })
})
// 方式2 - 通过 id 和 rev 删除
db.remove(doc._id, doc._rev, function(err, response) {
    if (err) { return console.log(err) }
  })
})