docusaurus中引入Excalidraw

431 阅读4分钟

0. 效果

image.png

这是一个用docusaurus搭建的文档系统,可以看到我在页面嵌入了Excalidraw,并且可以编辑查看文件。

1.封装Excalidraw组件

excalidraw文档

docusaurus文档

由于文档使用mdx文件,所以可以引入react组件,下面就来实现一个excalidraw组件。

image.png

mdx文件中引入就可以使用。

1.1. 引入错误

image.png 我将组件写在文档根目录src/components/excalidraw中,

按照文档的提示,安装excalidraw后引入。

import { Excalidraw as Editor } from "@excalidraw/excalidraw";
const Excalidraw = () => {
  return (
    <div style={{ height: 700 }}>
      <Editor
        langCode="zh-CN"
      />
    </div>
  );
};
export default Excalidraw;

不出意外项目应该会报错

image.png

看到process很明显不应该出现在网页中,应该在打包的时候就处理了。试试build一下,很遗憾也会失败。

通过观察Excalidraw源码,以及报错的docusaurus代码,在@excalidraw/excalidraw/main.js

image.png 使用了IS_PREACT环境变量,导致webpack打包的时候没有把这个变量替换成常量(默认只替换了process.env.NODE_ENV)。

下面记录了我的思考过程,主要是给自己做个备份。

解决方案可以直接看1.2.11.2.2章节的最后代码。

1.2. 解决引入错误

1.2.1 解决dev错误

思路很简单,就是使用DefinePlugin把这个环境变量注入替换。为了修改docusauruswebpack配置,我们需要写一个简单的docusaurus插件:

// excalidraw-plugin插件
import type { PluginModule } from "@docusaurus/types";
import webpack from "webpack";

const plugin: PluginModule = async function (context) {
  return {
    name: "excalidraw-plugin",
    // 修改原始webpack配置
    configureWebpack(webpackConfig, isServer, utils) {
      return {
        plugins: [
        // 添加webpack插件
          new webpack.DefinePlugin({
          // 将IS_PREACT替换成常量
            "process.env.IS_PREACT": "false",
          }),
        ],
      };
    },
  };
};
export default plugin;

// 在docusaurus.config.ts中使用
import excalidrawPlugin from "./plugins/excalidrawPlugin";
const config: Config = {
  //....
  plugins: [
    [excalidrawPlugin, {}],
  ],
  // ....
}

1.2.1. 解决build错误

很遗憾打包还是会报错

image.png 可以看到是server.bundle在报错,服务端中没有self,这里的self对于excalidraw就是window

解决方法:

import BrowserOnly from "@docusaurus/BrowserOnly";
import { Suspense, lazy } from "react";
const Excalidraw = () => {
  return (
  // 使用BrowserOnly,表示下面内容只运行在浏览器
    <BrowserOnly>
      {() => {
      // Excalidraw不能再外面引入,因为在外面引入代表server和client都要用
      // 会报错
        const Editor = lazy(async () => {
          return {
            default: (await import("@excalidraw/excalidraw")).Excalidraw,
          };
        });
       // 因为使用了react的lazy作为懒加载,所以必须Suspense包裹
       return (
          <Suspense fallback={<>loading....</>}>
            <div style={{ height: 700 }}>
              <Editor 
               angCode="zh-CN"
              />
            </div>
          </Suspense>
        );
      }}
    </BrowserOnly>
  );
};
export default Excalidraw;

这样就能解决devbuild的错误了。

1.3.ocusaurus架构

为了了解原因,有必要搞清楚docusaurus的架构。 文档

image.png 可以看到整个文档分为了serverclient,并不是代表有后端和前端,只是docusaurus有两种页面实现方式。

  • 一种是src中的普通页面,它会使用server bundle中的react dom server渲染,也就是ssr方式。
  • 一种是docs中的markdown文件,他会编译成js,在客户端渲染。

而开发阶段是没有区分渲染的,直接是csr,所以只有打包发生了之前的错误。也就是误认为写在src中的Excalidraw组件是ssr的组件,自然遇到调用windowapi时发生错误的事情。

使用BrowserOnly就是内部判断了下是不是浏览器环境,如果不是就不渲染。

image.png

2. 其他

2.1. 导入外部数据

Excalidraw可以导入导出数据,实际上就是一个json

 <Excalidraw
    initialData={import(`data.json`)}
  />

可以直接一个json对象,也可以是一个函数。不过直接导入可能会出现元素不居中的情况,原因是导出的时候元素就不是居中的。

excalidraw暴露的api中有scrollToContent方法,就是画布滚动到合适的位置。由于触发这个方法的实际必须是initialData渲染完成的时候,所以这里我用了一个比较hack的办法。

const excalidrawAPI = useRef<ExcalidrawImperativeAPI>(null);
const firstLoad = useRef<boolean>(true);
// ...
<Editor
    excalidrawAPI={(api) => (excalidrawAPI.current = api)}
    onScrollChange={() => {
      // 初始渲染之后会触发onScrollChange
      if (firstLoad.current) {
        // 如果是第一次渲染就设置居中
        excalidrawAPI.current.scrollToContent(
          excalidrawAPI.current.getSceneElements(),
          {
            fitToContent: true,
            animate: false,
          }
        );
        firstLoad.current = false;
      }
    }}
/>

3. 完整代码

import BrowserOnly from "@docusaurus/BrowserOnly";
import { lazy, Suspense, useEffect, useRef, useState } from "react";
import { useColorMode } from "@docusaurus/theme-common";
import {
  ExcalidrawImperativeAPI,
  ExcalidrawProps as EditorProps,
} from "@excalidraw/excalidraw/types/types";
import { Spin } from "antd";
export interface ExcalidrawProps extends EditorProps {
  height: number;
  /**
   * 加载的数据名称,必须放在 excalidrawDatas文件夹
   */
  dataFileName: string;
}
const Excalidraw = ({ height = 500, dataFileName, ...rest }: ExcalidrawProps) => {
  const { colorMode, setColorMode } = useColorMode();
  const excalidrawAPI = useRef<ExcalidrawImperativeAPI>(null);
  const firstLoad = useRef<boolean>(true);
  return (
    <BrowserOnly>
      {() => {
        const Editor = lazy(async () => {
          return {
            default: (await import("@excalidraw/excalidraw")).Excalidraw,
          };
        });
        return (
          <Suspense
            fallback={
              <Spin spinning>
                <div
                  className="flex items-center justify-center"
                  style={{ height }}
                >
                  Excalidraw组件加载中...
                </div>
              </Spin>
            }
          >
            <div style={{ height }}>
              <Editor
                {...rest}
                initialData={import(`../../excalidrawDatas/${dataFileName}.json`)}
                theme={colorMode}
                excalidrawAPI={(api) => (excalidrawAPI.current = api)}
                viewModeEnabled
                langCode="zh-CN"
                onScrollChange={() => {
                  if (firstLoad.current) {
                    excalidrawAPI.current.scrollToContent(
                      excalidrawAPI.current.getSceneElements(),
                      {
                        fitToContent: true,
                        animate: false,
                      }
                    );
                    firstLoad.current = false;
                  }
                }}
              />
            </div>
          </Suspense>
        );
      }}
    </BrowserOnly>
  );
};
export default Excalidraw;