基于 React 的 MarkdownEditor 组件开发指南

基于 React 的 MarkdownEditor 组件开发指南

前言

在现代Web应用中,无论是开发博客后台、文档系统还是评论功能,一个强大而友好的Markdown编辑器都是不可或缺的核心组件。一个简单的<textarea>已远不能满足需求,我们需要的是一个支持实时预览、语法高亮、图片上传等高级功能的"所见即所得"编辑器。

本文将深度剖析一个基于react-markdown-editor-litemarkdown-ithighlight.js封装的React Markdown编辑器组件。它不仅实现了上述核心功能,还通过ResizeObserver API实现了编辑器高度的动态自适应,极大地提升了用户体验。让我们一起深入代码,看看这个组件是如何构建的。

成品展示

在深入代码之前,我们先看一下最终要实现的编辑器组件所具备的核心能力:

  • 实时预览:左侧编写,右侧实时渲染。
  • 语法高亮:代码块根据不同语言自动高亮,提升可读性。
  • 图片上传:支持从本地拖拽或选择图片,自动上传并插入链接。
  • 安全可靠:可配置的图片上传前校验。
  • 高度自适应:编辑器高度能根据其容器大小动态调整,完美融入各种布局。

技术选型:强强联合

要构建这样一个功能完备的编辑器,我们站在了巨人的肩膀上,选择了以下三个核心库的强强联合:

  1. react-markdown-editor-lite: 一个轻量但功能强大的React Markdown编辑器UI框架。它为我们提供了编辑器的基本骨架、布局和丰富的配置API,如图片上传回调等。
  2. markdown-it: 一个高性能、高可扩展的Markdown解析器。我们将用它来将用户输入的Markdown文本实时转换为HTML,并在此过程中注入自定义功能。
  3. highlight.js : 一个业界知名的语法高亮库。通过与markdown-it的结合,它可以自动识别并高亮代码块,支持几乎所有主流编程语言。

组件架构设计:封装与扩展

我们的核心思想不是直接在业务页面中使用react-markdown-editor-lite,而是将其封装成一个自定义的MyMarkDown组件。这样做有几个显而易见的好处:

  • 封装复杂度:将解析器初始化、上传逻辑、样式导入等复杂配置封装在组件内部。
  • 统一行为:在整个应用中,所有Markdown编辑器的行为(如上传逻辑、高亮主题)保持一致。
  • 清晰的API :对外只暴露最必要的接口,如valueonChangefileSize,使用起来非常简单。

核心功能实现深度解析

下面,我们将分步拆解MyMarkDown组件的每一个核心功能的实现细节。

1. 初始化Markdown解析器与语法高亮

这是我们组件的"大脑"。我们需要一个能将Markdown文本解析为带高亮代码的HTML的解析器。

typescript 复制代码
// 初始化支持代码高亮的 Markdown 解析器
const mdParser = new MarkdownIt({
  html: true, // 启用 HTML 标签解析
  linkify: true, // 自动将链接文本转换为链接
  typographer: true, // 启用智能标点替换
  highlight: (str, lang) => {
    // 自定义高亮逻辑
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
      } catch (__) {}
    }
    // 如果没有指定语言或语言不受支持,则不进行高亮
    return ''; 
  },
});

代码亮点解析

  • 我们创建了一个MarkdownIt实例mdParser
  • 通过highlight配置项,我们将highlight.js的能力注入到了解析流程中。
  • 这个函数接收两个参数:str(代码块字符串)和lang(声明的语言,如'javascript')。
  • hljs.getLanguage(lang)判断highlight.js是否支持该语言。
  • hljs.highlight(...)执行真正的语法高亮,并返回处理后的HTML字符串。
  • 这样,当MdEditor调用mdParser.render(text)时,就能自动输出带有高亮样式的代码块了。

2. 实现可配置的图片上传功能

图片上传是编辑器的刚需。react-markdown-editor-lite为我们提供了onImageUpload回调属性,我们只需在此实现具体的上传逻辑即可。

