Excalidraw 支持离线 PWA 应用模式
在高铁穿行于信号盲区时突然想修改一张架构图,或是在教室 Wi-Fi 崩溃的瞬间需要完成一次教学演示——这些场景下,我们才真正意识到:一个“只能联网使用”的工具,本质上仍是个脆弱的网页,而非可靠的生产力应用。正是在这种现实痛点的推动下,Excalidraw 近期完成了关键演进:全面支持 PWA(渐进式 Web 应用)架构,实现了真正的离线可用能力。
这不仅是加了个“可安装到桌面”的功能那么简单。它意味着 Excalidraw 正从一个“浏览器里的小众白板”,蜕变为工程师、设计师和教育工作者手中随时可用的数字画布。而背后的支撑技术,是一整套现代 Web 能力的协同运作。
PWA 是如何让网页“活下来”的?
PWA 的核心目标很直接:当网络断了,你的应用还能继续工作。要做到这一点,光靠传统的页面缓存远远不够。Excalidraw 实现离线运行依赖三个关键技术组件的精密配合。
首先是manifest.json文件,它像是给浏览器的一份“身份说明书”。通过这个文件,Excalidraw 告诉浏览器:“我不是普通网页,我是可以独立运行的应用。” 其中最关键的是"display": "standalone"配置,这让应用启动后不再显示地址栏和导航按钮,完全模拟原生 App 的全屏体验。
{ "name": "Excalidraw", "short_name": "Excalidraw", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" } ] }但真正让离线成为可能的,是 Service Worker —— 一种运行在浏览器后台的独立脚本。它像一个智能代理,能拦截所有网络请求,并决定是从缓存中返回资源,还是发起真实请求。在 Excalidraw 中,它的注册过程非常轻量:
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js'); }); }一旦注册成功,Service Worker 就会在安装阶段预缓存关键静态资源:
const CACHE_NAME = 'excalidraw-v1'; const urlsToCache = [ '/', '/index.html', '/static/main.js', '/static/styles.css', '/assets/handwriting-font.woff2' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache)) ); });此后每次访问,无论是否有网,Service Worker 都会优先尝试从缓存中读取内容。只有未命中缓存的请求才会走网络。这种“缓存优先 + 网络回退”策略,确保了即使在网络中断时,用户依然能看到完整的界面并进行操作。
不过这里有个工程上的微妙权衡:如果所有请求都走缓存,那新版本更新怎么办?Excalidraw 的做法是,在后台静默检查新版本的 Service Worker,待旧实例空闲后自动激活,实现无感升级。同时对 API 请求保持网络优先,避免本地数据陈旧。
手绘风格背后:不是滤镜,而是算法生成
很多人第一次看到 Excalidraw 的图形时都会问:“这是用了什么手写笔刷?” 实际上,这些看似随意的线条和形状,完全是通过数学算法实时生成的。
其核心技术依赖于 rough.js 这个轻量级库。它不依赖图像处理,而是将每个几何图形拆解为多个采样点,再对这些点施加符合正态分布的随机偏移,最后用贝塞尔曲线平滑连接,从而模拟出手绘的自然抖动感。
比如画一条“手绘直线”,流程如下:
1. 在起点 A 和终点 B 之间均匀插入 N 个中间点;
2. 每个点沿垂直方向偏移一个随机值(幅度由roughness参数控制);
3. 使用平滑路径拟合这些扰动后的点。
代码层面极其简洁:
import RoughCanvas from "roughjs/bundled/rough.cjs"; const canvas = document.getElementById("canvas"); const rc = RoughCanvas(canvas); rc.rectangle(10, 10, 200, 100, { stroke: "black", strokeWidth: 2, roughness: 2.5, bowing: 2 });这里的roughness参数尤为关键。设为 0 时就是标准直线;值越高,边缘越“潦草”。我们在实际项目中发现,roughness=2~3是最佳平衡点——足够生动又不影响可读性。过高反而会让图表显得混乱,尤其是在高密度信息展示时。
更巧妙的是,这种渲染完全发生在前端,无需服务器参与。这意味着导出 SVG 或 PNG 时,图形本身就是矢量路径,放大不失真,也便于后续编辑。
多人协作是如何做到“秒级同步”的?
当你和同事同时打开同一个白板链接,你们的操作几乎实时互现。这种体验的背后,是一套高效且鲁棒的实时通信机制。
Excalidraw 并没有采用“定时拉取状态”这种低效方式,而是基于 WebSocket 构建了一个发布/订阅模型。每个白板对应一个逻辑上的“房间”,用户的操作被封装成增量消息广播给同房成员。
class CollaborativeEditor { constructor(roomId) { this.socket = new WebSocket(`wss://excalidraw.com/socket/${roomId}`); this.socket.onmessage = (event) => { const update = JSON.parse(event.data); this.applyUpdateLocally(update); }; } sendUpdate(element) { const update = { type: 'element/update', payload: { id: element.id, x: element.x, y: element.y, properties: element.getProperties() }, timestamp: Date.now(), clientId: this.clientId }; this.socket.send(JSON.stringify(update)); } }这种方式的优势非常明显:只传输变更部分,带宽消耗极低;延迟通常控制在 200ms 以内,接近即时反馈。而且由于每个元素都有唯一 ID,客户端收到更新后能精准定位并重绘目标对象,不会引发整体刷新。
当然,现实网络并不完美。连接中断怎么办?Excalidraw 在设计上做了多层容错:
- 客户端持续发送心跳包检测连接状态;
- 断线期间的操作暂存本地 IndexedDB;
- 重连后批量补发未同步变更;
- 若存在冲突(如两人同时改同一图形),以后来者为准或提示合并。
这套机制使得即使在网络波动频繁的移动环境下,协作也不会轻易崩溃。
整体架构与典型工作流
从系统角度看,Excalidraw 的架构清晰地分为三层:
graph TD A[前端层 Client] -->|HTTPS/WebSocket| B[服务层 Server] B --> C[存储层 Storage] subgraph A [前端层] A1[React UI] A2[Canvas 渲染引擎] A3[Service Worker] A4[Local Storage / IndexedDB] end subgraph B [服务层] B1[静态资源托管] B2[WebSocket 网关] B3[房间管理] B4[认证模块(可选)] end subgraph C [存储层] C1[S3/Blob 存储] C2[数据库 - 元数据] endPWA 的核心作用集中在前端层。Service Worker 负责离线资源加载,而 IndexedDB 则承担了本地数据暂存的任务。即便在网络中断时,用户仍可自由绘制、编辑,所有变更都会被记录下来。
典型的工作流程是这样的:
- 首次访问:浏览器下载 manifest 并注册 Service Worker,关键静态资源进入缓存。
- 安装 PWA:用户点击“添加到主屏幕”,生成独立图标。
- 离线使用:断网状态下启动应用,Service Worker 返回缓存页面,用户开始创作,数据保存至本地。
- 恢复同步:网络恢复后,客户端自动尝试上传本地变更。若远程已有更新,则触发冲突解决流程。
整个过程中,用户体验应当是无缝的。Excalidraw 通过状态栏明确提示“当前离线”“正在同步…”等信息,避免用户误判操作结果。
工程实践中的几个关键考量
在落地 PWA 支持的过程中,团队必须面对一系列现实挑战,而不仅仅是“把代码跑通”。
首先是缓存策略的设计。不能简单地缓存所有请求。例如/api/*接口必须保留网络优先,否则会导致数据陈旧。Excalidraw 采用了分层策略:
- 静态资源(JS/CSS/字体):Cache First
- 动态接口请求:Network Only 或 Stale While Revalidate
- 用户上传文件:根据 URL 特征动态判断
其次是本地存储容量问题。虽然 IndexedDB 可达数百 MB,但长期积累仍可能溢出。因此引入了自动清理机制:定期删除超过 30 天未访问的本地草稿,并提供手动清空选项。
另一个容易被忽视的问题是版本更新时的兼容性。新的 Service Worker 安装完成后,旧的可能仍在运行(尤其当用户开了多个标签页)。这时需监听waiting状态,并在适当时机调用skipWaiting()强制激活,或者弹窗提示用户刷新页面以启用新版功能。
最后是降级体验。尽管绝大多数现代浏览器都支持 PWA,但 IE 等老旧环境仍需保障基本可用性。为此,Excalidraw 对非 PWA 浏览器仅关闭“离线模式”提示,其余功能照常运行,真正做到渐进增强。
为什么说这不是一次简单的功能迭代?
Excalidraw 支持 PWA 离线模式的意义,远超“多了一个安装按钮”。
它标志着产品定位的根本转变:从“需要联网才能使用的网页工具”,进化为“始终可用的个人创作空间”。就像笔记本不会因为没 Wi-Fi 就写不了字一样,一个好的数字白板也不该受制于网络。
这种转变带来了实实在在的价值提升:
-差旅途中,飞机上也能完善方案草图;
-教学现场,即使投影仪连不上校园网,课程演示照常进行;
-敏感项目,涉及保密内容的初稿可在纯本地环境中暂存,杜绝外泄风险;
-快速协作,分享链接即可加入,无需下载 App、注册账号,极大降低参与门槛。
更重要的是,PWA 架构本身为未来扩展打下了坚实基础。随着 AI 功能的深入集成——比如语音转草图、自然语言生成图表、智能排版建议——这些计算密集型任务完全可以利用 Web Workers 在本地完成,进一步减少对外部服务的依赖。
某种意义上,Excalidraw 正在重新定义“数字黑板”的边界。它不再只是一个绘图工具,而是一个融合了离线可靠性、视觉亲和力和协作灵活性的知识表达平台。而这,或许正是下一代生产力工具应有的样子。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考