🔒
发现新文章,点击刷新页面。
✇吕立青的博客

序章:为什么都 2023 年了,我还在用 Roam Research 写笔记?

作者 吕立青
序章:为什么都 2023 年了,我还在用 Roam Research 写笔记?

展信佳,

曾经有无数新的笔记工具摆在我面前,我没有珍惜,等我失去的时候,我才后悔莫及,人世间最痛苦的事莫过于此。如果上天能够再给我一次机会,我会对那个笔记工具说三个字:我选你。如果非要为这个选择作出一个解释,我希望是……这份信。
序章:为什么都 2023 年了,我还在用 Roam Research 写笔记?

📝 笔记工具市场:Roam Research 的模仿者

在工具层面,同类型对比时,唯一的对手只有 Logseq,同为 Clojure & ClojureScript 开发,文学编程的默契“同源”。而且 Logseq 是开源的,融资额 400w 美元,现金充裕,生态很好,之前的 Roam Research 大V Ramses Oudt 被雇佣专门做 Marketing,最新官网的描述也很棒:Logseq: A privacy-first, open-source knowledge base.

Obsidian,作为最先的模仿者,但是选择了 Markdown 文件作为载体,所以受限很多,跟 Logseq 相比而言,在数据格式上并不占优。Logseq 和 Obsidian 都主打 local-frist,私有化数据,就像 Obsidian 的口号:A second brain, for you, forever.

但是对于普通用户来说,并不关心背后的 Markdown 或者是 Datalog 数据库,所以依然需要同步,自选方案当然有,比如 iCloud,但是 Apple 确实拉胯,导致基于文件系统的同步体验着实很差, 并且经常可能出错。

💰 商业化变现:Obsidian Publish 和 Logseq Pro

于是乎,Logseq 和 Obsidian 这对难兄难弟都选择推出了自己的 Pro 服务,即自带云同步。Obsidian 更甚,基于 Markdown 的好处在于面向文档,于是 Obsidian Publish 孕育而生,作为内容的托管服务,取代博客,且模仿了 Andy's Working Notes 样式,名为「数字花园」。只不过,同样价格很贵。

Logseq 本身是开源的,且天然支持 Git,从而程序员群体比较偏爱它。同时,爱折腾的程序员们,自然会更容易找到公开发布的方案,也更愿意为它开发插件。需要注意的是,Obsidian 本身并不开源,但它的插件生态和开发者文档做得很好。于是,诸多插件如雨后春笋般涌现,产品体验也随之走向了崩坏,原因也很简单:插件不考虑宿主,更不考虑用户。

💻 本地化 vs. 云端化:新兴的云笔记工具们

说完了本地化的两款代表笔记工具之后,基于云的 Roam Research 模仿者,值得一提的就只有两款:RemNote 和 Tana。RemNote 依托于靠谱的科学研究,较为重视 Remember Note 的相关产品设计,比如 Flashcard 等机制,对学生来说价值更高。

当然,继双链的概念大热之后,市场上既有的类似产品也随之跟上,最典型的比如 Workflowy 是我看好的一名选手。多年的功能开发之后,却保持了极简,着实不易。双向链接的引用,取名为 mirrors 也比较贴切,跟 Notion 的 synced block 有异曲同工之妙,但也仅限于此了。

而 Tana 则是新的搅局者,第二大脑的作者 Tiago Forte 称之为 "the new Roam",而社区更倾向于将其描述为 Notion + Roam Research 的孩子。Tana 的早期起势非常高调,社区大 V 们纷纷高潮,奔走相告,再加上邀请制一码难求,期待值非常高,堪比 Arc 浏览器。

🤖 AI 的崛起:ChatGPT 和 New Bing 带来的挑战与机遇

但是,就如 ChatGPT 所引发的一波高潮之后,微软祭出 New Bing 大杀器,让直接集成 ChatGPT 的 Edge 浏览器大杀四方。Arc 浏览器在功能层面上的创新显得微不足道,AI 加持的新生产力工具,才是未来的革命性创新。

序章:为什么都 2023 年了,我还在用 Roam Research 写笔记?

New Bing 弥补了 GPT-3 缺失实时网络搜索结果的缺憾,聚合总结的效果更加震撼。并且,Edge 浏览器中的 New Bing 能够直接与网页的内容直接进行交互,让 ChatGPT 根据用户实时浏览的内容进行智能问答。众所众知,浏览器作为互联网的入口,所浏览的内容不局限于网页,同样可以作用于 PDF、图片等其他内容,因此想象空间更为巨大。

由此,微软所打出的 New Bing 搜索引擎 + New Edge 浏览器,让这两个领域都占据先手优势的谷歌猝不及防,已有的 Google 搜索和 Google Chrome,不得不开始跟上脚步和踩准节拍。好玩的是,谷歌联合创始人拉里·佩奇(Larry Page)和谢尔盖·布林(Sergey Brin)都被紧急召回,更频繁地参与到公司业务当中,布林甚至亲自下场为谷歌聊天机器人Bard写代码。然而被给予厚望的先发产品 Bard,还是在发布会演示时就立马栽了跟头。

💡 思维链路:Roam Research 的独特价值与使用感受

现在,让我回过头重新选择工具的话,我依然对 Roam Research 寄予厚望。并不是因为我曾经一次性充值了 5 年 believer 的所谓信仰,也不是因为我已经退出微信群运营的 RoamCN 社区,曾经的 #roamcult 社区狂热早已退散。

但如同我的实践感受一样,有一种莫名的集体潜意识让我意识到,社区也在慢慢发现 Roam Research 确实做得更好。并且,使用的人也在慢慢回流,不光是因为他们在 Logseq 或 Obsidian 等其他地方碰了壁,也是因为对笔记本身这件事情的理解。

我确信,如果自己不曾坚持使用 Roam Research,我也不会发现在深层级的功能设计和操作细节之下,隐藏的所谓“思维链路”是多么重要,更无法悟得卢曼与卡片盒“沟通”的真谛。才明白,原来 Roam Research 一直在这里,扮演着最好的“沟通对象”角色,哪怕已经有了 ChatGPT 这样更加智能的“提问对象”。

🤖 ChatGPT vs. 写作:为什么选择卡片写作而非直接问 AI?

相比之下,AI 看似智能的直给,恰恰剥夺了思考的乐趣。直给答案,代表着丝滑,同样也意味着没有摩擦。但由于缺少了必要难度,大脑无法加强存储难度,反而培养了一种遇事不决,先问 AI 的惰性,因为提取难度更高,大脑总是会选择那条最小阻力之路。

「不写,就无法思考。」代表着就是通过写作的方式来刻意制造摩擦,游戏化就是主动克服本来不必要的麻烦,但是麻烦也不能太大,总不能一来就去打终极 BOSS。于是,笔记要选择从写卡片开始,这是给自我反馈的最佳方式,也是滑板鞋在地板上摩擦的乐趣所在,时尚!

我的滑板鞋时尚时尚最时尚

回家的路上我情不自禁

摩擦 摩擦

在这光滑的地上摩擦

🎨 从双向链接到视觉化白板:笔记工具们的新探索

除了笔记要卡片化这一共识以外,大家对双向链接本身的迷思陷入最深,自动化双链后,用户会不自觉地吐槽知识图谱未能成型,笔记厂商们则假惺惺地对图谱做做优化。但除了更难看之外,也开始做起了视觉化白板,便是摸到了新的方向,也将其纳为了商业化变现的 Pro 功能,既引得了用户们的欢呼,更赢得了投资者的期待。

对于视觉化白板,开头提到的那对难兄难弟 Logseq 和 Obsidian 分别称之为 Whiteboard 和 Canvas,简称 WC。我始终不确定白板的价值,只能将其拆分阶段为“小白板”和“大白板”,前者用于灵感阶段,后者用于梳理知识结构?可能吧,只是不同阶段理应使用不同的工具。

如果真的 All in 了视觉化白板,那肯定不得不提 Heptabase,内测一年多了,功能迭代飞速,每周发版从未间断。但不知为何,为了减少摩擦的白板功能,始终摩擦力巨大让人用不起来,这也变成了共识。是不是白板本身就错了呢?

📈 视觉化白板:框架与灵活度如何取舍的平衡之道

关键点在于,“大白板”和“小白板”之间,存在一个概念叫做“框架白板”,白板的灵活度本身就是汝之蜜糖,彼之砒霜。框架对应思维模型,思维导图如果算一种的话,那么它也算是一种自带框架的“白板”。

所以,在框架和灵活度之间取得平衡的,当属最老牌的第二大脑软件 TheBrain,本身就具备了父子兄三种层级的自由链接,还兼顾了大纲和思维导图的模式。更值得一提的是,使用 TB14 版本的用户,依然念念不忘 TB8 版本的扩展视图,可以称之为弥补白板灵活度的天花板,只要梳理过白板上节点关系时,尝试过拖拽节点,就会意识到它的价值所在。

TheBrain 的大纲视图,区别于 Roam Research 的大纲有一个很重要的点,它的连线,本质上是可活动的。连线可变更,所以灵活性高于子弹节点本身的移动。因此,TheBrain 更适合作为知识结构的持续迭代,在在不断梳理的过程中,断开链接,清理那些大脑自以为是的跳跃式联想。

而另一方面,学海无涯生有涯,我只能关注我所能关注的,TheBrain 在帮助梳理个人目标领域(Areas)的同时,不断让自己意识到能力的范围。因为,搭建结构的同时也是在塑造知识的边界,你需要积累更有限,但是更有用的知识卡片。

🔔 后续(可能的🙈)拖更计划:

序章:为什么都 2023 年了,我还在用 Roam Research 写笔记?

✇吕立青的博客

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

作者 吕立青

零、Roam Research 101 系列的初衷

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

欢迎来到 Roam Research 宫殿,Roam 的门槛其实很低,但是天花板特别高

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

Pantheon 罗马万神殿 “天使的设计”

最近一直在思考如何升级漫游研究所 Roam Newsletter(🆓 Substack 邮件订阅)的内容形式,想要更好地组织关于思维工具和知识创造的内容。

而且现在中文社区还没有一份完整的 Roam Research 系列教程,#roamcult 的社区氛围反而又难住了部分初学者。

0.1 Roam 的低门槛和高天花板

关于低门槛的内容,我想仿照 @少楠 的 Flomo 101 学院 做一个 101 系列,包括 Roam Research 入门干货和自我管理与知识创造的基本原则。

因为许多时候我们需要改变的不是工具,而是我们使用工具的思维方式。

突破天花板的事情,作为程序员的我一直都比较热衷,各种主题、插件玩得不亦乐乎,还有跟其他软件之间的互连互通,通过自动化相互联动。

当然,伴随 Roam 101 诞生与成长的,还有 RoamCN 微信群和 #roamcult 社区,我相信讨论能够激发出实际的知识创造的需求,以终为始,你能学到什么?我能得到什么?

0.2 自带方法论的工具才是好工具

我一直想写的都是带有普适性的文章,重点不在于工具,而在于使用工具背后的方法论和相关理论的最佳实践。甚至我有一个认识:自带方法论的工具才是好工具,就像我的 GTD 其实就是跟着 OmniFocus 学的,哪怕我最后并没有使用 OmniFocus。

虽然说一个厉害的人,有自己方法论的人用什么工具都可以,哪怕只是纸笔也能疯狂输出,但问题在于大部分人只是普通人。

工具在一定程度上给普通人赋能,就像自动驾驶等技术革命总是想让我们的生活变得更好,让我们的就业变得更充分,让人类的创造力得到更大的释放,人类没有必要去从事那些重复性的苦力型工作。

0.3 关于 Roam 101 专栏的内容规划

