最佳实践,Vue渲染富文本如何隔离样式【金石计划4.0】
大家好!今天带来的是一份渲染富文本的最佳实践(Vue3版)。
可能有些人会说:不就是渲染富文本吗,直接v-html不就行了吗。
v-html是可以渲染,但是v-html无法做到样式隔离,它渲染的内容可能会被富文本之外的样式影响,富文本内的样式也可能会污染外面的样式。
先说一说什么情况下富文本内外的样式会相互污染。
- 全局样式或者定义在scoped外的样式可能对富文本样式造成污染,多为标签选择器
- 富文本内容中带有style标签的情况下可能会污染富文本外的内容样式(常见的富文本多为内联样式,但无法排除有这种情况,作者就遇见过)
那么要如何避免样式相互污染的,答案就是要隔离样式。
说到隔离样式,很多人可能下意识想到的就是iframe。
先说结论,iframe能做,但不建议,比较麻烦,麻烦在哪呢
- iframe依赖一个url,意味着需要将富文本转换成一个url,很麻烦,当然,也可以直接设置iframe的innerHTML,具体写法是:document.getElementById("iframe").document.body.innerHTML = "xxxx"
- iframe需要指定一个高度,不然可能会出现滚动条,意味着需要根据富文本的内容计算富文本所需的高度
那除了iframe还有没有别的方案呢,答案肯定是有的,那就是 Shadow DOM
什么是Shadow DOM呢?
顾名思义,Shadow DOM直译的话就是影子dom,但我更愿把它理解为DOM中的DOM。因为他能够为Web组件中的 DOM和 CSS提供了封装,实际上是在浏览器渲染文档的时候会给指定的DOM结构插入编写好的DOM元素,但是插入的Shadow DOM 会与主文档的DOM保持分离,也就是说Shadow DOM不存在于主DOM树上(此解释来源于知乎的一篇文章:zhuanlan.zhihu.com/p/559759502…
如何做
代码实现还是挺简单的,先贴最简单的实现
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.directive('richText', {
mounted(el, binding) {
const shadowRoot = el.attachShadow({ mode: 'closed' })
let htmlText, styleText
if (typeof binding.value === 'object') {
htmlText = binding.value.htmlText
styleText = binding.value.styleText
} else {
htmlText = binding.value
}
shadowRoot.innerHTML = htmlText
if (styleText) {
// 给富文本添加样式
const style = document.createElement('style')
style.textContent = styleText
shadowRoot.appendChild(style)
}
},
}
...
如何使用
<!-- 不需要添加额外的样式 -->
<div v-richText="htmlText"></div>
<!-- 指定额外的样式 -->
<div v-richText="{htmlText, 'p {color: red;}'}"></div>
一般情况下这种方案是够用的,但如果富文本的内容是响应式变化的就不行了
下面是完善的解决方案,使用的是ts
useRichText.ts
import type { App } from 'vue'
const map = new WeakMap()
function toShadowDom(el: HTMLElement, htmlText: string, styleText = '') {
let shadowRoot
if (map.get(el)) {
shadowRoot = map.get(el)
} else {
shadowRoot = el.attachShadow({ mode: 'closed' })
map.set(el, shadowRoot)
}
shadowRoot.innerHTML = htmlText
if (styleText) {
// 给富文本添加样式
const style = document.createElement('style')
style.textContent = styleText
shadowRoot.appendChild(style)
}
}
function handleBindingVal(binding: any) {
let htmlText, styleText
if (typeof binding.value === 'object') {
htmlText = binding.value.htmlText
styleText = binding.value.styleText
} else {
htmlText = binding.value
}
return { htmlText, styleText }
}
const useRichText = {
install: (app: App) => {
app.directive('richText', {
mounted(el, binding) {
const { htmlText, styleText } = handleBindingVal(binding)
toShadowDom(el, htmlText, styleText)
},
updated(el, binding) {
const { htmlText, styleText } = handleBindingVal(binding)
toShadowDom(el, htmlText, styleText)
},
})
},
}
export default useRichText
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import useRichText from './useRichText'
const app = createApp(App)
app.use(useRichText)
...
用法跟上面最简单的实现是一样的
为方便食用,我将这个自定义指令发布到npm里了,有需要的朋友如果懒得自己配置也可以直接使用这个包 @zclzone/utils
pnpm i @zclzone/utils
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { useRichText } from '@zclzone/utils'
const app = createApp(App)
app.use(useRichText)
...
<!-- 不需要添加额外的样式 -->
<div v-richText="htmlText"></div>
<!-- 指定额外的样式 -->
<div v-richText="{htmlText, 'p {color: red;}'}"></div>
总结
总得来说,使用 Shadow DOM
算是一个比较优雅的方案了,目前主流的浏览器也早就已经支持,兼容性问题不大。
如果这篇文章对你有帮助,请点赞关注,后续会持续带来更实用的分享!!
转载自:https://juejin.cn/post/7243240589294420029