使用 node-json-db 为node项目添加一个简易的本地数据库

4/4/2022 Node

# 使用 node-json-db 为 node 项目添加一个简易的本地数据库

数据库大家都不陌生,对于前端来说也不算太熟悉。 很多时候可能会和我一样为了信息抓取会用 node 做一些小爬虫(当然别乱用:爬虫写得好,牢饭吃到饱)

可是 node 的容错性属实有点低,比如

  • 一共抓取 20 页的数据,抓到第 5 页,发现 dom 结构不同了,JS 报错了,进程结束
  • 抓取第六页的时候,被发现了,爬虫返回了 500, ajax 报错 进程又结束了
  • 抓取 20 页数据,抓到一半断网了。这时候数据没保存,又得从头抓起

针对以上的问题,其实无非就是因为我们抓取的每一页数据并没有实时的存储下来,导致遇到错误都得从头开始。那么 node-json-db (opens new window) 就极大方便了我们对数据的操作

# 搭建 node-json-db 的测试环境

终端执行

npm init -y
npm i koa koa-router nodemon node-json-db
1
2

新建几个目录:/src/index,/db

添加一个启动命令 package.json

"dev": "nodemon src/index.js",
1
  • /src/index.js
const { JsonDB } = require('node-json-db')
const { Config } = require('node-json-db/dist/lib/JsonDBConfig')

const Koa = require('Koa')
const Router = require('koa-router')

const path = require('path')
const fs = require('fs')

const app = new Koa()
var db = new JsonDB(new Config(path.join(__dirname, '../db/myDataBase.json'), true, true, '/'))
const router = new Router()

router.get('/add', function(ctx) {
  // ... 预留位置写db操作语句

  ctx.body = {
    code: 0,
    data: db.getData('/'), // 每次访问接口直接打印db所有的数据
    msg: ''
  }
})

app.use(router.routes())

