在 React 中为 RichTextEditor 添加图片库

Vite + React 示例中的 Insert Gallery 对话框,列出 Express 服务器上的 blog、products 和 team 文件夹以及四张图片

RichTextEditor 的 Insert Gallery 对话框在 React 应用中的工作方式与在普通页面中相同。不同的是连接方式:编辑器是一个不受 React 管理的浏览器脚本,组件必须负责清理它,而在开发期间,对话框的请求必须经由开发服务器到达您的 API 服务器。本页介绍一个同时满足这三点的 Vite + React 配置,并对接 Express 示例进行了测试。

在应用之前加载编辑器脚本

编辑器以普通脚本的形式提供,它们定义了 window.RichTextEditor。在 Vite 应用中,最简单又正确的位置是 index.html,放在启动 React 的模块脚本之前:

HTML · index.html
<!-- Plain browser scripts from public/, loaded before the React bundle. -->
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>

这些文件位于 public/richtexteditor/ 中,Vite 会原样提供它们。rte-files.js 把编辑器的上传钩子连接到 /rte/upload。没有它,图片库虽然可以浏览,但 Upload 按钮无从调用。<head> 中的传统脚本会在模块打包文件之前运行,因此任何组件挂载时,window.RichTextEditor 都已经存在。

组件

JavaScript · src/RichTextEditor.jsx
// window.RichTextEditor comes from the scripts in index.html.
export default function RichTextEditor({ options, initialHtml = "", editorRef }) {
  const container = useRef(null);

  useEffect(() => {
    const editor = new window.RichTextEditor(container.current, options);
    editor.setHTMLCode(initialHtml);
    if (editorRef) editorRef.current = editor;
    // Runs on unmount, and once in development: Strict Mode mounts, unmounts, mounts again.
    return () => {
      if (editorRef) editorRef.current = null;
      editor.destroy();
    };
  }, []); // create once; options are read at creation

  return <div ref={container} />;
}

编辑器在 useEffect 中、在 ref 指向的 div 上创建一次。清理函数调用 destroy()。这在开发环境中很重要,因为 React 严格模式会挂载每个组件、卸载它,然后再次挂载。有了清理函数,第一个编辑器会在创建第二个之前被销毁。在开启 <StrictMode> 的情况下,我们的浏览器检查发现页面上恰好只有一个编辑器,而编辑器的试用版提示在控制台中出现了两次:每次创建一次。省略清理函数的问题很容易被忽视。当我们在同一个 div 上不调用 destroy() 就创建第二个编辑器时,页面上仍然只显示一个编辑器,但第一个实例从未被清理。

空的依赖数组是有意为之。选项在创建编辑器时读取,如果每次渲染都重新创建编辑器,用户输入的内容就会丢失。要读取内容,请把实例保存在 ref 中,并调用 getHTMLCode():

JavaScript · src/App.jsx
const options = {
  toolbar: "full",
  galleryEndpoint: "/rte/gallery", // forwarded to Express by vite.config.js
  maxUploadFileSize: 4000000,
};

在示例中,Show HTML 按钮调用 editor.current.getHTMLCode(),输出了 <p>Hello from React.</p>。从图片库插入图片后,HTML 中会包含 <img src="/uploads/...">。

在开发期间访问 API 服务器

galleryEndpoint: "/rte/gallery" 是相对于页面的路径,而页面来自 Vite。Vite 的开发代理把 /rte/(图片库和上传路由)和 /uploads/(图片)转发到端口 5101 上的 Express 示例:

JavaScript · vite.config.js
// /rte/ and /uploads/ go to the Express sample (node-express, port 5101).
// Keep the trailing slashes: "/rte" alone would also catch /rte-files.js.
proxy: {
  "/rte/": "http://localhost:5101",
  "/uploads/": "http://localhost:5101",
},

浏览器只看到一个源,因此不需要设置 CORS 标头,编辑器选项中也不需要绝对 URL。请保留末尾的斜杠。Vite 把普通的键作为 URL 前缀进行匹配,所以键 "/rte" 也会匹配 /rte-files.js。我们试过:使用 "/rte" 时,/rte-files.js 返回时带有 X-Powered-By: Express,是由 API 服务器而不是应用提供的。使用 "/rte/" 时,Vite 提供的是自己的副本。

代理是开发阶段的功能。在生产环境中,请从同一个源提供构建后的应用和 API,例如让 Express 在路由旁边提供 dist/,或者放在同一个反向代理之后。

对话框显示什么

顶部的图片是这个 React 应用中的对话框,显示了 Express 服务器上的文件夹和图片。服务器端与 Node.js 图片库指南中的相同。有两个限制来自已发布版本的图片库,而不是 React。格式提示中列出了 JFIF、BMP 和 AVIF,而示例服务器会拒绝这些格式。被拒绝的文件会显示 "Upload failed for <name>.",而不是服务器返回的消息。

测试

下载本指南的示例项目和 Express 示例。把编辑器下载包中的 richtexteditor 文件夹复制到 Express 示例中,或者设置 RTE_DIR,然后用 npm install 和 npm start 启动它。接着在 React 项目中执行:

Shell
npm install
npm run dev
  VITE v8.3.5  ready in 440 ms
  ➜  Local:   http://localhost:5111/

curl "http://localhost:5111/rte/gallery?folder="
{"currentFolder":"","currentFolderDisplay":"/","parentFolder":null,"folders":[...],"images":[...]}

npm run dev 会先把编辑器文件复制到 public/ 中。请把 RTE_DIR 设置为您的编辑器文件夹,或者自己把该文件夹放到 public/richtexteditor。打开 http://localhost:5111,点击 Insert Gallery,选择一张图片,然后点击 Insert。

其他平台的同一指南

Vue · Angular

延伸阅读