ExcalidrawDIY项目计划:手工制作步骤分解
在远程协作日益成为常态的今天,团队沟通中的“信息落差”问题愈发突出——设计师苦于无法快速表达脑中构图,产品经理担心技术实现偏离预期,而工程师则疲于在文字需求和视觉呈现之间反复对齐。一个能即时将想法转化为图形、并支持多人实时共创的工具,正变得不可或缺。
传统流程图工具虽然功能完整,但往往过于刻板,操作复杂,像在填写表格而非自由创作。相比之下,手绘风格的白板应用因其轻松自然的视觉语言,逐渐成为敏捷团队的新宠。其中,Excalidraw以其极简设计、开放架构和出色的可扩展性脱颖而出。它不仅模拟了真实纸笔的绘画质感,还允许开发者深度定制,甚至集成 AI 能力,实现“一句话生成架构图”。
本项目“ExcalidrawDIY”,正是要基于这一开源框架,打造一个可本地部署、支持 AI 自动生成、具备实时协作能力的个性化白板系统。我们不追求大而全的功能堆砌,而是聚焦于三个核心能力的打通:画得像人、说得清意、协同无感。
技术实现路径拆解
手绘质感是如何“伪造”的?
Excalidraw 最初吸引人的,就是那股“潦草但专业”的手绘味儿。这并非简单的滤镜效果,而是一套精心设计的渲染逻辑。
整个系统基于 TypeScript 和 React 构建,所有图形元素(矩形、箭头、文本等)都以对象形式存储在内存中,并通过 HTML5 Canvas 进行绘制。关键在于,它没有直接画出“完美”的几何图形,而是引入了一个叫rough.js的底层库,专门负责“破坏”这种完美。
当你拖动鼠标画一条直线时,Excalidraw 并不会真的画一条数学意义上的直线。相反,它会记录起点和终点,然后调用 rough.js 的line()方法,传入一个roughness参数(粗糙度)。这个值越大,线条的抖动就越明显;同时还有一个seed(随机种子),确保同一图形每次重绘时抖动模式一致——否则用户刚画完一撇,刷新页面就变了样,体验会非常糟糕。
const createRectangle = (x: number, y: number, width: number, height: number): ExcalidrawElement => { return { type: "rectangle", version: 1, isDeleted: false, id: generateId(), fillStyle: "hachure", // 交叉线填充,增强手写感 strokeWidth: 2, strokeStyle: "solid", roughness: 2, // 核心参数:控制抖动强度 opacity: 100, x, y, width, height, seed: Math.floor(Math.random() * 100000), // 确保重绘一致性 strokeColor: "#000", backgroundColor: "#fff", }; };这段代码创建了一个典型的矩形元素。注意roughness和seed的组合使用——这是实现“可控随机”的关键。此外,所有元素必须有唯一 ID,且状态更新需遵循不可变原则(immutable update),避免破坏 React 的 diff 机制。实践中常见的坑是直接修改数组元素导致界面不刷新,务必通过复制新数组来触发重渲染。
更进一步,Excalidraw 的数据结构完全开放,每个元素都是一个明确定义的 JSON 对象。这意味着你可以用程序批量生成图表,也能轻松解析现有白板内容做二次分析。这种“数据即文档”的理念,为后续集成 AI 和协作功能打下了坚实基础。
让 AI 听懂你的“画意”
如果说手绘风格降低了表达的心理门槛,那么 AI 生成功能则直接跳过了动手环节——你只需要说清楚想表达什么,剩下的交给模型。
这里的本质是:将自然语言转化为结构化的绘图指令。我们通常借助大语言模型(LLM)来完成这一转换。例如,当用户输入“画一个用户登录流程,包括账号密码输入、验证码校验和跳转主页”,系统需要从中提取出实体节点(如“账号输入框”、“验证码模块”)、动作关系(如“校验后跳转”),并推断合理的布局顺序。
实现的关键在于提示工程(Prompt Engineering)。我们必须给 LLM 一个清晰的指令模板,强制其输出符合 Excalidraw 要求的 JSON 格式:
import openai import json def generate_diagram(prompt: str) -> list: system_msg = """ You are a diagram assistant for Excalidraw. Convert user descriptions into Excalidraw elements. Output ONLY a JSON array of objects with keys: - type ("rectangle" or "arrow") - label (text inside the shape) - x, y, width, height (approximate coordinates and size) - startObjectId, endObjectId (for arrows only) Arrange nodes logically: left-to-right for flows, top-to-bottom for hierarchies. Do not include any explanation or markdown formatting. """ response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": prompt} ], temperature=0.3, # 降低随机性,提升输出稳定性 max_tokens=800 ) try: result = json.loads(response.choices[0].message['content']) return result except json.JSONDecodeError: print("Failed to parse LLM output as JSON") return []这个函数看似简单,但在实际部署中需要考虑多个工程细节:
- 输出校验:LLM 偶尔会返回非 JSON 内容或字段缺失,必须做健壮性处理;
- 坐标估算:模型无法精确知道像素位置,因此只需给出相对布局建议,前端再根据画布尺寸自动排布;
- 增量编辑:支持“在右侧增加失败分支”这类上下文感知指令,需结合当前白板状态做联合推理;
- 隐私与安全:敏感系统架构不应发送到公有云 API,推荐在内网部署私有模型(如 Ollama + Llama3)作为替代方案。
有意思的是,随着提示词不断优化,你会发现模型不仅能画流程图,还能模仿 Excalidraw 的风格偏好——比如自动使用fillStyle: "hachure",甚至为不同模块分配标志性颜色。这种“风格迁移”能力,让 AI 不只是工具,更像是一个懂你审美的协作者。
多人协作如何做到“无冲突”?
当两个工程师同时在一个白板上修改同一个组件时,会发生什么?如果处理不当,轻则覆盖对方改动,重则导致数据错乱。Excalidraw 官方本身不提供服务器,但它的设计天然适合扩展协作功能。
我们通常采用 WebSocket 搭建双向通信通道,配合 OT(Operational Transformation)或 CRDT 算法来解决并发问题。对于中小型项目,OT 已足够可靠。
基本思路是:每个客户端维护一份完整的白板状态副本。一旦发生变更(如新增一个矩形),就生成一个“差异包”并通过 WebSocket 广播给其他成员。接收方收到消息后,将其合并到本地状态,并触发 UI 更新。
const socket = new WebSocket('wss://your-collab-server/ws'); // 监听本地变化并推送 excalidrawRef.current?.addEventListener('change', ({ elements }) => { const updateMessage = { type: 'ELEMENTS_UPDATE', clientId: getCurrentClientId(), timestamp: Date.now(), payload: elements.map(e => ({ id: e.id, type: e.type, x: e.x, y: e.y, width: e.width, height: e.height, updatedAt: e.updatedAt || Date.now() })) }; if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(updateMessage)); } }); // 接收远程更新 socket.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'ELEMENTS_UPDATE' && msg.clientId !== getCurrentClientId()) { applyRemoteUpdate(msg.payload); } };上面这段代码展示了最基础的同步机制。但在生产环境中,还需加入更多保障措施:
- 增量更新:不要每次都发送全部元素,只传变动部分以节省带宽;
- 去重与排序:网络可能乱序或重复投递消息,需按时间戳+客户端 ID 排序合并;
- 心跳检测:定期发送 ping/pong 消息,及时发现断连并尝试重连;
- 离线支持:断网期间仍可本地编辑,恢复连接后智能合并历史变更;
- 冲突消解:若两人同时修改同一元素,可通过最后写入获胜(last-write-wins)或弹窗提示人工选择。
值得一提的是,Excalidraw 社区已有成熟的协作插件(如 Excalidraw+Firebase),可以直接借鉴。但对于追求完全自主可控的企业场景,自建同步服务仍是首选。
如何落地:从原型到可用系统
整体架构设计
我们将系统划分为三层,便于独立开发与部署:
+---------------------+ | 用户界面层 | | Excalidraw Web App | +----------+----------+ | +------v------+ +------------------+ | 业务逻辑层 <-----> AI 生成服务 | | (React App) | | (Python + LLM) | +-----+--------+ +------------------+ | +-----v------+ +--------------------+ | 数据通信层 <-----> 协作同步服务 | | (WebSocket) | | (Node.js + Socket) | +------------+ +--------------------+- 用户界面层:基于
@excalidraw/excalidraw官方组件封装,增加“AI 生成”按钮、房间邀请链接、在线用户指示器等定制 UI; - 业务逻辑层:处理 AI 请求封装、本地状态管理、协作消息调度;
- 数据通信层:统一由 Node.js 编写的 WebSocket 网关承接前后端通信,转发 AI 和协作消息。
所有服务均可容器化打包为 Docker 镜像,通过 docker-compose 一键启动,极大简化部署流程。
典型工作流
设想一个典型的技术评审场景:
- 架构师打开本地部署的 ExcalidrawDIY 页面;
- 点击“AI 生成”,输入:“画一个微服务架构,包含 API 网关、用户服务、订单服务、支付服务和 MySQL 数据库”;
- 几秒钟后,一张初步草图自动生成,各服务间用箭头标明调用关系;
- 他稍作调整,添加注释,并生成共享链接发给团队;
- 团队成员陆续加入,有人修改服务命名,有人补充缓存组件,所有改动实时可见;
- 会议结束前,导出为 SVG 插入会议纪要,版本锁定归档。
整个过程无需切换工具、无需等待截图反馈,真正实现了“边想边画、边画边改、边改边存”。
关键设计考量
在实施过程中,以下几个经验值得分享:
- 性能边界:当白板元素超过 500 个时,Canvas 渲染可能出现卡顿。建议启用虚拟滚动(virtual scrolling),仅渲染可视区域内的图形;
- 移动端适配:触控操作精度较低,应增大点击热区,优化手势识别逻辑(如防误触双指缩放);
- 权限控制:可扩展角色体系,区分“查看者”、“编辑者”、“管理员”,防止误操作;
- 可访问性:为图形添加 ARIA 标签,支持屏幕阅读器,满足无障碍浏览需求;
- 降级策略:AI 服务宕机时,自动隐藏“生成”按钮或提示手动绘制;协作断连时切换至离线模式,保留本地更改。
尤其值得注意的是隐私问题。许多企业不愿将内部架构图上传至第三方 API。因此,在生产环境强烈建议使用本地化部署的开源大模型(如 Llama3、Qwen)替代 OpenAI,既保障数据安全,又能获得接近的生成质量。
这种高度集成的设计思路,正引领着智能协作工具向更可靠、更高效的方向演进。ExcalidrawDIY 不只是一个 DIY 项目,它代表了一种新型知识生产的范式:思维外化加速器、共识构建催化剂、数字资产沉淀载体。通过开源、可定制、智能化的方式重构传统白板,我们正在见证轻量化协作工具的下一次跃迁。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考