基于 React 的 MarkdownEditor 组件开发指南
前言
在现代Web应用中,无论是开发博客后台、文档系统还是评论功能,一个强大而友好的Markdown编辑器都是不可或缺的核心组件。一个简单的<textarea>
已远不能满足需求,我们需要的是一个支持实时预览、语法高亮、图片上传等高级功能的"所见即所得"编辑器。
本文将深度剖析一个基于react-markdown-editor-lite
、markdown-it
和highlight.js
封装的React Markdown编辑器组件。它不仅实现了上述核心功能,还通过ResizeObserver
API实现了编辑器高度的动态自适应,极大地提升了用户体验。让我们一起深入代码,看看这个组件是如何构建的。
成品展示
在深入代码之前,我们先看一下最终要实现的编辑器组件所具备的核心能力:
- 实时预览:左侧编写,右侧实时渲染。
- 语法高亮:代码块根据不同语言自动高亮,提升可读性。
- 图片上传:支持从本地拖拽或选择图片,自动上传并插入链接。
- 安全可靠:可配置的图片上传前校验。
- 高度自适应:编辑器高度能根据其容器大小动态调整,完美融入各种布局。
技术选型:强强联合
要构建这样一个功能完备的编辑器,我们站在了巨人的肩膀上,选择了以下三个核心库的强强联合:
react-markdown-editor-lite
: 一个轻量但功能强大的React Markdown编辑器UI框架。它为我们提供了编辑器的基本骨架、布局和丰富的配置API,如图片上传回调等。markdown-it
: 一个高性能、高可扩展的Markdown解析器。我们将用它来将用户输入的Markdown文本实时转换为HTML,并在此过程中注入自定义功能。highlight.js
: 一个业界知名的语法高亮库。通过与markdown-it
的结合,它可以自动识别并高亮代码块,支持几乎所有主流编程语言。
组件架构设计:封装与扩展
我们的核心思想不是直接在业务页面中使用react-markdown-editor-lite
,而是将其封装成一个自定义的MyMarkDown
组件。这样做有几个显而易见的好处:
- 封装复杂度:将解析器初始化、上传逻辑、样式导入等复杂配置封装在组件内部。
- 统一行为:在整个应用中,所有Markdown编辑器的行为(如上传逻辑、高亮主题)保持一致。
- 清晰的API :对外只暴露最必要的接口,如
value
、onChange
和fileSize
,使用起来非常简单。
核心功能实现深度解析
下面,我们将分步拆解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-lite
、markdown-it
和highlight.js
封装的React Markdown编辑器组件。它不仅实现了上述核心功能,还通过ResizeObserver
API实现了编辑器高度的动态自适应,极大地提升了用户体验。让我们一起深入代码,看看这个组件是如何构建的。
成品展示
在深入代码之前,我们先看一下最终要实现的编辑器组件所具备的核心能力:
- 实时预览:左侧编写,右侧实时渲染。
- 语法高亮:代码块根据不同语言自动高亮,提升可读性。
- 图片上传:支持从本地拖拽或选择图片,自动上传并插入链接。
- 安全可靠:可配置的图片上传前校验。
- 高度自适应:编辑器高度能根据其容器大小动态调整,完美融入各种布局。
技术选型:强强联合
要构建这样一个功能完备的编辑器,我们站在了巨人的肩膀上,选择了以下三个核心库的强强联合:
react-markdown-editor-lite
: 一个轻量但功能强大的React Markdown编辑器UI框架。它为我们提供了编辑器的基本骨架、布局和丰富的配置API,如图片上传回调等。markdown-it
: 一个高性能、高可扩展的Markdown解析器。我们将用它来将用户输入的Markdown文本实时转换为HTML,并在此过程中注入自定义功能。highlight.js
: 一个业界知名的语法高亮库。通过与markdown-it
的结合,它可以自动识别并高亮代码块,支持几乎所有主流编程语言。
组件架构设计:封装与扩展
我们的核心思想不是直接在业务页面中使用react-markdown-editor-lite
,而是将其封装成一个自定义的MyMarkDown
组件。这样做有几个显而易见的好处:
- 封装复杂度:将解析器初始化、上传逻辑、样式导入等复杂配置封装在组件内部。
- 统一行为:在整个应用中,所有Markdown编辑器的行为(如上传逻辑、高亮主题)保持一致。
- 清晰的API :对外只暴露最必要的接口,如
value
、onChange
和fileSize
,使用起来非常简单。
核心功能实现深度解析
下面,我们将分步拆解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开发中的核心理念。通过构建这样高质量的基础组件,我们能极大地提升开发效率,并保证整个应用的一致性和健壮性。希望这次深度解析能为您在未来的项目开发中提供有价值的参考和启发。