我是如何在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);
            }
        });
    }
}
相关推荐
冰红茶-Tea19 分钟前
typescript数据类型(二)
前端·typescript
slongzhang_22 分钟前
elementPlus消息组件多按钮案例
前端·javascript·vue.js
会发光的猪。1 小时前
vue中el-select选择框带搜索和输入,根据用户输入的值显示下拉列表
前端·javascript·vue.js·elementui
旺旺大力包1 小时前
【 Git 】git 的安装和使用
前端·笔记·git
雪落满地香1 小时前
前端:改变鼠标点击物体的颜色
前端
余生H2 小时前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿2 小时前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~2 小时前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫2 小时前
高德地图自定义折线矢量图形
前端·vue.js·vue
m0_748250932 小时前
html 通用错误页面
前端·html