typescript 复制代码
// !todo 处理文件上传
const handleUpload = async (file: File) => {
  // 1. 上传前校验
  const validationResult = ImageBeforeUploadValidate(file, fileSize);
  if (validationResult !== true) {
    // 校验失败,通过Context API弹出全局消息
    return messageApi.error(validationResult);
  }

  try {
    // 2. 调用API执行上传
    const res = await uploadFileInterface(file);
    if (res.data.success) {
      // 3. 上传成功,返回图片URL给编辑器
      return res.data.fileUrl;
    } else {
      console.error('图片上传失败', res.data);
      messageApi.error('图片上传失败');
    }
  } catch (err) {
    console.error('图片上传失败', err);
    messageApi.error('图片上传失败');
  }
};

代码亮点解析

  • 封装校验逻辑 :我们将文件大小和类型的校验逻辑封装在ImageBeforeUploadValidate函数中,使得代码更清晰且可复用。
  • 统一API调用 :上传操作通过统一的uploadFileInterface函数进行,符合项目的数据流规范。
  • 优雅的错误处理 :无论是校验失败还是上传过程中的网络错误,都通过try...catch捕获,并利用useMessageContext提供全局、一致的用户错误提示。
  • 遵循约定onImageUpload要求我们返回一个Promise,成功时resolve图片的URL,失败时可以什么都不返回。我们的实现完美遵循了这个约定。

3. 魔法般的自适应高度

这是该组件最具特色的亮点。通常,编辑器的固定高度在不同布局下会显得非常僵硬。我们利用现代浏览器的ResizeObserver API,让编辑器能够"感知"到其父容器的尺寸变化,并自动调整自身高度。

typescript 复制代码
const containerRef = useRef<HTMLDivElement>(null);
const [editorHeight, setEditorHeight] = useState(400); // 初始高度

// 监听容器高度变化 自适应高度
useEffect(() => {
  // 确保ref已挂载
  if (!containerRef.current) return;

  // 1. 创建一个ResizeObserver实例
  const resizeObserver = new ResizeObserver(entries => {
    for (const entry of entries) {
      // 2. 获取容器的实时高度
      const height = entry.contentRect.height;
      // 3. 更新state,触发编辑器重渲染
      setEditorHeight(height);
    }
  });

  // 4. 开始观察容器元素
  resizeObserver.observe(containerRef.current);

  // 5. 清理函数:组件卸载时停止观察,防止内存泄漏
  return () => {
    resizeObserver.disconnect();
  };
}, []); // 空依赖数组,确保只在组件挂载时执行一次

// ... 在JSX中使用

return (
  <div
    ref={containerRef}
    style={{
      height: '100%', // 让容器撑满其父元素
    }}
  >
    <MdEditor
      style={{ height: editorHeight }} // 将动态高度应用到编辑器
      // ... 其他props
    />
  </div>
)

代码亮点解析

  • ResizeObserver : 这是一个比window.onresize事件更高效、更精确的API,因为它只在被观察的元素尺寸变化时才触发回调,避免了不必要的计算。
  • Ref与State联动 :我们用useRef获取到外层div的DOM节点,并用useState存储编辑器的高度。当ResizeObserver检测到高度变化时,通过setEditorHeight更新状态,从而驱动MdEditor组件的style属性变化。
  • 清理副作用 :在useEffect的返回函数中调用resizeObserver.disconnect()是至关重要的最佳实践,它能确保在组件销毁时释放资源,避免潜在的内存泄漏问题。

完整代码与使用

最后,我们将所有部分组合起来,并展示如何在其他组件中使用它。

MarkdownEditor.tsx (完整组件)

typescript 复制代码
// yarn add react-markdown-editor-lite markdown-it highlight.js
// yarn add @types/markdown-it -D
import React, { useEffect, useRef, useState } from 'react'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
// 参数配置:https://github.com/HarryChen0506/react-markdown-editor-lite/blob/master/docs/configure.zh-CN.md
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
// 可以去 https://highlightjs.org/examples 查看样式效果导入主题样式 去highlight.js/styles里面选一个自己喜欢的
import 'highlight.js/styles/github.css'
import { uploadFileInterface } from '@/api'
import { ImageBeforeUploadValidate } from '@/components/UploadImage/validate'
import { useMessageContext } from '@/contexts/MessageContext'

