VitePress 自动替换视频为 ArtPlayer

VitePress 自动替换视频为 ArtPlayer
狂犬主子浏览器自带的原生播放器相信大家都用过,总体体验不怎么样。为了提高用户播放体验,我们会引入第三方播放器,比如 DPlayer、ArtPlayer 等。由于前者一直没有维护,此处我们选型为 ArtPlayer,当然也可以换成其他播放器。
通过本文的方法你可以将 Markdown 中直接使用 <video>
元素插入的视频就可以自动替换成 ArtPlayer,这样可以在提高用户体验的同时保留原始 Markdown 文件对不同软件的兼容。
Markdown 插入视频
对于 VitePress 这种以 Markdown 文件驱动的站点,这里我们可以采用 HTML 的 <video>
元素。
1 | <video src="movie.mp4" controls width="100%"></video> |
或者
1 | <video controls width="100%"> <source src="movie.webm" type="video/webm" /> <source src="movie.mp4" type="video/mp4" /> </video> |
这种通过 HTML 插入视频的方式最为方便,只要查看器支持渲染 Markdown 中的 HTML5 即可。尤其是我们一般写文章用的以浏览器内核(Chromium、Webkit)驱动的 Typora、VS Code、Obsidian 等,直接就可以预览。
还有一种方式是先写一个 Vue 组件,然后在 Markdown 中引用。但这种方式就有点麻烦了,因为通常你的编辑器中是不跑 Vue 的,自己搓的组件一般也无法被编辑器识别,兼容性也不好。
本文要解决的问题是如何给这个插入的视频获得更好的播放体验,因此需要将这个 <video>
元素替换成 ArtPlayer。
ArtPlayer 的加载(编写 Vue 组件)
先 NPM 安装一下:
1 | pnpm add artplayer |
由于 ArtPlayer 非 Web Component 组件,它的渲染原理是通过 JavaScript 代码在指定 DOM(<div>
)中插入播放器的 HTML 元素。
也就是说,使用 ArtPlayer 必须从 JavaScript 创建。没有 <Artplayer>
这个元素。无法实现简单的替换。
我们肯定不想在每个 Markdown 中都写一遍 JavaScript。
好在,这里使用的是 VitePress,支持在 Markdown 中插入 Vue 组件。众所周知,Vue 组件就可以实现自定义元素的效果。这就好办了,我们可以“手搓”一个基本兼容 <video>
元素的 Vue 组件,然后全局引用不就行了?
然后给 ArtPlayer 的 Vue 组件写出来。
因为我们只需要读取视频播放地址和封面,其他信息不重要,因此可以少传入一些属性。
这里使用组合式 API(
<script setup>
),因为非专业前端选项式 API 看球不懂。组合式 API 更符合函数式编程的直觉,但获取一些属性就需要调用一些 hooks。
由于我们上面提到的视频文件地址有两种方式进行定义,我们需要都进行兼容:
- 直接
<video src="..." />
,这样和 Vue 组件 Props 传值 的方式类似,可以直接通过属性读取。 <video><source src="aaa"><source src="bbb">...</video>
,这种情况就比较难受,因为里面的那个<source>
元素涉及到 插槽 Slots ,然后这种方式可以支持多线路。
Vue 使用的是 Virtual DOM 技术,最终生成的 div 是随机的,我们可以使用 Vue 的模板引用 refs 来获取到准确的 DOM,交给 ArtPlayer 进行渲染。
最终代码如下:
1 | <!-- src/.vitepress/theme/components/ArtVideo.vue --> <script setup lang="ts"> import Artplayer from "artplayer"; import { onMounted, onUnmounted, ref, useSlots } from "vue"; const props = defineProps({ src: { type: String, required: false, default: "", }, poster: { type: String, required: false, default: "", }, }); const container = ref<HTMLDivElement>(); let art: Artplayer; const slots = useSlots(); const sources = ref<{ src: string; type?: string }[]>([]); if (props.src) { sources.value.push({ src: props.src }); } if (slots.default) { const slotContent = slots.default(); const newSources = slotContent .filter((node) => node.type === "source" && node.props?.src) .map((node) => ({ src: node.props!.src, type: node.props?.type || undefined, })); sources.value = [...sources.value, ...newSources]; } onMounted(() => { if (container.value) { art = new Artplayer({ container: container.value, url: sources.value[0].src, poster: props.poster, pip: true, setting: true, playbackRate: true, aspectRatio: true, fullscreen: true, fullscreenWeb: true, mutex: true, theme: getComputedStyle(container.value).getPropertyValue( "--vp-c-brand-3" ), quality: sources.value.length > 1 ? sources.value.map((source, index) => ({ html: source.type || `线路 ${index + 1}`, url: source.src, default: index === 0, })) : [], }); art.on("ready", () => { art.autoHeight(); }); art.on("resize", () => { art.autoHeight(); }); console.log(art); } }); onUnmounted(() => { art?.destroy(); }); </script> <template> <div class="video-container"> <div ref="container" class="artplayer-app"></div> </div> </template> <style> .video-container { margin: 1rem 0; border-radius: 8px; overflow: hidden; } .artplayer-app { width: 100%; height: 400px; background: #000; } </style> |
上面这个组件已经满足本人的使用需求。如果你想要传更多参数对播放器进行控制,也是可以的,反正多传的参数原生组件识别不了就是了。
然后在 Markdown 中试用一下:
1 | <script setup> import ArtVideo from ".vitepress/theme/components/ArtVideo" </script> <ArtVideo src="movie.mp4" controls width="100%"></ArtVideo> |
应该是没有问题的。
引用 Vue 组件
我们不想在每个 Markdown 中都写 JavaScript 引入前面写的 ArtVideo
组件,对于 VitePress 可以全局引用。
https://vitepress.dev/zh/guide/custom-theme#theme-interface
错误示范:
1 | // src/.vitepress/theme/index.ts import Video from "./components/Video.vue"; export default { // ... enhanceApp({ app }) { // ... app.component("video", Video); }, }; |
然而没这么简单,当我们尝试全局引入,发现浏览器控制台报错:
1 | [Vue warn]: Do not use built-in or reserved HTML elements as component id: video |
看来我们不能全局引入名为 video
这种已经存在的元素的组件。因此,这里我们将其重命名为 ArtVideo
。
1 | // src/.vitepress/theme/index.ts import ArtVideo from "./components/ArtVideo.vue"; export default { // ... enhanceApp({ app }) { // ... app.component("ArtVideo", ArtVideo); }, }; |
现在我们在任意 Markdown 中插入 <ArtVideo>
,可以发现视频能够正常使用 ArtPlayer 播放了。
由于我们直接通过引入名为 <video>
组件的梦想已经破灭,接下来我们需要让 VitePress 在渲染 Markdown 的时候将 <video>
替换成 <ArtVideo>
。
我们需要查找相应的 API 完成对 Markdown 内容的替换。假如实在找不到,最后的一招还有正则表达式替换。
VitePress 的 Markdown 渲染
VitePress 在站点配置中给了 Markdown 的渲染配置:
https://vitepress.dev/zh/reference/site-config#markdown
1 | export default { markdown: {...} } |
VitePress 使用的是 Markdown-it 这个库作为解析器,将 Markdown 渲染成 HTML 作为 Vue 的 <template>
。因此我们需要知道 Markdown-it 的渲染原理,无非就是将 Markdown 解析成抽象语法树(AST),然后再生成 HTML。
可以通过这个 DEMO 的 Debug 选项看到它生成的语法树结构:
https://markdown-it.github.io/
上图是错误示范,这里的 HTML 没有启用。我们要知道 VitePress 的 HTML 解析默认是启用的。
这里我们可以看出来了,它会分别解析开放标签和闭合标签,类型都是 html_inline
。
那么我们就去翻它的文档。
https://markdown-it.github.io/markdown-it/
结果发现一脸懵,一堆 API,怎么用都不知道。后面在它 GitHub 仓库主页发现还有文档:
https://github.com/markdown-it/markdown-it/blob/master/docs/examples/renderer_rules.md
根据这篇开发者文档,我们就可以轻松编写一个插件,控制这个解析的流程,完成对 HTML 元素的替换。
最后的代码如下:
1 | // src/.vitepress/config.mts import { defineConfig } from "vitepress"; // https://vitepress.dev/reference/site-config export default defineConfig({ // ... markdown: { config: (md) => { // https://github.com/markdown-it/markdown-it/blob/master/docs/examples/renderer_rules.md // 自定义插件:替换 video 标签 md.use((md) => { // 1. 处理块中的 video 标签 const defaultHtmlBlockRender = md.renderer.rules.html_block || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.html_block = function ( tokens, idx, options, env, self ) { const content = tokens[idx].content; // console.debug(`处理 html_block #${idx}:`, content); if (content.includes("<video") || content.includes("</video>")) { console.debug(`发现 video 标签在 html_block #${idx}`, content); return content.replaceAll("video", "ArtVideo"); } return defaultHtmlBlockRender(tokens, idx, options, env, self); }; // 2. 处理内联的 video 标签 const defaultHtmlInlineRender = md.renderer.rules.html_inline || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.html_inline = function ( tokens, idx, options, env, self ) { const content = tokens[idx].content; // console.debug(`处理 html_inline #${idx}:`, content); if (content.includes("<video") || content.includes("</video>")) { console.debug(`发现 video 标签在 html_inline #${idx}`, content); return content.replaceAll("video", "ArtVideo"); } return defaultHtmlInlineRender(tokens, idx, options, env, self); }; }); }, }, }); |
现在 Markdown 中直接使用 <video>
元素插入的视频就可以自动替换成 ArtPlayer 了!
通过这个方法,也可以将其他 Markdown 中原生的 HTML 元素自动替换成 Vue 组件,比如 <audio>
替换成 Aplayer 等等。这样可以在提高用户体验的同时保留原始 Markdown 文件对不同软件的兼容。