我是如何在markdown编辑器中完成视频的插入和播放的

如果你有更好用的编辑器组件,请一定推荐给我!!!(最好附带使用说明🤓️)

介绍

在开发一个社区页面的时候,需要完成发帖、浏览帖子的能力。这里考虑接入markdown编辑器进行开发,也符合大多数用户的习惯。

首先是编辑器的选择,经过深思熟虑(随缘)后,确定了为 ByteMd, 主要是平时用掘金看到它们也是这个编辑器。

安装很简单:

js 复制代码
npm install bytemd

用起来更简单:

js 复制代码
    import '@/assets/static/editor_index.css' // 引入布局文件,防止样式错乱

    // 可以去了解下每个插件的功能,都是现有的,不再赘述
    const plugins = [gfm(), highlight(), breaks(), frontmatter(), footnotes(), gemoji(), mediumZoom()]
    <Editor
        locale={zhHans} // 选择语言
        value={content} // 内容区域
        plugins={plugins} //支持的插件
        onChange={(v) => {
            setContent(v);
        }}
        // 自定义内容区域媒体文件的上传
        uploadImages={async (files): Promise<Pick<Image, "alt" | "title" | "url">[]> => {
            console.log("files", files);
            const imageUrl = await uploadImage(files[0]);
            return [
                {
                    title: files.map((i) => i.name).join(),
                    url: imageUrl,
                },
            ];
        }}
    />

这样就很快的实现了一个markdown的编辑器。不出问题的话就要出问题了

要支持上传视频

复制代码
挠头,这个功能区没有上传视频的区域啊,这咋搞呢?去掘金上看看,掘金是有的,那肯定是可以有的。那么就看看如何在tools栏增加一个视频的icon
bytemd本身支持对tools bar做扩展,这样就简单了很多。可以拉下来源码看一下,新增一个tool的代码也很简单
js 复制代码
    export default function videoPlugin(saveEditorContext: (editorContext: BytemdEditorContext) => void) {
        const ADD_VIDEO = "url" // 视频tool的展示icon
        const handleUploadVideo = () => {
          window.dispatchEvent(showUploadAVDialog) // 点击时的事件处理,这里也是发通知给别处去处理了
        }
        
        return {
            actions: [
                {
                    title: '视频',
                    icon: `<img src="${ADD_VIDEO}" alt={"logo"} style="width: 24px; height: 24px;"/>`,
                    handler: {
                        type: 'action',
                        click(context: BytemdEditorContext) {
                            handleUploadVideo()
                            saveEditorContext(context)
                        },
                    },
                },
            ]
        }
    }

然后 把这个** videoPlugin** 加到前面的plugins列表里面

这样就有了一个上传视频的icon,点击后需要你来实现一下打开文件选择器 -> 选择视频 -> 上传到服务器 -> 处理上传后的链接 这套逻辑(不一定是这样,得看具体的业务流程)

当然,这肯定还没完,上传之后,需要像图片一样,在编辑区把视频展示出来吧。

一开始想得很简单,直接用一个<\iframe>或者 <\video> 标签,把视频播出了不就好了。but,这肯定是行不通的,为了防止XSS,这些特殊的标签都是不允许直接在输入框内进行使用的。掘金不太一样,它只能插入它们指定播放源的视频,也就是说要保证视频源的可靠才能插入。

我们业务暂时不需要考虑,都是自己人,也不会干这种事。于是参考了其他一些网站的实现,直接将视频内容展示为一个视频播放的缩略图。对,就是下面的 -- ![\AVFile](${url})\n --

js 复制代码
    const handleUploadSuccess = (url: string, file: File) => {
        if (editorContext) {
            // 创建一个视频播放器的 HTML 代码
            const videoHtml = `![AVFile](${url})\n`;
            const {line, ch} = editorContext.editor.getCursor();
            editorContext.editor.replaceRange(videoHtml, {line, ch});
            setContent(editorContext.editor.getValue());
        } else {
            message.error("上传失败,请重试")
        }
    };

在视频上传完成后,我们在插入视频的文本光标后面 主动添加视频的缩略图展示。

要注意一点,这里用到的 editorContext 是前面 videoPlugin组件中获取的,需要在用的组件内保存一下。

细心的你肯定会问:这里的url是视频的URL,用图片的语法展示会裂吧?

确实会有这个问题,于是我们还需要对整个编辑区的内容做一个处理,把展示的内容里面 视频的url替换成统一的视频缩略图(注意,只是展示位置的图片被替换了,实际上保存的还是视频的URL哈

于是我们再实现一个转换内容的插件,前提是基于你已经了解了 bytemd的 这几个接口的含义和调用时机,我不是来讲原理的,所以就不细嗦了。

js 复制代码
    export default function videoEmbedPlugin() {
        const DEFALUT_VIDEO_URL = 'http://cdn.qboost.woa.com/files/community_article_pic/%E8%A7%86%E9%A2%91%20%281%29_1716435376866.png'
        return {
            // @ts-ignore
            remark: (processor) =>
                // @ts-ignore
                processor.use(() => (tree) => {
                    visit(tree, 'image', (node) => {
                        if (node.alt === 'AVFile') {
                            // 替换图片 URL
                            node.url = DEFALUT_VIDEO_URL;
                        }
                    });
                }),
        };
    }

OK,编辑区支持上传视频的能力也算是大功告成了。不过,查看markdown文章的展示区也还需要适配,毕竟它是不可能自动播放你添加上去的视频的。

查看视频

对于展示区的处理,会简单很多,因为我们在上传视频的时候,对视频的url做了特殊处理,也就是在前面添加了[AVFile], 那么我们就可以在布局完成后,通过遍历展示区的html结点,找到 AVFile的img标签,然后将html中的这部分标签,替换为 <video>标签,就可以播放视频了

复制代码
// 替换为<video>标签
export function handlePicToVideo() {
    const markdownBodyElement = document.querySelector('.markdown-body');
    if (markdownBodyElement) {
        // 查找所有的 <p><img> 元素
        const images = markdownBodyElement.querySelectorAll('img.medium-zoom-image[alt="AVFile"]');

        images.forEach((img) => {
            const videoUrl = img.getAttribute('src');
            // 创建 video 元素
            const videoElement = document.createElement('video');
            videoElement.setAttribute('controls', 'controls');
            videoElement.setAttribute('width', 'auto');
            const sourceElement = document.createElement('source');
            sourceElement.setAttribute('src', videoUrl!);
            sourceElement.setAttribute('type', 'video/mp4');
            videoElement.appendChild(sourceElement);
            const noSupportText = document.createTextNode('Sorry, your browser doesn't support embedded videos.');
            videoElement.appendChild(noSupportText);

            // 替换 img 元素为 video 元素
            const parentParagraph = img.parentElement;
            if (parentParagraph) {
                parentParagraph.replaceChild(videoElement, img);
            }
        });
    }
}
相关推荐
无我Code9 小时前
前端-2025年末个人总结
前端·年终总结
文刀竹肃9 小时前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle
LYFlied9 小时前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
Bigger9 小时前
Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了
前端·macos·app
Bigger10 小时前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结10 小时前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
好游科技10 小时前
语聊APP新生态!一站式语聊房语音直播APP源码开发搭建
音视频·webrtc·im即时通讯·社交软件·社交语音视频软件
一招定胜负11 小时前
网络爬虫(第三部)
前端·javascript·爬虫
Shaneyxs11 小时前
从 0 到 1 实现CloudBase云开发 + 低代码全栈开发活动管理小程序(13)
前端
半山烟雨半山青11 小时前
微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor
前端·javascript·vue.js