← 返回文章列表

在 Astro 中嵌入 Excalidraw 交互画布

前端
  • # astro
  • # react
  • # excalidraw

我需要 Excalidraw 来实现画布,它支持导出 PNG/SVG。

直接使用导出的图片有两个问题:一是无法跟随页面主题切换改变颜色,二是需要先在 Excalidraw 应用中导出 PNG/SVG 文件再嵌入。

好在官方提供了 @excalidraw/excalidraw React 组件版本,可以直接在页面中嵌入交互画布,支持拖拽、缩放、主题切换等操作。

本文记录在 Astro 中集成 Excalidraw 的过程,参考了 bsdayo 的文章

准备工作

由于 Excalidraw 提供的组件仅有 React 版本,需要先为 Astro 添加 React 支持。可以前往官方文档查看详细信息。

文章文件需要使用 .mdx 后缀,以便在 Markdown 中引入 React 组件。

什么是mdx,为什么需要使用 mdx 文件 后缀?阅读 Astro 文档

添加好 React 支持后,安装 @excalidraw/excalidraw 包:

pnpm add @excalidraw/excalidraw

集成 Excalidraw 组件

在 .astro 组件中可以直接引入 Excalidraw,而在 .mdx 中则会报错:

Cannot find module /repo/node_modules/roughjs/bin/rough imported from /repo/node_modules/@excalidraw/excalidraw/dist/prod/index.js Did you mean to import “roughjs/bin/rough.js”?

解决方法,直接用 .astro 包一层。创建一个 ExcalidrawWrapper.astro:

---
import { Excalidraw } from '@excalidraw/excalidraw'
import type { ComponentProps } from 'react'

// 引入 Excalidraw 的样式,否则会报错。
import '@excalidraw/excalidraw/index.css'

type Props = ComponentProps<typeof Excalidraw>

const props = Astro.props
---

<div class=”excalidraw-wrapper”>
  <!-- 添加 client:only=”react”,否则会报错。 -->
  <Excalidraw client:only=”react” {...props} />
</div>

<style>
  /* Excalidraw 组件会填充整个父组件的空间 */
  .excalidraw-wrapper {
    width: 400px;
    height: 400px;
  }
</style>

注意点:

由于 Excalidraw 组件无法 SSR,需要在 <Excalidraw /> 上加入 client:only=”react” 否则报错:Cannot find module … Did you mean to import “roughjs/bin/rough.js”?

在 TS 部分 import @excalidraw/excalidraw/index.css 否则报错:CanvasRenderingContext2D.setTransform: Canvas exceeds max size. 然后就可以在其他地方引入 ExcalidrawWrapper.astro 组件了,例如:

---
import ExcalidrawWrapper from '../components/ExcalidrawWrapper.astro'
---
<ExcalidrawWrapper />

导入画布

---
import ExcalidrawWrapper from '../components/ExcalidrawWrapper.astro'
import test from './_test.excalidraw.json'
---

<ExcalidrawWrapper initialData={test} />

只读模式

---
import ExcalidrawWrapper from '../components/ExcalidrawWrapper.astro'
import test from './_test.excalidraw.json'
---

<ExcalidrawWrapper initialData={test} viewModeEnabled />

跟随主题变化

根据主题变化,需要在 ExcalidrawRaw.tsx 中添加 theme 属性,然后在 ExcalidrawWrapper.astro 中添加 theme 属性。代码如下:

---
const theme = 'dark'
---
<ExcalidrawWrapper initialData={test} viewModeEnabled theme={theme} />

隐藏菜单栏和右键菜单

修改 ExcalidrawWrapper.astro 中的样式:

.excalidraw-wrapper :global( .App-toolbar-content),
.excalidraw-wrapper :global( .context-menu) {
  display: none !important;
}

禁止拖拽

.excalidraw-wrapper-wrapper {
  pointer-events: none;
}

自动移动并缩放至内容

Excalidraw 画布中的元素是根据绝对坐标来定位的,导入的画布很有可能默认没有显示在组件之中(移动或者缩放才能看到)。

首先需要将 ExcalidrawWrapper.astro 中的部分内容拆分到一个单独的 React 组件,例如 ExcalidrawRaw.tsx,然后提供 excalidrawAPI 函数轮询是否初始化完成,最后调用 scrollToContent 方法。代码如下:

ExcalidrawRaw.tsx:

import { Excalidraw as Draw } from '@excalidraw/excalidraw'
import type { ComponentProps } from 'react'
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types'

export type ExcalidrawProps = ComponentProps<typeof Draw>

export function ExcalidrawRaw(props: ExcalidrawProps) {
  const excalidrawAPI = (api: ExcalidrawImperativeAPI) => {
    const interval = setInterval(() => {
      if (api.getSceneElements().length > 0) {
        api.scrollToContent(undefined, {
          fitToContent: true,
        })
        clearInterval(interval)
      }
    }, 10)
  }

  return <Draw viewModeEnabled excalidrawAPI={excalidrawAPI} {...props} />
}

ExcalidrawWrapper.astro:

---
import { ExcalidrawRaw, type ExcalidrawProps } from './ExcalidrawRaw'
import '@excalidraw/excalidraw/index.css'

type Props = ExcalidrawProps
const props = Astro.props
---

<div class="excalidraw-wrapper">
  <ExcalidrawRaw client:only="react" {...props} />
</div>

<style>
  .excalidraw-wrapper {
    width: 400px;
    height: 400px;
  }
</style>

效果展示