likes
comments
collection
share

最佳实践,Vue渲染富文本如何隔离样式【金石计划4.0】

作者站长头像
站长
· 阅读数 89

大家好!今天带来的是一份渲染富文本的最佳实践(Vue3版)。

可能有些人会说:不就是渲染富文本吗,直接v-html不就行了吗。

v-html是可以渲染,但是v-html无法做到样式隔离,它渲染的内容可能会被富文本之外的样式影响,富文本内的样式也可能会污染外面的样式。

先说一说什么情况下富文本内外的样式会相互污染。

  1. 全局样式或者定义在scoped外的样式可能对富文本样式造成污染,多为标签选择器
  2. 富文本内容中带有style标签的情况下可能会污染富文本外的内容样式(常见的富文本多为内联样式,但无法排除有这种情况,作者就遇见过)

那么要如何避免样式相互污染的,答案就是要隔离样式。

说到隔离样式,很多人可能下意识想到的就是iframe。

先说结论,iframe能做,但不建议,比较麻烦,麻烦在哪呢

  1. iframe依赖一个url,意味着需要将富文本转换成一个url,很麻烦,当然,也可以直接设置iframe的innerHTML,具体写法是:document.getElementById("iframe").document.body.innerHTML = "xxxx"
  2. 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
评论
请登录