尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Git、CRDT与Markdown构建实时协同文档系统

基于Git、CRDT与Markdown构建实时协同文档系统 在实际的软件开发、文档编写和团队协作中Git、CRDT 和 Markdown 是三种看似独立却又紧密关联的核心技术。Git 解决了代码版本管理的根本问题CRDT 为实时协同编辑提供了无冲突的理论基础而 Markdown 则以其简洁的语法成为技术文档和笔记的首选格式。许多开发者都曾设想过如果能将 Git 的版本控制能力、CRDT 的实时协同特性与 Markdown 的便捷编辑体验深度结合构建一个属于自己的、去中心化的“GitHub”那将极大地提升个人知识管理和团队协作的效率。本文将带你深入理解这三者的核心概念并实践如何将它们组合起来搭建一个具备基础版本历史和实时协同能力的 Markdown 文档系统。1. 理解三大基石Git、CRDT 与 Markdown 的角色在开始动手搭建之前我们需要清晰地理解 Git、CRDT 和 Markdown 各自解决了什么问题以及它们如何协同工作。1.1 Git分布式版本控制的基石Git 的核心价值在于分布式版本控制。它不仅仅是一个备份工具更是一套完整的内容寻址文件系统。当你执行git commit时Git 会为当前仓库的快照创建一个唯一的哈希值如a1b2c3d这个哈希指向一个包含文件树和元数据的对象。这意味着任何一次提交都是不可变的历史可以被追溯和验证。在我们的“个人 GitHub”场景中Git 扮演着历史记录与版本回溯的角色。每一篇 Markdown 文档的修改、每一次协同编辑的最终状态都可以通过 Git 提交来保存。你可以随时查看谁在什么时候修改了哪一行并且可以轻松地回滚到任何一个历史版本。与集中式的版本控制系统不同Git 的分布式特性意味着每个参与者都拥有完整的仓库副本这为离线工作和去中心化协作奠定了基础。1.2 CRDT无冲突实时协同的理论核心CRDT 是“无冲突复制数据类型”的缩写。它是一类数据结构的统称其核心特性是无论操作以何种顺序、在哪个副本上执行最终所有副本的状态都会收敛到一致。这与 Git 的“合并冲突”模型有本质区别。Git 需要你在推送时手动解决冲突而设计良好的 CRDT 可以在数据层面自动解决冲突。在我们的系统中CRDT 将负责实时协同编辑。想象一下你和同事同时编辑同一份 Markdown 文档。如果没有 CRDT你们可能会相互覆盖对方的修改。而使用基于 CRDT 的文本类型如 RGA 或 LSeq每个人的每一次插入或删除操作都会被赋予唯一的、可比较的标识符。即使网络有延迟这些操作最终在所有客户端上应用后大家看到的文档内容都是一致的。这为构建类似 Google Docs 的实时体验提供了可能。1.3 Markdown内容创作与呈现的桥梁Markdown 是一种轻量级标记语言。它用简单的符号如#、-、**来定义标题、列表、加粗等格式其纯文本特性使其非常适合用 Git 进行版本控制也易于被 CRDT 算法处理。在我们的项目中Markdown 是内容的载体和最终呈现的目标。用户在前端编辑器里编写 Markdown系统后端或前端需要将其转换为 HTML 进行渲染。同时因为 Markdown 是纯文本我们可以利用 Git 来追踪它的每一次变化也可以利用 CRDT 来同步这些文本变化。三者关系总结我们可以将 Git 视为一个时间轴记录文档的宏观版本变迁将 CRDT 视为一个同步层处理微观的、实时的字符级变更而 Markdown 则是这条时间轴上流动的内容本身。一个理想的系统是用户通过 CRDT 进行实时协同编辑编辑的内容是 Markdown 文本而每隔一段时间或达到某个条件系统自动将当前稳定状态提交到 Git 仓库形成一个版本快照。2. 环境准备与项目初始化我们将构建一个基于 Node.js 的简单演示系统。这个系统将包含一个后端服务用于协调 CRDT 同步和 Git 操作以及一个简单的前端用于编辑和展示 Markdown。2.1 基础环境配置首先确保你的开发环境已安装以下必备工具Node.js 与 npm: 用于运行 JavaScript 后端和安装依赖。Git: 核心的版本控制工具需要配置好用户信息。可以通过以下命令检查是否安装成功# 检查 Node.js 和 npm node --version npm --version # 检查 Git git --version git config --global user.name Your Name git config --global user.email your.emailexample.com2.2 初始化项目并安装核心依赖创建一个新的项目目录并初始化mkdir my-own-github cd my-own-github npm init -y接下来安装我们需要的核心 npm 包npm install express ws yjs # 后端Web框架、WebSocket、CRDT库 npm install --save-dev nodemon # 开发工具热重载express: 用于创建 HTTP 服务器提供静态文件服务和 API 接口。ws: WebSocket 服务器库用于在浏览器和服务器之间建立全双工通信这是实时同步的基础。yjs: 一个功能强大且成熟的 CRDT 实现库。它提供了多种数据结构如 Y.Text, Y.Array和网络通信协议Providers极大简化了构建协同应用的过程。nodemon: 在开发时监听文件变化并自动重启服务器。同时我们还需要一个前端库来连接 Yjs 和编辑器。我们将使用y-websocket作为服务器端的 WebSocket 连接处理器并使用monaco-editorVS Code 的编辑器核心作为前端的 Markdown 编辑器因为它对协同编辑有良好的支持。# 在项目根目录下安装服务器端连接器 npm install y-websocket # 我们将通过CDN在前端引入monaco-editor和yjs因此这里不需要安装对应的npm包。2.3 项目结构设计一个清晰的项目结构有助于管理代码。创建如下目录和文件my-own-github/ ├── package.json ├── server.js # 主服务器文件 ├── public/ # 静态资源目录 │ ├── index.html # 前端主页面 │ └── editor.js # 前端编辑器逻辑 ├── repos/ # Git仓库存储目录每个文档一个仓 │ └── .gitkeep # 保持空目录 └── README.md3. 构建后端集成 WebSocket、CRDT 与 Git 钩子后端服务器需要处理三件事1. 提供前端页面2. 通过 WebSocket 管理 CRDT 文档同步3. 在适当时机触发 Git 提交。3.1 创建基础服务器与 WebSocket 协同服务编辑server.js文件const express require(express); const path require(path); const WebSocket require(ws); const { setupWSConnection } require(y-websocket/bin/utils); const app express(); const PORT process.env.PORT || 3000; // 提供静态文件 app.use(express.static(path.join(__dirname, public))); // 创建 HTTP 服务器 const server app.listen(PORT, () { console.log(Server is running on http://localhost:${PORT}); }); // 创建 WebSocket 服务器并附加到同一个 HTTP 服务器上 const wss new WebSocket.Server({ server }); // 处理 WebSocket 连接使用 y-websocket 的逻辑 wss.on(connection, (ws, request) { // 从URL中解析出文档名例如 ws://localhost:3000/my-doc const docName request.url.slice(1); // 移除开头的 / console.log(New WebSocket connection for document: ${docName}); setupWSConnection(ws, request, { docName }); }); console.log(WebSocket server is running on ws://localhost:${PORT});这段代码创建了一个 Express 服务器它同时提供静态文件服务和 WebSocket 服务。y-websocket的setupWSConnection函数会帮我们处理 Yjs 文档的创建、加载、同步和持久化默认在内存中。当多个浏览器连接到同一个文档名如my-doc时它们通过这个 WebSocket 服务器同步 CRDT 更新。3.2 实现 Git 自动提交逻辑我们的目标是在文档内容稳定后例如所有用户停止编辑一段时间自动将当前内容提交到 Git 仓库。我们需要一个函数来执行 Git 命令。首先在server.js顶部引入child_process模块来执行 shell 命令const { exec } require(child_process); const fs require(fs).promises; const path require(path);然后添加一个工具函数来执行 Git 操作。为了安全我们将每个 Markdown 文档存储在一个独立的 Git 仓库中仓库位于repos/{docName}。/** * 初始化或更新指定文档的 Git 仓库并提交当前内容 * param {string} docName 文档名称 * param {string} content 文档内容 */ async function gitCommit(docName, content) { const repoPath path.join(__dirname, repos, docName); const filePath path.join(repoPath, README.md); // 用 README.md 存储内容 try { // 确保仓库目录存在 await fs.mkdir(repoPath, { recursive: true }); // 检查是否已经是 Git 仓库 let isRepo true; try { await execPromise(git rev-parse --git-dir, { cwd: repoPath }); } catch (e) { isRepo false; } if (!isRepo) { console.log(Initializing new git repo for ${docName}); await execPromise(git init, { cwd: repoPath }); // 设置默认分支为 main适配新版 Git await execPromise(git checkout -b main, { cwd: repoPath }); } // 写入最新内容到文件 await fs.writeFile(filePath, content, utf8); // 执行 Git 操作 await execPromise(git add README.md, { cwd: repoPath }); await execPromise(git commit -m Auto-commit: Update content via collaborative editor, { cwd: repoPath }); console.log(Git commit successful for ${docName}); } catch (error) { console.error(Git operation failed for ${docName}:, error.message); // 在实际项目中这里需要更精细的错误处理可能涉及锁机制 } } // 封装 exec 为 Promise便于使用 async/await function execPromise(command, options) { return new Promise((resolve, reject) { exec(command, options, (error, stdout, stderr) { if (error) { reject(new Error(Command failed: ${command}\n${stderr})); } else { resolve(stdout); } }); }); }现在我们需要一个机制来触发gitCommit函数。一个简单的策略是在 Yjs 文档更新后设置一个定时器。如果在定时器到期前没有新的更新则执行提交。这可以防止过于频繁的提交。我们需要修改 WebSocket 连接处理逻辑以获取文档内容并设置提交触发器。y-websocket允许我们传递一个配置对象。我们可以创建一个自定义的“持久化”逻辑这里我们用它来触发 Git 提交。修改server.js中wss.on(‘connection‘)部分// 引入 Yjs const Y require(yjs); // 存储每个文档的定时器 const saveTimeouts new Map(); wss.on(connection, (ws, request) { const docName request.url.slice(1); console.log(New WebSocket connection for document: ${docName}); // 自定义的“持久化”函数 const onUpdate (update, origin, doc) { // 清除旧的定时器 if (saveTimeouts.has(docName)) { clearTimeout(saveTimeouts.get(docName)); } // 设置新的定时器5秒无更新后提交 const timeoutId setTimeout(() { // 从 Yjs 文档中获取文本内容 const ytext doc.getText(content); // 我们约定使用 ‘content‘ 作为共享文本的键 const content ytext.toString(); gitCommit(docName, content).catch(console.error); saveTimeouts.delete(docName); }, 5000); // 5秒延迟 saveTimeouts.set(docName, timeoutId); }; // 传递给 y-websocket 的配置 const options { docName, // 注意y-websocket 内部会调用 gc: false我们主要用 onUpdate }; // 我们需要在连接建立后手动为文档绑定更新监听器 // setupWSConnection 会返回一个 handler我们可以稍后获取 doc const handler setupWSConnection(ws, request, options); // 监听连接就绪然后获取 Y.Doc 实例并绑定监听器 ws.on(message, (data) { // 这是一个简化的方式实际中可能需要更精确地判断同步状态 // 更稳健的做法是监听 handler 的某个事件或直接修改 y-websocket 代码 // 此处为演示我们假设连接建立后handler.doc 可用 if (handler.doc !handler.doc._updateListenerAdded) { const doc handler.doc; // 确保文档中存在我们需要的共享文本类型 if (!doc.share.has(content)) { const ytext doc.getText(content); } // 监听更新 doc.on(update, onUpdate); doc._updateListenerAdded true; console.log(Update listener attached for doc: ${docName}); } }); });注意上述与y-websocket的集成方式是一种简化演示。在生产环境中你可能需要深入研究y-websocket的源码或者使用y-leveldb等持久化后端在其onWrite钩子中触发 Git 提交这样更为稳健。这里的目的是展示如何将 CRDT 文档的更新事件与 Git 提交逻辑关联起来。4. 构建前端实时 Markdown 编辑器前端需要创建一个编辑器界面并连接到我们的 WebSocket 服务以实现协同。4.1 创建 HTML 骨架与样式编辑public/index.html!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMy Own GitHub: Collaborative Markdown Editor/title link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-light.min.css style body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, sans-serif; display: flex; height: 100vh; overflow: hidden; } #editor-container, #preview-container { flex: 1; padding: 20px; box-sizing: border-box; overflow-y: auto; } #editor-container { border-right: 1px solid #ddd; } #editor { width: 100%; height: 100%; border: none; outline: none; font-family: Monaco, Menlo, Ubuntu Mono, monospace; font-size: 14px; line-height: 1.6; resize: none; } .markdown-body { height: 100%; box-sizing: border-box; } .status-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #f6f8fa; border-top: 1px solid #ddd; padding: 8px 20px; font-size: 12px; color: #586069; display: flex; justify-content: space-between; } #doc-name-input { padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px; } #connect-btn { padding: 4px 12px; background: #2ea44f; color: white; border: none; border-radius: 4px; cursor: pointer; } /style /head body div ideditor-container div ideditor/div /div div idpreview-container div idpreview classmarkdown-body/div /div div classstatus-bar div Document: input typetext iddoc-name-input valuegetting-started placeholderDocument Name button idconnect-btnConnect Load/button span idstatusDisconnected/span /div div Users Online: span iduser-count0/span /div /div !-- 引入 Monaco Editor Loader -- script srchttps://unpkg.com/monaco-editor0.34.0/min/vs/loader.js/script !-- 引入 Yjs 及其 WebSocket 连接器 -- script srchttps://unpkg.com/yjs13.5.40/dist/yjs.js/script script srchttps://unpkg.com/y-websocket1.4.5/dist/y-websocket.js/script !-- 引入 Monaco Editor 的 Yjs 绑定 -- script srchttps://unpkg.com/y-monaco0.0.3/dist/y-monaco.js/script !-- 引入 Markdown 解析器 -- script srchttps://unpkg.com/marked4.0.0/lib/marked.umd.js/script !-- 引入 DOMPurify 用于安全地渲染 HTML -- script srchttps://unpkg.com/dompurify2.3.6/dist/purify.min.js/script script srceditor.js/script /body /html4.2 实现编辑器协同逻辑编辑public/editor.js// 全局变量 let editor null; let doc null; let provider null; let currentDocName getting-started; // 初始化 Monaco Editor require.config({ paths: { vs: https://unpkg.com/monaco-editor0.34.0/min/vs } }); require([vs/editor/editor.main], function () { // 创建编辑器实例 editor monaco.editor.create(document.getElementById(editor), { value: # Welcome to Collaborative Markdown Editor\n\nStart writing here..., language: markdown, theme: vs-light, automaticLayout: true, minimap: { enabled: false } }); // 初始化 Yjs 文档和 WebSocket 连接 initYjs(currentDocName); // 监听编辑器内容变化实时渲染 Markdown 预览 editor.onDidChangeModelContent(updatePreview); updatePreview(); // 初始渲染 }); // 初始化 Yjs 协同 function initYjs(docName) { // 如果已有连接先断开 if (provider) { provider.disconnect(); provider.destroy(); } // 创建 Yjs 文档 doc new Y.Doc(); const ytext doc.getText(content); // 与后端约定的共享类型键名 // 创建 WebSocket 连接提供者 const wsUrl ws://${window.location.host}/${docName}; provider new WebsocketProvider(wsUrl, docName, doc); // 监听连接状态 provider.on(status, event { const statusElem document.getElementById(status); statusElem.textContent event.status; // “connected” 或 “disconnected” statusElem.style.color event.status connected ? #2ea44f : #cb2431; }); // 监听用户列表变化 const awareness provider.awareness; awareness.on(change, () { const users Array.from(awareness.getStates().keys()); document.getElementById(user-count).textContent users.length; }); // 将 Yjs 的共享文本绑定到 Monaco Editor const binding new MonacoBinding( ytext, editor.getModel(), new Set([editor]), provider.awareness ); // 从 Yjs 文档加载初始内容到编辑器如果远程有内容 // MonacoBinding 会自动同步这里我们手动触发一次预览更新 doc.on(update, () { setTimeout(updatePreview, 50); // 小延迟确保内容已更新 }); console.log(Yjs connected to document: ${docName}); } // 更新 Markdown 预览 function updatePreview() { const content editor.getValue(); const html marked.parse(content); const sanitizedHtml DOMPurify.sanitize(html); document.getElementById(preview).innerHTML sanitizedHtml; } // 绑定文档切换按钮事件 document.getElementById(connect-btn).addEventListener(click, () { const newDocName document.getElementById(doc-name-input).value.trim(); if (newDocName newDocName ! currentDocName) { currentDocName newDocName; initYjs(currentDocName); // 更新 URL 以便分享可选 window.history.pushState(null, null, ?doc${currentDocName}); } }); // 处理 URL 中的文档参数 window.addEventListener(load, () { const urlParams new URLSearchParams(window.location.search); const docParam urlParams.get(doc); if (docParam) { document.getElementById(doc-name-input).value docParam; currentDocName docParam; // 编辑器初始化后会调用 initYjs这里不需要重复调用 } });5. 运行验证与结果分析现在让我们启动系统并验证核心功能。5.1 启动服务器在项目根目录下使用 nodemon 启动服务器npx nodemon server.js如果看到Server is running on http://localhost:3000和WebSocket server is running on ws://localhost:3000的输出说明启动成功。5.2 功能验证步骤访问编辑器打开浏览器访问http://localhost:3000。你将看到一个分屏界面左侧是 Monaco 编辑器右侧是 Markdown 预览。测试实时编辑在编辑器中输入一些 Markdown 文本例如## Hello World。右侧预览会立即更新。打开第二个浏览器窗口或标签页也访问http://localhost:3000。确保两个窗口的文档名都是getting-started。在第一个窗口中继续编辑。观察第二个窗口其内容应该会在几百毫秒内自动同步更新。这证明了 CRDT 协同在正常工作。测试 Git 自动提交在两个窗口中编辑内容然后停止操作。等待至少 5 秒我们在服务器端设置的延迟。查看服务器终端日志。你应该能看到类似Git commit successful for getting-started的信息。验证提交在项目目录下执行以下命令cd repos/getting-started git log --oneline你应该能看到至少一次提交记录提交信息是“Auto-commit: Update content via collaborative editor”。查看文件内容cat README.md文件内容应该与编辑器中的最新内容一致。5.3 预期结果与原理分析实时协同通过 Yjs 库和 WebSocket多个客户端对同一文档的修改插入、删除会以 CRDT 操作的形式广播和同步最终保证一致性。你可以在编辑器中同时看到其他用户的光标位置Yjs Awareness 功能。版本历史Git 仓库repos/getting-started完整记录了文档的变更历史。你可以使用git diff查看每次提交的具体变化或使用git checkout回滚到任意历史版本。Markdown 渲染前端使用marked库将 Markdown 文本实时转换为 HTML并用DOMPurify进行净化以防止 XSS 攻击最后在右侧面板渲染。至此一个集成了 Git版本历史、CRDT实时协同和 Markdown内容格式的“个人 GitHub”核心原型就完成了。它具备了最基础的功能多人实时编辑 Markdown并自动保存版本快照。6. 常见问题排查与优化在实际运行中你可能会遇到一些问题。下面是一些常见问题的排查路径和解决方案。6.1 连接与同步问题问题现象可能原因检查与解决步骤编辑器内容无法同步1. WebSocket 连接失败。2. 前后端文档名不一致。3. Yjs 共享类型键名不一致。1. 打开浏览器开发者工具F12的Network-WS标签查看 WebSocket 连接状态。应为 101 Switching Protocols。2. 确认前端initYjs(docName)中的docName与后端 URL (ws://host/${docName}) 匹配。3. 确认前端doc.getText(‘content‘)与后端监听器中的键名也是‘content‘一致。其他用户光标不显示Awareness感知功能未正常工作或前端未正确显示。1. 确认y-websocket和y-monaco版本兼容。2. 检查前端代码中MonacoBinding的第四个参数是否传入了provider.awareness。3. 更复杂的感知UI需要额外代码本例仅演示基础同步。服务器终端报git命令错误1. 系统未安装 Git。2. Git 未配置全局用户信息。3. 仓库目录权限问题。1. 运行git --version确认 Git 已安装且可在命令行访问。2. 运行git config --global user.name和git config --global user.email检查配置。3. 确保 Node.js 进程有对repos/目录的读写权限。6.2 Git 操作相关问题提交过于频繁或丢失中间状态我们使用了简单的 5 秒延迟提交策略。在生产环境中这可能导致两个问题1) 用户连续编辑时会不断重置定时器可能长时间不提交2) 服务器重启可能导致内存中的最新内容丢失。优化方案采用更稳健的策略如“防抖”提交停止编辑后 N 秒提交结合“定时”提交每 M 分钟强制提交一次并使用 LevelDB 或数据库持久化 Yjs 文档。合并冲突的错觉注意Git 提交的是 CRDT 同步后的最终一致状态。Git 层面不会出现合并冲突因为所有协同编辑的冲突已在 CRDT 层解决。Git 记录的是解决冲突后的快照。这是 CRDT 与 Git 传统工作流的关键区别。大文件或二进制文件本系统设计用于文本Markdown。Yjs 对文本协同优化Git 也能很好处理文本差异。但对于图片等二进制文件直接进行 CRDT 同步和 Git 版本控制效率低下。常见做法将文件存储在对象存储如 S3或专门的文件服务中只在 Markdown 中保存引用链接URL仅对链接文本进行同步和版本控制。6.3 安全与性能考量文档名安全当前直接从 URL 路径获取文档名 (request.url.slice(1))存在目录遍历攻击风险如请求ws://localhost:3000/../etc/passwd。修复对docName进行严格校验只允许字母、数字、短横线和下划线。// 在 server.js 的 WebSocket 连接处理中 const docName request.url.slice(1); if (!/^[a-zA-Z0-9_-]$/.test(docName)) { ws.close(1008, Invalid document name); return; }无认证授权任何知道服务器地址和文档名的人都可以连接并编辑。生产系统必须加入用户认证如 JWT和文档权限管理读/写。内存泄漏我们的saveTimeoutsMap 和 Yjs 文档会常驻内存。需要监听 WebSocket 连接关闭事件清理对应的定时器和文档引用。可扩展性当前所有协同状态都在单进程内存中。当用户量或文档数增长时需要引入分布式架构使用 Redis 或数据库作为 Yjs 的共享状态存储后端。7. 最佳实践与扩展方向基于这个原型你可以将其扩展为一个更实用的系统。7.1 工程化最佳实践前后端分离将前端React/Vue与后端Node.js/Go完全分离。后端提供 WebSocket 协同 API 和 Git 操作 REST API。使用成熟的 Yjs 后端放弃内存存储使用y-leveldb、y-mongodb或y-redis进行持久化这些后端通常提供了更可靠的onWrite钩子来触发 Git 提交。Git 操作队列化高并发下对同一仓库的频繁 Git 操作可能失败。需要引入一个队列系统如 Bull串行化处理提交请求。结构化数据除了文本Yjs 还支持 Map、Array 等类型。你可以用 Y.Map 来存储文档的元数据标题、标签、创建者用 Y.Array 来存储评论列表实现更丰富的协同功能。7.2 功能扩展方向版本浏览与对比构建一个 Web 界面展示 Git 仓库的提交历史、差异对比diff并支持一键恢复某个历史版本。分支与合并借鉴 Git 的概念允许用户创建文档的“分支”进行独立编辑然后通过 CRDT 的“合并”功能或人工审核后合并回“主分支”。离线编辑Yjs 支持离线编辑。结合浏览器的 IndexedDB使用y-indexeddbprovider用户可以在断网时继续编辑网络恢复后自动同步。接入 GitHub/GitLab将本地的 Git 仓库定期推送到远程的 GitHub 或 GitLab 仓库实现备份和更广泛的协作。富媒体支持集成图片上传、粘贴截图、表格编辑等功能。这些内容可以存储为 Markdown 的图片链接或 HTML 片段。7.3 部署清单将本项目部署到生产环境前请检查以下清单[ ]安全文档名输入已过滤避免路径遍历。[ ]安全WebSocket 连接已添加身份验证如基于 Cookie/Session 或 JWT。[ ]安全Git 执行命令已做参数化或白名单限制防止命令注入。[ ]持久化Yjs 文档状态已配置数据库后端非内存。[ ]持久化Git 仓库存储路径有定期备份策略。[ ]性能对于可能的大文档已测试 Yjs 同步性能和 Git 操作耗时。[ ]监控服务器添加了 WebSocket 连接数、内存使用、Git 操作失败率的监控和日志。[ ]可维护性Git 自动提交的触发条件延迟时间、内容变化阈值已提取为配置文件。通过以上步骤你不仅搭建了一个可运行的原型更深入理解了 Git、CRDT 和 Markdown 在构建现代协同应用中的角色与整合方式。这个项目是一个起点你可以根据实际需求在其基础上构建出功能强大、稳定可靠的“个人”或“团队”知识协作平台。
返回列表