一次编写,Node和浏览器通吃:NeDB存储抽象层设计解析与localforage实战
【免费下载链接】nedbThe JavaScript Database, for Node.js, nw.js, electron and the browser项目地址: https://gitcode.com/gh_mirrors/ne/nedb
NeDB 是一款纯 JavaScript 编写的嵌入式数据库,支持 Node.js、nw.js、Electron 和浏览器环境,API 兼容 MongoDB 子集。它的核心魅力在于:同一套文档数据库代码,在 Node 里落到文件,在浏览器里落到 localforage——这一切都归功于其精巧的存储抽象层设计。本文带你拆解这套跨端存储机制,并看看 localforage 是如何被"驯服"的。
一、什么是 NeDB:一份数据,两端运行
NeDB(Node Embedded DataBase)是一个文件型的嵌入式数据库,一个 Datastore 相当于一个 MongoDB 集合。你可以把它想象成一个"装在应用里的 MongoDB":
- ✅ 100% JavaScript,无二进制依赖,跨平台
- ✅ Node 端数据持久化到本地文件,浏览器端持久化到浏览器存储
- ✅ 服务端与浏览器端API 完全一致(
insert/find/update/remove) - ✅ 支持索引、唯一约束,性能充足
入口非常简洁,index.js 只做了两行事:
var Datastore = require('./lib/datastore'); module.exports = Datastore;真正的跨端魔法,藏在Datastore底层的持久化链路里。
二、存储抽象层:一张"统一存储接口"表
NeDB 的核心模块 lib/persistence.js 负责所有持久化任务,而它从不直接碰文件系统,而是调用一个storage对象。这个对象对外暴露一套与"文件操作"语义一致的接口:
| 接口方法 | 语义 | Node 端实现 | 浏览器端实现 |
|---|---|---|---|
exists | 文件是否存在 | fs.exists | localforage.getItem判空 |
readFile | 读取内容 | fs.readFile | localforage.getItem |
writeFile | 写入内容 | fs.writeFile | localforage.setItem |
appendFile | 追加内容 | fs.appendFile | 读出→拼接→写回 |
rename | 重命名 | fs.rename | 读旧→写新→删旧 |
unlink | 删除 | fs.unlink | localforage.removeItem |
mkdirp | 确保目录存在 | mkdirp | 空操作(浏览器无目录) |
crashSafeWriteFile | 崩溃安全写 | 临时文件+重命名+fsync | 退化为普通writeFile |
这就是依赖倒置的教科书案例:上层逻辑(lib/persistence.js、lib/datastore.js)只依赖"抽象",不依赖"具体"。换存储引擎,上层一行不改。
两个实现文件
- Node 版:lib/storage.js —— 基于
fs、mkdirp,外加崩溃安全读写函数 - 浏览器版:browser-version/browser-specific/lib/storage.js —— 基于 localforage,按浏览器能力自动选择 IndexedDB → WebSQL → localStorage
三、Node 端精华:崩溃安全写入是怎么做到的
lib/storage.js 中的crashSafeWriteFile实现了完整的崩溃防护流程:
- 刷目录与旧文件缓冲(
fsync父目录和已有数据文件) - 把新数据写入临时文件
filename~ - 再次 fsync 临时文件,确保数据真正落盘
rename原子替换(重命名操作是原子的,不会出现"半截文件")- 最后再刷一次父目录
配套的ensureDatafileIntegrity函数会在启动时检查:如果只发现临时文件而没有正式文件,说明上次写失败了,于是回滚到旧版本。这样即使进程在写入过程中被杀,数据库文件也不会损坏。
💡 这也是为什么文件名不能以
~结尾——这个后缀被保留给崩溃安全备份文件了。
四、浏览器端实战:localforage 如何接入 NeDB
浏览器没有文件系统,NeDB 的解法是让 localforage 充当"虚拟文件系统"。打开 browser-version/browser-specific/lib/storage.js,可以看到非常有趣的"语义翻译":
localforage.config({ name: 'NeDB', storeName: 'nedbdata' }); // "重命名文件" = 读出旧值 → 写入新键 → 删除旧键 function rename (filename, newFilename, callback) { localforage.getItem(filename, function (err, value) { if (value === null) { localforage.removeItem(newFilename, function () { return callback(); }); } else { localforage.setItem(newFilename, value, function () { localforage.removeItem(filename, function () { return callback(); }); }); } }); }几个设计取舍值得注意:
- 🧠
appendFile的实现是"读-拼-写",因为 localforage 的键值模型没有追加概念 - 🧠
mkdirp和ensureDatafileIntegrity直接空转——浏览器里既没有目录,也不会出现"写一半断电" - 🧠
crashSafeWriteFile直接别名到writeFile——浏览器存储引擎本身具备事务性,无需临时文件方案
五、自动切换的秘密:package.json 的 browser 字段
为什么一份代码能"零配置"跑在两端?答案在 package.json 中:
"browser": { "./lib/customUtils.js": "./browser-version/browser-specific/lib/customUtils.js", "./lib/storage.js": "./browser-version/browser-specific/lib/storage.js" }打包器(webpack、browserify 等)在处理浏览器构建时,会把require('./lib/storage')自动替换为浏览器版实现。Node 运行时则完全无视这个字段,走原生fs。上层代码写的是同一句require,拿到的却是两个不同的存储引擎——这就是"一次编写,两端通吃"的机制内核。
同样的替换也发生在 lib/customUtils.js 上:Node 端用crypto.randomBytes生成文档_id,浏览器端(browser-version/browser-specific/lib/customUtils.js)则用Math.random+ 自定义 base64 完成同样的事。
六、浏览器端快速上手
在 HTML 中引入构建产物后,全局对象Nedb立即可用,API 与服务端一模一样:
<script src="nedb.min.js"></script> <script> var db = new Nedb(); // 纯内存模式 var db2 = new Nedb({ filename: 'myData' }); // 持久化模式 </script>指定filename后,NeDB 会自动挑选当前浏览器最佳的存储后端(优先 IndexedDB,其次 WebSQL,兜底 localStorage),大多数浏览器下可存储数百 MB 数据。
⚠️重要提醒:NeDB 在 v1.3 到 v1.4 之间更换了底层存储系统,两者不兼容,升级后客户端需要重新同步数据。
七、给新手的 5 条实战建议
- 📌 浏览器端给
new Nedb(...)传filename才会持久化,否则数据只活在内存里 - 📌 Node 端数据文件是"追加式"日志,每次
loadDatabase时自动压缩,无需手动维护 - 📌 需要加密落盘时,使用
afterSerialization/beforeDeserialization钩子,但两者必须成对出现(NeDB 会自检,防止数据丢失) - 📌 高频查询字段记得
ensureIndex,万级文档下索引创建仅需约 35ms - 📌 浏览器兼容范围:Chrome、Safari、Firefox、IE9+,测试用例在 browser-version/test/ 目录
总结
NeDB 的存储抽象层是"面向接口编程"的优秀范例:
- lib/storage.js 与 browser-version/browser-specific/lib/storage.js 实现同一套接口,分别对接文件系统和 localforage
- Node 端通过临时文件 + 原子重命名 + fsync实现崩溃安全
- 浏览器端借助 localforage 自动适配 IndexedDB / WebSQL / localStorage
package.json的browser字段让打包器一键完成"换引擎"
掌握这套模式后,你自己写跨端应用时,也完全可以复刻"抽象接口 + 双端实现 + 构建时替换"的架构,真正做到一次编写、Node 与浏览器通吃。
【免费下载链接】nedbThe JavaScript Database, for Node.js, nw.js, electron and the browser项目地址: https://gitcode.com/gh_mirrors/ne/nedb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考