React Quill富文本编辑器实现社交媒体链接嵌入功能求助
实现React Quill将社交媒体链接转为嵌入内容
需求
希望React Quill富文本编辑器能将社交媒体链接(如TikTok链接https://www.tiktok.com/@epicgardening/video/7055411162212633903)转换为嵌入内容,最终输出为HTML字符串,效果类似react-social-media-embed库的展示形式。
现有代码
富文本编辑器组件代码
import { useCallback, useMemo, useEffect } from 'react'; import ImageResize from 'quill-image-resize-module-react'; import ReactQuill, { Quill } from 'react-quill'; import { message } from 'antd'; import { uploadFiles } from 'utils'; import 'react-quill/dist/quill.bubble.css'; import 'react-quill/dist/quill.snow.css'; import './styles.scss'; Quill.register('modules/imageResize', ImageResize); const RichTextEditor = ({ editorState, onChange, readOnly = false, setLoading = () => {} }) => { window.Quill = Quill; let quillRef = null; // Quill instance let reactQuillRef = null; // ReactQuill component useEffect(() => { attachQuillRefs(); }, []); const attachQuillRefs = useCallback(() => { if (typeof reactQuillRef.getEditor !== 'function') return; quillRef = reactQuillRef.getEditor(); }, []); const imageHandler = () => { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.click(); input.onchange = async () => { const file = input.files[0]; if (file.size > 1500000) return message.error('Image size exceeded 1.5Mb'); setLoading({ image: true }); const formData = new FormData(); formData.append('image', file); const fileName = file.name; const imgUrl = await uploadFiles(file, quillRef); const range = quillRef.getSelection(); quillRef.insertEmbed(range.index, 'image', imgUrl, 'user'); let existingDelta = quillRef.getContents(); const indexOf = existingDelta.ops.findIndex((eachOps) => { return eachOps.insert?.image === imgUrl; }); const selectedOps = existingDelta.ops[indexOf]; if (indexOf !== -1) { selectedOps.attributes = {}; selectedOps.attributes = { alt: fileName }; } quillRef.setContents(existingDelta); setLoading({ image: false }); }; }; const modules = useMemo( () => ({ toolbar: { container: [ [{ header: [1, 2, 3, 4, 5, 6, false] }], ['bold', 'italic', 'underline'], [{ list: 'ordered' }, { list: 'bullet' }], [{ align: [] }], ['link', 'image'], ['clean'], [{ color: [] }] ], handlers: { image: imageHandler } }, imageResize: { modules: ['Resize', 'DisplaySize'] } }), [] ); return ( <div className="react-quill-wrapper"> <ReactQuill readOnly={readOnly} theme={readOnly ? 'bubble' : 'snow'} ref={(e) => { reactQuillRef = e; }} value={editorState} modules={modules} placeholder="Add content of your article!" onChange={onChange} /> </div> ); }; export { RichTextEditor };
父组件调用代码
const [editorState, setEditorState] = useState(''); <RichTextEditor editorState={editorState} onChange={setEditorState} setLoading={setLoading} />
尝试思路
识别用户输入/粘贴的社交媒体链接,判断所属平台后使用react-social-media-embed生成嵌入内容,但该库输出为JSX,需转换为HTML字符串。
解决方案
步骤1:注册自定义HTML嵌入格式
Quill默认不支持直接插入HTML嵌入内容,需自定义一个Blot类型:
// 在组件顶部添加,Quill.register之前 const Embed = Quill.import('blots/embed'); class HtmlEmbed extends Embed { static create(value) { const node = super.create(value); node.innerHTML = value; node.setAttribute('contenteditable', 'false'); // 设置不可编辑 node.style.margin = '10px 0'; return node; } static value(node) { return node.innerHTML; } } HtmlEmbed.blotName = 'html'; HtmlEmbed.tagName = 'div'; Quill.register(HtmlEmbed);
步骤2:添加社交媒体链接检测函数
用于判断输入的文本是否为目标平台链接:
const isSocialMediaLink = (url) => { // 可扩展支持更多平台(Instagram、YouTube等) const tiktokRegex = /^https?:\/\/(www\.)?tiktok\.com\/@[\w-]+\/video\/\d+/; return tiktokRegex.test(url.trim()); };
步骤3:处理输入/粘贴的链接
在组件中添加监听事件,自动将链接替换为嵌入内容:
useEffect(() => { if (!quillRef) return; // 处理粘贴事件 const handlePaste = (event) => { const clipboardData = event.clipboardData || window.clipboardData; const text = clipboardData.getData('text').trim(); if (isSocialMediaLink(text)) { event.preventDefault(); // 生成TikTok嵌入iframe const videoIdMatch = text.match(/video\/(\d+)/); if (videoIdMatch) { const videoId = videoIdMatch[1]; const embedHtml = `<iframe src="https://www.tiktok.com/embed/v2/${videoId}" width="100%" height="480" frameborder="0" allowfullscreen></iframe>`; const range = quillRef.getSelection(); quillRef.insertEmbed(range.index, 'html', embedHtml); } } }; // 处理文本输入事件,自动转换单独成行的链接 const handleTextChange = () => { const content = quillRef.getContents(); const updatedOps = [...content.ops]; let hasUpdates = false; updatedOps.forEach((op, index) => { if (op.insert && typeof op.insert === 'string') { const lines = op.insert.split('\n'); const newLines = []; lines.forEach(line => { const trimmedLine = line.trim(); if (isSocialMediaLink(trimmedLine)) { const videoIdMatch = trimmedLine.match(/video\/(\d+)/); if (videoIdMatch) { const videoId = videoIdMatch[1]; const embedHtml = `<iframe src="https://www.tiktok.com/embed/v2/${videoId}" width="100%" height="480" frameborder="0" allowfullscreen></iframe>`; // 替换链接为嵌入内容 newLines.push({ insert: '\n' }); newLines.push({ insert: { html: embedHtml } }); newLines.push({ insert: '\n' }); hasUpdates = true; } else { newLines.push({ insert: line + '\n' }); } } else { newLines.push({ insert: line + '\n' }); } }); // 移除最后多余的换行 if (newLines.length && newLines[newLines.length - 1].insert === '\n') { newLines.pop(); } if (hasUpdates) { updatedOps.splice(index, 1, ...newLines); } } }); if (hasUpdates) { quillRef.setContents({ ops: updatedOps }, 'silent'); onChange(quillRef.root.innerHTML); } }; quillRef.root.addEventListener('paste', handlePaste); quillRef.on('text-change', handleTextChange); return () => { quillRef.root.removeEventListener('paste', handlePaste); quillRef.off('text-change', handleTextChange); }; }, [quillRef, onChange]);
步骤4:调整编辑器内容同步逻辑
确保onChange回调始终返回HTML字符串,上述代码中已通过quillRef.root.innerHTML直接传递最终的HTML内容。
说明
- 该方案直接使用平台官方的嵌入iframe,无需依赖
react-social-media-embed库,避免JSX转HTML的麻烦 - 可通过扩展
isSocialMediaLink中的正则表达式,支持更多社交媒体平台(如Instagram Reels、YouTube等) - 嵌入内容设置为不可编辑,避免用户误操作
内容的提问来源于stack exchange,提问作者Arian Nurrahman
相关产品推荐
相关产品推荐

