🚀女朋友说没找到好用的白板工具,我基于开源的魔改了一波🚀

1,113 阅读6分钟

前言

之前魔改了一个开源的流程图组件,让女朋友成功不再受 Processon 免费版的困扰——女朋友不想开Processon会员,我魔改了一个无限制的在线绘图软件

然后有一天,她跟我说:流程图都是比较正式的,而且没有自由画笔,有时候想画一个产品的原型草图,不太适合用流程图软件来画。这我一听,她不就是想要一个白板吗?

然后我就给她推荐了excalidraw,讲真的,这是我用过的最好用的、免费的白板软件。跟她说完之后,她就去用了,然后我就接着打游戏了。

image.png

用了一段时间之后她又跟我说,有点使用上的不便问题:

  • 数据是存在本地的,没有在云端管理文件/文件夹的能力
  • 手写字体是它的一个特别有特色的功能,但是它不支持中文的手写字体

image.png

于是我就看了一下, excalidraw 是开源的,那何必不拉下来改一下,改好了让她用用呢?

🌟体验地址🌟

开始魔改踩坑

下面我们来正式开始魔改,基于这个包——@excalidraw/excalidraw

如果你是使用 vite 开发的,在 excalidraw 里面会用到 process.env 这个变量, vite 中是没有这个变量的,所以需要在配置文件中配一下,不然嘎嘎报错:

  define: {
    "process.env": process.env,
  },

然后安装一下依赖 @excalidraw/excalidraw ,现在我们需要魔改这个库,用到的是 patch-package 这个包。

这里需要注意的是,我们需要改的包的版本号不能带前缀,应该写死版本:

image.png

比如说我们魔改一下 excalidraw 的入口文件:

image.png

然后执行npx patch-package @excalidraw/excalidraw,可以看到多了一个 patches 目录:

image.png

然后我们再新增一个 postinstall 脚本,这样每次重新安装依赖时都会应用 patch

image.png

我觉得手写体是 excalidraw 的一个很好很好的东西,奈何不支持中文,看仓库上也有不少人给他提过 issue ,然而好像并没有合并。

所以我就只能通过魔改的方式来支持,主要参考的是这个pr,在这里我主要改的也是 /dist/excalidraw.development.js 这个文件。

改完之后 patches 文件大概长这样子

image.png

好吧,那也看不出什么东西来,那我们就引入一下,看看改成功了没有:

import { Excalidraw } from "@excalidraw/excalidraw";
const ExDraw = () => {
  return (
    <div style={{ height: "100%" }}>
      <Excalidraw />
    </div>
  );
};

export default ExDraw;

看起来是成功的了:

image.png

对比下面官网的:

image.png

然后我们打包发布一下,看看效果如何:

image.png

可以看到打包后的包体积比较大,而且还是经过 gzip 之后的。为啥体积那么大呢,因为我们改的是 development 的包,而不是 production 的。为啥不改 production 的呢?是因为不喜欢吗?

因为 production 已经被压缩混淆过,根本无从改起。。。。

可恶,难道只能到这里了吗?

image.png

收拾完心情之后,我再找呀找,找到了上述的开源组件对应的仓库地址,既然组件不好改,那我就改它的源码自己再打包一份。

下载下来之后,按照上面说的 PR ,修改了一下,跑起来之后,效果依然是可以的。

image.png

然后进到 /src/packages/excalidraw 这个目录下, npm i 安装一下依赖后,执行 npm run build:umd 进行打包构建,构建的产物在该目录的dist目录下。

image.png

然后修改一下这个目录的 package.json ,准备发到我们自己的 npm 账户下

image.png

执行一下 npm login ,登录你的 npm 账号,然后执行 npm publish ,把我们自己打的包发到 npm 仓库里。

发完包之后,在我们需要用到这个包的仓库安装一下,比如我的包名是 @jayliang/excalidraw ,那么久 npm i @jayliang/excalidraw

