首页
/ Joplin 技术解析:离线优先笔记应用的架构、同步机制与开发工作流

Joplin 技术解析:离线优先笔记应用的架构、同步机制与开发工作流

2026-09-06 17:28:53作者:何举烈Damon

Joplin 是一款免费、开源的笔记与待办(to-do)应用,支持在 Windows、Linux、macOS、Android 和 iOS 上运行,所有应用共享同一套后端逻辑。本文以项目根目录 README.md 为主线,梳理其核心能力——Markdown 笔记、离线优先(offline first)、端到端加密(E2EE)多端同步、全文搜索、插件与主题扩展——并结合仓库源码,剖析其 monorepo 包结构、同步目标(Sync Target)抽象层与导入导出机制,最后给出构建、测试与贡献的开发工作流。

一、项目定位:面向隐私的跨平台笔记应用

README.md 对 Joplin 的定义非常明确:

  • 免费开源的笔记与待办应用,可以管理大量组织在笔记本(notebook)中的笔记;
  • 笔记以 Markdown 格式存储,支持搜索、复制、打标签和修改,既可以应用内编辑,也可以用自己的文本编辑器修改(详见 Markdown 指南);
  • 离线优先:数据始终保存在用户的手机或电脑上,无论是否有网络连接,笔记都可访问;
  • 笔记可以端到端加密同步到多种云服务(Nextcloud、Dropbox、OneDrive、Joplin Cloud 等),详见 同步文档E2EE 文档
  • 全平台提供全文搜索;应用可通过插件和主题定制,用户也可以自行开发;
  • 提供 Web Clipper(浏览器剪藏工具,支持 Firefox 与 Chrome),用于保存网页和截图,文档见 clipper.md
  • 支持从 Evernote 导入:包括格式化内容(转换为 Markdown)、资源(图片、附件)以及完整元数据(地理位置、更新时间、创建时间等),也支持导入纯 Markdown 文件,详见 导入导出文档

二、Monorepo 架构:共享后端 + 多端应用包

package.jsonworkspaces: ["packages/*"]lerna.json"version": "independent""useWorkspaces": true)可以看出,Joplin 采用 Yarn 4 workspaces + Lerna 独立版本管理的 monorepo 结构。贡献指南也明确说明:“所有应用共享同一后端(数据库、同步、设置、模型、业务逻辑等),因此在一个应用中修改后端时,要确保其他应用仍然工作”(见 readme/dev/index.md)。

packages/ 下的包可大致分为四层:

