以 React 组件嵌入 Excalidraw:@excalidraw/excalidraw 0.18 安装、SSR 集成与字体自托管实践
Excalidraw 官方包 @excalidraw/excalidraw 将整块白板导出为一个可直接嵌入应用的 React 组件。本文基于仓库内 packages/excalidraw/README.md 及配套源码、示例工程展开,覆盖安装方式、最小可用配置、Next.js/SSR 框架下的客户端渲染方案、0.18.x 类型导入路径迁移、字体 CDN 与自托管机制,以及面向 LLM/编码 Agent 的集成排障清单,帮助你在自己的产品中正确、稳定地嵌入 Excalidraw。
包概览:一个组件、一套类型、一张 exports 地图
从 packages/excalidraw/package.json 可以看到该包的定位与关键事实:
- 包名为
@excalidraw/excalidraw,当前仓库版本为0.18.0,许可证为 MIT,type: "module"; - 运行时产物为
./dist/prod/index.js(main/module均指向它),类型入口为./dist/types/excalidraw/index.d.ts; - React 作为 peer dependency,支持
^17.0.2 || ^18.2.0 || ^19.0.0(react 与 react-dom 同版本区间); - 直接依赖体现了白板的核心能力栈:手绘线条的
roughjs与perfect-freehand、状态管理的jotai、LaTeX 的@excalidraw/math、图表解析的@excalidraw/mermaid-to-excalidraw、图片压缩的pica等; - 包内还声明了
@excalidraw/common、@excalidraw/element、@excalidraw/math、@excalidraw/laser-pointer等 monorepo 内部包,说明该仓库采用“组件包 + 功能包”的分层结构,组件包依赖这些底层包完成元素、渲染与状态逻辑。
exports 字段还额外暴露了若干类型专用子路径,例如 ./common/*、./element/*、./math/*、./utils/* 分别映射到对应包的 .d.ts,以及 ./index.css 按 development/production 条件导出不同的 CSS 产物。这是后文“0.18 迁移”与“LLM 集成技巧”两节的底层依据。
安装
安装包的同时需要安装它的 React peer dependencies(见 README 安装章节):
npm install react react-dom @excalidraw/excalidraw
# or
yarn add react react-dom @excalidraw/excalidraw
如果你想尝鲜未发布的变更,可以使用 @excalidraw/excalidraw@next。由于 package.json 中 react 的支持区间覆盖 17/18/19 三个大版本,多数现代 React 项目可直接引入,无需担心 peer 依赖冲突。
快速上手:两个最容易被忽略的要求
最小可运行示例只有两条“易错点”,README 特别强调:
- 必须导入包的 CSS:
import "@excalidraw/excalidraw/index.css"; - Excalidraw 必须渲染在一个高度非零的容器内。
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
export default function App() {
return (
<div style={{ height: "100vh" }}>
<Excalidraw />
</div>
);
}
原因是 Excalidraw 会填满父容器的 100% 宽度和高度——若父容器没有显式高度(例如默认 height: auto 且无内容的块级元素),画布高度会坍缩为 0,表现就是“页面空白但没有任何报错”。开发文档 dev-docs/docs/@excalidraw/excalidraw/installation.mdx 中同样用“Dimensions of Excalidraw”一节说明了这一行为,官方文档的集成示例也都是把组件包在类似 height: "500px" 的容器里。
Next.js / SSR 框架:客户端组件 + 动态导入
Excalidraw 依赖浏览器 API(Canvas、FontFace、localStorage 等),不支持服务端渲染,因此必须只在客户端渲染。README 给出的标准做法是:
- 将
Excalidraw封装进一个带"use client"指令的客户端组件; - 在服务端页面里用
next/dynamic以ssr: false动态加载该组件:
// app/components/ExcalidrawClient.tsx
"use client";
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
export default function ExcalidrawClient() {
return (
<div style={{ height: "100vh" }}>
<Excalidraw />
</div>
);
}
// app/page.tsx
import dynamic from "next/dynamic";
const ExcalidrawClient = dynamic(
() => import("./components/ExcalidrawClient"),
{ ssr: false },
);
export default function Page() {
return <ExcalidrawClient />;
}
这里为什么不能直接 dynamic(() => import("@excalidraw/excalidraw").Excalidraw)?开发文档 dev-docs/docs/@excalidraw/excalidraw/integration.mdx 解释了限制:next/dynamic 的命名导出写法只对命名组件导出有效;一旦你还想从包里引入其他工具或常量(如 convertToExcalidrawElements),就需要像 README 这样写一个 wrapper 组件再整体动态导入。
仓库内 examples/with-nextjs 是完整可运行的对照工程,其中 examples/with-nextjs/src/excalidrawWrapper.tsx 就是上文模式的原型:文件顶部 "use client",随后 import { Excalidraw } from "@excalidraw/excalidraw" 并导入 index.css,再渲染进一个有尺寸的容器;该工程同时提供 pages 路由与 app 路由两套入口(src/pages/excalidraw-in-pages.tsx 与 src/app/page.tsx)。当文档与代码出现偏差时,直接参照这份示例是最稳妥的对齐方式。
Preact 与浏览器直用
同一份开发文档还记录了两个相关约束,供不同技术栈的读者参考:
- Preact:UMD 构建内联了
react/jsx-runtime与react-dom/client,与 Preact 冲突。官方为 Preact 单独提供了构建,需要把process.env.IS_PREACT置为true;若使用 Vite,需在define中显式注入"process.env.IS_PREACT": JSON.stringify("true"),因为 Vite 默认会剔除环境变量; - 无构建工具直接跑在浏览器:通过 import map 统一
react、react-dom、react/jsx-runtime版本,再引入包的 CSS 与入口 JS。仓库的 examples/with-script-in-browser 即此模式的完整工程。
面向 LLM / 编码 Agent 的集成技巧
README 专门列出了一节“LLM / agent tips”,对使用 AI 辅助搭建集成的人同样适用,其核心是“先跑通再增强”:
- 先用
100vh容器里的裸<Excalidraw />跑通基础嵌入,之后再叠加 ref、initialData、持久化或自定义 UI; - 画布空白时,优先检查两件事:CSS 是否导入、父容器高度是否为 0——这是最常见的两类集成失败;
- 在 Next.js 等 SSR 框架中,先默认按“纯客户端渲染”处理,用
"use client"+dynamic(..., { ssr: false }),再去排查 hydration 报错或window is not defined; - 当导入路径或入口不清晰时,直接查看
node_modules/@excalidraw/excalidraw/package.json,已安装包的 exports 才是唯一事实来源; - 除非你有意自托管字体/资源,否则不要设置
window.EXCALIDRAW_ASSET_PATH; - 文档与生成代码出现漂移时,复制本仓库中最近的可用示例(
examples/with-nextjs或examples/with-script-in-browser)。
第 4 条可以直接对照 packages/excalidraw/package.json 的 exports 字段验证:根路径 . 给出 types/development/production/default 四类条件,./index.css 区分 dev 与 prod CSS,而 ./common/*、./element/* 等子路径只声明了 types 条件——这从构建配置层面印证了“深度子路径仅供类型导入”的规则。
迁移到 @excalidraw/excalidraw@0.18.x:类型导入路径变更
0.18.x 移除了旧的 types/ 前缀深度导入路径。若你从 @excalidraw/excalidraw/types/... 导入类型,需要按下表切换到新的类型专用子路径:
| 旧路径 | 新路径 |
|---|---|
@excalidraw/excalidraw/types/data/transform.js |
@excalidraw/excalidraw/element/transform |
@excalidraw/excalidraw/types/data/types.js |
@excalidraw/excalidraw/data/types |
@excalidraw/excalidraw/types/element/types.js |
@excalidraw/excalidraw/element/types |
@excalidraw/excalidraw/types/utility-types.js |
@excalidraw/excalidraw/common/utility-types |
@excalidraw/excalidraw/types/types.js |
@excalidraw/excalidraw/types |
迁移规则有两点,均来自 README 迁移章节:
- 去掉
.js后缀。新包的exports地图会在无后缀的情况下解析这些路径(例如./element/*映射到./dist/types/element/src/*.d.ts); - 这些深度子路径仅限
import type使用。运行时导入必须来自包根路径,样式则单独导入@excalidraw/excalidraw/index.css:
import { exportToSvg } from "@excalidraw/excalidraw";
从源码结构看,包根入口 packages/excalidraw/index.tsx 确实聚合了这些运行时 API:restoreElements、exportToCanvas、exportToBlob、exportToSvg、convertToExcalidrawElements 等工具函数都由根导出提供,与“运行时走包根、类型走子路径”的约定一致。
字体自托管:默认 CDN 与 EXCALIDRAW_ASSET_PATH
默认情况下,Excalidraw 会从公共 CDN(esm.run 上的包 dist 产物)下载所需字体。若需要在内网、离线或自有 CDN 场景自托管,步骤是:
- 把
node_modules/@excalidraw/excalidraw/dist/prod/fonts的内容复制到你的静态资源目录(如public/); - 把
window.EXCALIDRAW_ASSET_PATH设置为同一路径:
<script>
window.EXCALIDRAW_ASSET_PATH = "/";
</script>
若资源托管在 CDN 根路径下,可以写成 https://my.cdn.com/assets/ 这样的绝对地址;在 Next.js 中也可以用 next/script 的 beforeInteractive 策略基于 location.origin 动态设置(完整写法见 dev-docs 安装文档)。
底层实现可以参考 packages/excalidraw/fonts/ExcalidrawFontFace.ts:ExcalidrawFontFace 类维护一组按优先级排列的字体 URL(自托管路径在前、CDN 兜底地址在后,即源码中的 ASSETS_FALLBACK_URL),getContent 会依次尝试抓取 woff2 并按当前文本 codepoint 做字体子集化,失败时回退到列表中的下一个 URL;fetchFont 使用 cache: "force-cache" 并利用 URL 中的稳定 hash 后缀控制字体新鲜度。这解释了为什么“设置了 EXCALIDRAW_ASSET_PATH 却没复制 fonts 目录”会导致字体加载失败——自托管路径只是把首选 URL 换掉,并不会改变抓取与回退逻辑。
进一步阅读
- 完整集成细节(含 Preact、浏览器 import map 用法):dev-docs/docs/@excalidraw/excalidraw/integration.mdx;
- API、props、
initialData、子组件等参考:dev-docs/docs/@excalidraw/excalidraw/api 目录; - 可直接运行的示例工程:examples/with-nextjs(Next.js,含 pages/app 双路由)与 examples/with-script-in-browser(无构建工具的浏览器直用);
- 包的构建入口与导出映射:packages/excalidraw/package.json 与构建脚本 scripts/buildPackage.js。
小结
嵌入 Excalidraw 的最小契约只有三条:导入 index.css、容器高度非零、SSR 框架下按纯客户端渲染。在此之上,0.18.x 的类型子路径迁移、exports 地图、EXCALIDRAW_ASSET_PATH 字体机制,都可以直接对照本仓库的 package.json、ExcalidrawFontFace.ts 和两个官方示例工程逐一验证——这也是 README 给集成者(无论人还是 Agent)的核心建议:以已安装包的 exports 和仓库内最近的可用示例为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00