// 初始化支持代码高亮的 Markdown 解析器
const mdParser = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: (str, lang) => {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang, ignoreIllegals: true })
          .value
        // eslint-disable-next-line no-empty
      } catch (__) {}
    }
    return ''
  },
})

interface MarkdownEditorProps {
  /**
   * 限制上传图片文件大小 默认 2MB
   */
  fileSize?: number
  /**
   * 渲染的内容
   */
  value: string
  /**
   * 内容变化时触发
   * 该函数会在内容变化时被调用,传入当前的 Markdown 文本内容。
   */
  onChange: (value: string) => void
}

/**
 * @description MarkDown编辑组件
 */
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
  value,
  onChange,
  fileSize = 2,
}) => {
  const { messageApi } = useMessageContext()
  const containerRef = useRef<HTMLDivElement>(null)
  const [editorHeight, setEditorHeight] = useState(400)

  // 监听容器高度变化 自适应高度
  useEffect(() => {
    if (!containerRef.current) return

    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const height = entry.contentRect.height
        setEditorHeight(height)
      }
    })

    resizeObserver.observe(containerRef.current)

    return () => {
      resizeObserver.disconnect()
    }
  }, [])

  // !todo 处理图片文件上传
  const handleUpload = async (file: File) => {
    const validationResult = ImageBeforeUploadValidate(file, fileSize)
    if (validationResult !== true) {
      return messageApi.error(validationResult)
    }

    try {
      const res = await uploadFileInterface(file)
      if (res.data.success) {
        return res.data.fileUrl
      } else {
        console.error('图片上传失败', res.data)
        messageApi.error('图片上传失败')
      }
    } catch (err) {
      console.error('图片上传失败', err)
      messageApi.error('图片上传失败')
    }
  }

  return (
    <div
      ref={containerRef}
      style={{
        height: '100%',
      }}
    >
      <MdEditor
        style={{ height: editorHeight }}
        value={value}
        renderHTML={text => mdParser.render(text)}
        onImageUpload={handleUpload}
        onChange={({ text }) => onChange(text)}
        placeholder="请在此处编写文章内容"
      />
    </div>
  )
}

export default MarkdownEditor

使用示例

jsx 复制代码
import React, { useState } from 'react';
import MarkdownEditor from './MarkdownEditor';

const ArticleEditor = () => {
  const [content, setContent] = useState('');

  return (
    <div style={{ height: 'calc(100vh - 100px)' }}> {/* 假设父容器有一个动态计算的高度 */}
      <h2>撰写新文章</h2>
      <MarkdownEditor 
        value={content}
        onChange={setContent}
        fileSize={5} // 自定义上传图片文件大小限制为5MB
      />
    </div>
  );
};

总结

通过将专业库进行组合与封装,我们成功构建了一个强大、可靠且体验优秀的MarkdownEditor组件。它不仅具备了现代Markdown编辑器的所有核心功能,还通过ResizeObserver解决了常见的布局难题,展示了现代Web API在提升用户体验方面的巨大潜力。

这个组件的设计思想------封装、组合、扩展------是React开发中的核心理念。通过构建这样高质量的基础组件,我们能极大地提升开发效率,并保证整个应用的一致性和健壮性。希望这次深度解析能为您在未来的项目开发中提供有价值的参考和启发。

前言

在现代Web应用中,无论是开发博客后台、文档系统还是评论功能,一个强大而友好的Markdown编辑器都是不可或缺的核心组件。一个简单的<textarea>已远不能满足需求,我们需要的是一个支持实时预览、语法高亮、图片上传等高级功能的"所见即所得"编辑器。

本文将深度剖析一个基于react-markdown-editor-litemarkdown-ithighlight.js封装的React Markdown编辑器组件。它不仅实现了上述核心功能,还通过ResizeObserver API实现了编辑器高度的动态自适应,极大地提升了用户体验。让我们一起深入代码,看看这个组件是如何构建的。

