显示需求和使用场景:
- 展示后台返回的富文本文章;
- 文章需要展示图片、播放视频,视频格式包括MP4及flv;
- 文章中的附件资源获取时需要带上当前用户的token;
目前考虑的解决方案:
- 开发图片组件,将文章图片附件同步获取请求方式改为异步请求,请求时就可以带上用户token了,然后再展示;
- 二次封装dplayer,开发自定义视频组件,实现多格式视频播放;
- 获取文章正文字符串后,使用正则匹配,去批量替换图片、视频的标签,在正文中插入自定义标签;
- 通过专门的渲染组件,将带组件标签的html字符串渲染展示出来;
具体开发:
- 封装图片组件
// custom-image.vue
<template>
<el-image :src="imageUrl">
<template #placeholder>
<div v-if="!loading" class="image-slot">加载中<span class="dot">...</span></div>
</template>
<template #error>
<div class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</template>
</el-image>
</template>
<script lang="ts" setup>
// service 是二次封装的 axios ,异步请求带 token 在这里面做,对返回数据格式结构也做了统一处理
import service from '@/api/http'
const props = defineProps({
src: {
type: String,
default: ''
}
})
const imageUrl = ref<string>('')
const loading = ref<boolean>(false)
const handleGetImage = async () => {
loading.value = true
try {
// 将图片地址以 blob 二进制流的方式异步获取
const response = await service.get<Blob>(props.src, {}, { responseType: 'arraybuffer' })
if (response.code === 200 && response.data) {
imageUrl.value = URL.createObjectURL(response.data)
} else {
imageUrl.value = ''
}
loading.value = false
} catch (error) {
console.log(error)
imageUrl.value = ''
loading.value = false
}
}
onMounted(() => {
if (props.src) {
handleGetImage()
}
})
</script>
- 封装视频组件
// custom-video.vue
<template>
<div ref="videoRef" class="player" :style="{ width: defaultOptions.width, height: defaultOptions.height }" />
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import flvJs from 'flv.js'
import Dplayer, { DPlayerVideo, Preload } from 'dplayer'
import service from '@/api/http'
type VideoOptions = {
width: string
height: string
autoPlay: boolean
loop: boolean
lang: string
hotkey: boolean
preload: Preload
volume: number
playbackSpeed: number[]
src?: string
}
const props = defineProps({
options: {
type: Object as PropType<VideoOptions>,
default: () => ({
width: '800px',
height: '450px',
autoPlay: false,
loop: false,
lang: 'zh-cn',
hotkey: true,
preload: 'auto',
volume: 0.7,
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2]
})
},
title: {
type: String,
default: ''
},
src: {
type: String,
default: ''
}
})
const defaultOptions = computed<VideoOptions>(() => ({
...props.options,
src: props.src || '',
title: props.title
}))
const isFlv = computed(() => {
if (defaultOptions.value.src) {
const link = defaultOptions.value.src.split('?')
const newLink = link[0].split('.')
return newLink[newLink.length - 1].toLocaleLowerCase() === 'flv'
}
return false
})
const videoRef = ref()
const loading = ref<boolean>(false)
const handleInitPlayer = async () => {
loading.value = true
try {
const response = await await service.get<Blob>(props.src, {}, { responseType: 'arraybuffer' })
if (response.code === 200 && response.data) {
let video: DPlayerVideo = {
url: URL.createObjectURL(response.data) // 指定视频播放链接
}
if (isFlv.value) {
video = {
url: URL.createObjectURL(response.data),
type: 'customFlv',
customType: {
customFlv(videoDom: any) {
const flvPlayer = flvJs.createPlayer({
type: 'flv',
url: videoDom.src
})
flvPlayer.attachMediaElement(videoDom)
flvPlayer.load()
}
}
}
}
const dp = new Dplayer({
// 初始化视频对象
container: videoRef.value, // 指定视频容器节点
autoplay: defaultOptions.value.autoPlay,
loop: defaultOptions.value.loop,
lang: defaultOptions.value.lang,
hotkey: defaultOptions.value.hotkey,
preload: defaultOptions.value.preload,
volume: defaultOptions.value.volume,
playbackSpeed: defaultOptions.value.playbackSpeed,
video
})
}
loading.value = false
} catch (error) {
loading.value = false
}
}
onMounted(() => {
nextTick(() => {
handleInitPlayer()
})
})
</script>
- 封装自定义html字符串渲染组件
渲染关键就是关键就是 compile 函数,不过我这种写法目前只支持展示,click 等事件只能在组件内部实现,不能通过子组件将事件传递给父组件,定义在组件字符串标签上的事件是不能加载的,会报事件未定义错误;
经过测试,想通过字符串的方式渲染的组件,也需要提前注册,并且挂载到 vue 实例上,也就是 全局注册;如果想通过事件做操作,可以考虑使用 pinia 状态管理去触发事件。
// custom-render.vue
<script lang="ts">
import { compile, VNode } from 'vue'
export default defineComponent({
props: {
html: { type: String, required: true }
},
computed: {
template(): string {
return this.html
}
},
render(): VNode {
return h(compile(this.template), { ...this.$attrs })
}
})
</script>
使用
// main.ts 注册全局
...
import CustomVideo from '@/components/custom-video.vue'
import CustomImage from '@/components/custom-image.vue'
...
const app = createApp(App)
...
app.component('CustomVideo', CustomVideo)
app.component('CustomImage', CustomImage)
app.mount('#app')
// article.vue
<template>
<custom-render :html="content"></custom-render>
</template>
<script setup lang="ts">
const article = `
<p>我是文章</p>
<p>巴拉巴拉</p>
<p>我是图片:</p>
<p><img src="http://xxx.com/200x200/abc.jpg"/></p>
<p>我是MP4视频:</p>
<p><video src="http://xxx.com/video/123.mp4"></video></p>
<p>我是FLV视频:</p>
<p><video src="http://xxx.com/video/123.flv"></video></p>
`
const content = computed(() => {
let str = article.replaceAll('\n', '')
const regImage = /<img.*?(?:img>|\/img>)/gi
const images = str.match(regImage)
images?.forEach(item => {
str = str.replace(item, item.replace('<img ', '<custom-image ').replace(' img>', ' custom-image>'))
})
const regVideo = /<video.*?(?:video>|\/video>)/gi
const videos = str.match(regVideo)
videos?.forEach(item => {
str = str.replace(item, item.replace('<video', '<custom-video ').replace(' video>', ' custom-video>'))
})
return `<div>${str}</div>`
})
</script>
ps:
本文的思路是在项目里面验证实现过的,但本文代码是脱敏后手打的,不保证照搬能运行起来,思路肯定没问题,照着这个思路写是可以实现文章头部的需求,如果你也有这个需求,代码参考下就行。
请无视代码中的很多写法,我水平真的一般,正则也写的垃圾,欢迎大佬帮忙优化,感激不尽;
目前确实没有能力实现在字符串组件上绑定事件功能,欢迎探讨,如果有解决方案,也请大佬共享下,确实有这个需求。
感谢您的阅读