trilium:0.63.7 源码分析

Trilium v0.63.7 源码全景分析

分析基于 GitHub Release v0.63.7 源码。


一、项目结构概览

Trilium-0.63.7/
├── src/                          # 主源码
│   ├── www.js                    # 服务器入口 (CLI: node src/www.js)
│   ├── app.js                    # Express 应用工厂
│   ├── becca/                    # 数据模型 (Backend Entity Cache & Access)
│   │   └── entities/             # Note, Branch, Attribute, Revision, Blob
│   ├── share/                    # 分享模块 (独立只读)
│   │   ├── routes.js             # 分享页 HTTP 路由
│   │   ├── content_renderer.js   # 按笔记类型渲染内容
│   │   ├── share_root.js         # _share 根节点常量
│   │   ├── sql.js                # 只读 SQLite 连接
│   │   └── shaca/                # 轻量只读缓存 (SHArd CAche)
│   ├── routes/                   # Express 路由定义
│   │   ├── routes.js             # 主路由表 (所有页面+API+分享)
│   │   ├── assets.js             # 静态文件服务
│   │   └── api/                  # ~40个 API 处理器
│   ├── services/                 # 后端服务 (~50文件)
│   │   ├── config.js             # INI 配置解析器
│   │   ├── asset_path.js         # 资源路径版本化
│   │   ├── sql.js                # SQLite 数据库访问
│   │   ├── search/               # 全文检索引擎
│   │   └── sync.js               # 同步引擎
│   ├── public/                   # 浏览器静态资源
│   │   ├── app/share.js          # 分享页客户端 JS (23行)
│   │   ├── app-dist/             # Webpack 打包 (desktop.js, mobile.js)
│   │   ├── stylesheets/          # share.css, style.css, theme-dark.css...
│   │   └── fonts/                # Montserrat, JetBrains Mono
│   └── views/                    # EJS 模板
│       └── share/
│           ├── page.ejs          # 分享页主模板 ⭐
│           ├── tree_item.ejs     # 递归目录树
│           └── 404.ejs           # 分享页404
├── libraries/                    # 第三方库 (normalize, ckeditor)
├── config-sample.ini             # 配置示例
├── package.json                  # 依赖和脚本
├── webpack.config.js             # 打包配置
└── Dockerfile

二、运行时架构

2.1 启动流程

$ trilogy 或 node src/www.js
  → src/www.js
    → 全局错误处理 (unhandledRejection, SIGINT)
    → 调用 src/app.js 构建 Express 应用
    → 加载中间件: compression → helmet → body-parser → cookie-parser → sessionParser
    → 注册路由组: assets, routes, custom, error_handlers
    → 初始化 WebSocket (ws.init())
    → 启动后台计时器: sync, backup, consistency_checks, scheduler
    → 监听端口 (默认 8080)

2.2 请求生命周期

请求 → sessionParser → cookieParser → bodyParsers → helmet
  → router (auth → csrf → handler 中间件链)
  → handler → EJS 渲染 或 API 逻辑
  → error_handlers (无匹配路由时 404/500)

2.3 关键技术选型

  • HTTP 框架: Express 4.18.2
  • 模板引擎: EJS 3.1.9 (不含布局/继承功能)
  • 数据库: better-sqlite3 8.4.0 (同步 SQLite)
  • WebSocket: ws 8.14.2
  • HTML 解析: jsdom 22.1.0
  • 数学渲染: KaTeX 0.16.9
  • 图表: Mermaid 10.6.1
  • 客户端: jQuery 3.7.1, Webpack 5.89.0 (打包 desktop/mobile)

三、分享模块深度解析 (src/share/)

3.1 分享页 URL 模式

路径说明
/share/分享首页
/share/{noteId}按 noteId 或 shareAlias 访问
/share/api/notes/{noteId}笔记 JSON 数据
/share/api/notes/{noteId}/download下载笔记内容 (CSS 就是通过这个加载的!)
/share/api/images/{noteId}/{filename}图片/画布/Mermaid 输出
/share/api/attachments/{id}/image/{file}附件图片

3.2 分享页完整渲染管线

