Vue 3 中集成 Excalidraw 实现手绘风格在线白板
在团队协作日益依赖可视化表达的今天,一张能快速勾勒想法、支持自由创作的“数字草图本”变得不可或缺。无论是产品原型讨论、架构设计推演,还是教学演示场景,传统的规整图形工具往往显得过于僵硬,而手绘风格则更贴近人类原始的思维流动。
Excalidraw 正是为此而生——它不是一个普通的绘图工具,而是一种思维方式的数字化延伸。其标志性的“手绘风”渲染让每一条线都带着温度,避免了机械对齐带来的距离感。尽管它是用 React 构建的,但这并不意味着 Vue 开发者只能望洋兴叹。借助现代前端模块化的能力,我们完全可以在 Vue 项目中无缝嵌入这个强大的白板引擎。
下面我们就来一步步实现一个功能完整、体验流畅的手绘白板,并探讨其中的关键技术细节与工程考量。
从零开始:在 Vue 3 + Vite 项目中引入 Excalidraw
要将一个 React 组件库集成到 Vue 环境中,核心思路是绕过框架模板系统,直接通过 DOM 操作完成渲染。幸运的是,@excalidraw/excalidraw提供了独立的 UI 包,允许我们在任意 JavaScript 环境下手动挂载其组件。
安装依赖
首先安装必要的包:
npm install react react-dom @excalidraw/excalidraw这里需要注意:即使你的项目是纯 Vue 技术栈,也必须引入react和react-dom。因为 Excalidraw 本质上是一个 React 函数组件,它的生命周期和状态管理都依赖于 React 运行时。构建工具(如 Vite)会将其作为外部依赖处理,不会影响 Vue 主体逻辑。
配置 Vite:解决环境变量问题
Excalidraw 内部使用了process.env.NODE_ENV来判断运行环境,但在默认配置下,Vite 并不会向浏览器注入process对象,这会导致运行时报错process is not defined。
解决方案是在vite.config.js中显式定义:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], define: { 'process.env': {} } })这个空对象足以满足运行时检查需求,无需真实传入环境变量值。
核心实现:跨框架渲染与状态管理
创建一个名为ExcalidrawBoard.vue的组件,结构如下:
<template> <div class="excalidraw-container"> <header class="board-header"> 在线手绘白板 <button class="save-btn" @click="save">💾 保存</button> </header> <div ref="excalidrawWrapper" class="excalidraw-wrapper"></div> <footer class="board-footer"> Powered by Excalidraw & Vue | @lzugis 2024 </footer> </div> </template> <script setup> import { onMounted, onUnmounted, ref } from 'vue' import { createRoot } from 'react-dom/client' import React from 'react' import { Excalidraw } from '@excalidraw/excalidraw' const excalidrawWrapper = ref(null) let root = null let app = null // 存储 Excalidraw API 实例 const loadFromStorage = (key, defaultValue = null) => { try { const data = localStorage.getItem(key) return data ? JSON.parse(data) : defaultValue } catch (e) { console.warn(`Failed to parse ${key} from localStorage`, e) return defaultValue } } onMounted(() => { const wrapper = excalidrawWrapper.value if (!wrapper) return root = createRoot(wrapper) const savedElements = loadFromStorage('excalidraw-elements') const savedLibs = loadFromStorage('excalidraw-library') const savedState = loadFromStorage('excalidraw-state', { theme: 'light', zoom: { value: 1 }, offsetLeft: 0, offsetTop: 0 }) root.render( React.createElement(Excalidraw, { initialData: { elements: savedElements, libraryItems: savedLibs, appState: { ...savedState, langCode: 'zh-CN' // 启用中文界面 } }, onChange: (elements) => { localStorage.setItem('excalidraw-elements', JSON.stringify(elements)) }, onLibraryChange: (items) => { localStorage.setItem('excalidraw-library', JSON.stringify(items)) }, excalidrawAPI: (api) => { app = api window.excalidrawAPI = api // 方便调试 }, UIOptions: { canvasActions: { export: true, saveToActiveFile: false, loadScene: true } } }) ) }) onUnmounted(() => { if (root) { root.unmount() root = null } }) const save = () => { if (app) { const state = app.getAppState() const elements = app.getSceneElements() localStorage.setItem('excalidraw-state', JSON.stringify(state)) localStorage.setItem('excalidraw-elements', JSON.stringify(elements)) alert('✅ 画板内容已保存至本地!') } } </script> <style scoped> .excalidraw-container { width: 100%; height: 100vh; display: flex; flex-direction: column; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .board-header { height: 48px; background-color: #0d6efd; color: white; padding: 0 16px; display: flex; align-items: center; justify-content: space-between; font-size: 1.1rem; font-weight: 500; } .save-btn { background-color: #fff; color: #0d6efd; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.95rem; } .save-btn:hover { background-color: #f0f0f0; } .excalidraw-wrapper { flex-grow: 1; background-color: #f0f0f0; } .board-footer { height: 36px; background-color: #0d6efd; color: white; text-align: center; line-height: 36px; font-size: 0.9rem; } </style>关键点解析
跨框架渲染机制
Vue 和 React 是两个独立的 UI 框架,彼此无法直接解析对方的组件语法。因此我们采用“原生 DOM 容器 + 手动挂载”的方式:
- 使用
ref获取 DOM 元素 - 通过
createRoot(el).render()将 React 组件渲染进指定节点 - 整个过程不涉及
.jsx或模板编译,纯粹是 JavaScript 层面的操作
这种方式虽然牺牲了一点“声明式”的优雅,但换来的是极高的灵活性,适用于任何需要嵌入第三方 React 库的场景。
数据持久化的取舍
目前采用了localStorage实现本地保存,适合轻量级应用或离线使用场景。三个关键数据分别存储:
| 数据类型 | 存储 Key |
|---|---|
| 图元元素 | excalidraw-elements |
| 组件库 | excalidraw-library |
| 应用状态 | excalidraw-state |
其中onChange回调会在每次图形变动后触发,自动同步图元数据;而点击“保存”按钮才手动写入完整状态(包括主题、缩放等),避免频繁操作带来性能损耗。
⚠️ 注意:
localStorage有容量限制(通常为 5–10MB),对于复杂图表可能溢出。生产环境中建议结合 IndexedDB 或服务端存储。
中文支持与用户体验优化
只需设置langCode: 'zh-CN',Excalidraw 即可自动切换为中文界面,无需额外加载语言包。这是其国际化做得非常友好的一点。
此外,自定义头部和底部栏不仅提升了品牌辨识度,也为后续扩展功能预留了空间——比如添加用户信息、项目名称、AI 快捷入口等。
常见问题与避坑指南
❌process is not defined
这个问题几乎成了 Vite + Excalidraw 的“标配”报错。根本原因是 Node.js 环境变量未被浏览器识别。
解决方案:务必在vite.config.js中添加:
define: { 'process.env': {} }否则即使打包成功,运行时也会崩溃。
❌ 白板区域空白或样式错乱
常见原因包括:
.excalidraw-wrapper没有实际高度 → 解决方案:确保父容器有明确高度,且该元素设置flex-grow: 1- 外层容器使用了
transform→ 影响定位计算,导致菜单错位 → 避免在祖先节点上使用transform overflow: hidden导致弹窗被裁剪 → 可临时移除或调整层级结构
这类问题本质是 CSS 布局冲突,建议使用浏览器开发者工具逐层排查盒模型。
❌ 复杂对象无法正确序列化
localStorage只接受字符串,因此必须手动JSON.stringify。注意某些特殊对象(如Set,Map,Date)会被错误转换。
建议封装统一的存储工具函数:
function safeSave(key, data) { try { localStorage.setItem(key, JSON.stringify(data)) } catch (e) { console.error(`Failed to save ${key}`, e) } } function safeLoad(key, fallback = null) { try { const item = localStorage.getItem(key) return item ? JSON.parse(item) : fallback } catch (e) { console.warn(`Failed to load ${key}`, e) return fallback } }扩展潜力:不只是画图,更是智能创作平台
Excalidraw 的插件系统为其打开了通往“AI 辅助设计”的大门。设想这样一个场景:
用户输入:“帮我画一个用户登录流程图,包含邮箱验证和密码重置”
系统即可调用 LLM(如 GPT、通义千问)解析语义,生成对应的图形结构并注入画布:
async function generateFromPrompt(prompt) { const response = await fetch('/api/generate-diagram', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }) const newElements = await response.json() if (app) { app.addElements(newElements) } }这种“自然语言 → 草图”的能力,正在成为新一代生产力工具的核心竞争力。
除此之外,多人协作也是值得投入的方向。虽然 Excalidraw 本身不提供实时同步能力,但可以通过 WebSocket 结合 CRDT(Conflict-free Replicated Data Type)算法实现真正的协同编辑,打造类似 Figma 的体验。
总结与思考
将 Excalidraw 成功集成进 Vue 项目,本质上是一次“框架互操作性”的实战演练。它提醒我们:优秀的前端架构不应局限于单一技术栈,而应具备整合多元生态的能力。
在这个案例中,我们看到了几个重要的工程实践原则:
- 渐进集成优于全盘重构:不必为了用一个功能就迁移到新框架,合理封装即可复用优质资产。
- 运行时兼容性优先于开发便利性:即便增加了 React 依赖,只要不影响构建效率和用户体验,就是可接受的技术债。
- 本地优先,云端扩展:先保证基础功能可用,再逐步叠加网络同步、AI 增强等高级特性。
未来你可以进一步将其封装为全局插件或可复用组件库,甚至构建企业级知识协作平台。当手绘的灵感与数字的力量结合,每一次涂鸦都可能成为改变世界的起点。
技术的价值,从来不只是“能不能”,而是“如何让更多人轻松地做到”。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考