因为太忙,鸽了几个月,现在把这个系列收个尾。

进阶篇介绍了 Trilium 的核心使用方法后,这篇文章将涉及 Trilium 中需要一定编程和折腾基础才会用到的功能,包括大家很关心的自建服务器同步和移动端。

最后,经过这几个月的深度使用,以及参考前两篇文章的反馈,我收集了一些常见的坑和小技巧,在此分享出来,希望能帮到遇到同样问题的人。

自建服务器远程同步

为了在多台机器间同步,并使用移动端,我们需要一个自己的 VPS 或类似的东西,并在上面搭建 Trilium 同步服务。此处仅分享我自己的配置,我用的是手动安装版本,另有打包版、docker 版等等,不在本篇介绍范围内,大家可以自行尝试。

另,这一节大量摘抄我在别处发布的 Trilium 配置笔记,所以如果看到遣词造句完全一样的文章不要惊慌,那也是我写的,自己抄自己。

参考资料:

环境:

  • vultr 底配( $5/mo,还跑着其他服务,绰绰有余了;觉得贵的,可以把它和梯子等服务都搭在一台 VPS 上)
  • Ubuntu 19.10

首先安装 nodejs 10.x,版本必须在 10.5 之后。然后根据官方指南安装所有依赖:

sudo apt install libpng16-16 libpng-dev pkg-config autoconf libtool build-essential nasm libx11-dev libxkbfile-dev

然后克隆最新稳定版源码并安装 npm 包:

git clone -b stable https://github.com/zadam/trilium.git
cd trilium
npm install

这期间我遇到过一个报错:

g++: fatal error: Killed signal terminated program cc1plus

没有搜索到什么解决方法,某个回答说是内存不够,再次运行 npm install 就好了。

到这一步,原本直接运行就可以,但因为这个服务器上还运行了别的应用,我打算把它部署在子域名下,所以需要配置一下端口和反向代理。如果你没有这个需求,以下三段可以不看。

我安装的是 nginx,因为我对运维也是一知半解,别的服务器怎么写配置文件,我暂且蒙在鼓里。总之,在 /etc/nginx/sites-available/ 下建立 trilium,并写入如下内容:

