0. 效果
这是一个用docusaurus
搭建的文档系统,可以看到我在页面嵌入了Excalidraw
,并且可以编辑查看文件。
1.封装Excalidraw
组件
由于文档使用mdx
文件,所以可以引入react
组件,下面就来实现一个excalidraw
组件。
在mdx
文件中引入就可以使用。
1.1. 引入错误
我将组件写在文档根目录
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;
不出意外项目应该会报错
看到process
很明显不应该出现在网页中,应该在打包的时候就处理了。试试build
一下,很遗憾也会失败。
通过观察Excalidraw
源码,以及报错的docusaurus
代码,在@excalidraw/excalidraw/main.js
中
使用了
IS_PREACT
环境变量,导致webpack
打包的时候没有把这个变量替换成常量(默认只替换了process.env.NODE_ENV
)。
下面记录了我的思考过程,主要是给自己做个备份。
解决方案可以直接看1.2.1
加1.2.2
章节的最后代码。
1.2. 解决引入错误
1.2.1 解决dev
错误
思路很简单,就是使用DefinePlugin
把这个环境变量注入替换。为了修改docusaurus
的webpack
配置,我们需要写一个简单的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
错误
很遗憾打包还是会报错
可以看到是
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;
这样就能解决dev
和build
的错误了。
1.3.ocusaurus
架构
为了了解原因,有必要搞清楚docusaurus
的架构。
文档
可以看到整个文档分为了
server
和client
,并不是代表有后端和前端,只是docusaurus
有两种页面实现方式。
- 一种是
src
中的普通页面,它会使用server bundle
中的react dom server
渲染,也就是ssr
方式。 - 一种是
docs
中的markdown
文件,他会编译成js
,在客户端渲染。
而开发阶段是没有区分渲染的,直接是csr
,所以只有打包发生了之前的错误。也就是误认为写在src
中的Excalidraw
组件是ssr
的组件,自然遇到调用window
的api
时发生错误的事情。
使用BrowserOnly
就是内部判断了下是不是浏览器环境,如果不是就不渲染。
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;