然后从 @jayliang/excalidraw 中导入 excalidraw 组件,别忘了把 vite.config.ts 的这个配置改成 production

image.png

然而,我这么改了之后,加载的还是 development 的包,无语了,有没有懂 vite 的老哥解释下这个。

image.png

image.png

索性我就不纠结这个事情了,直接改一下这个 main.js 入口文件,反正我们上面已经用 patch-package 改过一次了,轻车熟路了属于是。

image.png

改完之后,执行一下 npx patch-package @jayliang/excalidraw

最后部署一下看看,有多大的提升:

image.png

一个从 2M 变成了 400K ,一个从 4.7M 变成了 950K ,属于是非常舒服,现在打开的速度十分丝滑。

数据初始化

大概介绍一下excalidraw核心的数据:

  • elements :节点
  • files :文件,比如上传的图片
  • appState :全局的一些信息

我是把数据存在 mongo 里,所以初始化的时候根据 id 去拿一下数据,然后反序列化一下交给组件就行:

  const [initData, setInitData] = useState<any>({});
    getFileDetail(id).then((res) => {
      try {
        let content = res.data.content || "{}";
        content = JSON.parse(content);
        setInitData(content);
      } catch (error) {
        console.log("error", error);
        setInitData({});
      } finally {
        setInit(true);
      }
    });

      <Excalidraw
        initialData={{
          appState: initData.appState || {},
          elements: initData.elements || [],
          files: initData.files || {},
        }}
        onChange={handleChange}
        langCode="zh-CN"
      />

数据更新

数据更新主要监听组件的 onChange 事件,在这里,组件会把最新的 elementsappStatefiles 回调出来,这里需要注意的是,这个 onChange 触发会非常频繁,所以需要加一个防抖

  const handleChange = debounce(
    (
      elements: readonly ExcalidrawElement[],
      appState: AppState,
      files: BinaryFiles
    ) => {
      const obj = { elements, appState, files };
      if (!equal(obj, oldData.current)) {
        oldData.current = obj;
      }
    }
  );

由于这个 onChange 触发十分频繁,我就没有在这里做把数据同步到后端的逻辑,而是做了一个保存按钮,需要手动点击或者 ctrl+s

以下是保存数据的代码:

  const handleSave = async () => {
    message.destroy();
    const excalidrawAPI = excalidrawAPIRef.current;
    const files = excalidrawAPI?.getFiles();
    if (files) {
      let promise: any = [];
      const map: any = {};
      Object.keys(files).forEach((id) => {
        if (!fileMap.current[id]) {
          fileMap.current[id] = true;
          const current = files[id];
          const file = dataURItoFile(current.dataURL, id);
          const form = new FormData();
          form.append("file", file);
          promise.push(uploadFile(form));
        } else {
          promise.push(Promise.resolve());
        }
        map[promise.length - 1] = id;
      });
      if (promise.length > 0) {
        const res = await Promise.all(promise);
        for (let i = 0; i < res.length; i++) {
          const cur = res[i];
          let url = cur?.data;
          if (url) {
            const id = map[i];
            files[id].dataURL = url;
          }
        }
      }
    }
    const appState = excalidrawAPI?.getAppState();
    const elements = oldData.current.elements;
    const params = {
      files: files || {},
      appState: appState || {},
      elements: elements || [],
    };
    await updateFile({
      id,
      content: JSON.stringify(params),
    });
    message.success("已保存");
  };

这里插入 file 的时候默认是base64格式,所以我们要把 base64 上传换一个 url 之后再把数据落库。

咱们这个 excalidraw 组件也算是成功魔改接入了

image.png

最后

以上就是一个魔改第三方 npm 包的一个过程,如果你也有这种需求,不妨参考以下两种方式:

  1. 使用 patch 的方式直接改 node_modules 的文件
  2. 从仓库中拉一份代码下来改完、打包发到你自己的 npm 仓库上

这就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~