Grtsinry43 的前端札记
NO. 0211 手记 2025.02.11

扩展编辑器,整合搜索...问题解决清单捏:)

浏览 185 喜欢 3 评论 3

浅更新一下,最近压力比较大,几个项目同时并行,还要修好多大佬提出的 bug,有点痛并快乐的感觉吧,闲言少叙…

扩展 Toast UI Markdown 编辑器 React 组件,实现图片上传与其他功能

这篇简述一下 Toast UI 编辑器的自定义事件处理(以图片上传为例)

首先知道我们要处理什么,一个是插入图片是将原来的直接输入 base64 的方式改为上传到后端,然后要封装为通用组件在所有地方使用

图片上传

首先我们追一下源码

image-20250211211633837

image-20250211211607407

我们发现这里预留了一个 hook,其接收两个参数,一个是文件(二进制),还有一个回调函数

于是我们可以写一个函数来完成上传

typescript
const handleImageUpload = async (
      blob: Blob,
      callback: (url: string, altText: string) => void,
    ) => {
      const formData = new FormData();
      formData.append('file', blob);

      const response = await fetch('/api/v1/upload', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + getToken(),
        },
        body: formData,
      });

      const res = await response.json();
      if (res.code === 0) {
        message.success('图片已上传并插入');
      } else {
        message.error('图片上传中遇到问题');
      }
        
      const imageUrl = `${window.location.href.replace(location.pathname, '')}${
        res.data
      }`;

      callback(imageUrl, 'image');
    };

封装组件

组件这里可能就是一点不太一样,因为我们需要 ref 拿到其实例上的函数 getMarkdown(),我们需要将 ref 做一下传递:

typescript
useImperativeHandle(ref, () => ({
      getInstance: () => editorRef.current?.getInstance(),
      getRootElement: () => editorRef.current?.getRootElement() || null,
      setMarkdown: (markdown: string) => {
        editorRef.current?.getInstance().setMarkdown(markdown);
      },
      context: editorRef.current?.context,
      setState: editorRef.current?.setState,
      forceUpdate: editorRef.current?.forceUpdate,
    }));

我们使用 useImperativeHandle 传递之后,然后可以封装为组件

typescript
import { getToken } from '@/utils/token';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import '@toast-ui/editor/dist/toastui-editor.css';
import { Editor } from '@toast-ui/react-editor';
import { Button, message } from 'antd';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';

type EditorHandle = {
  getInstance: () => Editor | null;
  getRootElement: () => HTMLElement | null;
  context: any;
  setState: any;
  forceUpdate: any;
};

const CustomEditor = forwardRef<EditorHandle, NonNullable<unknown>>(
  (props, ref) => {
    const editorRef = useRef<Editor>(null);
    const [isFullscreen, setIsFullscreen] = useState(false);

    useImperativeHandle(ref, () => ({
      getInstance: () => editorRef.current?.getInstance(),
      getRootElement: () => editorRef.current?.getRootElement() || null,
      setMarkdown: (markdown: string) => {
        editorRef.current?.getInstance().setMarkdown(markdown);
      },
      context: editorRef.current?.context,
      setState: editorRef.current?.setState,
      forceUpdate: editorRef.current?.forceUpdate,
    }));

    const handleImageUpload = async (
      blob: Blob,
      callback: (url: string, altText: string) => void,
    ) => {
      const formData = new FormData();
      formData.append('file', blob);

      const response = await fetch('/api/v1/upload', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + getToken(),
        },
        body: formData,
      });

      const res = await response.json();
      if (res.code === 0) {
        message.success('图片已上传并插入');
      } else {
        message.error('图片上传中遇到问题');
      }
      const imageUrl = `${window.location.href.replace(location.pathname, '')}${
        res.data
      }`;

      callback(imageUrl, 'image');
    };

    function enterFullscreen(element: HTMLElement) {
      if (element.requestFullscreen) {
        element.requestFullscreen();
      }
    }

    function exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      }
    }

    const toggleFullscreen = () => {
      setIsFullscreen(!isFullscreen);
      if (!isFullscreen) {
        enterFullscreen(document.documentElement);
      } else {
        exitFullscreen();
      }
    };

    return (
      <div
        style={
          isFullscreen
            ? {
                display: 'flex',
                flexDirection: 'column',
                transition: 'all 0.5s',
                position: 'fixed',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
                background: 'white',
                zIndex: 9999,
                padding: '20px',
              }
            : {
                display: 'flex',
                flexDirection: 'column',
                transition: 'all 0.5s',
              }
        }
      >
        <div
          style={{
            marginBottom: '10px',
            padding: '8px',
          }}
        >
          <span>
            想要写点什么呢 ٩(๑˃̵ᴗ˂̵๑)۶ {!isFullscreen && '| 更好的编辑体验?试试'}
          </span>
          <Button
            type="link"
            size={'small'}
            icon={
              isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />
            }
            onClick={toggleFullscreen}
          >
            {isFullscreen ? '退出全屏' : '全屏'}
          </Button>
        </div>
        <Editor
          initialValue=""
          ref={editorRef}
          initialEditType="markdown"
          previewStyle="vertical"
          height={isFullscreen ? '100vh' : '600px'}
          useCommandShortcut={true}
          hooks={{
            addImageBlobHook: handleImageUpload,
          }}
        />
      </div>
    );
  },
);