成品展示

在深入代码之前,我们先看一下最终要实现的编辑器组件所具备的核心能力:

  • 实时预览:左侧编写,右侧实时渲染。
  • 语法高亮:代码块根据不同语言自动高亮,提升可读性。
  • 图片上传:支持从本地拖拽或选择图片,自动上传并插入链接。
  • 安全可靠:可配置的图片上传前校验。
  • 高度自适应:编辑器高度能根据其容器大小动态调整,完美融入各种布局。

技术选型:强强联合

要构建这样一个功能完备的编辑器,我们站在了巨人的肩膀上,选择了以下三个核心库的强强联合:

  1. react-markdown-editor-lite: 一个轻量但功能强大的React Markdown编辑器UI框架。它为我们提供了编辑器的基本骨架、布局和丰富的配置API,如图片上传回调等。
  2. markdown-it: 一个高性能、高可扩展的Markdown解析器。我们将用它来将用户输入的Markdown文本实时转换为HTML,并在此过程中注入自定义功能。
  3. highlight.js : 一个业界知名的语法高亮库。通过与markdown-it的结合,它可以自动识别并高亮代码块,支持几乎所有主流编程语言。

组件架构设计:封装与扩展

我们的核心思想不是直接在业务页面中使用react-markdown-editor-lite,而是将其封装成一个自定义的MyMarkDown组件。这样做有几个显而易见的好处:

  • 封装复杂度:将解析器初始化、上传逻辑、样式导入等复杂配置封装在组件内部。
  • 统一行为:在整个应用中,所有Markdown编辑器的行为(如上传逻辑、高亮主题)保持一致。
  • 清晰的API :对外只暴露最必要的接口,如valueonChangefileSize,使用起来非常简单。

核心功能实现深度解析

下面,我们将分步拆解MyMarkDown组件的每一个核心功能的实现细节。

1. 初始化Markdown解析器与语法高亮

这是我们组件的"大脑"。我们需要一个能将Markdown文本解析为带高亮代码的HTML的解析器。

typescript 复制代码
// 初始化支持代码高亮的 Markdown 解析器
const mdParser = new MarkdownIt({
  html: true, // 启用 HTML 标签解析
  linkify: true, // 自动将链接文本转换为链接
  typographer: true, // 启用智能标点替换
  highlight: (str, lang) => {
    // 自定义高亮逻辑
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
      } catch (__) {}
    }
    // 如果没有指定语言或语言不受支持,则不进行高亮
    return ''; 
  },
});

代码亮点解析

  • 我们创建了一个MarkdownIt实例mdParser
  • 通过highlight配置项,我们将highlight.js的能力注入到了解析流程中。
  • 这个函数接收两个参数:str(代码块字符串)和lang(声明的语言,如'javascript')。
  • hljs.getLanguage(lang)判断highlight.js是否支持该语言。
  • hljs.highlight(...)执行真正的语法高亮,并返回处理后的HTML字符串。
  • 这样,当MdEditor调用mdParser.render(text)时,就能自动输出带有高亮样式的代码块了。

2. 实现可配置的图片上传功能

图片上传是编辑器的刚需。react-markdown-editor-lite为我们提供了onImageUpload回调属性,我们只需在此实现具体的上传逻辑即可。

typescript 复制代码
// !todo 处理文件上传
const handleUpload = async (file: File) => {
  // 1. 上传前校验
  const validationResult = ImageBeforeUploadValidate(file, fileSize);
  if (validationResult !== true) {
    // 校验失败,通过Context API弹出全局消息
    return messageApi.error(validationResult);
  }

  try {
    // 2. 调用API执行上传
    const res = await uploadFileInterface(file);
    if (res.data.success) {
      // 3. 上传成功,返回图片URL给编辑器
      return res.data.fileUrl;
    } else {
      console.error('图片上传失败', res.data);
      messageApi.error('图片上传失败');
    }
  } catch (err) {
    console.error('图片上传失败', err);
    messageApi.error('图片上传失败');
  }
};

