如何在单篇笔记里实现轻量数据库
介绍
众所周知,在 Obsidian 里实现类似「数据库」的需求一直都是痛点。
虽然借助插件能实现,但往往得给每一项数据都创建单独页面……颇为麻烦,完全不像 Notion 那样舒适且无感。
很久以前我就有一个设想:
如果我有一篇笔记专门收集某个专题的内容,想要把它做成一个「小数据库」——不要求编辑,只求汇总展示里面各个内容的特定属性,能行吗?
这个问题困扰了我半年之久,终于在最近,随着对 OB 和 Dataview 插件的学习,我实现了这个陈年需求。
展示
效果展示:
可以看到,通过笔记内的多个标题,就能自动生成一个「轻量数据库」进行呈现。
支持特性:
- 点击标题可以直接跳转到对应内容
- 只需要一篇笔记就可以创建数据库
- 支持多个属性的组合
- 根据元数据自适应生成表格列
- (大体上)实时更新,具体取决于 Dataview 的刷新间隔
- 简单易用:只需要三行代码加一个 JS 脚本文件即可实现
工作原理
笔记的书写
需要借助 Dataview 插件,以及固定的格式:
## 标题
[属性::值] [属性2::值]
- 元数据行紧挨着标题行写(标题层级不限制——几个井号都行)
- 可以有多个元数据,使用
[key::value]
的格式,一行内的多个属性之间要 隔开 2 个空格 ⚠️ - 同一个属性(例如
标签
)可以在一个内联属性里用逗号写多个值,也可以一行里写多个同名属性(会自动合并)- 第一种:
[标签:Obsidian,css]
- 第二种:
[标签:Obsidian] [标签:css]
- 第一种:
文章在开始的那张图片,源代码模式下就长这样儿:
关于内联属性
详见 Dataview 的文档:Adding Metadata - Dataview
数据库的呈现
首先,打开 Dataview 插件的设置,确保这几个选项都是打开的:
然后需要将这个 JS 文件放入你的笔记库,命名为 liteDatabase.js
:
const useList = false;
const curNote = dv.current();
if (!curNote){
dv.span("当前文档未加载,请重新打开");
return;
}
let tarFile = await app.vault.getAbstractFileByPath(curNote.file.path);
// 获取当前文件的 meta 数据
const curFileMeta = app.metadataCache.getFileCache(tarFile);
const headings = curFileMeta.headings;
const headingsList = headings.map( k => k.heading )
// 获取当前文章的文本内容
const curFilePath = curNote.file.path;
const curTFile = await app.vault.getFileByPath(curFilePath)
const content = await app.vault.cachedRead(curTFile);
if (!headings) {
dv.span("当前文档缺少标题");
return;
}
const meta = content.matchAll(/#+ (.*)\n(\[.*\:.*\])/gm);
// 标题作为 key,元数据作为 value,做成 DV 表格
let tableHeads = ["标题"]
// 先存成一个 dict
let metaValues = []
if (meta) {
for (let m of meta) {
let title = m[1]
// 检查 title 是否在 headings 内(避免把一些代码块内的内容也当作元数据)
if (headingsList.indexOf(title) == -1) {
continue
}
let metaList = m[2].split(" ")
let metaDict = {}
// 添加元数据的 key 到表头
for (let i = 0; i < metaList.length; i++) {
let keyValue = metaList[i].replace("[", "").replace("]", "").split("::")
// console.log(metaList[i], keyValue)
let key = keyValue[0].trim()
if (tableHeads.indexOf(key) == -1) {
tableHeads.push(key)
}
if (useList){
// 1. 列表形式,重复属性会合并
let value = [keyValue[1]]
metaDict[key] = metaDict[key] ? metaDict[key].concat(value) : value
} else {
// 2. 文本形式,重复属性会使用最新的
let value = keyValue[1]
metaDict[key] = metaDict[key] ? metaDict[key] + `,${value}` : value
}
}
let returnValuesList = []
for (let i = 0; i < tableHeads.length; i++) {
if (i == 0) {
returnValuesList.push(`[[#${m[1]}]]`)
} else {
returnValuesList.push(metaDict[tableHeads[i]] ? metaDict[tableHeads[i]] : "")
}
}
metaValues.push(returnValuesList)
}
}
dv.table(
tableHeads,
metaValues.map( k => k )
)
你也可以直接在 Github Gist 中查看或下载这个代码文件:DVJS Code - Lite Database for Obsidian
(后续如果有更新会直接传到 Gist 里)
然后,在笔记中插入:
```dataviewjs
dv.view("liteDatabase")
```
在笔记书写符合规则的情况下,现在你应该就能看到轻量数据库的呈现了!
测试文本
如果暂时没写内容,你也可以直接复制这段内容进笔记查看效果:
## 传说之下
[英文名::Undertale] [标签::感人,剧情向]
「你落入了地底,然后——」
关于友情的故事。
## 星际拓荒
[英文名::Outer Wilds] [标签::探索,感人]
「嘿,尝试过掉进黑洞吗?」
关于探索的故事。
## 生化奇兵:无限
[标签::好玩] [标签::感人,剧情向] [英文名::BioShock Infinite]
「过去多少年,我依旧在你身边」
关于时间的故事。
拓展
因为用的是 JS 语言,所以理论上来讲形式和内容都有很大的拓展可能性。
比如你也可以不用 Dataview 内联属性,就用自己定义的某种格式。
或者你想对获取到的元数据再进行二次处理……
修改 JS 文件(或者让 GPT 帮你修改)就可以按自己想要的方式进行定制化!
碎碎念时间
起源
一开始是今年一月份的时候,看 GameOff 的游戏开发比赛发现好多作品非常有趣,然后就一边记录一边想着「打标记」:
你可以看到,当时我就在尝试类似的「标题 + 下方的标记」这种方式来进行记录,但是苦于 没有办法把它们汇总到一起展示出来。
(甚至开始怀念起了 Notion 的数据库 lol)
但是这个问题放到今天,只需要把当时还在探索的「标记方式」替换成 Dataview 的内联属性,就能得到:
——每个游戏是不是 Godot 开发的,有哪些值得注意的亮点,一目了然!
属性的定义
关于元数据定义形式,主要是参考了 Github Badges 的样式:
这样一排小徽标排在一起看着还挺好看的。
(应该也可以通过 css 来实现多彩的背景色)
另外就是……放在同一行也可以让正则表达式的筛选更简化一些。
最早其实也尝试过「标签」还有「内联代码」这两种形式。
但标签的话……会污染我的标签池,这种内容我还是希望就局限在「这个页面内」;内联代码倒是没啥问题,想用也可以用的。
至于多个 KeyValue 中间的 两个空格,这个可能是 Dataview 自己的限制?只有一个空格的情况下似乎会有一些识别问题,所以就强行规定两个空格了。
(但我自己都经常忘记打两个,嗨呀)
讨论
若阁下有独到的见解或新颖的想法,诚邀您在文章下方留言,与大家共同探讨。
反馈交流
其他渠道
版权声明
版权声明:所有 PKMer 文章如果需要转载,请附上原文出处链接。