1. 用户访问 /share/{shareId}
2. Express 路由匹配 → src/share/routes.js 处理器
3. shacaLoader.ensureLoad() → 从只读 SQLite 加载 _share 子树
4. 笔记解析: shaca.aliasToNote[id] || shaca.notes[id]
5. checkNoteAccess() → 权限验证 (shareCredentials)
6. contentRenderer.getContent(note) → 按类型渲染:
   - text:   JSDOM 解析 → 重写内部链接 → 注入 KaTeX
   - code:   <pre> 包裹
   - mermaid: <img> + 可折叠源码
   - image:  <img> 标签
   - file:   PDF <iframe> 或下载按钮
7. getSharedSubTreeRoot(note) → 沿父链找 _share 直系子
8. 模板检查: ~shareTemplate? → 自定义 EJS 渲染
9. 默认: res.render('share/page', opts)
10. 客户端: share.js 添加 toggleMenuButton 事件

3.3 SHACA 只读缓存架构

分享模块使用独立的只读 SQLite 连接和轻量缓存 SHACA (SHArd CAche):

  • snote.js — 分享笔记实体 (类比 Note)
  • sbranch.js — 分享分支实体 (类比 Branch)
  • sattribute.js — 分享属性实体 (类比 Attribute) ⭐
  • 首次请求时递归加载 _share 根下的所有笔记/分支/属性
  • 主应用有变更时重置 shaca.loaded = false
  • 属性继承: 支持 owned + inherited + template/inherit 传递 + 去重

四、分享页 HTML 结构 (page.ejs 输出)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="description">          <!-- #shareDescription 标签 -->
    <link rel="shortcut icon">          <!-- ~shareFavicon 关系 -->
    <script src="../{appPath}/share.js"></script>
    <link href="normalize.min.css">     <!-- 默认 CSS -->
    <link href="share.css">             <!-- 默认分享 CSS -->
    <link href="ckeditor-content.css">  <!-- 文本笔记样式 -->
    <link href="api/notes/{noteId}/download" rel="stylesheet">  <!-- ~shareCss 注入处! -->
    <script type="module" src="...">   <!-- ~shareJs 注入处 -->
    <meta name="robots">               <!-- #shareDisallowRobotIndexing -->
    <%- header %>                      <!-- KaTeX 脚本等 -->
    <title>{note.title}</title>
</head>
<body data-note-id="{noteId}"
      data-ancestor-note-id="{subRoot.noteId}">
  <div id="layout">
    <div id="main">                    <!-- DOM 第一位 ← 坑! -->
      <nav id="parentLink">            <!-- 面包屑(非根节点) -->
      <h1 id="title">{note.title}</h1>
      <div id="content">...</div>    <!-- 渲染内容 -->
      <nav id="childLinks">...</nav>  <!-- grid 或 list -->
    </div>
    <button id="toggleMenuButton"></button>
    <nav id="menu">                   <!-- DOM 最后 ← 默认 share.css 用 row-reverse -->
      <!-- 递归 tree_item.ejs 渲染 -->
    </nav>
  </div>
</body>
</html>

4.1 DOM 顺序的关键陷阱

分享页 HTML 中 #main#menu 前面。默认 share.css 通过 flex-direction: row-reverse 交换视觉顺序,让 #menu 显示在左侧。

如果自定义 CSS 写成 flex-direction: row#main 就会出现在左侧、#menu 在右侧——这就是之前「目录树不在最左侧」的原因。

4.2 重要 DOM 属性

  • body[data-note-id] — 当前笔记 ID
  • body[data-ancestor-note-id] — 子树根节点 ID
  • #content[class="type-{type} ck-content"] — 内容区,text 笔记带 ck-content
  • #childLinks[class="grid"] — 有子笔记时的导航 (grid/list)
  • #menu strong — 当前笔记 (非当前是 <a>)
  • #menu a[class="type-{type}"] — 目录树链接

五、shareCss 注入机制详解

5.1 注入代码 (page.ejs 第 23-25 行)

<% for (const cssRelation of note.getRelations("shareCss")) { %>
    <link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
<% } %>