Roam 101 系列的每一篇文章大概会包括 3 个部分:

  1. Roam 官方邮件组的新手教程,我采取的方式是意译而非直译,切换上下文并结合实际使用场景练习(Context 上下文是使用 Roam Research 时最重要的概念)
  2. 结合新出版的《卡片笔记写作法》书中的内容来谈一谈我的个人体会,即关于卢曼的卡片盒笔记法相关读书笔记的摘要(Context 语境同样是 #Zettelkasten 最重要的概念之一)
  3. 我在 Roam Research 中有关「自我管理和知识创造」的最佳实践,就像现在这篇文章就是在 Roam 里面完成了卡片素材收集、大纲组织和正文书写等输出流程。

虽然是 Roam Research 专属,但依旧期望给使用不同工具的你以启示,比如 MOC: Map of Contents - 管理链接而非本体这个概念,让我意识手动创建位置固定的图像的重要性。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

既是我对 Roam Research 自动生成的双向链接 Graph 无用性的反思,也是更重视 Diagram 手动画图功能的开始。更有趣的是《卡片笔记写作法》中也有关于“文字是固定化结构的图像”的描述,也让我对中文方块字有了不一样的认识和理解。

让我和你保持“好奇心”,开始接下来的旅程吧!

一、“罗马不是一天建成的”:每日笔记和页面引用

虽说“罗马不是一天建成的”,但用十天的时间来打造属于你自己的罗马(Roam),听起来感觉怎么样呢?

Roam Research 101 系列就是想要帮助你学习 Roam 的所有主要功能,加入少数派专栏和 RoamCN 中文社区微信群参与互动,我相信你很快就会像 #roamcult 专业人士一样使用 Roam。

唯一的秘诀是:不要退缩,只要开始写!剩下的事情由 Roam Research 来做。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

1.1 Daily Notes 每日笔记

当你打开 Roam Research Graph 数据库时,首先映入眼帘的是 “Daily Notes”:一个空白的页面,上面是今天的日期,比如 [[July 16th, 2021]]。

那么你该怎么做呢?开始写呗!

你可以把 Daily Notes 每日笔记当成每天的独立空间,“每天都是一个崭新的开始”,在这里开始写下你的所思所想和所见所闻。没错,以前你会在其他任何地方,想要记下的任何东西都可以放进来,不要有任何压力:

  • Ideas 以前会记到笔记本里的想法,或者是涂鸦在便签纸上的想法
  • Emails 以前放在草稿箱里的电子邮件
  • Word 文档中的论文或摘要
  • Videos 哔哩哔哩或 YouTube 视频
  • Notes 备忘录,或是保存在 IM 应用中的“文件传输助手” 😉
  • Tasks 你会在手机应用中记录的任务
  • ……

1.2 Outliner “平平无奇”的大纲视图

Roam Research 拥有最好的大纲式编辑体验,我确实太过于喜欢“放大缩小”这个概念,就像摄影镜头焦距变化一样,我可以进入细节,但也可以着眼于全局。

大纲式笔记的子弹形式,加上缩进和不缩进的可能性,可以帮助你轻松构建一些“结构”。但是在最开始的时候,你没有必要按照任何层次或模板进一步组织。

“结构”、“联系”、“模式”将自下而上地出现:而且每天都是新的迭代!

网络化思维的魔力,将会由 [[]] 这个魔力符号来开启…

1.3 Page References 页面引用

将单词或短语(甚至是 Emoji 😍 表情符号!)放在一对方括号 [[]]之间,就可以创建一个指向新创建 Page 页面的[[双向链接]]。

当你通过点击链接进入页面时,你会在页面底部看到,该页面被引用的所有地方。

假设你在不同的情境下创建了[[睡眠]]这个页面:当你早上写日记的时候,当你阅读一篇关于睡眠健康的文章的时候,当你计划下一次自驾旅行的时候。所有这些引用都会以其原始语境(Context)出现在[[睡眠]]页面的底部。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

1.4 还有一些小贴士

  • 在搜索栏中输入 Page 页面名称来快速创建新的 Page 页面。
  • Shift+鼠标 点击一个[[]]链接,可以在右侧 Sidebar 打开一个页面。
  • 通过右键点击每个 Block 前的圆点,可以探索 Block 的格式化(即每个项目的信息)。
  • 输入/触发下拉菜单选择 “Current Time”命令,可以给你的想法快速打上时间戳。

二、《卡片笔记写作法》 读书笔记极简实操

2.1 小挑战一

这里有一个挑战,你可以亲自尝试一下,往后的每一小节都会包含一个复杂程度越来越高的新挑战。

现在开始每天的日记练习,在你的每日笔记中写下 [[晨间日记]] 页面(或任何其他标签)然后添加你的想法。

对于我自己来说,我觉得最有用的是间歇日记,每次停下来就写“间歇日记”:时间戳+感谢+行动。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

未来使人焦虑,过去使人抑郁。可以说是间歇日记的记录方式配合 Roam Research,治愈了我的 抑郁 和 焦虑。 同时,跟大家推荐一下相关的 3 本书:

  • 《为什么佛学是真的》
  • 《无为:自发性的艺术和科学》
  • 《被讨厌的勇气》

读书时最受用的一些概念,你也可以随手记录到 Daily Notes 每日笔记里面,一元化笔记 + 卢曼 卡片盒笔记法,一元化笔记可转化为具体的永久笔记放入卡片盒。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

via @Innis 😉

2.2 闪念笔记、项目笔记、永久笔记 示例

《卡片笔记写作法》所提到的几个概念,跟 Daily Notes 每日笔记最相关的,应该是闪念笔记(Fleeting Notes)。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

对于我来说,如果当下是有足够多的时间并且手边有电脑打开 Roam Research,那就无须闪念;如果当下是在读书的话, 直接记录的就是文献笔记,而读书的时候一定要拿上一支笔写写画画,我目前使用的是 iPad + Apple Pencil 的电子化组合。

  • 闪念笔记(Fleeting Notes)其实是可以不记的,只是信息的提醒,可以用任何一种方式来写,最后会在一两天内被扔进垃圾桶。
  • 项目笔记(Project Notes)只与某一特定项目有关,保存在特定项目的文件夹内,项目结束后可丢弃或存档。比如说,在 Roam Research 里面可以把当前的写作文章作为项目,我可以同时开启多个项目,跟着自己的兴趣走。
  • 永久笔记(Permanent Notes)我更愿意用 Evergreen Notes(常青笔记)来称呼它,这种笔记以一种永久可理解的方式、标准的格式包含了必要的信息,并且永远不会被扔掉。
“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

不过这里只是一个简要的介绍,后续的文章会再结合实例,仔细来讲讲卡片笔记写作法的每一个环节。

2.3 好的工具,帮助你减少对主要工作的干扰

好的工具并不是为你增加更多的功能,而是帮助你减少对主要工作的干扰。

你的主要工作是思考,而 Roam Research 就像一张张白纸,尽可能减少了页面跳转、鼠标移动、材料引用、动画刷新等功能或工具行为对思考的干扰。

对,就是写代码时“手指不离开键盘”般的爽快感。思考无干扰,但能创造无限结构。

同理,一个好的方法论也能帮助你减少对主要工作的干扰。

Zettelkasten 闪念笔记 Fleeting (Literature) Notes 是为了在你忙于阅读时快速捕捉想法,不打断你的阅读流程专注于一段文字。

此时在记录笔记时,要非常简短,要有极强的选择性,要使用自己的文字。

More is unnecessary, less is impossible. 多了没必要,少了不可能。

2.4 什么是原则?书中有哪些受用的原则?

瑞·达利欧在《原则》中传递的最重要的东西是一种以原则为基础的生活方式,是它帮助我们发现真相是什么,并据此如何行动。而其中最重要的便是拥有属于自己的原则,而《卡片笔记写作法》这本书同样帮助我构建了关于知识管理和知识创造的宗旨和原则。

举个例子:在挑选记录闪念笔记的工具时,我历经了从“一个人的 Twitter” 到 Roam Research 自带的 Quick Capture 功能,再到 Drafts vs Flomo 等 App 的漫长过程,最后基于书中“减少选择”的原则由此选定了系统默认的 Notes 备忘录。

特别是 iPadOS 15 为备忘录加入了非常重要的一项新功能 QuickNote 在全局右下角可以快速呼出 QuickNote,用户可以在这里进行手写、打字记录,Quick Note 窗口同样支持 Live Text 快速识别文本的功能,还可以左右轻扫快速浏览不同的 Quick Note。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

如果创建的 Quick Note 存在网页链接或者应用中的文本高亮内容,点击 Quick Note 还可以快速跳转至相应 App 中的特定内容位置快速定位进行查看,也就是说 Apple 在系统层面实现了双向链接,直接做到了天花板级别的用户体验。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

对于使用其他非 Apple 生态系统的同学,也可以基于“减少选择”的原则,推荐直接使用 Edge 浏览器的集锦功能即可。

三、我在 Roam Research 里面的实践

我在之前写过的 Obsidian 文章从卡片链接到大脑联想,基于 Obsidian 的卡片盒笔记法实践中引用过:

「人们高估了独立思考的能力。 没有外部帮助,记忆、思维和推理都会受到限制… …真正的力量来自于设计能够提高认知能力的外部辅助设备。」
—— 唐·诺曼,全球最具影响力的设计师,《设计心理学》的作者,苹果的前用户体验架构师《Things That Make Us Smart 让我们变聪明的科技: 在机器时代捍卫人类属性》

继 Obsidian 之后,Roam Research 是更好的一款能帮我提高认知能力的外部辅助设备,也可以称之为“第二大脑”,我会谈一谈为什么会在接触 Obsidian 之后再次选择 Roam Research 的背后原因。

最重要的原因在于,Roam Research 可以将文本的颗粒度拆分得更细。组块(block)是认知科学上的一个常用概念,而 Roam 每一个文本组块(Block)正是这个概念的具象化。我们都知道,当你记忆一个手机号码时,比如,13912345678,很难直接记住。当你把它拆成139-1234-5678这样三个组块时,就更容易记忆。

3.1 第二大脑的“抽象与再具象化”

《卡片笔记写作法》 在书中提到关于“抽象与再具象化”的片段:

要想与思想共舞,我们首先要通过抽象和再具体化的手段,把想法从原来的上下文中剥离出来。只有通过抽象和再具体化,我们才能将想法应用于独特的且总是不同的现实世界(Loewenstein, 2010)

这句话给我的启示同样是对 Obsidian 能做的事情进行抽象化,根据方法论寻找一款思考工具(A note-taking tool for networked thought)才是我的目的,而 Roam Research 则是符合这一愿景的再具象化,满足我对于笔记工具诉求的最佳输出方式。

我们在做文献笔记,并将其转换成符合卡片盒内上下文形式的时候,同样需要考虑这么做,将原文中的内容进行抽象化,然后再具象化到卡片盒内所关注的主题。

3.2 关注已完成而非未完成

前文我们已经接触到了如何记录间歇日记和读书笔记,不过都是以 Daily Notes 为入口记录的一元笔记。有了这些时间戳的闪念笔记,我通过 [[roam/js]] 插件把 Daily Notes 按时间顺序显示为一个 Calendar 日历视图,用于更好地回顾我过去所记下的笔记。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

这会让你更关注已完成而非未完成。现阶段所有的任务管理工具都是围绕未完成来做的,对于已完成事项的利用价值远远不够。而在 Roam Research 中所记下的间隙日记,你做事情是为了获取洞见,而不仅仅只是把事情做完。

哪怕是 Review,之前的我也总是在回顾那些未完成的项。从而导致自己很焦虑,与此同时我们也总是在自责,回避型人格,中国的传统家庭教育少了太多的鼓励。其实间歇日记或感恩日记,都是让自己重新关注那些已完成的东西,比如 Things 3 里面的 log 日志页面或 Flomo 的热力图 都让自己知道自己有多棒。持续积累素材到自己的数字花园,以此为动力,心情愉悦,马达轰鸣。

3.3 Roam Research 乃写作的“最佳后厨 ”

Roam Research 是文本类知识创作的最佳后厨,大纲式的编辑体验极佳,无往而不利。理想情况下,我认真筛选/整理/管理 Daily Notes 里面的内容放到卡片盒,精心编辑我的收藏内容,其目的都是为了在 Roam Research 里面方便调用,配合 Roam 最佳后厨专心负责拼装。或许,相比之下,我的 DEVONthink 更像是地下室,或者楼阁,一般不会去找,但如果要翻找,也能方便地搜索。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

我的 📝 Zettelkasten 卡片盒

那么接下来我会演示一下如何在 Roam Research 的具体页面中进行输出,只有知识创造才能发挥 Roam Research 的最佳价值,比如以本文的创作过程为例,我会创建一个 [[P/Roam Research 101]] 页面,进入某一个页面再进行写作,可以称之为项目笔记(Project Notes)。

在 Roam Research 里面左侧称之为「主页面」,而「侧边栏」则可以打开多个其他页面,由此进入到第一阶段,在主页面快速记录下自己能够想到的任何相关内容,然后在侧边栏 Sidebar 视图快速打开所需要的所有素材。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

将所有素材摆在一起之后,第二阶段便可以通过 /diagram 打开 Roam Research 内置的Diagram视图对内容进行排序处理,以线性的方式顺序排列组合出文章的大纲。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

通过 Diagram 视图产出大纲

最后的第三阶段,则是通过树状的句法,用线性的文字展开,即通过逻辑语句完整地描述想要表达的内容。至此,Roam Research 帮助我完成了整个写作流程,快写慢改,一篇文章最终新鲜出炉。

「写作之难,在于将网状的思想,通过树状的句法,用线性的文字展开」—— 史蒂芬· 平克
“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

这就是我对于一篇文章的创作流程的理解,即厨房的隐喻,Mise en place,在法语中的意思是指「在烹饪前,将所有的材料准备好,摆放在一个地方,让你一眼能看到」。这样做不仅仅是告诉观众需要哪些食材,更重要的是帮助厨师更好更从容地烹饪。厨房是知识型工作的终极隐喻,因为厨师必须在紧张的时间压力下,将精雕细琢的产品提供给苛刻的观众。

四、结语:知识创造而不是知识管理

4.1 我的 Roam Research 使用体验隐喻之 DIKIWI 模型

我在少数派[Matrix 圆桌 网状结构笔记工具是一阵风吗?](https://sspai.com/post/61886)中就提到了这段 Roam Research 的使用体验隐喻:
  • 你每天都在河边 Daily Note 玩耍,看着河里的水 [[Information]] 哗啦啦地流
  • 你拿出相机拍拍照 Block,每天会有新发现 Zettel,然后带回自己的花园 Digital Garden
  • 花园里种着一颗颗的种子 Theme,有些已经长大了,需要修剪 Iteration
  • 今天很特别,你在河里遇到了你前天拍过照的一条鱼 [[Insight]]
  • 于是你把它带了回去,到花园里摘摘菜 [[Knowledge]],做成一道糖醋鲤鱼 [[Wisdom]]
  • 然后拿出手机拍了张照片,并且分享到了微信群里 [[Impact]]

为什么是拍照呢?我们能看到的能记住的只能是知识的切片 Flash Card,因此要有 Version 即版本管理的概念,而 Roam 恰恰是支持的。我最喜欢 Roam 的一点就是「一处修改,处处更新」,而不断迭代 Iteration 和持续改进才是学习的关键。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

这个隐喻其实就对应着 DIKW 体系,它将数据、信息、知识、智慧纳入到一个金字塔形的层次体系中,从最底层的数据到最上层的智慧,它们之间既有联系,又有区别,同时整个过程也是双向通道,自下而上让数据加工成信息,再提炼为知识,最终形成智慧。也可以是自上而下,通过智慧消费产生知识、信息,最后又可以落到最底层的数据上。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

4.2 “没有写作,就无法思考”

卢曼说,“没有写作,就无法思考”。本质上,思考就是一种链接,而驱动我的原动力就是好奇心和创造点什么。

“创造力只是将事物联系起来。当你向有创造力的人询问他们是如何做某件事时,他们会感到有点内疚,因为他们认为自己并没有真的在创造性地做事,只是看到了一些东西而已。”(史蒂夫·乔布斯)

其实《卡片笔记写作法》对我最大的启发倒不是什么 Process 工作流,而是莫大的一种鼓舞:跟着兴趣走。

“罗马不是一天建成的”:Roam Research 101 系列之每日笔记与页面引用

✇吕立青的博客

如何把 Roam Research 安利给你的另一半? | Roam Newsletter 漫游研究所周报 2021W12

作者 吕立青

Tweet by @Jimmy_JingLv on [[March 13th, 2021]]:

📮 Roam Newsletter 漫游研究所周报 #2021W12


#[[Roam 插件]] 我一直在寻找的 Chrome 插件,支持将 Roam Research 一直悬停在浏览器侧边栏

https://twitter.com/roamresearchFR/status/1370531473594408963


#[[Roam 插件]] #Roam42 支持新的 Jump nav 命令,能够快速跳转到父级或同级相邻 Block

https://twitter.com/roamhacker/status/1370637978121428992


#[[Roam 技术]] 基于 puppeteer 实现的 Roam Research 私有 API

https://twitter.com/artpi/status/1299754234863390720


#[[Roam 技术]] 在 HTML 元素添加 data-link-uid 属性,能够极大地方便开发者对 Block 内容进行查询或修改

https://twitter.com/roamhacker/status/1370864539386970112


#[[Roam 技术]] {roam/render} 将直接支持渲染 React 组件,直接通过 JavaScript 运行代码即可

https://twitter.com/roamhacker/status/1368312682504458243


#[[Roam 周边]] 可视化的数据处理流程,代码和结果共同展现

https://twitter.com/_paulshen/status/1371213300080336897


#[[Roam 用例]] Roam 是专为网状思维而设计的笔记工具,作者的使用经验分享

https://twitter.com/adam_keesling/status/1196864429695987713


#[[Roam 插件]] Roam Research OCR 插件,能够直接对图片内容进行文本识别

https://twitter.com/MitchSchwartzCo/status/1321632468735270914

https://twitter.com/cococ_rr/status/1371333171023855617


#[[Roam 用例]] 每天都是崭新的开始,Daily Notes 使用体验

https://twitter.com/wirtzdan/status/1371352813725487105


#[[Roam 用例]] 在 Roam Research 中学习语言的工作流

https://twitter.com/TheIndLang/status/1371206275774697474


#[[Roam 用例]] 作者指导学员使用 Roam Research 的经验分享

https://twitter.com/rjnestor/status/1371597262426673156


#[[Roam 社区]] Roam Research 生日蛋糕 🎂

https://twitter.com/ahmedlhanafy/status/1371227669858283525


#[[Roam 教程]] Roam Research 系列教程之 3:通过 Roam 在上下文中漫游

https://twitter.com/kvistgaard/status/1370427396478930944


#[[Roam 用例]] Roam Research 健身健康知识公开 Graph

https://twitter.com/tombielecki/status/1371955773434044419


#[[Roam 插件]] #Roam42 #SmartBlocks 新命令,支持垂直和水平的屏幕布局控制,类似 Tmux 的窗口管理体验

https://twitter.com/PascalPrecht/status/1371855289285246977


#[[Roam 洞见]] 不断迭代内容产出的形式:推文 -> 博客 -> 视频

https://twitter.com/cortexfutura/status/1372248564752125955


#[[Roam 用例]] “如何像 Power User 一样进行上下文切换”

https://twitter.com/roamhacker/status/1372279886119907333


#[[Roam 周边]] 梦想中的“文字处理器”,类似语法大爆炸的效果

https://twitter.com/elzr/status/1372391789332725763


#[[Roam 洞见]] Roam Research 就是创作者的“想法乐高”

https://twitter.com/JESSCATE93/status/1372718805957234691


#[[Roam 插件]] 通过 #SmartBlocks 自动移动 Block 的位置

https://twitter.com/roamhacker/status/1372806097933717506


#[[Roam 生态]] 在 Roam Research 里面直接制作和发布 Newsletter

https://twitter.com/roamreads/status/1372585116619317252


#[[Roam 周边]] Workflowy 终于支持图片和文件附件

https://twitter.com/WorkFlowy/status/1372581468770201600


#[[Roam 生态]] zoteroRoam 更新,支持从 Zotero 跳转至 Roam 页面

https://twitter.com/AlixLahuec/status/1372384259969851394


#[[Roam 模板]] 关于会议讨论和项目计划的有用模板

https://twitter.com/henry_cck/status/1372783124451524615


#[[Roam 洞见]] #ADHDd 最佳治疗方案是使用 Roam Research

https://twitter.com/Conaw/status/1263036306654785536


#[[Roam 社区]] 作者分享自己如何把 Roam Research 安利给自己老婆的有趣故事

https://twitter.com/MarcKoenig_/status/1372994139059580932


#[[Roam 教程]] 25 分钟的 Roam Research 技巧分享

https://twitter.com/MarcKoenig_/status/1361457957431615488


#[[Roam 插件]] 把 Roam Research 内容快速变成 Slides

https://twitter.com/danfinlay/status/1373033177707470850


#[[Roam 洞见]] 写作能够有助于研究,让读者和自己都能有所收获,且有更多的突破

https://twitter.com/danfinlay/status/1373033177707470850


#[[Roam 教程]] 用好 Roam Research 20%的基础功能,就能享受 80%的好处

https://twitter.com/pcsoundscaping/status/1373265546637156352


#[[Roam 教程]] 使用间隔重复 SR 进行学习的魔力

https://twitter.com/roamhacker/status/1373750089680875531


#[[Roam 图谱]] 知识管理第二大脑的双向链接动态变化图

https://twitter.com/pietronickl/status/1373632884830056454


#[[Roam 技术]] 从想法到项目,作者分享自己如何创造 Roam Excalidraw 画图插件

https://twitter.com/zsviczian/status/1373712129694560260

✇吕立青的博客

事物间丰富的关联,是我们抽象、感知和思考的重要部分 | Roam Newsletter 漫游研究所周报 2021W11

作者 吕立青

Tweet by @Jimmy_JingLv on [[March 9th, 2021]]:

📮 Roam Newsletter 漫游研究所周报 #2021W11


#[[Roam 教程]] “Blocks in Roam Research” 系列视频,帮助你了解什么是 Block,Page 和 Block 的类型,“Block” 是 Roam Research 的基石,[[Trees in the forest]]

https://twitter.com/roamhacker/status/1369522658677776384


#[[Roam 技术]] Roam Research API 已支持 Write 写操作,从技术上来说可以做很多有趣的插件和集成操作!比如说做一个类似 FlomoPlus 的数据导入插件 github.com/JimmyLv/roam-plus

https://twitter.com/roamhacker/status/1352553777790902274


#[[Roam 历史]] Conor 关于 Roam Research 最初的想法来源,“超链接 Hypertext” 之父 Ted Nelson 上都计划

https://twitter.com/azlenelza/status/1272600877493137408

https://twitter.com/Conaw/status/1369509424654557187


#[[Roam 生态]] Codex 编辑器想要打造的是 知识工作者的操作系统(the Knowledge Worker’s OS)

https://twitter.com/codexeditor/status/1369515923262992384


#[[Roam 技巧]] 通过特定域名查询关键词 {{query: {and: [[ethereum]] {or: “[medium.com](https://medium.com)” “[substack.com](https://substack.com)”}}}

https://twitter.com/RobertHaisfield/status/1343911948526612480


#[[Roam 技巧]] 通过 ``` 查找特定代码段,甚至包括特定语言的代码段

https://twitter.com/RobertHaisfield/status/1369420330968743941


#[[Roam 插件]] 直接在 Roam Research 里面运行 Python 代码, Alt + Enter 可运行当前代码块,Alt + Shift + Enter 可运行当前笔记本中的所有代码块。

https://twitter.com/adam_krivka/status/1367932251959353347


#[[Roam 用例]] 通过 Roam Research 学习希腊语,能够主动发现语言中的常见构造

https://twitter.com/KyleStratis/status/1369378108583927830


#[[Roam 洞见]] Conor 引用孙子兵法的「机不可失」来解释 Roam 为什么没有 Roadmap

https://twitter.com/Conaw/status/1369364863772921862


#[[Roam 社区]] 第二届 #RoamSummit Roam 峰会,美国东部时间 3 月 13 日,星期六,下午 2 点,主题是探索 Roam 编写的书籍

https://twitter.com/RoamBrain/status/1369357631194796040


#[[Roam 插件]] Roam Weather SmartBlock,可动态插入天气卡片,并支持手动更新

https://twitter.com/roamhacker/status/1369274865123999749


#[[Roam 技术]] 使用新的 Filesystem API 可以使 @RoamResearch 能够直接编辑本地的博客 Markdown 文件

https://twitter.com/adam_krivka/status/1368524087639826432


#[[Roam 教程]] 何时在 Roam Research 中使用页面,标签或属性?

https://twitter.com/yak_collective/status/1369230545125793793


#[[Roam 周边]] 使用 flowchart.fun 可以直接通过文本生成 五行 关系图

https://twitter.com/SteveYang331/status/1369543055922126849


#[[Roam 洞见]] 费曼学习法,写作即是思考

https://twitter.com/roamhacker/status/1369667930850537477


#[[Roam 教程]] 使用间歇日记替代 To-Do List 提高效率

https://twitter.com/roamhacker/status/1369917340847116289


#[[Roam 插件]] [[roam/js]] 插件 TODO 爆炸效果,快去把事情搞定吧!

https://twitter.com/artpi/status/1362813924802977797


#[[Roam 洞见]] Roam Research 最棒的一点在于 Composability(可组合性)

https://twitter.com/artpi/status/1358432819027058695


#[[Roam 洞见]] 事物之间丰富的联系(relationships, connections)是人们抽象、感知和思考的重要部分 —— @TedNelson

https://twitter.com/Jimmy_JingLv/status/1369543916832190469


#[[Roam 周边]] 想看看一篇天体物理学论文是如何诞生的吗?

https://twitter.com/realscientists/status/1369995661253480450


#[[Roam 教程]] 60s 极简 Roam Research 教程,第 8 期 Block Reference

https://twitter.com/markmcelroy/status/1370356478599704576


#[[Roam 插件]] 通过 Telegram Bot 自动发送内容到 Roam Research Daily Notes

https://twitter.com/arn4v/status/1370405473367695360


#[[Roam ]] #roamcult 学术界 Discord 社区,Academia Roamana

https://roamresearch.com/#/app/AcademiaRoamana

https://twitter.com/cortexfutura/status/1369740588015378433


#[[Roam 用例]] 在 Roam Research 做计划真的很简单:问自己三个问题:我想完成什么?我将如何开始?有没有什么障碍? [[WOOP]]

https://twitter.com/cortexfutura/status/1370325041578131458

https://twitter.com/cortexfutura/status/1370325020774428675


#[[Roam 生态]]roam.garden 数字花园也将支持 块折叠 功能

https://twitter.com/VladyslavSitalo/status/1370249616885653506


#[[Roam 洞见]] 磨刀记得砍柴

https://twitter.com/760mph/status/1370063171814297600


#[[Roam 教程]] 系列 Thread Tweets —— 什么是 Roam Research 的 Block?

https://twitter.com/roamhacker/status/1364296274468614151


#[[Roam 技巧]] Multibar 可以快速折叠文章段落,Roam 页面标题也可以加上背景图片 :D

https://twitter.com/roamhacker/status/1370041146169294853


#[[Roam 洞见]] 如果页面滚动时,其他部分的文本只是被压缩了 —— 即保持可见状态但不会消失在屏幕外,这样既能突出显示又能直接显示搜索字词的缩略图

https://twitter.com/azlenelza/status/1370159999691919364

✇吕立青的博客

【译】Roam Research 自定义组件 —— 跟 {{roam/render}} 来一次亲密接触!

作者 吕立青
Roam Research 采用的是 Clojure 技术栈的 Datomic/datascript Datalog 数据库,能够将内容同步到不同的设备,并管理非常复杂的撤销操作,还能够支持各种程度的自定义组件和插件功能定制,方便开发者利用 Reagent 渲染组件,并支持与 JavaScript 互操作。本文就将硬核解析 Roam 背后原理,发掘 Roam 基于 Block 的深层技术优势,帮助你迎接 Roam API 时代的到来!
原文地址:A closer look at {roam/render} —— Zsolt Viczián

Swiss Army Knife

Roam 就好像一把优秀的瑞士军刀,竟然包含一个完整的 ClojureScript 开发环境。在过去两周里,我逐渐熟悉了 ` {{roam/render}} `。这篇文章可以算作我自己的笔记,总结整理一下我的所见所学。

这篇文章是针对开发者和 Roam 黑客的。如果你还不属于这些阵营,你很可能会对这些内容感到吃力。

我并不是一个 Web 开发者,React、Clojure、Reagent 和 Datalog 对我来说完全是崭新的事物。我是一个典型的无知者无畏的终端用户,我知道如何构建一个基于宏的庞然大物,但却无法保持其健壮/可测试/可维护。在玩roam/render的过程中,有一次我几乎把我的整个 roam 数据库都毁掉了 —— 这与roam/render无关,一切都是因为我太笨了。请谨慎尝试我的结论和例子,欢迎在评论中分享更好的解决方案,或者联系我进行勘误。

什么是 roam/render?

` {{roam/render}} `是Roam中用于构建自定义组件的原生特性。这些组件可以是简单的表格、计算器、滑块,也可以是复杂的交互式工具,比如数据透视表、图表、流程图等。

roam/render 也是一个使用 Reagent 在 ClojureScript 中实现的 React 组件。你可以使用 Datalog 在 Roam 的 Datomic Graph 中访问你的数据。

其实就在几周前,以上两句话中的每一个单词对我来说都是陌生的。让我们先来快速看看每个词的意思:

  • React是一个用于构建用户界面的 JavaScript 库。*
  • Clojure(/ˈkloʊʒər/, 很像 Closure 闭包的拼写)是一种支持动态类型和函数式编程的 Lisp 方言。和其他 Lisp 方言一样,Clojure 将代码视为数据,并有一个 Lisp 宏系统。*
  • ClojureScript是一个针对 JavaScript 的Clojure编译器。*
  • Reagent是一个 React 的 ClojureScript 封装。它可以帮助你轻松创建 React 组件。Reagent 有三个主要的功能,使其易于使用:使用函数创建React 组件,使用Hiccup 生成 HTML,以及在Reagent Atoms中存储状态。Reagent 可以让你写出简洁可读的 React 代码。*
  • Hiccup是一个用于呈现 HTML 的 Clojure 库。它使用 vectors 来表示元素,使用 maps 来表示元素的属性。* 试着玩一玩HTML2Hiccup,可以帮助你更好地了解 hiccup 的工作原理。另外,你也可以直接在 Roam 中输入 hiccup,只要在一个空块中输入:hiccup [:code "Hellow World!"],看看会发生什么。
  • Atoms是 Clojure 中的一种数据类型,它提供了一种管理共享、同步、独立状态的方法。一个 Atom 就像其他编程语言中的任何引用类型一样。Atom 的主要用途是保存 Clojure 的不可变数据结构。 *
  • Datomic是一种新型数据库。Datomic 不是将数据收集到表格和字段中,而是由Datoms [entity-id attribute value transaction-id] 建立。这种架构为改变和扩展数据库 Schema 提供了高度的灵活性,而不会影响现有的代码。*
  • Datalog是一种基于 Prolog 的声明式、基于形式逻辑的查询语言。*我分享了一些简单的 Datalog 实例,在这里查看如何生成 Roam Graph 有关的基本统计数据。

入门指南

开启自定义组件

自定义组件功能默认是禁用的。在你搞事情之前,你必须在用户设置中启用它。

enable custom components in user settings

Hello World!

你可以通过在 Block 中添加以下代码来嵌入 roam/render: ` {{roam/render: ((block-ref))}} ` 其中 block-ref 指的是你的脚本代码的块引用。脚本代码必须至少有一个函数。如果这个 Block 有多个函数,那么在组件创建时只会调用最后一个函数。

被引用的代码块必须包含一个设置为 Clojure 类型的代码块。代码块的创建是通过使用三个反引号 ` ``` ` 来完成的。如果你不知道键盘上的反引号键在哪里,你也可以使用/菜单,然后选择 Clojure Code Block。

Hello World Roam Render Demo

一步一步来

  1. 在一个新的 Block 中输入: {{roam/render:((...))}}
  2. 当你开始在双括号之间打字时,Roam 会弹出 “search for blocks”。选择 “Create as block below”。这将在你当前 Block 下面创建一个 Block,并自动在括号之间放置一个块引用。
  3. 导航到新的 Block,并添加两个额外的反引号``来创建一个代码块。
  4. 将代码的语言设置为 Clojure。
  5. 最好给每个组件都有自己的命名空间(ns myns.1),虽然 Hello World 没有它也能工作。这将为你在以后的探索省去很多头疼的问题,因为当同名的函数相互竞争时,调试会让人非常头大。我遵循一个简单的模式,按顺序给我的测试命名空间编号。当我准备好了一个组件,我才会给它自己合适的命名空间。
  6. 代码块应该至少有一个函数:(defn hello-world [])
  7. [:div"..."] hiccup 这种写法等价于 <div>Hello World</div>
(ns blogpost.1)
(defn hello-world []
  [:div "Hello World"])

恭喜你!你已经使用 ClojureScript 成功创建了你的第一个 Roam 组件!现在你应该看到 Hello World 出现在代码上方的 Block 中。

接收 input 参数

当你开始为实际用例构建组件的时候,你很快就会意识到,如何知道当前组件正在运行的 Block 的 ID 呢?你需要这个 ID 来创建能够感知上下文的组件,即它们需要知道自己选择在哪个 Page 上,上级或上级有哪些 Block 等等。

我花了好几天的时间才发现如何做到这一点。我想让你省点力气! 当一个组件被调用时,Roam 会把 block-id 作为第 0 个参数传递给主函数(注意:你代码块中的最后一个函数)。当然,Roam 还会传递当你调用组件时,所输入的其他任何参数。

在新的 Block 输入下面的脚本,将产生以下输出。(当然,请注意,当你在自己的 Graph 中尝试这个脚本时,Block 的 ID ((Vy8uEQJiL))会有所不同,所以对应的 block-uid “eR7tRno7B “也会不同。)

{ { roam/render: ((Vy8uEQJiL)) 10 "input 1" ["input" "vector" "with" 5 "elements"] {:key1 "this is a map" "key2" "value 2" :key3 15} (1 2 3) #{"a" "b" "c"} } }
datatypes / arguments example

这里的内容很多。让我们借此机会学习一下 Clojure 数据结构。我正在向自定义组件传递六个参数。除了这六个参数之外,Roam 默认将 block-uid 作为第一个参数传递。如果你想深入了解 Clojure 数据结构,我推荐这篇文章

  • 第 0 个参数是block-uid。它是作为一个单一元素的 map 传递的。
  • 接下来,我添加到组件的第一个参数是一个 integer 整数。我可以简单地把它作为一个数字传递给组件。
  • 然后是一个 string 字符串。Clojure 只接受双 “引号” 标记的字符串。单个 ‘引号’(单引号)有不同的含义。使用'会产生一个不被计算的形态,下文还会提到如何在 datalog 查询中使用单引号。
  • 第三个输入参数是一个 vector。请注意,在 Clojure 中,你可以用空格隔开一个 vector 的多个元素。这个 vector 总共有四个元素,三个字符串,一个整数。
  • 接下来是一个有三个 keys-values 的 map。你可以使用 “string” 作为 key,就像在 JavaScript 对象中一样,然而,Clojure 还提供了使用:keywords 作为 key 的方式。关键词以 : 冒号开头。你可以使用 , 逗号来分隔 key-value 键值对,但这不是必须的。注意在输入中我没有使用逗号。
  • 第五个参数(Arg 4)是一个 list 列表。列表的主要用途是表示未被计算的代码,在你进行元编程时,用来编写生成或操作其他代码的代码。
  • 最后一个参数是一个 set 集合。set 和 vector 很像,关键的区别是 set 中的每个值都是唯一的。同样根据设计,set 中每一项的顺序是任意的。

代码本身是自解释的。你应该注意到我是如何使用[:b],相当于<b>...</b> 用来表示粗体文本。clojure.core/map-indexed将把输入向量 args 中的每个元素传给匿名函数fn[i n],其中i是索引号,n是正在处理的 vector 的当前元素。

(ns blogpost.2)
(defn main [ & args]
  [:div
   [:b "Number of arguments received: "] (count args)
   (map-indexed (fn[i n] [:div [:b "Arg " i ": "] (str n) ]) args)])

现在我们知道第 0 个参数总是带有 block-uid 的 map,我们可以稍微修改一下我们的代码,将 block-uid 接收到一个专用变量中。这是我在所有组件中使用的 “standard” 主函数声明。 (defn main [{:keys [block-uid]} & args]。将前面的例子用标准 main 函数稍微改写一下,就会变成这样。

(ns blogpost.2)
(defn main [{:keys [block-uid]} & args]
  [:div
   [:b "The block-uid is: " ] (str block-uid) [:br]
   [:b "Number of arguments received: "] (count args)
   (map-indexed (fn[i n] [:div [:b "Arg " i ": "] (str n) ]) args)])

一个简单的 Reagent 表单

Reagent 是表单交互的关键。它们允许你根据用户输入的数据,动态地改变组件的 HTML 渲染,无论是组件本身还是在 Graph 中的 Page 页面。

为了理解下一个例子,你首先需要了解 Clojure 变量。Clojure 中的所有变量都是不可变的,这意味着它们是作为常量创建的。变量可以有不同的作用域,也就是说,你可以在命名空间的层次上定义变量,或者只是在一个函数中定义变量。但是一旦设置,它们的值就不能改变。

有几个方法可以绕过这种不可变性。其中之一是通过使用 atoms。 atom 的工作原理我就不深究了(顺便说一句,就算我想解释也解释不清楚),Atom 就是对一个值的引用。使用 swap! reset!可以把这个引用改成一个新的值。在clojure.core/swap!中,你可以在给 Atom 分配新的值之前访问它的前一个值,在clojure.core/reset!中,你只需给 Atom 分配一个新的值,而不用去管它的前一个值。

roam/render中,我使用reagent.core/atom而不是clojure.core/atom。Reagent atoms 与普通 atoms 完全一样,只是它们会跟踪derefs(即访问它们的值)。Reagent 组件如果 derefs(解引用)发现其中的值发生变化,就会自动重新渲染。在实践中,这就意味着你可以根据数据的变化动态地改变组件。

我们将构建一个简单的表单,只包含一个输入字段。当你在 Graph 中输入一个页面的名称时,这个表单就会返回该页面的 UID。例如,你可以用这个来创建一个直接超链接到你的页面,URL 格式为 https://roamresearch.com/#/app/YOUR-GRAPH/page/9char-UID

(ns blogpost.3
  (:require
   [reagent.core :as r]
   [roam.datascript :as rd]))

(defn query-list [x]
  (rd/q '[:find ?uid .
          :in $ ?title
          :where [?e :node/title ?title]
                 [?e :block/uid  ?uid]]
        x ))

(defn main []
  (let [x (r/atom "TODO")]
    (fn []
     [:div
     [:input {:value @x
      :on-change (fn [e] (reset! x (.. e -target -value)))}]
     [:br]
     (query-list @x)])))

有几件事值得解释一下:

  • 请注意命名空间声明下的:require块。这些是我们代码中引用的命名空间。我稍后会分享一个简单的组件,你可以用它来探索 Roam 中所有可用的命名空间。使用:as你可以指定一个别名来引用你代码中的命名空间。
  • query-list函数中,我们使用 roam.datascript(rd) 命名空间中的q函数执行一个简单的数据记录 query。请注意查询'[:find ?uid .开头的单引号 '。另外,请注意?uid后面的点 .。这个点 . 将查询的结果转换为一个标量。我在这里使用它,是因为我想要找一个单一的 uid 值作为函数返回值。
  • 依然是 query-list 函数,请注意我们没有在函数的结尾有一个类似于 JavaScript 的 return的语句。在 Clojure 中,函数中最后一次调用的结果总是被返回。由于query-list只包含一次调用,所以在执行 query 时,将直接返回rd/q的结果。
  • 请注意,在main函数中,x 被定义为一个初始值为 “TODO” 的 Reagent atom。将声明放在(fn[]后面的匿名函数之外,可以确保在创建组件时,只会设置一次 atom,而不是在每一次往 INPUT 框输入任何文本时,都将其重置为默认值。

以上是由Conor的一个例子改编的。你可以在 Roam help 数据库 这里 找到 Conor 的版本。我的解决方案和 Conor 的解决方案之间的一个关键区别是,他使用的是roam.datascript.reactive,而不是仅仅使用roam.datascript。在这个具体的例子中,从我的理解来看它们是没有区别。如果我的理解是正确的,Datascript reactive 提供了一种创建查询的方法,当其结果集发生变化时,它可以自动识别。它们可以用于创建交互式组件,比如 {{table}}

如何存储组件的属性值

当你重新打开页面或关闭并重新打开 Roam 时,组件会被重新初始化。在大多数情况下,你会希望以某种方式存储组件的属性,这样当你下次打开页面时,你会发现组件处于你离开时的状态。

你可以选择将信息写入 Block 中,并决定写入哪一个 Block。你可以写到嵌套在组件下的 Block ,或者写到一个工具页面上的 Block 里面,例如[[my component/data]] 或者更新执行组件的那个 Block。最后一种选择涉及更新 {{roam/render: ((block-UID)) }} 与 input 参数,这与我们在前面的例子中打印 input 参数的方式有些类似。我将在一个非常简单的例子中演示如何做到这一点。

顺便说一下,我用datascript.core做了一个实验,将自定义实体写入到 Roam 数据库。我也能够执行查询,但是没有找到一种方法让 Roam 保存更改。手动编辑自定义实体到 EDN 文件中是可行的(演示),所以使用datascript添加自定义实体应该也是可行的。

(ns blogpost.4
  (:require
   [roam.datascript :as rd]
   [roam.block :as block]
   [clojure.string :as str]))

(defn save [block-uid & args]
  (let [code-ref (->> (rd/q '[:find ?string .
                              :in $ ?uid
                              :where [?b :block/uid ?uid]
                         	     [?b :block/string ?string]]
                            block-uid)
                      (str)
                      (re-find #"\({2}.{9}\){2}"))
        args-str (str/join " " args)
        render-string (str/join ["{{roam/render: " code-ref " " args-str "}}"])]
    (block/update
      {:block {:uid block-uid
               :string render-string}})))

(defn main [{:keys [block-uid]} & args]
  (let [some-value [1 2 "string" {:map "value"}]]
    [:div [:strong "args: "] (str/join " " args) [:br]
     [:button
      {:draggable true
       :on-click (fn [e] (save block-uid some-value))}
      "Save"]]))

这个组件将保存 some-value 的值。在这个例子中,为了简单起见,它是硬编码的,但当然,你可以构建任何你想要的数据结构来代替 some-value。请注意以下几点:

  • 我的 save 函数是在 main 函数中按钮的:on-click事件中调用的。在我的实验中,每次组件改变其值时自动调用 save 的效果并不好,因为每次覆盖 {{roam/render: ((block-UID))}} 的时候,组件都会重新初始化,使得无法填写表单,或者通过交互的方式使用组件。
  • 我在 save 函数中定义了三个变量。code-refarg-strrender-string
  • code-ref将保存 block-string 的当前值,因为我在执行 datalog 查询的时候,会读取:block/string属性的当前值,并通过block-uid过滤。
  • ->>是一个函数,它通过一组 (str)(re-find) 的形式来执行表达式。它的唯一目的是让代码更易读。
  • re-find中的正则表达式会找到 {{roam/render: 后面的((block-UID))
  • 一旦render-string准备好了,我就调用roamAlphaAPIblock/update 将 Block 更新为我的 string 文本。

下面是这段代码的运行情况:

Saving state

似乎还有一种方法,可以使用 Reagent 在 Form-3 Components 中所提供的:component-will-unmount事件处理器,来触发保存组件。虽然我还没有尝试过这种方法,但根据文档,这应该提供了一种方法来存储组件的属性,当你导航到不同的页面时,它就会从当前视图中消失。如果你对此感兴趣,你可以阅读这里

如何与 Javascript 互操作

ClojureScript 提供了一种调用 JavaScript 函数的简单方法。当你想访问 DOM document 属性或函数时,这就非常实用了。比如调用 roam42 函数或创建一个 JavaScript 钩子来处理roam/render组件的数据(可以节省你学习 Clojure 的时间)。

第一个例子是返回页面的 location 位置。

(ns blogpost.5)
(defn main []
  [:div (. (. js/document -location) -href)])

请注意 JavaScript DOM document 是如何使用js/document访问的。

属性是用-property符号来访问的。用于访问对象属性的 JavaScript . 点符号,需要转化为嵌套的小括号,每个括号都调用下一个属性或函数。

使用->可以使代码更易读,特别是在属性链特别长的时候。

(ns blogpost.6)
(defn main []
  [:div (-> js/document
          (.-location)
          (.-href))])

现在,我们将再做一遍上面的 Reagent 表单简单示例 (blogpost.3),但是将(defn query-list [x]替换为 JavaScript 来执行我们的 datalog 查询。

javascript interoperability

下面是 ClojureScript 的代码。

(ns blogpost.7
  (:require
   [reagent.core :as r]))

(defn main []
  (let [x (r/atom "TODO")]
    (fn []
     [:div
     [:input {:value @x
       :on-change (fn [e] (reset! x (.. e -target -value)))}]
      [:br]
      (.myDemoFunction js/window @x)])))

以及 ` {{roam/js}} ` 的代码。

window['myDemoFunction'] = function (x) {
  return roamAlphaAPI.q(`[:find ?uid .
                          :in $ ?title
                          :where [?e :node/title ?title]
                                 [?e :block/uid ?uid]]`, x);
}

如果你想把多个变量传给 JavaScript,你可以在函数调用(.myFunction js/window variable-1, @an-atom, block-uid)之后简单地列出这些变量。

Roam 已有的命名空间

roam/render 中有以下命名空间。

  • Clojure: cljs.pprint, clojure.core, clojure.edn, clojure.pprint, clojure.repl, clojure.set, clojure.string, clojure.template, clojure.walk。
  • Roam: roam.block, roam.datascript, roam.datascript.reactive, roam.page, roam.right-sidebar, roam.user, roam.util。
  • 其他: datascript.core、reagent.core。

这段代码将列出每个命名空间中所有可用的函数。

(ns blogpost.8)

(defn main []
  [:div (map (fn [x] [:div
              [:strong (pr-str x)]
              [:div (map
                     (fn [n] [:div (pr-str n)])
                     (->> (ns-publics x)
                          (seq)
                          (sort)))]])
          (into [] (->> (all-ns)(map ns-name)(sort))))])

Tips 和技巧

一些有用的链接

下面是我自己写的一些组件,因为我在继续深入研究roam/render。请在你的测试/开发 Graph 中尝试它,一定要格外小心。虽然我相信这些例子,应该可以在不损坏你的数据库的前提下工作,但如果出了问题,我概不负责哦……

你也可以随意浏览我的 Graph,里面有很多有关 Clojure 和 Reagent 成功和不成功的尝试。我会尽量把成功/不成功的解决方案都标出来。

调用组件

` {{roam/render: ((block-UID))}} 引用组件的形式不是很友好。它需要点击几次才能得到 block-uid` 并将其插入到渲染的 Block 中。我发现了两种选择。

你可以在 roam/templates 中创建一个简单的本地漫游模板,然后在那里用一个友好的名字添加你的组件。然后,当你需要它时,你可以使用;;template-name将组件插入到你的文档中。

你也可以 Hack 一下 block 的 UIDs。我在这里提供了一个解决方案。这将允许你用更友好的名字来创建组件,比如我的 query 查询组件。 ` {{roam/render: ((str_query))}} `。

前置声明

这个让我头疼了几个小时。 Clojure 采用的是单通道(single-pass)编译。这意味着,如果一个函数在声明之前被调用,编译器会抛出一个错误。在某些情况下,不可能按照函数的使用顺序来声明函数,比如在涉及两个函数的迭代中。这时clojure.core/declare就很用了,你可以做一个正向声明,编译器就会知道,在这之后会有这个函数的定义。

输出 Roam 的原生链接

如果你正在构建组件,当你想输出 Roam 原生链接时将会遇到一个问题。你可以直接点击链接,就会打开页面。你也可以shift-click的链接,就会在侧边栏中打开页面。我发现有三种方法可以实现输出这种可工作的链接。

最简单的方法是使用roamAlphaAPI创建一个 Block,并在这个 Block 字符串中放置一个page linkblock-ref。这将转换为一个正确的 Roam 链接。

另一种方法是创建一个包含 Roam 查询的 Block,并在查询参数中包含你想显示的链接: {{query: {or: page linkblock-ref}}

由于 Roam 在几天前发布了roam.right-sidebar命名空间,现在可以完全模仿 Roam 原生链接了。我还没有时间去试验这第三个选项,但它看起来是可行的。

如果你对此感兴趣,不妨到我的Graph中看看,也许从写这篇文章的时候起,我已经实现了解决方案。 我将创建一个响应点击事件的 Reagent 组件。在点击时,我将把浏览器导航到一个类似于下面 JavaScript 例子的链接。在shift-click时,我将使用roam.right-sidebar来打开链接。

function getRoamLink(uid) {
  if (window.location.href.includes('/page/'))
    return window.location.href.substring(window.location.origin.length + 1, window.location.href.length - 9) + uid
  else
    return (
      window.location.href.substring(window.location.origin.length + 1, window.location.href.length) + '/page/' + uid
    )
}

Debug 调试

我使用的是以下代码进行调试。通过将 slient 开关转为 true,我可以禁用console.log调试信息。可能无需多言,input 参数只是为了这个例子。

Debug demo
console.log
(ns blogpost.9)

(def silent false)
(defn debug [x]
  (if-not silent (apply (.-log js/console) x)))

(defn main [{:keys [block-uid]} & args]
  (debug ["(main)  block-uid: " block-uid "  args: " args])
  [:div "debug demo"])

一些限制

基于 Roam 是一个笔记应用这个前提,拥有 ClojureScript 环境当然很牛逼。但是,就像瑞士军刀的微型锯子一样,在某些情况下,它是很方便的。但如果你想砍一棵树,请去买一把电锯。

roam/render 有两个巨大的限制。它没有文档,更重要的是,它缺乏适当的调试工具。它确实会将一些错误信息输出到控制台日志中,如果有一致的调试信息,就有可能追踪到错误,但它严重缺乏基本的调试功能,如 breakpoints、watches 等等。当你排除 ClojureScript 代码故障时,就会比本来要花费的时间要长很多。

比较好的一点是,你可以将代码分别放到兄弟层级的 Block,这样就可以更好地添加注释了,利用中间的 Block 加入流程图、解释等,有点类似于 Python 中的 Jupiter Notebook。但是,你无法将可能频繁使用的函数,放到同一个共享的命名空间,以便在组件之间重用。我尝试将引用共享名称空间的 Block 作为组件的同级 Block,但不起作用。一个变通的办法是把所有组件的源代码放在同一个页面上,并放到同一个命名空间中。这样做是可行的,但如果你有很多组件,恐怕会变得很乱。这也会让你很难跟其他人共享组件。

Datascript 也有一些限制。你不能创建自己的转换函数。你不能在 Roam 数据库中创建自定义实体。还有一些 Clojure 命名空间和函数缺失。例如,clojure.string/lower-case无法使用。我想使用小写字母来支持对大小写不敏感的搜索,幸运的是,实际情况下配合 clojure.core/re-findclojure.core/re-pattern(?i) flag 有一个可以变通的方法。

最终结论

在笔记应用中拥有一个 Clojure 执行环境绝对是很酷的。但是,如果没有相应的调试工具和文档,开发组件的效率其实非常低。

比较好的一点是 roam/render 和 ClojureScript 可以用来输出渲染自定义组件,比如表单、数据透视表和其他交互工具。

基于我现有的知识,其实与 JavaScript 互操作似乎是最好的方式。你可以使用 roam/render,顾名思义,渲染输出的部分,然后你可以使用 JavaScript 来构建应用程序的逻辑部分。如果这样做的话,你就可以同时得到两个世界里最棒的部分了。你可以在 Reagent 中轻松地渲染响应式组件,这样就是在一个真正的开发环境中,使用满足需求的调试工具完成大部分的开发工作。此外,你可以使用你已经熟悉的语言进行开发(假设你已经具备了 JS 技能),你还能够在 JavaScript 中创建可重用的代码,这样还可以跟其他人共享复用代码。

✇吕立青的博客

为什么说 Roam 远不只是一个笔记应用 | Roam Newsletter 漫游研究所周报 2021W10

作者 吕立青

Tweet by @Jimmy_JingLv on [[March 2nd, 2021]]:

📮 Roam Newsletter 漫游研究所周报 #2021W10


#[[Roam 插件]] 增强版 PDF 扩展,作者分享了他的学术阅读工作流

https://twitter.com/cococ_rr/status/1366654227506991105


#[[Roam 用例]] 学术论文阅读的五个步骤:文献管理、论文阅读与瞬时笔记写作、永久笔记写作、永久笔记管理、笔记回顾

“双向链接的自动呈现大大简化了我的工作流,也加强了我构建网状知识库的意识。 ”

https://twitter.com/Jimmy_JingLv/status/1366674785636208644 %}


#[[Roam 特性]] 右上角添加了可调整内容区域大小的按钮

https://twitter.com/roamhacker/status/1368969582917132289


#[[Roam 生态]] @Airtable 标语:我们需要更多的软件开发者,而不仅仅是用户

https://twitter.com/airtable/status/1305491922258071554


#[[Roam 技术]] Roam Research 面试题之一就是打造一个小型 Excel

https://twitter.com/Conaw/status/1332814451368624129


#[[Roam 周边]] Readwise + Roam 集成的演示视频

https://twitter.com/RoamResearch/status/1367976244403261444


#[[Roam 特性]] Roam Research 删除页面时,提示当前页面的引用数量

https://twitter.com/roamhacker/status/1368171019861647360


#[[Roam 技术]]@craftdocsapp 写作软件的实现非常优秀,基于 macOS Catalyst 跨平台原生应用

https://twitter.com/andy_matuschak/status/1324769586697269249


#[[Roam 插件]] 跨 Block 思维导图的概念实现,配合 Stick Blocks Map 插件使用很有 Ginkgo 那个感觉

https://www.craft.do/s/wzKthqKQEz5NoM/b/451806EB-72C0-40E5-AE80-6D2AD32222E5/Block_Map

https://twitter.com/roamhacker/status/1367367351369400320


#[[Roam 周边]] 直接从大纲式纯文本生成 Graph 图例

https://twitter.com/SteveYang331/status/1367432144654495751


#[[Roam 洞见]] #知识管理 魔幻新词语

https://twitter.com/Jimmy_JingLv/status/1367394277530230791


#[[Roam 周边]] 可直接与 Roam Research 交互的播客笔记软件

https://twitter.com/jgoodhcg/status/1364225811709120512


#[[Roam 书籍]] 使用 Roam Research 进行专业的项目管理

https://twitter.com/Roamfu/status/1358766013215502340


#[[Roam 特性]] Roam Research 新增命令面板功能,可直接通过 Cmd+P 快速执行命令

https://twitter.com/roamhacker/status/1367354454316621824


#[[Roam 插件]]roamjs.com 新扩展:Timeline 模式 [[roam/js/timeline]] 可用来可视化特殊标签的事件时间线,比如 #Deadline

https://twitter.com/dvargas92495/status/1367249990171656192


#[[Roam 课程]] 学术笔记课程正式开课

https://twitter.com/LaptopLifeLisa/status/1367113625022967809


#[[Roam 特性]] Roam Research 官方内置的自动备份功能,再也不用担心数据丢失啦

https://twitter.com/Raz0r/status/1367308205701926913


#[[Roam 生态]] Flomo Plus 支持多种国内常用软件的笔记或高亮内容

https://twitter.com/plidezus/status/1367297192763879425


#[[Roam 用例]] 使用 Roam Research 网格 CSS 制作漫画

https://twitter.com/z9sx7wox/status/1366825424932790272


#[[Roam 洞见]] 记忆 vs 理解

https://twitter.com/visualizevalue/status/1366812844050759684


#[[Roam 技术]] 【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

https://twitter.com/Jimmy_JingLv/status/1369206733122367489

✇吕立青的博客

【译】深度解析 Roam 数据结构 —— 为什么 Roam 远不只是一个笔记应用

作者 吕立青
随着 Roam Research 的大热,双向链接和基于 Block 的笔记软件层出不穷,而他们(葫芦笔记logseqAthens)无一例外都采用了 Clojure 技术栈的 Datomic/datascript Datalog 数据库,这不免让我感到好奇想要深入探索一番。本文就将硬核解析 Roam 背后原理,发掘 Roam 基于 Block 的深层技术优势,帮助你迎接 Roam API 时代的到来!
原文地址:Deep Dive Into Roam’s Data Structure - Why Roam is Much More Than a Note Taking App —— Zsolt Viczián

你想不想知道以下问题的答案?

  1. 你的 Graph 笔记库中最长的段落是那一段?
  2. 上周你编辑或者创建了哪些页面?
  3. 你的笔记库中总共有多少段文字?
  4. 在某个给定的命名空间下你总共有哪些页面?(例如:meetings/

(译注:问题 1 可通过 Roam Portal Chrome 插件 查看,问题 2 可查看 #Roam42 DB Stats,但本文将帮助你深入理解插件背后的原理。)

Roam Research 是一个全功能型数据库,相信你已经用上了 ` {{query:}} ` 的查询方法,但其实远不止如此,你还可以问它更多的问题。这篇文章会让你对 Roam 的底层数据结构基础有一个很好的理解。

上周我一直在深入研究 Roam 的数据结构,玩得非常开心,也学到了很多。这篇总结写给我自己,也分享给你,我尝试通过写作来加深我对 Roam 的理解程度。如果你发现这太过于技术向了,很抱歉,我会尽力用一种容易理解的方式来传达信息,从最基本的概念慢慢过渡到更为复杂的概念。

在我的探索过程中,我还构建了一组用于查询的 SmartBlocks,和相应的几个查询示例,你可以在这里找到它。虽然你不一定想要了解具体细节,但也会发现这些例子非常有趣。

随着我的深入,我对 Roam Research 的赞叹就更进一步,我也愈发相信 Roam 一定能够占领市场。在不久的将来,Roam 将以全文的形式保存你我所读到的一切:笔记、书籍和文章摘要等等,都将能够方便地追溯其原始出处,只需在一个系统中点击即可访问。“Roam” 未来可期!

我的文章也参考了很多极具价值的文章和参考资料。我特别想分享以下的几篇文章,如果你读了我的概述发现还想了解更多,那我强烈建议你继续探索:

而这篇文章,将会提到以上文章都没有涵盖的两个点:

  1. 对 Roam Research 数据结构的详细讨论,包括非常基础和复杂的介绍
  2. 一套基于 #42SmartBlocks 可以在 Roam 中执行高阶查询。如果你对基础部分不感兴趣,想要直接看 SmartBlock 部分的话,点击跳转

让我们开始吧!期望你像我一样享受这次旅程!

基本概念

Roam 基于 Datomic 数据库构建。简而言之,一个 Datom 是一个独立的 fact,它是一个带值的属性,包括四个元素:

  • Entity ID 实体 ID
  • Attribute 属性
  • Value 值
  • Transaction ID 交易 ID

你可以把 Roam 想象成一组扁平化的 Datoms 集合,看起来就像这样:

[<e-id>	<attribute>	<value>			        <tx-id>  ]
...
[4 	:block/children	5 			            536870917]
[4 	:block/children	9 			            536870939]
[4 	:block/uid 	    "01-19-2021" 	        536870916]
[4 	:node/title 	"January 19th, 2021" 	536870916]
[5 	:block/order 	0 			            536870917]
[5 	:block/page 	4 			            536870918]
[5 	:block/parents 	4 			            536870918]
[5 	:block/refs 	6 			            536870920]
[5 	:block/string 	"check Projects"    	536870919]
[5 	:block/uid 	    "r61dfi2ZH" 	    	536870917]

共享相同事务 id 的 Datoms 就是在同一个事务中添加的。其中,这种基于事务的方法使 Roam 能够将内容同步到不同的设备,并管理非常复杂的撤销操作。

具有相同 entity-id 的 Datoms 就是同一个 Block 的 facts。

如果你想基于块引用来查询一个 Block 的 entity-id,你就可以写:

[:find ?e-id
 :where
 [?e-id :block/uid "r61dfi2ZH"]]

从上面的数据可以看出,这个查询将返回值 5。

Attributes 属性

Roam 使用 :block/ 属性来存储关于段落(paragraphs)页面(pages)的 facts。页面段落之间有一些细微的差别,我在一分钟内就会解释。但是,你必须理解的基本概念是,页面只是一种特殊类型的块(block)。大多数情况下,Roam 将页面(page)段落(paragraph)一视同仁。两者都是 Blocks

区块 Block 的两个 ID

  • Hidden ID 隐藏 ID:

这个 entity-id 才是真正的 block-id,即使它在 Roam 用户界面是看不到的。这是用于将数据库中的信息绑定在一起的 ID。Entity ID 标识了有关 Block 的 facts,描述了父子层级关系和对 Block 的引用。

  • Public ID 公共 ID:

公共 ID 是段落(paragraph)的块引用,例如 GGv3cyL6Y,或者是页面(pages) 的 Page Title(页面标题)。请注意,页面(pages)还有一个 UID,长度也为九个字符串 —— 非常类似于块引用。例如,你可以使用它们来构造指向 Graph 中特定页面的 URLs。

(译者注:比如现在这篇文章在 Roam Research URL 中的 /page/mdz6JIWDD https://roamresearch.com/#/app/Note-Tasking/page/mdz6JIWDD

所有区块的公共属性

每个块都有以下属性:

  • :block/uid 公共 ID,即 9 个字符长的块引用
  • :create/email 创建块的作者 Email 地址
  • :create/time 以毫秒为单位的时间,纪元计时(1970 年 1 月 1 日 UTC/GMT 午夜)
  • :edit/email 编辑该块的作者 Email 地址
  • :edit/time 最新一次块的编辑时间
[10 :block/uid		"p6qzzKa-u"     536870940]
[10 :create/email	"foo@gmail.com" 536870940]
[10 :create/time	1611058803997   536870940]
[10 :edit/email		"foo@gmail.com" 536870940]
[10 :edit/time		1611058996600   536870949]

森林中的树木(Trees in the forest)

Roam 数据库就像一片森林。每一页都是一棵树。树的根是页面(page),树的枝干是更高层次的段落(paragraphs);树的叶子(block)就是嵌套在页面(page)最深层次的段落(paragraphs)

Page
* Branch
  * Branch
    * Leaf
    * Leaf
  * Leaf
  * Branch
    * Branch
      * Leaf
* Branch
  * Leaf
...

对于每个段落(paragraph),Roam 总是创建两个指针。 子级 Block 使用:block/parents 引用其父级的 entity-id,父级则使用: :block/children 引用其子级的 entity-id

[4	:block/children	5	536870917]
[5	:block/parents	4	536870918]

父级会在 :block/children 属性中保留其子级的列表。这个列表只会包含其直系后代的 entity-id,而不包括隔代的孙辈。一个 Page 只会将 Page 顶层的段落(paragraphs)作为子段落列出来,而不会列出嵌套的段落(paragraphs)。类似地,段落将只列出嵌套在它下面的块(block),而不是嵌套在嵌套块下面的块。嵌套中最低层级的 Block 块(叶子)则没有 :block/children 属性。

子级同样会在 :block/parents 属性中保留其父级的列表。与 :block/children 相反的是,父级列表包括所有祖先的 entity-id,即祖父母、曾祖父母等。嵌套的段落(paragraphs)将包含对父段落(paragraphs)页面(page)的引用。页面的顶层段落(paragraphs):block/parents 属性中具有页面(page)entity-id,而嵌套在另一段落下的段落(paragraphs)将具有更高层级段落的 entity-id 和当前页面(page)entity-id

页面 Page 独有属性

所有的页面都有标题属性,而没有任何段落会有标题。

如果要查找数据库中的所有页面,则需要查询 :node/title,因为此属性只包含页面的值。通过执行以下查询,你将得到一个包含两列的表格:?p 参数下每个页面的 entity-id?title 参数下每个页面的标题。

[:find ?p ?title
 :where [?p :node/title ?title]]

如果你还希望查到每个页面的九个字符的 UID,例如,要构造指向该页面的链接,则需要通过 :block/uid 属性来查找 ?p entity-id。下面是 query 查询语句的样子。注意 ?p 是如何出现在 where 子句的两种模式中的。这告诉查询引擎查找同一实体的 titleuid

[:find ?p ?title ?uid
 :where [?p :node/title ?title]
        [?p :block/uid ?uid]]

段落 Paragraph 的独有属性

每个段落都有以下属性:

  • :block/page 页面上的每个段落,不管它们的嵌套级别如何,都会引用他们的页面entity-id
  • :block/order 这是页面中块的顺序,或者是段落下嵌套的级别。你需要对这个值进行排序,以便按照适当的顺序检索出现在文档中的段落
  • :block/string 块的内容
  • :block/parents 段落的祖先们。对于顶层段落,它就是当前的页面。对于嵌套的段落,该属性会列出通向(包括)页面的所有祖先。

其他可选属性

Roam 只会在你改变特定块的默认值时才会设置这些属性(只存在于数据库中的段落),例如,你将块的文本对齐方式从左对齐改为居中。

  • :children/view-type 指定如何显示块的子元素。可识别的值是“列表”模式、“文档”模式、“编号”模式
  • :block/heading 你可以将块的标题级别设置为 H1、 H2 或 H3。允许的值是 1,2,3
  • :block/props 这是 Roam 存储图像或 iframe 的大小、slider(滑块)的位置、 Pomodoro 番茄计时器设置等信息的地方
  • :block/text-align 段落对齐的方式。值为“左”、“中间”、“右”、“对齐”

Roam 数据结构

如果你想知道如何查找数据库中存在哪些属性,我有一个好消息!使用一个简单的查询,你就可以列出数据库中的所有属性:

[:find ?Namespace ?Attribute
 :where [_ ?Attribute]
[(namespace ?Attribute) ?Namespace]]

以下就是所有属性的列表。说实话,上面的查询不会对值进行排序,也不会创建最后一列。我在可下载的 roam.json 文件中包含了稍微高级一点的查询版本,它将可用于排序。我在 clojure.core 文档中找到了namespace 函数。

Namespace Attribute :Namespace/Attribute
attrs lookup :attrs/lookup
block children :block/children
block heading :block/heading
block open :block/open
block order :block/order
block page :block/page
block parents :block/parents
block props :block/props
block refs :block/refs
block string :block/string
block text-align :block/text-align
block uid :block/uid
children view-type :children/view-type
create email :create/email
create time :create/time
edit email :edit/email
edit seen-by :edit/seen-by
edit time :edit/time
entity attrs :entity/attrs
log id :log/id
node title :node/title
page sidebar :page/sidebar
user color :user/color
user display-name :user/display-name
user email :user/email
user photo-url :user/photo-url
user settings :user/settings
user uid :user/uid
vc blocks :vc/blocks
version id :version/id
version nonce :version/nonce
version upgraded-nonce :version/upgraded-nonce
window filters :window/filters
window id :window/id

Queries 查询

如果你对如何编写 Roam 查询语句感兴趣,那么你应该仔细阅读 Learn Datalog Today 的九个章节。它的内容非常有趣,且包含对应的练习。

接下来,我将几乎逐字逐句地引用教程中的几段话,当然会改变例子以适用于 Roam。其余的内容,请访问上面的教程。

我还推荐以下 Stuart Halloway 的 YouTube 视频,它在 11 分钟内总结了 Datomic Datalog 查询语言的关键特性。

核心概念

查询是一个以 :find 关键字开头的矢量,后面跟着一个或多个模式变量(以 ? 符号开头,e.g. ?title)。find 子句之后是 :where 子句,它将查询限制在与给定的数据模式(data patterns)相匹配的 datoms 上。而使用 _ 符号作为通配符,则表示你希望忽略的数据模式部分。

例如,如果你想根据一个段落的块引用来查找文本,你需要这样写:

[:find ?string
 :where [?b :block/uid "r61dfi2ZH"]
        [?b :block/string ?string]]

根据本文章开头表格里面的例子,这个查询将返回 “Check Projects”

这里需要注意的是,模式变量?b在两个数据模式中都会使用。当一个模式变量在多个地方使用时,查询引擎要求它在每个地方都绑定为相同的值。因此,这个查询只会找到具有 uid r61dfi2ZH的块的字符串。

一个实体的 datoms 可能出现在不同命名空间的属性中。例如,如果我想找到包含r61dfi2ZH段落的页面的标题,我会编写以下查询。请注意,我首先读取页面的 entity-id?block/page 属性,并将其存储在 ?p 当中。 然后,我用它来定位页面的 ?note/title?block/uid

[:find ?title ?uid
 :where [?b :block/uid "r61dfi2ZH"]
        [?b :block/page ?p]
        [?p :node/title ?title]
        [?p :block/uid  ?uid]]

考虑到上面的例子,这将返回 “January 19th, 2021”“01-19-2021”

:in 子句为查询提供了输入参数,这与编程语言中的函数或方法参数的作用非常相似。以下是上一个查询的样子,注意其中有一个用于 block_reference 的输入参数。

[:find ?title ?uid
 :in $ ?block_ref
 :where [?b :block/uid ?block_ref]
        [?b :block/page ?p]
        [?p :node/title ?title]
        [?p :block/uid  ?uid]]

这个查询需要两个参数。$就是当前数据库本身(隐含值,如果没有指定:in子句),block_ref则可能是段落的块引用。

你可以使用 window.roamAlphaAPI.q(query,block_ref); 执行上述操作。如果没有为 $ 提供值,则查询引擎将隐式假定的是默认数据库。因为你将只查询你自己的 Roam 数据库,所以没有必要声明数据库。也许一旦 Roam 提供了跨数据库的链接,这将会变得非常有趣!

现在我将跳过本教程,以涵盖在 Roam 中稍有不同的几个主题。如果你对你错过了什么感兴趣,请阅读我跳过的详细教程。有一个关于元组、集合和关系(Tuples, Collections, and Relations)非常有用的讨论,它们提供了执行逻辑 OR 和 AND 操作的方法。

Predicates 断言

断言子句可以过滤结果集,只包括断言返回 true 的结果。在 Datalog 中,你可以使用任何 Clojure 函数或 Java 方法作为谓词函数。根据我的经验,在 Roam JavaScript 的实现中,Java 函数是不可用的,只有少数 Clojure 函数可以使用。

除了clojure.core命名空间之外,Clojure 函数必须是完全命名空间限定的。遗憾的是,在核心命名空间之外,我只找到了几个在 Roam 中能用的函数。这些函数包括clojure.string/includes?clojure.string/starts-with?clojure.string/ends-with?。另外一些来自核心命名空间的有用函数包括,返回属性命名空间的 namespace 和返回字符串长度的 count。一些无处不在的断言,也可以在没有命名空间限定的情况下使用,比如<, >, <=, >=, =, not=, !=等等。

这里有两个使用断言的例子。第一个函数指的是根据 block_reference 计算段落中的字符数。

[:find ?string ?size
 :in $ ?block_ref
 :where [?b :block/uid ?block_ref]
        [?b :block/string ?string]
        [(count ?string) ?size]]

第二个列出了在给定日期之后修改的所有 Blocks。

[:find ?block_ref ?string
 :in $ ?start_of_day
 :where [?b :edit/time ?time]
        [(> ?time ?start_of_day)]
        [?b :block/uid ?block_ref]
        [?b :block/string ?string]]

Transformation 转换函数

遗憾的是,我无法让转换功能在 JavaScript 中工作。只有当您在桌面上安装了 Datalog 数据库,并加载 Roam.EDN 进行进一步的操作时,这些功能才有可能工作。

唯一可用的变通方法是在查询后对结果进行后处理。下面的例子将过滤页面标题,以大小写不敏感的方式查找文本片段 (“temp”),然后按字母顺序对结果进行排序。此查询将返回包括 “Template”、”template”、”Temporary”、”attempt “等词的页面。

let query = `[:find ?title ?uid
              :where [?page :node/title ?title]
        	     [?page :block/uid ?uid]]`;

let results = window.roamAlphaAPI.q(query)
                     .filter((item,index) => item[0].toLowerCase().indexOf('temp') > 0)
                     .sorta,b) => a[0].localeCompare(b[0];;

Aggregates 聚合

Aggregates,则可以像预期的那样工作。有许多可用的 Aggregates,包括sum、max、min、avg、count。你可以在这里阅读更多关于 Aggregates 的信息。

例如,如果你不知道某个属性的用途,或者不知道允许使用哪些值,只需查询数据库就可以找到现有的值。下一个例子列出了:children/view-type的值。需要注意的是,如果你只在 Graph 中使用 bullet 列表模式,查询将只返回一个值:”bullet”。我使用了独特的 Aggregates 函数,如果没有这个函数,我将得到一个可能有数千个值的列表,每个指定了视图类型的块都有一行。

[:find (distinct ?type)
 :where
 [_ :children/view-type ?type]]

Rules 规则

你可以将查询的可重用部分抽象为规则,给它们起有意义的名称,然后忘记其实现细节,就像你可以使用自己喜欢的编程语言编写函数一样。

Roam 中一个典型的规则例子是祖先规则。这些规则利用:block/children来遍历嵌套块的树。一个简单的祖先规则是这样的。这条规则基于 ?parent entity-id 来寻找 ?child

[[(ancestor ?child ?parent)
 [?parent :block/children ?child]]]

第一个矢量称为规则的 head,其中第一个符号就是规则的名称。规则的其余部分称为 body。

你可以用(...)[...]将其括起来,但常规的做法是用(...)来帮助你的眼睛区分 head 和 body 的规则,也可以区分规则调用(rule invocations)和正常的数据模式(data patterns),我们将在下面看到示例。

你可以将规则看作一种函数,但请记住,这是逻辑编程,因此我们可以使用相同的规则,根据子 entity-id 找到父实体,根据父 entity-id 找到子实体。

换句话说,你可以在 (ancestor ?child ?parent) 中使用?parent?child作为输入和输出。如果你既不提供值,你将得到数据库中所有可能的组合。如果你为其中一个或两个都提供值,它将如你所期望的那样限制查询返回的结果。

[:find ?uid ?string
 :in $ ?parent
 :where [?parent :block/children ?c]
        [?c :block/uid ?uid]
        [?c :block/string ?string]]

现在就变成了:

[:find ?uid ?string
 :in $ ?parent %
 :where (ancestor ?c ?parent)
        [?c :block/uid ?uid]
        [?c :block/string ?string]]

:in子句中的%符号代表规则。

乍一看,这似乎并不是一个巨大的成就。但是,规则是可以嵌套的。通过扩展上面的规则,你可以使它不仅返回子树,而且返回?parent下的整个子树。规则可以包含其他规则,也可以自己递归调用。

[[(ancestor ?child ?parent)
 [?parent :block/children ?child]]
 [(ancestor ?child ?grand_parent)
 [?parent :block/children ?child]
 (ancestor ?parent ?grand_parent)]]]

例如,我们现在可以使用这条规则来计算一个给定块的所有子孙数量。

window.roamAlphaAPI.q(`
     [:find ?ancestor (count ?block)
      :in $ ?ancestor_uid %
      :where  [?ancestor :block/uid ?ancestor_uid]
              [?ancestor :block/string]
              [?block :block/string]
    	      (ancestor ?block ?ancestor)]`
      ,"hAfIHN6Gi",rule);

当然,在这个例子中,我们最好使用:block/parent属性,这样可以使查询更加简单。

[:find ?ancestor (count ?block)
 :where  [?ancestor :block/uid "hAfIHN6Gi"]
         [?ancestor :block/string]
         [?block :block/parents ?ancestor]]

Pull 拉

这篇文章已经太长,而且技术性太强。出于这个原因,我完全省略了关于(pull ) requests 的讨论 —— 尽管在 roam.json 中的例子中,我将会提到一部分。(pull ?e [*])是一种强大的从数据库中获取数据的方法。如果你想了解更多,这里有两个值得阅读的参考文献。

Datomic Pull in the Datomic On-Prem Documentation

Introduction to the Roam Alpha API on Put Your Left Foot.

Roam 查询 SmartBlock

我们可以在 SmartBlocks 内和浏览器中的开发者工具控制台中运行查询。然而,结果很难查看,因为它们是以嵌套的 JSONs 等晦涩的数据结构返回的。

2021 年 1 月 28 日更新:

同时我了解到,你也可以在 Roam 中使用块中的 :q 命令原生运行简单的查询。试试下面的命令:

:q [:find(count ?t):where[_ :node/title ?t]]

它不会像我的 SmartBlock 一样显示拉动或有页面链接,但仍然非常酷……

2021 年 2 月 22 日进一步更新

我使用 :q 创建了一个长长的统计查询样本清单。你可以在这里找到它们。


我想让查询体验更加方便,并将其集成到 Roam 中。因此,我创建了一组 SmartBlocks,它们可以帮助将查询嵌入到你的 Roam 页面中,就像你在文档中包含的任何其他组件一样。

这里是可以导入到 Roam Graph 中的 DatomicQuery.JSON 文件链接。包括两个页面,SmartBlocks 和大量查询示例。继续阅读,可以了解如何使用它们。

你可以选择简单查询和高级查询。简单查询不接受输入参数,也不能包含规则。当然,你可以直接在查询中包含输入参数,你可以在下面的例子中看到。而高级查询则可以给你更充分的灵活性。

页面链接与日期链接

我做的 SmartBlock 可以把 query 查询结果格式化变成表格。它使用::hiccup在单个 Block 中返回结果,这样就可以避免在 Graph 中创建不必要的 Block。还有个额外的好处是,我加上了一些简单的显示逻辑,将页面标题(page titles)转换为可点击的页面链接(URL),可以将时间转成相对应的 Daily Notes 页面的链接。

要使用页面链接功能,你需要以一种特殊的方式生成查询:

  • 通过在字段名后面添加:name来指定标题字段,例如:?title:name
  • 将 uid 放在紧跟?title:name字段的后面,并在字段名的末尾加上:uid,指定相应的 uid。例如:?title:uid
  • 在字段末尾添加:date,指定一个你想转换为 Daily Notes 页面链接的字段,例如:?time:date
[:find ?title:name ?title:uid ?time:date
 :where [?page :node/title ?title:name]
        [?page :block/uid ?title:uid]
        [?page :edit/time ?time:date]
        [(clojure.string/starts-with? ?title:name "roam/")]]
Query example

Pull 表达式

SmartBlock 还会将嵌套的结果显示为一个表格,在表格里显示得更整齐。当你执行包含(pull )语句的查询时,它的结果其实是一棵树,而不是一张表。所以我按照下面的逻辑来呈现查询结果。

  • 我会把结果集的顶层显示为表格的行,值为列。
  • 结果集中的嵌套层会交替以列或行的方式呈现。
  • 为了避免结果集过大,MAXROWS 默认设置为 40。在高级查询中,你可以更改这个数字。
  • 在嵌套层,我使用 MAXROWS/4 来限制显示的行数。即使这样设置,生成的表格也可以达到数百行。(40x10x10x…)

这是一个 (pull ) 结果所显示的表格。只拉取 1 个层级的深度:

Pull example - 1 level deep

拉取 2 个层级的深度:

Pull 2 levels deep

Query 查询模板

要为你的查询生成模板,请运行相应的 Roam42 SmartBlock

  • Datomic simple-template 简单模板
  • Datomic advanced-template 高级模板

一旦准备好你的查询,只需按下嵌套在查询下的按钮即可执行。

结束语

经过一周的时间,我还没有成为这方面的专家。如果我写的东西很傻,比如我的查询或 SmartBlock 有错误的话,请告诉我。你可以在下面的评论中联系我,或者在 Twitter 上@zsviczian

另外,我很想了解你是如何使用从这篇文章中学到的知识,以及如何使用 SmartBlock 的。请分享你的想法和成果。谢谢你!

  • 没有更多文章
❌