☰
LokiJS 快速上手:JavaScript 嵌入式内存数据库的创建、查询、链式操作与动态视图实战
2026/10/7 9:57:20 网站建设 项目流程
  • 数据库
  • 后端

【免费下载链接】LokiJS

javascript embeddable / in-memory database

项目地址:https://gitcode.com/gh_mirrors/lo/LokiJS
点击查看免费下载

本文是一份基于 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

项目地址:https://gitcode.com/gh_mirrors/lo/LokiJS
点击查看免费下载

相关推荐

上一篇:PostHog AI 可观测性成本拆分指南:一份覆盖模型、用户、Trace 与缓存经济学的 SQL 配方集
下一篇:VictoriaMetrics 与 VictoriaLogs 的 OpenTelemetry 接入实战:Kubernetes 部署、OTLP 采集与 Go 应用埋点

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询