5.2 完整流程

  1. 创建关系: 用户在笔记上添加 ~shareCss relation 指向 CSS 笔记
  2. SHACA 加载: sattribute.js 读取并存储为 SAttribute (type='relation', name='shareCss')
  3. 属性继承: SNote.getAttributes() 合并 owned + inherited + template/inherit 传递,去重
  4. 模板渲染: note.getRelations("shareCss") 返回所有匹配的 SAttribute 数组
  5. 生成标签: 每个 relation 生成一个 <link href="api/notes/{targetNoteId}/download" rel="stylesheet">
  6. 浏览器加载: 请求 /share/api/notes/{noteId}/download → 返回 CSS 笔记内容 + MIME 类型

5.3 同样机制的其他注入

注入点模板行说明
~shareCss23-25CSS 链接
~shareJs26-28JS 脚本
~shareFavicon10-12图标链接
~shareTemplaterender 之前自定义 EJS 模板
#shareDescription5-7meta description
#shareDisallowRobotIndexing29-31robots meta
#shareOmitDefaultCss16-19 控制跳过默认 CSS

六、share.js 客户端代码

// 文件: src/public/app/share.js (仅 23 行)
async function fetchNote(noteId = null) {
    if (!noteId) noteId = document.body.getAttribute("data-note-id");
    const resp = await fetch(`api/notes/${noteId}`);
    return await resp.json();
}

document.addEventListener('DOMContentLoaded', () => {
    const toggleMenuButton = document.getElementById('toggleMenuButton');
    const layout = document.getElementById('layout');
    if (toggleMenuButton && layout) {
        toggleMenuButton.addEventListener('click',
            () => layout.classList.toggle('showMenu'));
    }
}, false);
  • 仅做两件事: fetchNote() 工具函数 + toggleMenuButton 事件绑定
  • 不处理 include-note 动态加载 (那部分在桌面版 JS 里)
  • showMenu class 切换是唯一可用的 CSS 交互钩子

七、配置系统 (config-sample.ini)

[General]
instanceName=                     # 实例名称 (api.getInstanceName())
noAuthentication=false            # 跳过认证 (Server 构建)
noBackup=false                    # 关闭备份

[Network]
port=8080                         # HTTP 端口
host=0.0.0.0                      # 监听地址
https=false                       # 启用 HTTPS
trustedReverseProxy=false         # 信任反向代理

[Sync]                            # 同步配置
syncServerHost=
syncProxy=
syncSecure=true

环境变量覆盖: TRILIUM_PORT, TRILIUM_DATA_DIR, TRILIUM_SAFE_MODE


八、源码学习对自定义 CSS 开发的影响

  1. 布局修正: 必须用 flex-direction: row-reverse 匹配 DOM 顺序

  2. CSS 注入无限制: shareCss 支持多个、支持继承。可在根节点设置,所有子节点自动生效

  3. JS 能力极有限: share.js 只有 toggleMenu。没有 include-note 动态加载。CSS 无法修复内容占位问题

  4. 无第三方 CSS 限制: 自定义 CSS 会覆盖默认 share.css 和 normalize.css,需要自己处理所有基础样式(body重置、图片max-width等)

  5. showMenu class: 是 CSS 可以配合的唯一 JS 交互钩子,可用于移动端侧栏控制

  6. shareOmitDefaultCss: 如果设置此标签,默认 CSS 完全不加载,需要自己写全部样式

  7. shareJs: 可以注入自定义脚本绕过 CSS 限制(如 include-note 动态加载、侧栏滚动定位),但需自行编写


九、关键文件速查

文件路径作用
分享页模板src/views/share/page.ejs整个分享页 HTML 结构
递归目录树src/views/share/tree_item.ejs侧栏导航树生成
分享路由src/share/routes.js分享页所有 HTTP 处理
内容渲染src/share/content_renderer.js按笔记类型渲染 HTML
默认 CSSsrc/public/stylesheets/share.css基础布局/响应式
客户端 JSsrc/public/app/share.js仅 toggleMenu
属性模型src/share/shaca/entities/sattribute.js关系/标签处理
笔记模型src/share/shaca/entities/snote.js属性继承、子节点访问
主路由表src/routes/routes.js全部路由注册
服务器入口src/www.js启动流程
应用工厂src/app.jsExpress 中间件和视图配置