- 数据库
- 嵌入式数据库
【免费下载链接】lowdb
Simple and fast JSON database
本篇技术指南围绕 lowdb 官方
src/examples/目录下的四个示例展开,逐一拆解 CLI 命令行工具、Express 服务器、浏览器 localStorage 以及测试环境内存模式的具体写法,并下沉到 预设(Presets)、核心类 Low/LowSync 与各类 适配器 的源码实现,帮助你快速掌握在不同运行环境中以最小代码量落地 lowdb 的完整套路。读完本文,你将能独立把 lowdb 嵌入 Node CLI、HTTP 服务、浏览器应用与单元测试四种典型场景。
一、官方示例总览:一条文档带你看遍四种运行环境
lowdb 的官方示例集中放在仓库的 src/examples/ 目录,配套的 src/examples/README.md 以一张清单的形式给出了四个示例的定位,分别对应四种典型使用场景:
| 示例文件 | 应用场景 | 核心适配器/预设 |
|---|---|---|
| cli.ts | 命令行工具(CLI) | JSONFileSyncPreset(同步文件预设) |
| server.ts | Express HTTP 服务器 | JSONFilePreset(异步文件预设) |
| browser.ts | 浏览器页面 | LocalStoragePreset(localStorage 预设) |
| in-memory.ts | 测试中的快速读写 | MemorySync(内存同步适配器) |
这四个示例恰好覆盖了 lowdb 面向 Node、Electron 与浏览器三大运行环境的核心用法,也印证了项目根目录 package.json 中定义的三套子路径导出:
lowdb/node—— 文件类适配器与 Node 端预设(JSONFile、JSONFileSync、TextFile、DataFile等);lowdb/browser—— 浏览器端适配器与预设(LocalStorage、SessionStorage等);lowdb(根入口)—— 与运行环境无关的核心类与内存适配器(Low、LowSync、Memory、MemorySync)。
本文接下来的每个小节都会先讲示例本身的用法,再结合 src/presets 与 src/adapters 中的源码解释其底层原理。
二、CLI 示例:用同步预设实现零配置命令行工具
cli.ts是四个示例中最短的一个,完整代码如下(见 src/examples/cli.ts):
import { JSONFileSyncPreset } from '../presets/node.js' type Data = { messages: string[] } const message = process.argv[2] || '' const defaultData: Data = { messages: [] } const db = JSONFileSyncPreset<Data>('file.json', defaultData) db.update(({ messages }) => messages.push(message))它的行为很简单:从命令行第二个参数(process.argv[2])读取一段文本,追加到file.json的messages数组中。由于命令行工具通常需要"即调即用",所以这里选用同步预设JSONFileSyncPreset,无需await即可完成读写。
2.1 同步预设的源码解析
JSONFileSyncPreset定义在 src/presets/node.ts,其内部逻辑是:
export function JSONFileSyncPreset<Data>( filename: PathLike, defaultData: Data, ): LowSync<Data> { const adapter = process.env.NODE_ENV === 'test' ? new MemorySync<Data>() : new JSONFileSync<Data>(filename) const db = new LowSync<Data>(adapter, defaultData) db.read() return db }这里有一个值得注意的细节:当环境变量NODE_ENV等于test时,预设会自动改用内存适配器MemorySync,而不是真正读写文件。这正是项目 README 中所宣称的"测试期间自动切换为快速内存模式"(Automatically switches to fast in-memory mode during tests)的实现位置。也就是说,同一个预设代码在测试与生产环境中会表现出不同的持久化行为,这为后续第四节的测试思路埋下了伏笔。
2.2 同步链路与 update 方法
JSONFileSyncPreset返回的是 src/core/Low.ts 中的LowSync实例。示例中调用的db.update(({ messages }) => messages.push(message))对应LowSync.update的实现:
update(fn: (data: T) => unknown): void { fn(this.data) this.write() }它的语义是"先执行修改函数,再自动落盘",一次调用完成"改数据 + 写文件"两步。如果去掉update,等效写法是:
db.data.messages.push(message) db.write()从源码可以看到,LowSync.read()只有在适配器返回了有效数据时才覆盖this.data:
read(): void { const data = this.adapter.read() if (data) this.data = data }因此在 CLI 场景下,file.json首次不存在时,db.data会保持构造时传入的defaultData(即{ messages: [] }),随后update写入第一条消息,file.json即被创建。你可以用node cli.ts "hello"连续执行几次,观察file.json的内容如何累积。
三、服务器示例:Express 异步处理与避免阻塞
src/examples/server.ts 展示了如何在 Express 服务器中使用 lowdb,完整代码为:
import express from 'express' import asyncHandler from 'express-async-handler' import { JSONFilePreset } from '../presets/node.js' const app = express() app.use(express.json()) type Post = { id: string body: string } type Data = { posts: Post[] } const defaultData: Data = { posts: [] } const db = await JSONFilePreset<Data>('db.json', defaultData) // db.data can be destructured to avoid typing `db.data` everywhere const { posts } = db.data app.get('/posts/:id', (req, res) => { const post = posts.find((p) => p.id === req.params.id) res.send(post) }) app.post( '/posts', asyncHandler(async (req, res) => { const post = req.body as Post post.id = String(posts.length + 1) await db.update(({ posts }) => posts.push(post)) res.send(post) }), ) app.listen(3000, () => { console.log('listening on port 3000') })3.1 为什么服务器要用异步适配器
cli.ts用同步JSONFileSync,而server.ts改用异步JSONFile,注释里明确说明了取舍(见 src/examples/server.ts):
如果你在开发一个本地服务器且不会遇到并发请求,用
JSONFileSync会更省事;但如果需要避免阻塞请求,就应该使用JSONFile。
原因在于:同步文件 I/O 会阻塞 Node.js 事件循环,一旦同时到达多个请求,磁盘读写期间的请求都会排队等待;而JSONFile走的是异步适配器接口(read(): Promise<T | null>、write(data): Promise<void>,定义见 src/core/Low.ts),由Low类驱动,不会阻塞事件循环。
3.2 异步预设与"读取即就绪"
JSONFilePreset同样定义在 src/presets/node.ts:
export async function JSONFilePreset<Data>( filename: PathLike, defaultData: Data, ): Promise<Low<Data>> { const adapter = process.env.NODE_ENV === 'test' ? new Memory<Data>() : new JSONFile<Data>(filename) const db = new Low<Data>(adapter, defaultData) await db.read() return db }注意它返回的是Promise<Low<Data>>,并且在返回前已经执行过await db.read()。所以示例中await JSONFilePreset(...)之后,db.data里就已经装载了db.json的既有内容,随后const { posts } = db.data的解构可以直接使用。
3.3 一个值得留意的更新语义
示例的 POST 路由里:
post.id = String(posts.length + 1) await db.update(({ posts }) => posts.push(post))由于posts是db.data.posts的解构引用,posts.push(post)实际修改的就是db.data中的数组;update执行完修改函数后自动调用write(),把整个db.data序列化写回db.json。这里体现了 lowdb 的核心设计——db.data就是一个普通 JavaScript 对象/数组,没有任何魔法("no magic"),你用原生数组方法修改它,再通过write()落盘即可。
3.4 并发请求的适用前提
需要明确:lowdb 并不是为高并发写入设计的数据库。仓库根 README.md 的 Limits 一节明确指出,lowdb 不支持 Node 的 cluster 模块;如果确有并发、多进程写入需求,应改用 PostgreSQL、MongoDB 等真正的数据库。因此本示例更适合本地开发、原型搭建或低频写入的场景。
四、浏览器示例:localStorage 预设实现页面持久化
src/examples/browser.ts 是浏览器端最小示例:
import { LocalStoragePreset } from '../presets/browser.js' type Data = { messages: string[] } const defaultData: Data = { messages: [] } const db = LocalStoragePreset<Data>('db', defaultData) db.update(({ messages }) => messages.push('foo'))这段代码做了三件事:以db作为 localStorage 的存储键名创建预设 → 从localStorage读取数据(不存在则使用默认值)→ 追加一条消息并写回。执行后,开发者工具中localStorage的db键下会存有{"messages":["foo"]}。
4.1 浏览器预设源码
LocalStoragePreset定义在 src/presets/browser.ts:
export function LocalStoragePreset<Data>( key: string, defaultData: Data, ): LowSync<Data> { const adapter = new LocalStorage<Data>(key) const db = new LowSync<Data>(adapter, defaultData) db.read() return db }同目录下还提供了等价的SessionStoragePreset(见 src/presets/browser.ts),差别仅在底层使用sessionStorage。二者都基于LowSync——因为 Web Storage API 本身是同步的。
4.2 LocalStorage 适配器实现
适配器实现在 src/adapters/browser/LocalStorage.ts,其核心契约满足 src/core/Low.ts 中SyncAdapter的接口:read()返回数据或null,write(data)写回存储。此外浏览器目录下还有SessionStorage适配器(见 src/adapters/browser/SessionStorage.ts),用于 session 级的临时持久化。
使用前提:LocalStorage/SessionStorage只能运行在浏览器环境(存在window全局对象),且存储值必须是可被JSON.stringify序列化的数据。若要在 Node 中模拟,则需配合内存适配器或文件适配器。
五、内存示例:在测试中替代文件 I/O
src/examples/in-memory.ts 演示了如何用内存适配器让测试读写"零 I/O":
import { LowSync, MemorySync, SyncAdapter } from '../index.js' import { JSONFileSync } from '../node.js' declare global { namespace NodeJS { interface ProcessEnv { NODE_ENV: 'test' | 'dev' | 'prod' } } } type Data = Record<string, unknown> const defaultData: Data = {} const adapter: SyncAdapter<Data> = process.env.NODE_ENV === 'test' ? new MemorySync<Data>() : new JSONFileSync<Data>('db.json') const db = new LowSync<Data>(adapter, defaultData) db.read() // Rest of your code...它的思路非常直接:根据NODE_ENV环境变量选择适配器——测试环境用MemorySync,其他环境用JSONFileSync。由于MemorySync的write()只是把数据存进内存变量(见 src/adapters/Memory.ts),示例开头的注释明确写道:"With this adapter, callingdb.write()will do nothing"(调用write()不会真正落盘),因此测试运行既快又不会污染磁盘上的db.json。
5.1 与预设内置的"测试自动切换"呼应
这一写法与第二节提到的JSONFilePreset/JSONFileSyncPreset内置逻辑殊途同归——预设已经在内部实现了NODE_ENV === 'test'时自动切换为Memory/MemorySync。两者的关系是:
- 用预设:框架替你做了适配器切换,代码更简洁;
- 手动选择适配器(本例):把切换逻辑显式暴露出来,便于在测试中注入其他定制适配器或 Mock 数据。
5.2 内存适配器源码
MemorySync与异步版本Memory均定义在 src/adapters/Memory.ts:
export class MemorySync<T> implements SyncAdapter<T> { #data: T | null = null read(): T | null { return this.#data || null } write(obj: T): void { this.#data = obj } }可以看到write只是把对象引用保存到私有字段#data,不涉及任何磁盘、网络开销;read在无数据时返回null。这也解释了为什么它被称为"测试专用加速器"——省去了JSON.stringify序列化与磁盘写入的时间。仓库中 src/adapters/Memory.test.ts 即对该行为进行了单元测试验证。
六、串起来看:预设、类与适配器的协作关系
四个示例虽然面向不同环境,但共享同一套由"预设 → 类 → 适配器"组成的三层架构。以异步链为例,调用链为:
JSONFilePreset └─ new JSONFile<Data>(filename) // 适配器:文件读写 + JSON 序列化 └─ extends DataFile // 组合 TextFile,注入 parse/stringify └─ new Low<Data>(adapter, defaultData) └─ await db.read() // 从适配器读取,填充 db.data └─ db.update(fn) / db.write() // 修改数据并写回其中:
- 预设层(src/presets/node.ts、src/presets/browser.ts)提供四种常用组合:
JSONFilePreset、JSONFileSyncPreset、LocalStoragePreset、SessionStoragePreset,内部完成"选适配器 + 建 Low 实例 + 读取数据"三步; - 核心类层(src/core/Low.ts)提供
Low(异步)与LowSync(同步)两套实现,方法只有read()、write()、update(fn)三个,data属性直接存放业务数据; - 适配器层(src/adapters)按环境分为
node/(JSONFile、JSONFileSync、TextFile、DataFile)、browser/(LocalStorage、SessionStorage)与通用的Memory/MemorySync。
6.1 文件适配器的序列化细节
值得补充的是JSONFile的默认格式化行为。查看 src/adapters/node/JSONFile.ts 可以看到它继承自DataFile,并注入了序列化参数:
export class JSONFile<T> extends DataFile<T> { constructor(filename: PathLike) { super(filename, { parse: JSON.parse, stringify: (data: T) => JSON.stringify(data, null, 2), }) } }其中stringify使用JSON.stringify(data, null, 2)以两空格缩进写出 JSON,保证db.json文件可读;而 src/adapters/node/DataFile.ts 则把"读取文本 → parse"与"stringify → 写入文本"两段逻辑解耦,这为换用 YAML、加密、压缩等其他格式提供了扩展点(项目 README 的 Adapters 章节 对此有专门介绍)。
6.2 快速跳转清单
如果你想把本文的示例直接改造成自己的项目,可参考以下路径:
- 四个示例的完整源码:src/examples/cli.ts、src/examples/server.ts、src/examples/browser.ts、src/examples/in-memory.ts;
- 预设实现:src/presets/node.ts、src/presets/browser.ts;
- 核心类:src/core/Low.ts;
- 各适配器:src/adapters/node、src/adapters/browser、src/adapters/Memory.ts;
- 测试参考:src/core/Low.test.ts、src/adapters/Memory.test.ts、src/adapters/node/JSONFile.test.ts。
七、边界与注意事项
在使用以上示例时,有几点需要结合 lowdb 的设计边界加以留意(依据来自仓库根 README.md 的 Limits 一节):
- 不支持 Node 的 cluster 模块:多进程共享同一
db.json无法保证写入一致性,分布式部署场景请改用真正的数据库。 - 全量序列化开销:每次
db.write()都会对整个db.data执行JSON.stringify并整体写入。当数据量达到约 10~100MB 级别时可能遇到性能问题。官方建议的做法是批量操作、按需落盘(尽量集中修改后再调用一次write()),而不是每次小改动都写一次。 - 运行环境要求:本项目为纯 ESM 包,
package.json中"type": "module",并声明engines.node >= 18。浏览器适配器仅在存在window的环境中可用;在 Node 中使用时请从lowdb/node子路径导入,在浏览器中使用时从lowdb/browser子路径导入。
综上,src/examples/的四个示例几乎就是 lowdb 最小可用用法的"标准答案":CLI 用同步预设、服务器用异步预设、浏览器用 Web Storage 预设、测试用内存适配器。理解了这一套对应关系,你就能在任何 JavaScript/TypeScript 环境中快速落地一个"读即建、改即存"的本地 JSON 数据库。
- 数据库
- 嵌入式数据库
【免费下载链接】lowdb
Simple and fast JSON database
相关推荐
WebdriverIO 示例仓库实战指南:从云服务、多浏览器到自定义扩展的全套官方示例
WebdriverIO 示例仓库实战指南:从云服务、多浏览器到自定义扩展的全套官方示例 WebdriverIO 的 examples https://link.
测试质量保障Polly.js 官方示例全解析:从浏览器拦截到 Node 与 Puppeteer 的七套实战方案
Polly.js 官方示例全解析:从浏览器拦截到 Node 与 Puppeteer 的七套实战方案 本指南以 docs/examples.md https://
测试开发工具Apache Thrift Node.js 示例实战指南:从 RPC 服务到浏览器与 HTTP 跨语言调用
Apache Thrift Node.js 示例实战指南:从 RPC 服务到浏览器与 HTTP 跨语言调用 本篇技术指南以 lib/nodejs/example
后端RPC框架序列化代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考