news 2026/4/16 13:42:01

Vue中集成Excalidraw实现在线画板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue中集成Excalidraw实现在线画板

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 技术栈,也必须引入reactreact-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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 14:19:40

基于springboot的在线预约挂号系统

随着医疗信息化的推进&#xff0c;在线预约挂号系统应运而生&#xff0c;为患者就医提供了极大便利。该系统采用 Java 语言进行开发&#xff0c;因其强大的跨平台能力和丰富的类库支持&#xff0c;能够确保系统在不同环境下稳定运行且具备高效的数据处理能力。借助 Spring Boot…

作者头像 李华
网站建设 2026/4/16 14:11:42

LangChain-Chatchat:基于本地知识库的中文问答系统

LangChain-Chatchat&#xff1a;打造中文本地知识库问答系统的实践之路 在企业级 AI 应用逐渐从“通用对话”走向“垂直场景落地”的今天&#xff0c;如何让大模型真正理解并准确回答特定领域的专业问题&#xff0c;成为开发者面临的核心挑战。尤其是在政府、金融、医疗等行业…

作者头像 李华
网站建设 2026/4/16 13:32:23

电脑硬盘满了? 怎么挂载网盘

1.通用方法&#xff1a;用CloudDrive挂载&#xff08;支持Windows和Mac&#xff09;(电脑硬盘多出2T&#xff0c;真香)2T超大容量&#xff01;点击领取 >>https://login.123pan.com/centerlogin?registerinvite&refiG1tVv准备工作下载CloudDrive软件&#xff1a;htt…

作者头像 李华
网站建设 2026/4/16 2:20:45

如何轻松管理多个Blender版本:告别切换烦恼的终极解决方案

如何轻松管理多个Blender版本&#xff1a;告别切换烦恼的终极解决方案 【免费下载链接】Blender-Launcher Standalone client for managing official builds of Blender 3D 项目地址: https://gitcode.com/gh_mirrors/bl/Blender-Launcher 在3D创作领域&#xff0c;Blen…

作者头像 李华
网站建设 2026/4/15 15:00:59

30天卖了10-25万,分享一个抖音男装赛道起号新思路

说句大实话&#xff0c;真正做过的都知道女装爆率非常高&#xff0c;但退货率也是真的高&#xff0c;尺码、色差、身材不符&#xff0c;一个理由就能退。 反而是男装&#xff0c;稳定、复购低但退货率低&#xff0c;非常适合普通人起号。这一期给大家拆一个起号成功率非常高、而…

作者头像 李华
网站建设 2026/4/6 16:35:28

Mybaits的优点缺点?

大家好&#xff0c;我是锋哥。今天分享关于【Mybaits的优点&缺点&#xff1f;】面试题。希望对大家有帮助&#xff1b; Mybaits的优点&缺点&#xff1f; 超硬核AI学习资料&#xff0c;现在永久免费了&#xff01; 下面是 MyBatis&#xff08;一个常用的 Java 持久层框…

作者头像 李华