代码亮点解析

  • 封装校验逻辑 :我们将文件大小和类型的校验逻辑封装在ImageBeforeUploadValidate函数中,使得代码更清晰且可复用。
  • 统一API调用 :上传操作通过统一的uploadFileInterface函数进行,符合项目的数据流规范。
  • 优雅的错误处理 :无论是校验失败还是上传过程中的网络错误,都通过try...catch捕获,并利用useMessageContext提供全局、一致的用户错误提示。
  • 遵循约定onImageUpload要求我们返回一个Promise,成功时resolve图片的URL,失败时可以什么都不返回。我们的实现完美遵循了这个约定。

3. 魔法般的自适应高度

这是该组件最具特色的亮点。通常,编辑器的固定高度在不同布局下会显得非常僵硬。我们利用现代浏览器的ResizeObserver API,让编辑器能够"感知"到其父容器的尺寸变化,并自动调整自身高度。

typescript 复制代码
const containerRef = useRef<HTMLDivElement>(null);
const [editorHeight, setEditorHeight] = useState(400); // 初始高度

// 监听容器高度变化 自适应高度
useEffect(() => {
  // 确保ref已挂载
  if (!containerRef.current) return;

  // 1. 创建一个ResizeObserver实例
  const resizeObserver = new ResizeObserver(entries => {
    for (const entry of entries) {
      // 2. 获取容器的实时高度
      const height = entry.contentRect.height;
      // 3. 更新state,触发编辑器重渲染
      setEditorHeight(height);
    }
  });

  // 4. 开始观察容器元素
  resizeObserver.observe(containerRef.current);

  // 5. 清理函数:组件卸载时停止观察,防止内存泄漏
  return () => {
    resizeObserver.disconnect();
  };
}, []); // 空依赖数组,确保只在组件挂载时执行一次

// ... 在JSX中使用

return (
  <div
    ref={containerRef}
    style={{
      height: '100%', // 让容器撑满其父元素
    }}
  >
    <MdEditor
      style={{ height: editorHeight }} // 将动态高度应用到编辑器
      // ... 其他props
    />
  </div>
)

代码亮点解析

  • ResizeObserver : 这是一个比window.onresize事件更高效、更精确的API,因为它只在被观察的元素尺寸变化时才触发回调,避免了不必要的计算。
  • Ref与State联动 :我们用useRef获取到外层div的DOM节点,并用useState存储编辑器的高度。当ResizeObserver检测到高度变化时,通过setEditorHeight更新状态,从而驱动MdEditor组件的style属性变化。
  • 清理副作用 :在useEffect的返回函数中调用resizeObserver.disconnect()是至关重要的最佳实践,它能确保在组件销毁时释放资源,避免潜在的内存泄漏问题。

完整代码与使用

最后,我们将所有部分组合起来,并展示如何在其他组件中使用它。

MarkdownEditor.tsx (完整组件)

typescript 复制代码
// yarn add react-markdown-editor-lite markdown-it highlight.js
// yarn add @types/markdown-it -D
import React, { useEffect, useRef, useState } from 'react'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
// 参数配置:https://github.com/HarryChen0506/react-markdown-editor-lite/blob/master/docs/configure.zh-CN.md
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
// 可以去 https://highlightjs.org/examples 查看样式效果导入主题样式 去highlight.js/styles里面选一个自己喜欢的
import 'highlight.js/styles/github.css'
import { uploadFileInterface } from '@/api'
import { ImageBeforeUploadValidate } from '@/components/UploadImage/validate'
import { useMessageContext } from '@/contexts/MessageContext'

// 初始化支持代码高亮的 Markdown 解析器
const mdParser = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: (str, lang) => {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang, ignoreIllegals: true })
          .value
        // eslint-disable-next-line no-empty
      } catch (__) {}
    }
    return ''
  },
})