层次 职责
共享后端 packages/lib 数据库、模型(models/ 目录含 57 个文件)、同步器(Synchronizer.ts)、文件 API、设置、业务逻辑、本地化等
桌面端 packages/app-desktop Electron 应用,含 GUI(gui/)、命令(commands/)、服务(services/
移动端 packages/app-mobile React Native 应用,含 Android/iOS 原生工程与 components/
剪藏器 packages/app-clipper 浏览器扩展,含 manifest.json、content scripts 与 service worker
终端端 packages/app-cli 命令行客户端
渲染与编辑 packages/editor(CodeMirror/ProseMirror 封装)、packages/renderer(MdToHtml 渲染链) 编辑器与 Markdown 渲染
服务端 packages/serverpackages/transcribe 自托管服务器与转写服务
工具链 packages/toolspackages/generator-joplinpackages/default-plugins 构建、发布、插件脚手架等

几个可以从源码结构看出来的设计细节:

  • packages/lib/initLib.ts 要求每个“应用”包在启动时调用 initLib(globalLogger),让 lib 与宿主应用共享同一个 Logger 实例——这正是“同一后端被多个应用复用”的具体体现;
  • packages/lib/ 下的 database-driver-better-sqlite.tsdatabase-driver-node.tsfs-driver-node.tsfs-driver-dummy.ts 等驱动文件,体现了一种驱动化的抽象:同一套模型层可以通过不同数据库/文件驱动在 Electron、Node CLI、React Native 等环境中运行;
  • CLAUDE.md 等开发约定要求 SQL 查询只允许出现在 packages/lib/models 中,进一步印证模型层是数据访问的唯一入口。

三、Markdown:CommonMark 基线 + 可开关插件

readme/apps/markdown.md 说明 Joplin 遵循 CommonMark 规范,并通过插件扩展功能;桌面与移动端都能同时展示 Markdown 原文与渲染后的富文本。

Joplin 扩展语法(Extras)

  • 笔记间链接:在 URL 中写入目标笔记 ID,如 Link to my note。桌面端可拖拽笔记到其他笔记内生成链接,或右键“Copy Markdown link”;移动端在笔记右上菜单中选择“Copy Markdown link”;
  • 数学公式:KaTeX 语法,行内用 $EXPRESSION$,块级用 $$ ... $$;启用数学公式后 mhchem 化学方程式插件自动生效;
  • 图表:Mermaid 语法放入 ```mermaid 代码块即可渲染(Mermaid 图形始终渲染在白色背景上,避免主题色冲突);
  • 乐谱:ABC notation 放入 ```abc 代码块渲染为乐谱。全局选项在配置界面的 Markdown 区以 JSON5 对象设置(如 { foregroundColor: "#ff0000", scale: 2 });单曲谱可用 --- 分隔的头部覆盖全局选项(如 {tablature: [{instrument: 'violin'}]});
  • 复选框- [ ] Milk / - [x] Rice,可在桌面与移动端直接勾选;
  • HTML 支持:当 Markdown 表达力不足(如删除线)时可直接写 HTML,例如 This is <s>strikethrough</s> mixed with **Markdown**

可开关的 Markdown 插件(需在配置界面的 Markdown 区启用/禁用):

插件 语法示例 默认启用
Soft breaks 将换行渲染为软换行(Joplin 默认是硬换行 <br>
Typographer (c) → © 等排版替换
Linkify 自动识别 URL 并转为可点击链接
KaTeX $math$ / $$math$$
Fountain ```fountain 代码块,剧本写作标记语言
Mermaid ```mermaid 代码块
Mark ==marked==<mark>marked</mark>
Footnote Simple inline footnote ^[I'm inline!]
TOC ${toc}[[toc]] 等插入目录
Sub / Sup X~1~ / X^2^
Deflist / Abbr / Emoji / Insert / Multitable markdown.md 完整表格

文档同时提醒:这些插件功能不属于 CommonMark 规范,在其他 Markdown 阅读器中不保证可用——这是“隐私优先、数据自有”理念下对格式兼容性的诚实说明。渲染侧的实现位于 packages/renderer/(如 MdToHtml.tsMarkupToHtml.ts),编辑侧位于 packages/editor/(CodeMirror 与 ProseMirror 封装)。

四、同步体系:无硬依赖的 Sync Target 抽象层

readme/apps/sync/index.md 阐明了同步层的设计目标:不绑定任何特定公司或服务(无论是 Evernote、Google 还是 Microsoft)。同步过程的大部分逻辑运行在抽象层,对外部服务(Nextcloud、Dropbox 等)的访问通过轻量驱动完成——驱动只需提供类文件系统的接口(读取、写入、删除、列出条目),因此新增服务、切换服务都很简单。

当前支持的目标:Joplin Cloud、Nextcloud、S3、WebDAV、Dropbox、OneDrive 或本地文件系统。配置完成后,应用在运行时会后台自动同步(本地内容变更后自动触发后台同步),也可以点击“Synchronise”手动同步。若安装了终端客户端,可在终端执行 joplin sync,用于在界面外同步,例如用 cron 每 30 分钟同步一次:

*/30 * * * * /path/to/joplin sync

4.1 源码中的同步目标注册机制

packages/lib/SyncTargetRegistry.ts 是这一抽象的核心:一个静态注册表,键为目标 id,值为继承自 BaseSyncTarget 的类。SyncTargetInfo 接口暴露了每个目标的元数据:

export interface SyncTargetInfo {
	id: number;
	name: string;
	label: string;
	supportsSelfHosted: boolean;       // 是否支持自托管
	supportsConfigCheck: boolean;     // 是否支持配置校验
	supportsRecursiveLinkedNotes: boolean;
	supportsShare: boolean;
	description: string;
	classRef: typeof BaseSyncTarget;
}

每个具体目标(如 SyncTargetWebDAV.tsSyncTargetDropbox.tsSyncTargetOneDrive.tsSyncTargetNextcloud.ts 等)以静态方法声明自身元信息。以 WebDAV 为例:

  • id() 返回 6targetName() 返回 'webdav'requiresPassword() 返回 truesupportsConfigCheck() 返回 true
  • 描述文案为:“WebDAV 协议允许用户在服务器上创建、修改和移动文档。许多服务器兼容 WebDAV,包括 SeaFile、Nginx 或 Apache”;
  • newFileApi_()pathusernamepasswordignoreTlsErrors 等配置项装配为 WebDavApi,再包装成 FileApiDriverWebDav 并交给统一的 FileApifileApi.setSyncTargetId(syncTargetId));
  • checkConfig() 通过 fileApi.stat('') 探测目标目录是否可达,失败时返回 ok: false 及错误信息——这就是配置界面“检查连接”按钮背后的调用链。

SyncTargetRegistry.optionsOrder() 返回 ['0', '10', '7', '3'](注释标明依次是 None、Joplin Cloud、Dropbox、OneDrive),用于配置界面中目标下拉框的排序;isJoplinServerOrCloud() 则把 joplinServerjoplinCloudjoplinServerSaml 三类 id 归为一组处理。从源码结构看,注册表以数字 id 为键(addClassid() 注册),而设置界面展示时通过 nameToId / idToName 在名称与 id 之间转换——这意味着新增一个同步目标,只需实现 BaseSyncTarget 子类并注册,其余同步逻辑(增量、冲突处理、E2EE)完全复用

真正的同步算法集中在 packages/lib/Synchronizer.ts,配合 BaseSyncTarget 定义的抽象接口运行;文件读写则被进一步下沉到 file-api-driver-*(local、memory、dropbox、onedrive、webdav、amazon-s3、joplinServer)与 fs-driver-* 系列驱动中。

4.2 端到端加密(E2EE)

readme/apps/sync/e2ee.md 说明 Joplin 在所有应用上支持 E2EE:只有数据拥有者能读取笔记、笔记本、标签和资源,运营商、网络提供商乃至 Joplin 开发者都无法解密。文档同时提醒:E2EE 存在加解密的额外开销,应评估是否真的需要。

启用步骤(关键点全部保留):

  1. 由于 Joplin 的分布式特性,E2EE 必须先在单一设备上手动启用(这会创建由密码保护的加密主密钥 Master Key),再同步到其余设备。建议在桌面端或终端端先启用——它们通常运行在性能更强的设备上,加密初始数据更快;
  2. Configuration screen 的 Encryption 区点击“Enable encryption”;
  3. 输入 Master Key 密码。注意:出于安全原因密码不可找回
  4. 等待一次完整同步完成,使所有笔记以密文发送到同步目标。数据量大时可能耗时很长(可过夜执行),看似卡住时不要取消;
  5. 打开下一台设备,点击“Synchronise”,设备会收到主密钥,输入密码后 E2EE 即在该设备自动启用;完成后再次同步;
  6. 对每台设备重复步骤 5。不要在多台设备上并行手动启用加密,否则可能产生多个加密密钥(Joplin 支持多密钥,但通常不是期望结果)。

全部设备同步完成并启用 E2EE 后,加解密基本透明;偶尔可能看到未解密的条目,它们最终会在后台解密。禁用 E2EE 的流程与启用对称:逐台设备禁用并等待同步完成。更底层的技术描述见 readme/dev/spec/e2ee/index.md

五、导入与导出:ENEX、Markdown、OneNote 与 JEX

readme/apps/import_export.md 是数据迁移的权威文档:

从 Evernote 导入(ENEX):可导入完整笔记本、笔记、标签、资源与元数据(作者、地理位置等)。两点已知差异:

  • 识别数据(OCR):Evernote 图片附带的识别文本不会保留;若已在 Joplin 中启用 OCR,该数据会以 Joplin 兼容格式重建;
  • 颜色/字号/字体:Evernote 的 HTML 在导入时转为 Markdown,对纯文本和基础格式(粗体、斜体、列表、链接)是近乎无损的,表格也会转为 Markdown 表格;但颜色、字号、字体不保留。文本本身无论如何都会完整导入;若必须保留这些格式,Joplin 支持将 ENEX 按 HTML 导入。
  • 笔记间链接:大部分保留。ENEX 格式缺少定位链接目标的完整信息(Evernote 使用 ID 但 ID 未关联目标笔记),Joplin 按笔记标题猜测,多标题重名或标题不一致时可能失败,失败则保留原 Evernote 链接。

操作步骤:先在 Evernote 中将笔记本导出为 ENEX 文件。桌面端走 File > Import > ENEX 并选择文件,笔记会导入一个新建的独立笔记本;终端端在 命令行模式 下执行 import /path/to/file.enex,笔记导入以文件名命名的新笔记本。两种方式都支持单文件或包含多个 ENEX 文件的目录:导入单文件时创建同名笔记本导入全部笔记;导入目录时为每个文件创建一个笔记本。实现位于 packages/lib/import-enex.ts 及配套的 import-enex-html-gen.tsimport-enex-md-gen.ts

从 Markdown 导入:桌面端 File > Import > MD - Markdown (file) 导入单个文件到当前选中的笔记本;MD - Markdown (directory) 导入整个目录,目录结构映射为“笔记本 > 子笔记本 > 笔记”。终端端:import --format md /path/to/file.mdimport --format md /path/to/directory/

从 OneNote 导入

  • OneNote Online:要求 Joplin ≥ v3.5.1,笔记本需存放在 OneDrive;从 OneNote Web 进入 OneDrive 下载笔记本文件夹(ZIP,可能较大,2-4 GB 以上的笔记本可能下载不完整),再用桌面端 File > Import > ZIP - OneNote Notebook 导入;
  • OneNote Windows 桌面版:要求 Joplin ≥ v3.5.5;在 OneNote 桌面版 File > Export 中选择“Section → OneNote 2010-2016 Section(*.one/*.onex)”,或“Notebook → OneNote Package(*.onepkg)”(*.onepkg 仅支持在 Windows 上运行 Joplin 时导入),再同样通过 ZIP 导入入口导入。OneNote 解析由独立的 Rust 包 packages/onenote-converter/ 承担(含 parser/renderer/ 等子模块)。

导出:Joplin 支持导出 JEX(Joplin Export file)格式——一个可包含多篇笔记、笔记本等的 tar 文件,无损保留笔记与地理位置、更新时间、标签等元数据,便于备份并可重新导入 Joplin;另有 raw 格式,与 JEX 相同但数据保存为目录、每个条目一个文件。此外还支持 HTML、PDF 等格式的导出,可针对单篇笔记、整个笔记本或全部内容。

六、构建、测试与开发工作流

package.json 声明了环境要求:"node": ">=22.12""yarn": "4.12.0"packageManageryarn@4.16.0)。关键脚本与 readme/dev/index.md 对应:

# 构建(工作区按拓扑顺序并行构建,随后编译 TypeScript)
yarn buildParallel

# 运行全部工作区的测试(并行、每包 2 job)
yarn test

# 进入单个包运行其测试,例如 packages/lib 下:
yarn test

# 只运行某个测试文件 / 某个用例
yarn test markdownUtils
yarn test markdownUtils --filter="should handle conflict"

贡献指南中的测试要点:

  • 测试框架为 Jest;新测试文件以 .test.ts 命名并放在同一目录;
  • 多数测试工具函数位于 @joplin/lib/testing/test-utils,可参考 packages/lib/models/Note.test.ts 了解带数据库支持与同步器支持的测试搭建方式;
  • React Hooks 测试使用 @testing-library/react-hooks,示例见 useLayoutItemSizes.test.ts
  • 若确实无法写单测,需提供手动测试计划(至少 5 个测试步骤,覆盖边界输入)以及“相关功能未被破坏”的验证步骤。

其他工程细节:

  • package.jsonpostinstallhusky && gulp build,即安装后自动执行 git hooks 安装与 Gulp 构建;resolutions 中对 react-native、pdfjs-dist 等多个依赖打了 patch: 补丁,保证跨端行为一致;
  • CLAUDE.md 总结了编码约定:Tab 缩进、单引号、避免 any、优先 // 注释、复制大段代码需注明出处、新增 TypeScript 文件后运行 yarn updateIgnored、用 yarn tsc 编译 / yarn tsc --noEmit 类型检查、桌面端样式使用 RSCSS + SCSS(不用 styled-components);
  • 贡献流程要求:PR 解决已讨论并被接受的具体问题;超过 50 行的改动应先论坛讨论;一个 PR 只解决一个问题;自动化批量修改(拼写、样式等)的 PR 一般不接受;所有贡献者需签署个人贡献者协议(readme/cla.md)。

七、透明度与社区

README.md 提供了 Warrant Canary 签名密钥,用于公开承诺未收到政府取数请求:

社区方面,README 列出了论坛(支持讨论、用户帮助、新功能讨论与 beta 版本发布)、Patreon、Bluesky、Mastodon、YouTube、Discord、LinkedIn 与 Lemmy 社区等渠道;开发文档入口为 readme/dev/index.md,完整帮助文档见 readme/index.md。README 末尾还内嵌了自动生成的贡献者名单(CONTRIBUTORS-TABLE-AUTO-GENERATED 注释块),体现该项目由广泛的社区共同维护。

八、小结

README.md 的骨架可以归纳出 Joplin 的技术主线:以 Markdown 纯文本为数据载体、以本地数据库为唯一事实源(离线优先)、以可插拔的 Sync Target 驱动层实现多云同步、以 E2EE 保证隐私、以 monorepo 共享后端保证多端一致。这种“后端一份代码、端上薄壳驱动”的架构,使其能够用同一套模型层、同步器与文件 API 支撑 Electron 桌面端、React Native 移动端、CLI 与浏览器剪藏器,也为自托管(packages/server/)与后续新同步服务的接入留出了清晰的扩展点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388