- 数据库
- 后端
【免费下载链接】LokiJS
javascript embeddable / in-memory database
本文是一份基于 LokiJS 官方文档(OVERVIEW.md)与仓库源码的实战入门指南,面向需要在浏览器或 Node.js 环境中嵌入一个轻量级内存数据库的开发者。你将学会用十几行代码完成数据库创建、集合管理、文档插入、find/where 查询、Resultset 链式操作、命名 Transform 与 Dynamic View 动态视图,并能对照仓库中的可运行示例(examples/quickstart-core.js 等)在本地直接复现验证。
一、LokiJS 是什么:从项目定位看核心概念
LokiJS 是一个 JavaScript 嵌入式 / 内存数据库(javascript embeddable / in-memory database),核心实现集中在 src/lokijs.js(约 7700 行,单文件、零外部依赖)。它最大的特点是不需要独立数据库服务进程:你把数据库实例当作普通对象在应用进程内创建,数据驻留内存,写入时通过适配器序列化到磁盘、浏览器 IndexedDB / LocalStorage 或远端存储。官方文档 OVERVIEW.md 明确将其定位为一套由 jsdoc 驱动的、分布式维护的准确文档,并用一组"Getting Started"代码串联了全部核心 API。
在开始之前,先建立三个贯穿全文的核心概念:
- Loki 实例:整个数据库对象,对应 Loki 构造函数,内部维护
collections数组,负责集合的增删查与持久化调度; - Collection(集合):同一类文档的容器,类似关系数据库的"表",每个文档会被自动分配唯一的
$loki主键与meta元信息(见 Collection.prototype.insertOne 相关实现); - Resultset / Dynamic View / Transform:三种查询与结果组织方式——一次性查询结果集(Resultset)、可增量维护的"活视图"(Dynamic View)、可命名复用的查询链模板(Transform)。
仓库在 examples 目录提供了与本文每个小节一一对应的可运行示例(quickstart-core.js、quickstart-chaining.js、quickstart-transforms.js、quickstart-dynview.js),建议边读边跑。
二、创建数据库与添加集合
官方文档给出的最小启动流程只有两步:
var db = new loki('example.db');var users = db.addCollection('users');第一步在内存中创建名为example.db的数据库实例。从源码看,Loki(filename, options)构造函数会以filename || 'loki.db'作为默认文件名(src/lokijs.js#L903-L905),因此文件名是可选的;真正把数据落盘需要配合持久化适配器与saveDatabase()/autosave等机制(Node.js 环境默认使用LokiFsAdapter,见 src/loki-fs-adapter.js 同族的 src/loki-fs-sync-adapter.js 等适配器)。
第二步在数据库内注册名为users的集合。查看 Loki.prototype.addCollection 的实现可以发现两个值得注意的行为:
- 同名集合会被去重:如果已存在同名集合,直接返回既有引用而不会重复创建;
- 集合名可以携带可选的
options参数(如disableMeta、disableChangesApi、ttl等),并且当disableMeta: true与disableChangesApi: false、disableDeltaChangesApi: false或启用ttl同时出现时会抛出异常,防止产生无法追踪元信息的集合。
与创建对应的还有db.getCollection(name)(src/lokijs.js#L1216-L1229)——找不到时返回null并发出warning事件,这在后面配合持久化做"首次运行初始化"判断时非常常用。
三、插入文档:单个与批量
官方文档展示了两种插入形态,源码实现也正好对应两条路径(Collection.prototype.insert):
users.insert({ name: 'Odin', age: 50, address: 'Asgard' }); // alternatively, insert array of documents users.insert([{ name: 'Thor', age: 35}, { name: 'Loki', age: 30}]);- 传入单个普通对象时,内部转调
insertOne(src/lokijs.js#L5918),为文档生成$loki唯一主键、写入meta(含created/revision等)、触发pre-insert/insert事件,并同步维护唯一索引、二分索引等结构; - 传入数组时,走批量插入路径(可选
bulkInsert静默模式),适合初始化阶段一次性灌入大量数据。
在实际项目中,插入前往往先建好集合上的索引(如users.ensureIndex('age')),这样后续范围查询会直接命中二分查找,效率更高;相关性能主题可参考 tutorials/Indexing and Query performance.md 与 spec/generic/binaryidx.spec.js 测试。
四、查询文档:find、findOne 与 where
4.1 查询对象与 find
文档的"简单 find 查询"示例:
var results = users.find({ age: {'$gte': 35} }); var odin = users.findOne({ name:'Odin' });find接受一个查询对象(Query Object),支持:
- 隐式等值:
{ age: 35 }等价于{ age: { $eq: 35 } }; - 比较操作符:
$eq、$gt、$gte、$lt、$lte、$between等; - 组合逻辑:对象键隐式
$and,也可显式写{ $and: [...] }、$or; - 点路径:
{ "attributes.eyes": 1 }直达嵌套字段; - 数组操作:
$contains、$size等。
findOne与find的区别在于只返回第一条匹配文档(无匹配返回null)。从源码看,Collection.prototype.find就是chain().find(query).data()的语法糖(src/lokijs.js#L7086-L7088),findOne则通过Resultset.prototype.find的firstOnly参数提前终止遍历(src/lokijs.js#L7042-L7046)。
仓库中的 examples/quickstart-core.js 对这套查询对象做了更完整的演示:{ age: { $eq: 1000 } }显式等值、{ age: { $gt: 500 } }范围、{ age: 29, gender: "f" }隐式与、{ $and: [...] }显式与、{ age: { $between: [20, 40] } }区间、{ "attributes.eyes": 1 }嵌套路径、{ items: { $contains: "eski" } }数组包含、{ items: { $size: 2 } }数组长度,以及$loki主键删除等操作,可作为完整 API 清单逐条运行对照。
4.2 where:函数式过滤
当查询对象无法表达复杂业务逻辑时,用where传入过滤函数:
var results = users.where(function(obj) { return (obj.age >= 35); });where是纯 JavaScript 函数式过滤(src/lokijs.js#L7180-L7182),每次执行都会遍历全部文档并调用你的函数,因此无法利用索引加速,只适合小数据量或查询对象难以表达的临时条件;高频路径建议优先用find+ 索引。
五、链式操作:Resultset 组合查询
官方文档的链式示例:
var results = users.chain().find({ age: {'$gte': 35} }).simplesort('name').data();chain()返回一个 Resultset(惰性求值的结果集对象,内部通过filteredrows记录符合条件的行号),后续所有过滤、排序、分页操作都以链式叠加,直到调用data()才真正产出结果数组。常见的链式方法包括:
find(query)/where(fn):叠加过滤条件(可连续调用多个find,条件取交集);simplesort(propname, options):按字段排序,第二个参数为true时降序(src/lokijs.js#L3209),还有基于索引的sort变体;limit(n)/offset(pos):分页(src/lokijs.js#L3013、src/lokijs.js#L3034);update(fn)/remove():对结果集内的所有文档执行批量更新 / 删除,无需取回数据(src/lokijs.js#L3877);data(options):终止链,可传{ removeMeta: true }剥离$loki与meta字段,或{ forceClones: true }强制返回克隆对象(src/lokijs.js#L3795-L3864)。
examples/quickstart-chaining.js 完整覆盖了以上用法,其中几个片段很能说明问题:
// 按年龄降序取前两名 result = users.chain().simplesort("age", true).limit(2).data(); // 混合 find 与 where 过滤器 result = users.chain().find({ age: 29 }).where(function(obj) { return obj.gender === "f" }).data(); // 对结果集内文档批量 +1 岁(无需 data()) users.chain().find({ age: { $between: [30, 40] } }) .update(function(obj) { obj.age = obj.age+1; }); // 去除元信息的浅克隆结果 result = users.chain().data({ removeMeta: true });从源码看,update在集合启用了克隆/增量变更追踪时会先克隆文档、执行你的函数再回写collection.update,以保证变更记录完整,这一点在需要 Changes API 追踪数据变更的场景中至关重要。
六、命名 Transform:把查询链存成"存储过程"
官方文档的 transform 示例:
users.addTransform('progeny', [ { type: 'find', value: { 'age': {'$lte': 40} } } ]); var results = users.chain('progeny').data();Transform 是查询链的对象化表示:把find、where、simplesort、limit、offset、update等步骤按顺序写成一个对象数组,用addTransform(name, steps)命名保存后,就能像调用存储过程一样通过chain(name)复用(Collection.prototype.addTransform)。配套 API 还有getTransform、setTransform、removeTransform(src/lokijs.js#L5336-L5357)。
Transform 还支持[%lktxp]前缀的参数占位符。例如 examples/quickstart-transforms.js 中的分页模板:
users.addTransform("paged", [ { type: 'offset', value: '[%lktxp]pageStart' }, { type: 'limit', value: '[%lktxp]pageSize' } ]); var page = 1, pageSize = 5, start = (page - 1) * pageSize; var result = users.chain("paged", { pageStart: start, pageSize: pageSize }).data();调用chain(name, params)/transform(name, params)时传入的参数对象会替换模板中的[%lktxp]xxx占位符(替换逻辑见 src/lokijs.js#L72-L76 附近的参数解析辅助函数),这让"分页""热门筛选"这类高频查询只需定义一次。
七、Dynamic View:持续维护的"活视图"
官方文档的动态视图示例:
var pview = users.addDynamicView('progeny'); pview.applyFind({ 'age': {'$lte': 40} }); pview.applySimpleSort('name'); var results = pview.data();Dynamic View(Collection.prototype.addDynamicView)与一次性chain()的最大区别在于增量维护:视图在集合上注册后,通过applyFind、applyWhere、applySimpleSort、applySort设定过滤与排序规则(DynamicView.prototype.applyFind、DynamicView.prototype.applySimpleSort),此后每当集合insert/update/remove,视图会自动评估新文档是否进入结果集并调整顺序,pview.data()拿到的始终是"最新结果",无需重新全量扫描。这对于"展示层需要实时跟随数据变化"的场景(如下拉列表、实时报表)尤其合适。
Dynamic View 还支持对当前结果做二次查询——branchResultset()会基于视图现有结果派生一个 Resultset,并可选套用 Transform:
// 在 'over 500' 视图结果中继续筛选男性 result = ov500.branchResultset().find({ gender: 'm' }).data(); // 视图结果 + 命名 transform 组合成命名"抽取" result = ov500.branchResultset("paged", { pageStart: start, pageSize: pageSize }).data();对应实现见 DynamicView.prototype.branchResultset,完整示例见 examples/quickstart-dynview.js。
八、落地实践:持久化与自动初始化
前面各节都是纯内存操作;真实应用中通常还需要把数据保存到磁盘。综合 examples/quickstart-dynview.js 与 examples/quickstart-transforms.js 的用法,一个带持久化的最小模板如下:
const loki = require('../src/lokijs.js'); var db = new loki('app.db', { autoload: true, // 启动时自动加载既有数据库文件 autoloadCallback: databaseInitialize, // 加载完成后回调 autosave: true, // 自动保存 autosaveInterval: 4000 // 每 4 秒保存一次 }); function databaseInitialize() { var users = db.getCollection("users"); // 首次运行集合不存在,则创建 if (!users) { users = db.addCollection("users"); } // 首次运行创建动态视图,applyFind 的过滤条件会随库持久化 if (!users.getDynamicView("over 500")) { let ov500 = users.addDynamicView("over 500"); ov500.applyFind({ age: { $gte: 500 } }); ov500.applySimpleSort('age', true); } // 首次运行注册命名 transform,同样会持久化 if (!users.getTransform("females")) { users.addTransform("females", [{ type: 'find', value: { gender: 'f' } }]); } if (users.count() === 0) { seedData(); // 首次运行灌入种子数据 } runProgramLogic(); }要点与源码佐证:
autoloadCallback是"恢复现场"的标准入口:动态视图的 find 过滤条件、命名 transform、集合本身都会随数据库 JSON 持久化;但applyWhere的函数过滤无法序列化,每次加载后都需要在回调里重新调用applyWhere恢复(examples/quickstart-dynview.js 中对此有明确注释);db.getCollection在集合不存在时返回null(src/lokijs.js#L1216-L1229),这正被用来区分"首次运行"与"已有数据";autosave由定时器驱动,Node.js 进程退出前若未到保存间隔,可在SIGINT等退出事件中调用db.close()强制落盘(db.close()仅在集合标记为 dirty 时执行保存)。
在浏览器端,LokiJS 同样可用,只需换成浏览器适配器(如 src/loki-indexed-adapter.js、src/loki-localstorage-adapter.js 系列的 IndexedDB / LocalStorage 适配器),API 使用方式完全一致。
九、结语与延伸阅读
至此,官方 OVERVIEW.md 展示的全部核心 API——loki实例、addCollection、insert、find/findOne、where、链式chain、命名 Transform、Dynamic View——都已覆盖,并且每一处都从 src/lokijs.js 源码与 examples 示例中找到了对应实现与完整用法。如果你想继续深入,仓库还提供了:
- 更完整的中文可读教程:tutorials/Autoupdating Collections.md、tutorials/Collection Transforms.md、tutorials/Query Examples.md、tutorials/Indexing and Query performance.md;
- 类级 API 参考:docs 目录下由 jsdoc 生成的 Loki.html、Collection.html、Resultset.html、DynamicView.html;
- 全套行为测试:如 spec/generic/collection.spec.js、spec/generic/dynamicview.spec.js、spec/generic/transforms.spec.js,可用来校验你对 API 语义的理解。
按官方推荐,你也可以直接用npm install lokijs引入依赖(当前仓库的 package.json 即记录了 lokijs 的包结构),然后在自己的 Node.js 或浏览器项目中复刻本文各小节示例。
- 数据库
- 后端
【免费下载链接】LokiJS
javascript embeddable / in-memory database
相关推荐
推荐使用LokiJS:超快速的JavaScript内存数据库
推荐使用LokiJS:超快速的JavaScript内存数据库 如果你正在寻找一个高性能、轻量级且适应多环境的JavaScript数据库解决方案,那么LokiJS
数据库后端如何用Tesla-Menu解锁Switch隐藏功能:从覆盖菜单到系统定制的完整路径
如何用Tesla Menu解锁Switch隐藏功能:从覆盖菜单到系统定制的完整路径 Tesla Menu是Nintendo Switch平台上的一款革命性覆盖菜
嵌入式Cayley 作为 Go 库使用快速上手:内存图、路径查询与持久化后端接入实战
Cayley 作为 Go 库使用快速上手:内存图、路径查询与持久化后端接入实战 本指南基于 Cayley(一个开源图数据库)的官方库使用文档,讲解如何将 Cay
图数据库数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考