interface MarkdownEditorProps {
  /**
   * 限制上传图片文件大小 默认 2MB
   */
  fileSize?: number
  /**
   * 渲染的内容
   */
  value: string
  /**
   * 内容变化时触发
   * 该函数会在内容变化时被调用,传入当前的 Markdown 文本内容。
   */
  onChange: (value: string) => void
}

/**
 * @description MarkDown编辑组件
 */
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
  value,
  onChange,
  fileSize = 2,
}) => {
  const { messageApi } = useMessageContext()
  const containerRef = useRef<HTMLDivElement>(null)
  const [editorHeight, setEditorHeight] = useState(400)

  // 监听容器高度变化 自适应高度
  useEffect(() => {
    if (!containerRef.current) return

    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const height = entry.contentRect.height
        setEditorHeight(height)
      }
    })

    resizeObserver.observe(containerRef.current)

    return () => {
      resizeObserver.disconnect()
    }
  }, [])

  // !todo 处理图片文件上传
  const handleUpload = async (file: File) => {
    const validationResult = ImageBeforeUploadValidate(file, fileSize)
    if (validationResult !== true) {
      return messageApi.error(validationResult)
    }

    try {
      const res = await uploadFileInterface(file)
      if (res.data.success) {
        return res.data.fileUrl
      } else {
        console.error('图片上传失败', res.data)
        messageApi.error('图片上传失败')
      }
    } catch (err) {
      console.error('图片上传失败', err)
      messageApi.error('图片上传失败')
    }
  }

  return (
    <div
      ref={containerRef}
      style={{
        height: '100%',
      }}
    >
      <MdEditor
        style={{ height: editorHeight }}
        value={value}
        renderHTML={text => mdParser.render(text)}
        onImageUpload={handleUpload}
        onChange={({ text }) => onChange(text)}
        placeholder="请在此处编写文章内容"
      />
    </div>
  )
}

export default MarkdownEditor

使用示例

jsx 复制代码
import React, { useState } from 'react';
import MarkdownEditor from './MarkdownEditor';

const ArticleEditor = () => {
  const [content, setContent] = useState('');

  return (
    <div style={{ height: 'calc(100vh - 100px)' }}> {/* 假设父容器有一个动态计算的高度 */}
      <h2>撰写新文章</h2>
      <MarkdownEditor 
        value={content}
        onChange={setContent}
        fileSize={5} // 自定义上传图片文件大小限制为5MB
      />
    </div>
  );
};

总结

通过将专业库进行组合与封装,我们成功构建了一个强大、可靠且体验优秀的MarkdownEditor组件。它不仅具备了现代Markdown编辑器的所有核心功能,还通过ResizeObserver解决了常见的布局难题,展示了现代Web API在提升用户体验方面的巨大潜力。

这个组件的设计思想------封装、组合、扩展------是React开发中的核心理念。通过构建这样高质量的基础组件,我们能极大地提升开发效率,并保证整个应用的一致性和健壮性。希望这次深度解析能为您在未来的项目开发中提供有价值的参考和启发。

相关推荐
JiaLin_Denny9 分钟前
javascript 中数组对象操作方法
前端·javascript·数组对象方法·数组对象判断和比较
代码老y11 分钟前
Vue3 从 0 到 ∞:Composition API 的底层哲学、渲染管线与生态演进全景
前端·javascript·vue.js
LaoZhangAI19 分钟前
ComfyUI集成GPT-Image-1完全指南:8步实现AI图像创作革命【2025最新】
前端·后端
LaoZhangAI20 分钟前
Cline + Gemini API 完整配置与使用指南【2025最新】
前端·后端
Java&Develop25 分钟前
防止电脑息屏 html
前端·javascript·html
Maybyy28 分钟前
javaScript中数组常用的函数方法
开发语言·前端·javascript
国王不在家29 分钟前
组件-多行文本省略-展开收起
前端·javascript·html
夏兮颜☆31 分钟前
【electron】electron实现窗口的最大化、最小化、还原、关闭
前端·javascript·electron
LaoZhangAI32 分钟前
Cline + Claude API 完全指南:2025年智能编程最佳实践
前端·后端