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]— 当前笔记 IDbody[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 完整流程
- 创建关系: 用户在笔记上添加
~shareCssrelation 指向 CSS 笔记 - SHACA 加载:
sattribute.js读取并存储为SAttribute(type='relation', name='shareCss') - 属性继承:
SNote.getAttributes()合并 owned + inherited + template/inherit 传递,去重 - 模板渲染:
note.getRelations("shareCss")返回所有匹配的SAttribute数组 - 生成标签: 每个 relation 生成一个
<link href="api/notes/{targetNoteId}/download" rel="stylesheet"> - 浏览器加载: 请求
/share/api/notes/{noteId}/download→ 返回 CSS 笔记内容 + MIME 类型
5.3 同样机制的其他注入
| 注入点 | 模板行 | 说明 |
|---|---|---|
~shareCss | 23-25 | CSS 链接 |
~shareJs | 26-28 | JS 脚本 |
~shareFavicon | 10-12 | 图标链接 |
~shareTemplate | render 之前 | 自定义 EJS 模板 |
#shareDescription | 5-7 | meta description |
#shareDisallowRobotIndexing | 29-31 | robots meta |
#shareOmitDefaultCss | 16-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 里)
showMenuclass 切换是唯一可用的 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 开发的影响
布局修正: 必须用
flex-direction: row-reverse匹配 DOM 顺序CSS 注入无限制:
shareCss支持多个、支持继承。可在根节点设置,所有子节点自动生效JS 能力极有限: share.js 只有 toggleMenu。没有 include-note 动态加载。CSS 无法修复内容占位问题
无第三方 CSS 限制: 自定义 CSS 会覆盖默认 share.css 和 normalize.css,需要自己处理所有基础样式(body重置、图片max-width等)
showMenu class: 是 CSS 可以配合的唯一 JS 交互钩子,可用于移动端侧栏控制
shareOmitDefaultCss: 如果设置此标签,默认 CSS 完全不加载,需要自己写全部样式
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 |
| 默认 CSS | src/public/stylesheets/share.css | 基础布局/响应式 |
| 客户端 JS | src/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.js | Express 中间件和视图配置 |