export default CustomEditor;

然后我们在所需地方直接使用即可:

typescript
<CustomEditor ref={editorRef} />

用 ES 迁移到 MeiliSearch

要被es折磨疯了…对于小服务器和轻需求,它实在太重了!!!

实现迁移非常简单,首先是配置文件:

java
package com.grtsinry43.grtblog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Config;

/**
 * @author grtsinry43
 * @date 2025/2/11 14:20
 * @description 热爱可抵岁月漫长
 */
@Configuration
public class MeiliSearchConfig {

    @Bean
    public Client meiliSearchClient() {
        return new Client(new Config("http://localhost:7700", "your_api_key"));
    }
}

实现搜索逻辑:

java
package com.grtsinry43.grtblog.service;

import com.grtsinry43.grtblog.dto.AggregatedSearchResult;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedArticleDocument;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedMomentDocument;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedPageDocument;
import com.grtsinry43.grtblog.esdao.ArticleDocument;
import com.grtsinry43.grtblog.esdao.MomentDocument;
import com.grtsinry43.grtblog.esdao.PageDocument;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.model.Searchable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class MeiliSearchService {
    private final Client meiliSearchClient;

    @Autowired
    public MeiliSearchService(Client meiliSearchClient) {
        this.meiliSearchClient = meiliSearchClient;
    }

    public AggregatedSearchResult searchAll(String keyword) {
        AggregatedSearchResult result = new AggregatedSearchResult();
        result.setPages(searchPages(keyword));
        result.setArticles(searchArticles(keyword));
        result.setMoments(searchMoments(keyword));

        return result;
    }

    // 下面是详细搜索逻辑
}

最后配置数据同步:

java
package com.grtsinry43.grtblog.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.grtsinry43.grtblog.entity.Article;
import com.grtsinry43.grtblog.entity.Page;
import com.grtsinry43.grtblog.entity.StatusUpdate;
import com.grtsinry43.grtblog.esdao.ArticleDocument;
import com.grtsinry43.grtblog.esdao.MomentDocument;
import com.grtsinry43.grtblog.esdao.PageDocument;
import com.grtsinry43.grtblog.esrepo.ArticleRepository;
import com.grtsinry43.grtblog.esrepo.MomentRepository;
import com.grtsinry43.grtblog.esrepo.PageRepository;
import com.grtsinry43.grtblog.mapper.ArticleMapper;
import com.grtsinry43.grtblog.mapper.PageMapper;
import com.grtsinry43.grtblog.mapper.StatusUpdateMapper;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Index;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
public class MeiliDataSyncService {
    private final Client meiliSearchClient;

    // ...构造函数

    public void syncAllRecent() {
        syncArticlesToMeiliSearch(articleDocuments);
        syncMomentsToMeiliSearch(statusUpdateDocuments);
        syncPagesToMeiliSearch(pageDocuments);
    }

    public void deleteContent(Long id, String type) {
        Index index = null;
        if ("article".equals(type)) {
            index = meiliSearchClient.index("articles");
            index.deleteDocumentsByFilter("id=" + id);
        } else if ("moment".equals(type)) {
            index = meiliSearchClient.index("moments");
            index.deleteDocumentsByFilter("id=" + id);
        } else if ("page".equals(type)) {
            index = meiliSearchClient.index("pages");
            index.deleteDocumentsByFilter("id=" + id);
        }
    }

    public void deleteAllContent() {
        Index articleIndex = meiliSearchClient.index("articles");
        articleIndex.deleteAllDocuments();
        Index momentIndex = meiliSearchClient.index("moments");
        momentIndex.deleteAllDocuments();
        Index pageIndex = meiliSearchClient.index("pages");
        pageIndex.deleteAllDocuments();
    }

    private void syncArticlesToMeiliSearch(List<ArticleDocument> articleDocuments) {
        Index index = meiliSearchClient.index("articles");
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String json = objectMapper.writeValueAsString(articleDocuments);
            index.addDocuments(json);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
    
    //... 另外的实现
}

实测其性能和体验有很大提高。

喜欢 3
手记
2025.02.11
Loading comments...