1. 为什么需要私有化手绘白板
最近两年远程协作成为新常态,线上会议中经常需要快速绘制示意图说明想法。市面上虽然有不少在线白板工具,但要么功能过于复杂,要么存在数据隐私隐患。Excalidraw这个开源项目恰好解决了这个痛点——它用最简洁的笔触还原了纸上手绘的流畅感,同时支持多人实时协作。
我在技术团队内部推广使用时发现,将Excalidraw部署到私有服务器能带来三个显著优势:首先所有绘图数据完全自主可控,不用担心商业产品的突然收费或服务终止;其次可以深度定制工具栏和素材库,比如预置团队常用的架构图元件;最重要的是能与企业现有账号体系集成,实现权限管控。下面就以最新版Excalidraw v0.16.2为例,完整演示从服务器准备到深度定制的全流程。
2. 基础环境搭建
2.1 服务器选型建议
虽然Excalidraw对硬件要求不高,但考虑到多人协作时的实时同步压力,建议选择至少2核CPU+4GB内存的配置。实测阿里云ECS的ecs.c6.large实例(2核4G)在20人同时绘图时CPU负载保持在30%以下。如果预算有限,1核2G的轻量应用服务器也能满足小团队需求。
操作系统推荐Ubuntu 22.04 LTS,其长期支持周期与Node.js的兼容性都经过充分验证。避免使用CentOS等即将停止维护的发行版,以免后续安全更新缺失。
2.2 依赖安装与配置
# 更新系统并安装基础工具 sudo apt update && sudo apt upgrade -y sudo apt install -y git curl gnupg2 # 安装Node.js 18.x(LTS版本) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs # 验证安装 node -v # 应输出v18.x.x npm -v # 应输出9.x.x注意:不要使用Ubuntu默认源的Node.js版本(通常是v12.x),过旧的版本会导致构建失败。如果之前安装过旧版,需要先执行
sudo apt remove --purge nodejs npm彻底卸载。
3. Excalidraw部署详解
3.1 源码获取与构建
# 克隆官方仓库(建议fork后使用自己的仓库) git clone https://github.com/excalidraw/excalidraw.git cd excalidraw # 安装依赖(国内服务器建议先配置淘宝镜像) npm config set registry https://registry.npmmirror.com npm install --legacy-peer-deps # 生产环境构建 npm run build:app:prod构建完成后,生成的静态文件位于/app/dist目录。这里有个关键细节:默认配置下每次构建会生成带哈希值的文件名(如main.abc123.js),这不利于后续更新维护。建议修改vite.config.ts中的配置:
// 在vite.config.ts中找到build选项 build: { rollupOptions: { output: { entryFileNames: `[name].js`, chunkFileNames: `[name].js`, assetFileNames: `[name].[ext]` } } }3.2 Nginx服务配置
使用Nginx作为前端服务比Node.js直接运行更节省资源。以下是经过优化的配置示例:
server { listen 80; server_name whiteboard.yourdomain.com; root /path/to/excalidraw/app/dist; index index.html; # 开启gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; # 处理前端路由 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } }配置完成后执行sudo nginx -t验证语法,然后sudo systemctl reload nginx应用更改。如果使用HTTPS,建议使用Certbot自动获取Let's Encrypt证书:
sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d whiteboard.yourdomain.com4. 高级定制与优化
4.1 自定义素材库
在src/data目录下新建customLibrary.ts,按以下格式添加自定义元件:
export const customLibraryItems: LibraryItems = [ { status: "published", elements: [ { type: "rectangle", version: 100, versionNonce: 123456, id: "custom-server", fillStyle: "hachure", strokeWidth: 1, strokeStyle: "solid", roughness: 1, opacity: 100, angle: 0, x: 100, y: 100, strokeColor: "#000000", width: 50, height: 50, seed: 123456 } ], id: "custom-shapes", name: "Infra Components", created: Date.now() } ];然后在src/app.tsx中导入并合并到默认库:
import { customLibraryItems } from "../data/customLibrary"; // 在初始化代码段中添加 AppState.currentLibraryItems = [ ...AppState.currentLibraryItems, ...customLibraryItems ];4.2 持久化存储方案
默认情况下绘图数据只保存在浏览器本地存储。要实现服务端存储,需要对接自建数据库。以下是PostgreSQL的集成示例:
- 安装pg模块:
npm install pg @types/pg- 创建存储服务:
import { Pool } from 'pg'; const pool = new Pool({ user: 'dbuser', host: 'database.server.com', database: 'excalidraw', password: 'securepassword', port: 5432, }); export async function saveDrawing(roomId: string, data: string) { const client = await pool.connect(); try { await client.query( 'INSERT INTO drawings(room_id, data) VALUES($1, $2) ' + 'ON CONFLICT (room_id) DO UPDATE SET data = $2', [roomId, data] ); } finally { client.release(); } }- 在Socket事件处理器中调用存储方法:
socket.on('room:update', async (roomId, data) => { await saveDrawing(roomId, JSON.stringify(data)); socket.to(roomId).emit('remote:update', data); });5. 运维监控与调优
5.1 性能监控配置
使用PM2管理Node进程并收集指标:
npm install pm2 -g pm2 start "npm run start" --name excalidraw # 安装监控插件 pm2 install pm2-prom-exporter然后在Prometheus中添加抓取配置:
scrape_configs: - job_name: 'excalidraw' static_configs: - targets: ['your-server:9326']Grafana仪表板建议监控以下关键指标:
- 内存使用率(超过70%需要扩容)
- 每个房间的WS连接数
- 操作延迟百分位(P99应<200ms)
5.2 安全加固措施
- 限制上传文件类型:
// 在文件上传处理逻辑中添加 const ALLOWED_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/svg+xml' ]; if (!ALLOWED_MIME_TYPES.includes(file.type)) { throw new Error('Unsupported file type'); }- 启用CSP防护:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:";- 定期更新依赖:
# 使用npm-check-updates工具 npx npm-check-updates -u npm install6. 常见问题排查
6.1 构建失败问题
症状:npm install时报错"Could not resolve dependency"解决方案:
- 删除node_modules和package-lock.json
- 使用
npm install --legacy-peer-deps - 如仍失败,尝试指定具体版本:
npm install react@18.2.0 react-dom@18.2.06.2 实时同步延迟
症状:多人协作时操作响应慢优化步骤:
- 检查WebSocket连接质量:
sudo tcpdump -i eth0 'port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420)'- 优化Nginx的WS配置:
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400;6.3 字体显示异常
症状:中文输入显示为方框解决方法:
- 在public目录添加中文字体(如NotoSansSC-Regular.woff2)
- 修改src/constants.ts中的字体配置:
export const FONT_FAMILY = { Virgil: "Virgil, Noto Sans SC", Helvetica: "Helvetica, Noto Sans SC", Cascadia: "Cascadia, Noto Sans SC" };7. 扩展功能开发
7.1 插件系统集成
Excalidraw支持通过插件扩展功能。以下是开发自定义插件的模板:
const MyPlugin = { name: "my-plugin", onInstall(app) { app.registerToolbarButton({ name: "Magic Tool", icon: "<svg>...</svg>", onClick: () => { const elements = app.getSceneElements(); // 处理元素逻辑 } }); } }; // 在初始化时加载 window.ExcalidrawApp.initialize({ plugins: [MyPlugin] });7.2 导出PDF增强
默认的PDF导出功能较简单,可以通过pdf-lib库实现高级排版:
import { PDFDocument, rgb } from 'pdf-lib'; async function exportToPDF(elements, options) { const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage([options.width, options.height]); elements.forEach(element => { if (element.type === 'text') { page.drawText(element.text, { x: element.x, y: options.height - element.y, size: element.fontSize, color: rgb(0, 0, 0) }); } // 处理其他元素类型 }); return await pdfDoc.save(); }8. 移动端适配技巧
虽然Excalidraw官方对移动端支持有限,但通过以下修改可以显著改善体验:
- 调整触摸事件处理:
// 在src/utils.ts中修改 export const isTouchDevice = () => true; // 强制启用触摸模式- 优化工具栏布局:
/* 在src/styles.css中添加 */ @media (max-width: 768px) { .ToolIcon { width: 3rem; height: 3rem; } .layer-ui__wrapper { padding: 0.5rem; } }- 添加手势支持:
// 使用hammer.js添加手势识别 import Hammer from 'hammerjs'; const mc = new Hammer(canvasElement); mc.on('pinch', (e) => { // 实现缩放逻辑 });这套私有化部署方案已在笔者所在的技术团队运行超过6个月,支撑了日均200+的绘图会话。最关键的经验是:初期一定要做好监控埋点,因为性能问题往往在用户量增长后才暴露;另外建议建立定期备份机制,虽然Excalidraw数据量不大,但创意内容丢失的损失难以估量。如果遇到本文未覆盖的特殊需求,Excalidraw良好的代码结构使得二次开发成本相对可控,大部分功能都能通过阅读源码找到扩展点。