server {
    listen    80;
    listen    [::]:80;
    server_name     www.example.com; // 自定义域名
    location / {
        proxy_pass http://127.0.0.1:8080/; // 自定义端口
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

注意 proxy_pass 必须用 http 协议,否则会 502。

不知为何,我的 /home/[user]/ 目录下并不存在官方说的 .local/share,因此只能自己手动建立一个 data directory:

cd [你要的目录]
mkdir trilium-data
export TRILIUM_DATA_DIR= ~/trilium-data // 设一下环境变量

然后把 config.ini 复制过来:

cp <dir>/trilium/config-sample.ini ~/trilium-data/config.ini

把这个 config.ini 里的端口改掉,就齐活了。

现在可以运行:

cd trilium
nohup node src/www

nohup 就是 no hangup,这样即使用户登出,还是能在后台运行服务。在这一步又踩了两个坑,一个是没装 sqlite3,于是手动 npm install sqlite3;另一个是提示 DB uninitialized,只要配置正确后跳转到 /setup 登录完成初次使用导航就好了。

登录和同步完成后,又发现一个问题,就是加载实在太慢。可以调整一下 payload 大小限制,在 server{} 里加上:

client_max_body_size 0;

其实我不太确定这一行是不是真的有用,但这么做之后,加载速度可以接受了,就姑且认为灵验吧。

最后,照例 certbot 配置 SSL 证书,就完成了。考虑到看到这一步的朋友多少有些建站基础,在此不赘言。

打开客户端,在「Options/Sync」面板填写远程服务器地址和超时时间(我填了 5000),然后保存,Test sync 看看能不能连上。成功之后,直接用手机登录网页,就可以享受移动端体验。

需要备份时,备份之前建立的 trilium-data 目录即可。

欢迎大家也分享自己的部署经验,给后来人多一点参考和选择。

自定义插件

由于我没有时间钻研写复杂脚本,只改过几个官方插件,本文仅对 Trilium 可以实现的脚本功能进行介绍,抛砖引玉。

前文提到,Trilium 用 attributes 系统来管理各种文件,调度插件脚本。所谓的插件,就是文档树里的一个 js 代码笔记,因此一旦建立了同步,这些插件在各个平台(包括网页、手机)上都可以起效,无需反复安装(在这一点上 Obsidian 不免相形见绌)。

Trilium 给插件提供了前端和后端两套 API,前端在浏览器中运行,主要就是改改界面元素,获取当前笔记之类的;后端则在 node.js 中运行,涉及到数据操作,比如创建笔记、父笔记/子笔记、获取 Label、加密等。根据我的观察,两者之间有一些通用的接口。但在创建 js 代码笔记时,Trilium 会要求你注明是前端还是后端。

然而,仅仅是添加脚本文件还不够。Trilium 不可能在启动时自动运行文档树里所有 js 文件,那既不安全也不合理。所以我们需要告诉 Trilium,这个脚本要在启动时运行。方法是:给脚本文件添加 attribute run,值为 frontendStartupbackendStartup

为了获得更直观的认识,我们来找几个例子。

最简单的插件类型可能就是按钮了——右上角默认有一排快捷按钮,如点击「Today」可以新建或跳转到当天的 day note。我们来看看它的实现原理:

api.addButtonToToolbar({
    title: 'Today',
    icon: 'calendar',
    shortcut: 'ctrl+shift+d',
    action: async function() {
        const todayNote = await api.getTodayNote();

        await api.waitUntilSynced();
        
        api.activateNote(todayNote.noteId);
    }
});

这段代码给 addButtonToToolbar (顾名思义,给工具栏加个按钮)API 传了一个对象,它由四部分组成:title,按钮文字;icon,按钮图标;shortcut,快捷键;action:点击按钮后的动作,这里面又调用了一堆 API,暂且不作解释。

(真不敢相信,我居然在这篇文章里讲解代码,但来都来了……)

聪明的读者们读到这里应该明白了,我们不用很累很麻烦或懂得 js 代码,就可以通过模仿官方插件做出自己想要的功能。折腾的秘诀,就在于缝缝补补又三年。例如,我现在想做一个快捷键,点击后可以跳转到某个指定的页面,该怎么做呢?

我给出的答案,是大胆抄袭 Weight Tracker 按钮——这个按钮的功能,恰好是跳转到体重统计页面。很好,我们来看看这个按钮的 action 部分是怎么写的:

action: async () => api.activateNote(await api.startNote.getRelationValue('targetNote'))

我可以不懂 js,也可以不懂那堆 async/await 是什么意思,但我大胆推测,startNote 应该就是跳转到笔记吧。那么,它到底跳转到了哪个笔记呢?脚本告诉我们,它也不知道,它要去笔记里面找一个叫 targetNote 的 relation,然后跳转到它指向的笔记。

果然,笔记的 attributes 窗口赫然写着:

@targetNote=Weight Tracker

这说明,我们只要复制这个 Button 脚本到别的位置,改动它的 targetNote relation 指向的笔记,就可以通过快捷键跳转到该笔记了。

以上是一些适合初涉脚 zhe 本 teng 的使用者的插件魔改思路,当然,掌握了全套 API 后,你也可以写出像官方提供的统计和 GTD 插件一样复杂的脚本,甚至对 Trilium 数据库增删改查(api.sql),一切皆有可能。

官方插件 showcase 在这里,默认笔记里也有一份,可以自行观光。

理论上,你甚至可以写自己的 widget,比如字数统计插件。但我复制了官方的插件,照葫芦画瓢操作一番,并没有成功显示出来,所以很疑惑这个功能是否实装了。如果有尝试成功的朋友请告诉我。

另外,开发者提醒大家,API 还在实验阶段,随时会天翻地覆,所以谨慎使用。

静态页面部署 (Render note)

这一节需要一些基本的有关网页的知识。如果对网页的实现原理不了解,简单来说,浏览器负责把 html、css 样式、js 脚本等纯文本文件「解读」成我们看到的样子。

Trilium 是一个基于 electron 的应用,所以视窗相当于一个浏览器,自然可以渲染静态的 html 页面。官方提供了一些范例,例如用 chartjs 实现的体重变化图表:

实现的原理是,在日记模板里定义一个叫「weight」的 attribute,每天填写体重数据。这个页面会通过前文提到的 API 抓取这些数据,然后绘制成图表。

图表页面所在的笔记是一种叫做「render note」的特殊笔记类型,要让上述内容显示出来,要经过如下几个步骤:

  1. 把要显示页面的笔记改成 render note 类型
  2. 新建一个 html 笔记(code note),在里面写静态页面的布局
  3. 新建一个 js 笔记,同样写好抓取体重和绘制图表的代码
  4. 在 render note 上定义一个叫 renderNote 的关系,并指向刚才新建的 html 笔记
  5. voilà!

这个功能的文档对文件之间的引用关系语焉不详(也有可能是我没找到),所以我凭着自己的经验,并参考官方插件,总结了几条:

  • css 可以独立于 html,在全局范围内起效,只要 class 名唯一,且拥有 @appCss 属性
  • js 必须是 html 的子笔记,否则无法拿到 html 里的元素
  • js 可以引用外部库,只要把整个库保存到 Trilium 文档树里

自定义 API

这是一个非常神秘而强大的功能,我还没有找到使用的机会。根据文档的意思,你可以在 Trilium 里定义一个 REST API。什么概念?这意味着 Trilium 可以和其它 app 打通,例如给 slack 写一个 bot,调用你的 Trilium API,输入命令给某个笔记添加子笔记,等等。

要制作自己的 API,首先,建立一个 js 代码笔记,给它添加 customRequestHandler 标签。这个标签的值是一个正则表达式,用来匹配 API 端点的名称(如果你看到这里已经晕了,不用勉强,直接跳过吧)。使用正则表达式的原因是,可能客户端要请求一些带参数的路径,比如官方范例里的 notes/123 ,用正则就可以匹配到所有 notes 后面带数字编号的路径,从而返回对应的笔记。

API 的实现细节也不赘述了,请直接参考上述范例。除了添加笔记之外,可以通过 API 实现分享单篇笔记、甚至接通 gatsby 等静态网站生成器发布博客的功能。但要注意的是 authentication 需要自己实现,防止滥用。

先想到这么多,会慢慢增补。

  1. 导出:Trilium 可以导出 html,markdown 或 opml。当年我写大纲的时候,html 导出还有一个十分蛋疼的问题,就是只能生成 tar 压缩包。而在 windows 上主流解压缩软件解开这个包,非 ascii 标题的文件都会出现乱码,因为它用了 Pax headers(我不知道这是什么,但它确是问题所在)。好在提 issue 之后,开发者加入了导出 zip 的功能,所以这个坑算是填上了。
  2. 根据 Issue #855,以及之前文章评论里的反馈,markdown table 粘贴导入有问题,至今仍未修复。
  3. 同步:我曾经遇到过一次数据库损坏,发生在下载使用后几天之内。当时不太理解同步机制,妄想把服务器上的备份同步到本地,万万没想到本地会先同步到服务器,导致服务器反被污染。所幸我把数据库备份在坚果云上,利用坚果云自带备份找回了。这类问题正确的解决思路,是先找到本地的数据库备份(备份位置详见基础篇),然后抢救最近一份快照。不要试图用远程服务器来恢复本地数据。
  4. 由于 CKEditor 只能富文本编辑,很多格式无法像 Typora 一样精确控制,再加上缺乏快捷键和数学公式,老实说,不太适合需要复杂格式的文本编辑。所以我非常节制格式的使用。
  5. 之前有人提到笔记里添加的属性被清空的问题,很有可能是链接类属性(如果不是的话,请另外详细描述情况)。这似乎是设计上有意如此——超链接只能自动添加,不能手动。但在 0.42 版本后作者幡然醒悟,取消了自动更新链接的功能,所以应该是可以手动添加了吧(我这台电脑上的客户端仍未更新,暂时没法测试)。

工作流分享

目前,我个人的工作流完全基于 day note。本来我把网页剪辑都放在一个固定的 inbox 下,后来发现这么做不利于回想当时的上下文,所以改成了默认的方式——剪藏到 day note 下面。

平时,我会写零碎的读书笔记和文本段落,这些统一收纳于 day note 下,晚上整理时,再分门别类 clone 到各自的主题笔记。

这么做的好处是:

  1. day note 给这些笔记提供了丰富的上下文信息,展示出时间维度上的关联,便于回忆;而且,比起复杂的条件搜索,几乎不需要任何理解和检索成本,只要点击「Today」按钮或者查看知识图谱就能看到。
  2. 方便利用碎片时间收集长篇笔记的材料。我后来知道,这差不多就是 Zettelkasten 和 evergreen note 提倡的方法。

day note 的模板我也小作修改,加入了一个链接分区,用于保存当天看到的一些网页书签,日后可以重读。

为了方便收藏书签,我参考这个 gist,做了一个可以复制当前网站标题和链接为 markdown 的 js bookmarklet(方法很简单,添加浏览器书签,填写按钮名字,然后把代码复制到网址栏保存即可),然后在 Trilium 里「paste as markdown」。自从用了这个小按钮,我的心得是——太香了!请有类似需求的诸位务必试试!

另外,在 day note 里 @ 一个项目页面,可以理解为「今天我做了这项工作」。点进该页面的知识图谱,可以直观地看到有哪些 day note 和它关联。如果有空做个插件的话,还能把这份数据可视化为图表,记录项目进度。

每日工作流差不多就是这样,我也不是什么魔鬼,没那么多时间折腾,笔记软件还是趁手最重要。如果大家有独树一帜的工作流,请不吝分享。

Zettelkasten

最近 Zettelkasten 成了一个很火的概念。莫名其妙的是,我去年九月在网上随机冲浪,就误打误撞冲到了它的官网上,围观了一番,走了。当时,我完全没想到它在 2020 年会火爆如斯,wiki 类笔记工具也有死灰复燃烈火燎原的趋势。

虽然这一切似乎都有赖于 roam,我正式实践 Zettelkasten,却是受到了 TiddlyWiki 的影响(其实,填这个坑的动力也是因为想尽早开始写一篇 TW 相关的文章)。有段时间,我试用了下 TW 的双向链接版本 TiddlyBlink,发现 TW 早在十几年前就有了时间戳笔记的概念,而有了双向链接后,我可以做到:

  1. 先建立一个空白主题页面
  2. 每次有空就写点卡片式的时间戳笔记
  3. 这些笔记会按时间顺序显示在主题页面里

没错,我现在的 Trilium 工作流就是这么来的。而在这之后我才具体地了解了 Zettelkasten,不得不说真是该死的甜美。它解决了一个至关重要的问题,就是如何化零为整,从底部慢慢建构笔记,用收集的原材料创造未曾有过的想法——而不是先预设好笔记的结构,再进行填充。我大概花了两年时间,才在用 workflowy 的时候想通这一点,找到了适合自己的笔记方式:在一个由碎片构成、缓缓旋转的星辰之海里漫游、连接、组合。

在 Trilium 里实践这个概念并不难,因为快捷键 alt+t 可以直接在标题里插入当前时间戳。接下来的事情就无比自然,利用 clone,我把碎片组合成各种各样的专题,让笔记真正为自己所用。

除了用 clone 整理小纸条外,另一个在 Trilium 里使用 Zettelkasten 的技巧,是「间接引用」。例如,对于网页剪藏下来的内容,可以在时间戳笔记里 @ 它,记录心得体会。这样就能在知识图谱里看到自己对一则素材在不同时间点产生的想法。

和 obsidian 的对比

之前有朋友评论,希望能对比两个软件。嗯,我确实也试用了 obsidian……

先回答评论里提的问题吧,Trilium 能不能模仿链接到块的效果,据我所知并不能。 Trilium 本身并不是一个以双向链接为卖点的笔记软件,它只是在知识图谱里显示出和当前笔记相关的一些笔记(如果点进 show full 再调高显示的笔记上限,可以看到并不是和这则笔记直接相关的笔记也包括在内)。另外,理论上,在 Trilium 里可以写迷你笔记(块),再用 Book View 统一浏览。也就是说,手动调整笔记粒度。但因为无法同时看到多条笔记(而 TiddlyWiki 可以),这个功能略鸡肋。

说回 obsidian。这个软件很有潜力,只要它能做到编辑器所见即所得。它采用纯 markdown 单文件格式,这意味着一个 obsidian vault 可以直接喂进静态网站生成器里(比如这个超赞的 digital garden generator),变成博客或别的形式。obsidian 是输入输出兼备的利器,而 Trilium 更适合做输入和消化端(考虑到极其方便的剪辑功能,和颇费一番功夫的导出)。

 和 Trilium 的全能相比,obsidian 似乎更想「做加法」,提供最基础的功能,然后用插件慢慢堆上去,粗粗看了下甚至有渲染成幻灯片、记忆卡的插件。和 Trilium 的一体化相比,obsidian 是一个非常灵活松散的工具。举个例子,你甚至可以在一个 vault 的子目录下面打开另一个 vault。vault 根本上是本地文件系统的浏览器,加上了插件后,它还可以是自定义标记语言的渲染器。我觉得它的野心很大,可能要像 devonthink 一样,朝管理整个文件系统发展。而无论是 Trilium 还是 TiddlyWiki,甚至 roam,都是沙盒式的知识管理软件,在自己的小天地里玩耍。

选择一个笔记软件,往往就是选择了一种工作方式,强行魔改自带功能可能并不是一个好想法。obsidian 的潜力就在于,它就像用户 DIY 组装机,可以自由选配自己需要的插件。我比较期待它能有分享插件配置文件、一键安装的功能,因为现在自带的插件配置起来已经很麻烦了。

最后说说两个软件在设计和使用思路上的共同点:它们都要求用户决定一则笔记在文档树中的位置。不像 roam 和 TW 建立的笔记可以遗世独立,抛在脑后,这个选择新建位置的设定(至少对我来说)是额外的心智负担。在 Trilium 里我用 day note 来绕开这一层,而 obsidian 则可以用它内建的 Zettelkasten 插件,把笔记临时丢在一个「盒子」里。

很长的后记

  1. 有些朋友问,“可不可以建一个群”,我当然是支持的,只是从来没有建这种讨论群的经验,第一步就遇到了问题:把群建在哪里?微信和 telegram 都需要先拉人,所以看起来 QQ/slack 是比较可行的选择。后者有点太臃肿,我倾向于 QQ。希望大家能提点建议,或者在评论区讨论一下方案,我们就把群建起来。
    7.23 edit: 没事了,我已经会了。telegram 群聊地址 https://t.me/triliumsspai
  2. 发第二篇文章的时候有个朋友提问,“你有没有用过 roam”,我说我用了五分钟没加载出来就退出了。后来,我换了个网速比较好的环境,于是收获了一个价值连城的账号。平心而论,roam 有它无法替代的地方,它的块级引用做得太好了,但它的退出成本也实在太高了,这个格式几乎无法导出成纯文本,在别的编辑器里流通。
    关于 roam,已经有很多文章介绍,如果仅和 Trilium 作对比,我会说 roam 的 day note 体验完胜后者。后者不仅无法同时编辑多个文件,日记内容也很难按标签归类检索。但是,Trilium 也有独一无二的地方,那就是网页剪藏——每天剪藏的网页,都会自动收纳到 day note 下。这一点,同类软件都还没有一个可以匹敌的成熟解决方案(obsidian 可以利用 md 导出插件做到,但比较复杂)。
    我重度依赖 Trilium 的剪藏功能,再加上 roam 的退出成本问题,感觉还是 Trilium 更能满足我的基本需求。
    至于如何在别的工具里高度模拟 roam 体验,我的体会是,坚持 Zettelkasten 方针不动摇。roam 的核心特性就是块级引用的粒度特别小,而我们可以手动模拟这种粒度,写最精悍短小的笔记,再用交叉引用把这些笔记串起来。不过,我觉得 Trilium 面向的用户群和 roam 还是有相当的区别,其中不乏从印象笔记迁移过来的,习惯了写长笔记,Trilium 的文档树结构也对这类笔记更友好。
  3. 如上文所说,接下来我打算写一点关于 TiddlyWiki 的文章。这个社区经过十多年积淀,发展出了不少令人眩晕的奇技淫巧,而国内对此的介绍还比较少。正如上文另一处所说,Trilium 主要用于输入和消化端,TiddlyWiki 则相当适合输出——因为它本身就是一个巨大的网页文件,随便找个托管服务(ghpages,netlify,vercel……)一部署,五分钟内就可以分享给别人。
    在这些文章里,我计划详细讲讲 TW 是如何塑造了我现在使用 Trilium 的习惯,并顺带考古,探访 wiki 这个信息组织形式的前世今生,以及围绕它产生的种种有趣思考。也欢迎对这个题目感兴趣的朋友一起交流。