app.listen(8000, function() {
  console.log('app run at http://localhost:8000/')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

运行后,访问 localhost:8000/ 看到如下

qbupg1.png

# 基础 API

看文档也有具体的说明,我只是把几个常用的 API 列举一下

# 引入 node-json-db

const { JsonDB } = require('node-json-db')
const { Config } = require('node-json-db/dist/lib/JsonDBConfig')
1
2

# 初始化

const db = new JsonDB(new Config(path.join(__dirname, '../db/myDataBase.json'), true, false, '/'))
1

几个参数的含义:

export declare class Config implements JsonDBConfig {
    filename: string;
    humanReadable: boolean;
    saveOnPush: boolean;
    separator: string;
    syncOnSave: boolean;
    constructor(filename: string, saveOnPush?: boolean, humanReadable?: boolean, separator?: string, syncOnSave?: boolean);
}
1
2
3
4
5
6
7
8
  1. filename: 就是数据库保存的位置和名称
  2. saveOnPush: 是否每当调用 push 方法的时候都存到本地 json 中,如果使用 false 则需要自己调用 save 方法进行保存
  3. humanReadable: 可读性。存入本地 JSON 的时候,默认会进行数据压缩,如果该配置为 true ,则会相当于编辑器格式化 JSON 一样有缩进有换行
  4. separator: 分隔符 默认是 /
  5. syncOnSave: 使用同步写入(毕竟 nodejs 写入文件是支持异步写入的)

上面说到的 separator 分隔符,可以这么理解

const user = { name: 'Jioho', age: 18 }

// 正常我们获取 user 下的 name 属性写法如下
console.log(user.name)
// 修改:
user.name = 'jio'
1
2
3
4
5
6

分隔符就好像上面代码中的 . ,所以在 node-json-db 中,假如你想修改 user.name ,那么就调用相应的 api,其中键值为 user/name

展示不能理解后面有完整的示例

# 增删改查

# 增 db.push()

调用 db.push 方法进行 操作(如果saveOnPush 为 false,则还需要调用 db.save() 才能保存数据到本地)

但是和我们认识的增有一定的区别

举个例子:

// ... 省略引入和初始化db步骤

// 该部分代码写入刚才预留db操作的位置
// 连续调用 3 次 push
db.push('/user', { name: 'Jioho', age: 18 })
db.push('/user', { name: 'Jioho2', age: 19 })
db.push('/user', { name: 'Jioho3', age: 20 })
1
2
3
4
5
6
7

调用 /add 接口,最后的返回值只有一条数据:

{
  "code": 0,
  "data": {
    "user": {
      "name": "Jioho3",
      "age": 20
    }
  },
  "msg": ""
}
1
2
3
4
5
6
7
8
9
10

因为 /user 是一个对象,并不是一个数组,如果想要递增的效果,改为如下代码,明确 user 对象是一个数组类型

PS: 直接修改代码后运行会报错,因为在本地的数据中 user 已经存储了一个对象类型,所以需要手动把本地的 JSON 文件清空掉在运行代码

所以定义数据结构必须一开始想好,否则后面想改就比较麻烦了

db.push('/user[]', { name: 'Jioho', age: 18 })
db.push('/user[]', { name: 'Jioho2', age: 19 })
db.push('/user[]', { name: 'Jioho3', age: 20 })
1
2
3

#

查询的 API 目前有如下几个方法:

db.getData(dataPath) 传入需要查询的数据路径,返回值为 any
毕竟这只是 JSON 操作,所以并不能像 SQL 一样比如指定查询出 user 数组下的所有 name,需要自行写逻辑(查询出所有的 user 数据,然后筛选 name 属性出来)

// 当前JSON 中所有的数据
db.getData('/')
// 查询所有的 user 数据
db.getData('/user')

// 查询第一条
db.getData('/user[0]')
1
2
3
4
5
6
7

db.getObject(dataPath) 该方法和 db.getData() 一样,只是可以指定返回值类型,如果是在 TS 环境中

interface User {
  name: String
  age: Number
}

const userList: User[] = db.getObject<User[]>('/user[0]')
const user: User = db.getObject<User>('/user[0]')
1
2
3
4
5
6
7

db.exists 判断路径下的数据是否存在,这个还是比较实用的,防止数据未定义情况下就去调用

if (db.exists('/user')) {
  return db.getData('/user')
} else {
  return []
}
1
2
3
4
5

db.count 返回当前数据类型的条数

db.count('/user')
1

db.getIndex(dataPath: string, searchValue: (string | number), propertyName?: string): number 找到所匹配的的一条查询的值需要是字符串或者数字类型,不能为数组类型


返回值为查询到的第一个索引,即便有多条匹配的数据,返回值也是第一个匹配的索引,如果没找到则返回 1

// 查询所有等于19岁的用户
db.getIndex('/user', 19, 'age')
1
2

db.getIndexValue

同 getIndex 一样,getIndex 返回的是索引,getValue 返回的则是对应的数据


db.filter<T>(rootPath: string, callback: FindCallback): T[] | undefined; 和 JS 的 filter 一样,传入需要查询的对象的路径,然后传入筛选的方法,返回值为一个数组(支持泛型操作)

// 查询所有等于19岁的用户
db.filter('/user', function(value, index) {
  if (value.age >= 19) {
    return true
  }
  return false
})
1
2
3
4
5
6
7

db.find

用法和 filter 一样,返回值为第一条匹配的数据(也是支持泛型)

#

修改数据其实还是用到 db.push 方法

还记得一开始说的 方法中如果 push 的对象不是数组则是替换原理吗?所以改也是用到 push,其实 push 还有第三个参数就是 是否覆盖源对象 默认是 true

db.push('/vipuser', { name: 'Jio', age: 18 })

// 这时候的对象是 user: { name: 'Jio', age: 18 }

// 修改年龄为19
db.push('/vipuser', { name: 'Jio', age: 19 })
// user: { name: 'Jio', age: 20 }

// 修改年龄为20
db.push('/vipuser', { age: 20 })
// ! 注意这里的 name 属性被覆盖了
// user: { age: 20 }

// 这里用上了第三个参数,标记为false,而且没有写入 age 属性
db.push('/vipuser', { name: 'Jioho', job: '前端' }, false)
// user: { name:'Jioho' age: 20, job: '前端' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

以上就是覆盖和不覆盖的区别

假设需要修改数组中指定某一项的值,只需要指定好路径,进行修改即可

db.push('/user[0]', { age: 20 }, false)

// 等价于
db.push('/user[0]/age', 20)

// 如果不指定索引,这时候修改的的则是 user 数组中的最后一项的 age 属性
db.push('/user[]/age', 22)
1
2
3
4
5
6
7

#

db.delete(path),传入要删除的路径即可

# 更新

db.reload()
因为还有 db.save 方法的存在,个人猜测所有的的数据在初始化后都读到了内存中,这样可以减少每次查数据的时候还要读一次文件的操作。

然而既然是文件,可能不止 node-json-db 可以操作这份 JSON 文件,node 自身也可以写入文件,所以当自己操作 node api 写入文件后,可以通过 db.reload() 进行把最新的数据重新读到内存中


db.resetData(data)
重置当前的数据为data,这将会重置所有的数据

# 最后

node-json-db 的出现让一些 node 小工具有更好的拓展性,称之为“数据库”可能会有点夸张,毕竟数据库最基础的 where 方法之类的并没有实现,只有简单的 JS 的 filter,find 作为支撑

不过对于一些小应用,用这套 JSON 操作的方法来存储一些配置(比如很多脚手架新建项目后都会询问是否保存配置下次使用)。这个库就能很好的胜任了,包括各种查询的方便的操作

最后有一个小提示,就是指定索引 push 或者删除等操作之前,记得先判断 exists~

node-json-db 是一个非常贴近前端API的本地JSON类型数据库,node-json-db的出现让一些 node 小工具有更好的拓展性

Last Updated: 1/7/2024, 5:51:59 PM