首页
/ 以 React 组件嵌入 Excalidraw:@excalidraw/excalidraw 0.18 安装、SSR 集成与字体自托管实践

以 React 组件嵌入 Excalidraw:@excalidraw/excalidraw 0.18 安装、SSR 集成与字体自托管实践

2026-09-04 19:14:41作者:冯爽妲Honey

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.jsmain/module 均指向它),类型入口为 ./dist/types/excalidraw/index.d.ts
  • React 作为 peer dependency,支持 ^17.0.2 || ^18.2.0 || ^19.0.0(react 与 react-dom 同版本区间);
  • 直接依赖体现了白板的核心能力栈:手绘线条的 roughjsperfect-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.cssdevelopment/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 特别强调:

  1. 必须导入包的 CSSimport "@excalidraw/excalidraw/index.css";
  2. 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 给出的标准做法是:

  1. Excalidraw 封装进一个带 "use client" 指令的客户端组件;
  2. 在服务端页面里用 next/dynamicssr: 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.tsxsrc/app/page.tsx)。当文档与代码出现偏差时,直接参照这份示例是最稳妥的对齐方式。

Preact 与浏览器直用

同一份开发文档还记录了两个相关约束,供不同技术栈的读者参考:

  • Preact:UMD 构建内联了 react/jsx-runtimereact-dom/client,与 Preact 冲突。官方为 Preact 单独提供了构建,需要把 process.env.IS_PREACT 置为 true;若使用 Vite,需在 define 中显式注入 "process.env.IS_PREACT": JSON.stringify("true"),因为 Vite 默认会剔除环境变量;
  • 无构建工具直接跑在浏览器:通过 import map 统一 reactreact-domreact/jsx-runtime 版本,再引入包的 CSS 与入口 JS。仓库的 examples/with-script-in-browser 即此模式的完整工程。

面向 LLM / 编码 Agent 的集成技巧

README 专门列出了一节“LLM / agent tips”,对使用 AI 辅助搭建集成的人同样适用,其核心是“先跑通再增强”:

  1. 先用 100vh 容器里的裸 <Excalidraw /> 跑通基础嵌入,之后再叠加 ref、initialData、持久化或自定义 UI;
  2. 画布空白时,优先检查两件事:CSS 是否导入、父容器高度是否为 0——这是最常见的两类集成失败;
  3. 在 Next.js 等 SSR 框架中,先默认按“纯客户端渲染”处理,用 "use client" + dynamic(..., { ssr: false }),再去排查 hydration 报错或 window is not defined
  4. 当导入路径或入口不清晰时,直接查看 node_modules/@excalidraw/excalidraw/package.json已安装包的 exports 才是唯一事实来源
  5. 除非你有意自托管字体/资源,否则不要设置 window.EXCALIDRAW_ASSET_PATH
  6. 文档与生成代码出现漂移时,复制本仓库中最近的可用示例(examples/with-nextjsexamples/with-script-in-browser)。

第 4 条可以直接对照 packages/excalidraw/package.jsonexports 字段验证:根路径 . 给出 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:restoreElementsexportToCanvasexportToBlobexportToSvgconvertToExcalidrawElements 等工具函数都由根导出提供,与“运行时走包根、类型走子路径”的约定一致。

字体自托管:默认 CDN 与 EXCALIDRAW_ASSET_PATH

默认情况下,Excalidraw 会从公共 CDN(esm.run 上的包 dist 产物)下载所需字体。若需要在内网、离线或自有 CDN 场景自托管,步骤是:

  1. node_modules/@excalidraw/excalidraw/dist/prod/fonts 的内容复制到你的静态资源目录(如 public/);
  2. window.EXCALIDRAW_ASSET_PATH 设置为同一路径:
<script>
  window.EXCALIDRAW_ASSET_PATH = "/";
</script>

若资源托管在 CDN 根路径下,可以写成 https://my.cdn.com/assets/ 这样的绝对地址;在 Next.js 中也可以用 next/scriptbeforeInteractive 策略基于 location.origin 动态设置(完整写法见 dev-docs 安装文档)。

底层实现可以参考 packages/excalidraw/fonts/ExcalidrawFontFace.tsExcalidrawFontFace 类维护一组按优先级排列的字体 URL(自托管路径在前、CDN 兜底地址在后,即源码中的 ASSETS_FALLBACK_URL),getContent 会依次尝试抓取 woff2 并按当前文本 codepoint 做字体子集化,失败时回退到列表中的下一个 URL;fetchFont 使用 cache: "force-cache" 并利用 URL 中的稳定 hash 后缀控制字体新鲜度。这解释了为什么“设置了 EXCALIDRAW_ASSET_PATH 却没复制 fonts 目录”会导致字体加载失败——自托管路径只是把首选 URL 换掉,并不会改变抓取与回退逻辑。

进一步阅读

小结

嵌入 Excalidraw 的最小契约只有三条:导入 index.css、容器高度非零、SSR 框架下按纯客户端渲染。在此之上,0.18.x 的类型子路径迁移、exports 地图、EXCALIDRAW_ASSET_PATH 字体机制,都可以直接对照本仓库的 package.jsonExcalidrawFontFace.ts 和两个官方示例工程逐一验证——这也是 README 给集成者(无论人还是 Agent)的核心建议:以已安装包的 exports 和仓库内最近的可用示例为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384