【问题标题】:How to safely render tokenized HTML in Vue?如何在 Vue 中安全地呈现标记化的 HTML?
【发布时间】:2022-01-05 20:02:20
【问题描述】:

我的文本包含需要替换为链接的某些标记。例如:

@peter and @samantha went on a date in #Paris to see the movie #HouseOfGucci

结果应该是:

<a href="/user/peter">@peter</a> and <a href="/user/samantha">@samantha</a> went on a date in <a href="/topic/paris">#Paris</a> to see the movie <a href="/topic/houseofgucci">#HouseOfGucci</>

我有一个要替换的令牌列表(并非所有都将被替换)以及如何替换(用户与主题)。已解决in here

现在的问题是我使用的是 Vue3,我可以将 html 渲染为简单的&lt;div v-html="text"&gt;&lt;/div&gt;,但这会产生问题,因为链接会导致 Vue 前端(即 SPA)的全页重新加载。

正确的解决方案是使用一个组件来标记整个文本并通过 if 条件适当地呈现每个标记(见下文)。不仅如此,还需要考虑断线。

<template>
  <template v-for="(item, index) in lines" :key="index">
    <router-link :to="item.route" v-if="'route' in item">{{ item.value }}</router-link>
    <br v-else-if="'break' in item">
    <template v-else>{{ item.value }}</template>
  </template>
</template>

所以我试图弄清楚如何处理输入字符串并正确标记它,并且有点卡在如何正确递归循环所有标记以便它们被正确替换/附加/插入到最终数组中渲染。


Here is a Go code of the desired end-result。我将尝试在 JS 中实现它,但如果有人有更好的,请将其发布为答案。此外,它没有正确处理前缀和后缀的正则表达式,因此foo@samanthabar 将匹配@samantha

【问题讨论】:

  • 我会尝试一个更简单的解决方案。将使用哈希作为锚点来防止重新加载,然后使用一些导航守卫来监听哈希路由的变化......;最后路由到相关路由。希望我很清楚。

标签: javascript vue.js vuejs3


【解决方案1】:

好的,我猜你希望我们做你的功课而不学任何东西,这不是应该的,所以我会尽力帮助你,而不是为你提供所有预先制作和测试的东西:

  1. 您可以首先对文本进行标记,如 here 所示,拆分所有内容,同时将单词之间的空格也视为常规标记。处理完链接后,这将使您可以恢复文本的整体结构。

  2. 然后您可以使用 &lt;template v-for="token in tokens"&gt; 循环遍历令牌

  3. 最后,您决定使用 &lt;template v-if=""&gt; / &lt;template v-else-if=""&gt;/ &lt;template v-else&gt; 渲染什么,如下所示:

<template v-if="token.charAt(0) === '@' && persons.includes(token.substring(1, token.length))">
  <router-link :to="`/persons/${token.substring(1, token.length)}`">
    {{ token }}
  </router-link>
</template>
<template v-else-if="token.charAt(0) === '#' && topics.includes(token.substring(1, token.length))">
  <router-link :to="`/topics/${token.substring(1, token.length)}`">
    {{ token }}
  </router-link>
</template>
<template v-else>
  {{ token }}
</template>

你看到了吗?根本没有递归!只是简单的线性代码。您可能会遇到大量人员/主题的性能问题,但是一旦您到达那里,您应该知道在数组中搜索某些内容是这里的罪魁祸首。

【讨论】:

  • 一个问题的解决方案。如果搜索数组可能是个问题,那么为什么要将所有这些逻辑直接包含在模板中而使情况变得更糟呢?所有来自personstopics 查找表的解析和令牌查找都应提取到computed 中,因此它不会在每次渲染时执行,而是仅在输入数据中的某些内容发生变化时执行
  • 我添加了我所追求的解决方案的 Go 代码。
  • @MichalLevý 当然,它可能会更有效。可以在计算中将查找实现为Sets(如果主题/人员存在,则使用has() 进行查找),并且还可以在计算属性中进行转换,以便最终您只有 O(n)整个文本转换,只有在发生任何变化时才会触发。但从我的角度来看,这超出了 OP 的需要/需要。他问如何开始解决他的主要问题。
  • @glen 很酷,您构建了一个可行的解决方案!一些感想:我个人觉得这里没必要建树形结构,是不是有原因呢?另外:您循环遍历每一行的每个句柄/主题/符号并尝试匹配它,这是最佳情况 O(n*m),效率很低。有点像@MichalLevý 对我的回答的评论。因此,我首先要拆分文本,这也可以防止foo@samanthabar 问题。不过,您可能希望修改拆分正则表达式以允许 ./,/;/: 充当分词符。然后我会从 Sets 中查找项目,这使得每次查找 O(1)。
【解决方案2】:

所以,这就是我想出的。我认为这很丑陋,但它确实有效。

<template>
  <template v-for="(item, index) in lines" :key="index">
    <router-link :to="item.Route" v-if="item.Type === 'route'">{{ item.Value }}</router-link>
    <br v-else-if="item.Type === 'break'">
    <template v-else>{{ item.Value }}</template>
  </template>
</template>

<script>
import { defineComponent, h } from 'vue';

export default defineComponent({
  name: 'TokenizedText',
  props: {
    text: {
      type: String,
      required: true,
    },
    symbols: {
      type: Array,
      default: () => []
    },
    handles: {
      type: Array,
      default: () => []
    },
    topics: {
      type: Array,
      default: () => []
    },
  },
  computed: {
    lines() {
      return new Token("str", this.text).Tokenize(this.symbols || [], this.handles || [], this.topics || []).Build()
    }
  },
})

function Token(type, value) {
  this.Type = type
  this.Value = value
  this.Route = {}
  this.Children = []
}

Token.prototype.Build = function() {
  let out = []
  if (this.Value.length > 0 ||this.Type === 'break') {
    out.push(this)
  }
  for (let i in this.Children) {
    out.push.apply(out, this.Children[i].Build())
  }
  return out
}

Token.prototype.Tokenize = function(symbols, handles, topics) {
  if (this.Type === "str") {
    let breakLines = this.Value.split("\n")
    if (breakLines.length > 1) {
      this.Value = ""
      for (let key in breakLines) {
        this.Children.push(new Token("str", breakLines[key]).Tokenize(symbols, handles, topics))
        if (key < breakLines.length - 1) {
          this.Children.push(new Token("break", ""))
        }
      }

      return this
    }
  }

  handles.sort((a, b) => a.length - b.length)

  for (let h in handles) {
    if (this.Value.toLowerCase() === "@"+handles[h].toLowerCase()) {
      this.Type = "route"
      this.Route = {name: "user.view", params: {handle: handles[h]}}
      return this
    }

    let handleLines = this.Value.split(new RegExp(`@\\b${handles[h]}\\b`, 'gmi'))
    let handleOriginals = this.Value.match(new RegExp(`@\\b${handles[h]}\\b`, 'gmi')) || []

    if (handleLines.length > 1) {
      this.Value = ""
      for (let l in handleLines) {
        if (handleLines[l].length > 0) {
          this.Children.push(new Token("str", handleLines[l]).Tokenize(symbols, handles, topics))
        }
        if (l < handleLines.length-1) {
          let line = handleOriginals[l] || "@"+handles[h]
          console.log(line)
          this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
        }
      }

      return this
    }
  }

  topics.sort((a, b) => a.length - b.length)

  for (let h in topics) {
    if (this.Value.toLowerCase() === "#"+topics[h].toLowerCase()) {
      this.Type = "route"
      this.Route= {name: "topic.view", params: {tag: topics[h]}}

      return this
    }

    let topicLines = this.Value.split(new RegExp(`#\\b${topics[h]}\\b`, 'gmi'))
    let topicOriginals = this.Value.match(new RegExp(`#\\b${topics[h]}\\b`, 'gmi')) || []

    if (topicLines.length > 1) {
      this.Value = ""
      for (let l in topicLines) {
        if (topicLines[l].length > 0) {
          this.Children.push(new Token("str", topicLines[l]).Tokenize(symbols, handles, topics))
        }
        if (l < topicLines.length-1) {
          let line = topicOriginals[l] || "#"+topics[h]
          this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
        }
      }

      return this
    }
  }

  symbols.sort((a, b) => a.length - b.length)

  for (let h in symbols) {
    if (this.Value.toLowerCase() === "$"+symbols[h].toLowerCase()) {
      this.Type = "route"
      this.Route= {name: "symbol.view", params: {symbol: symbols[h]}}
      return this
    }

    let symbolLines = this.Value.split(new RegExp(`\\$\\b${symbols[h]}\\b`, 'gmi'))
    let symbolOriginals = this.Value.match(new RegExp(`\\$\\b${symbols[h]}\\b`, 'gmi')) || []

    if (symbolLines.length > 1) {
      this.Value = ""
      for (let l in symbolLines) {
        if (symbolLines[l].length > 0) {
          this.Children.push(new Token("str", symbolLines[l]).Tokenize(symbols, handles, topics))
        }
        if (l < symbolLines.length-1) {
          let line = symbolOriginals[l] || "$"+symbols[h]
          this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
        }
      }
      return this
    }
  }

  return this
}

</script>

【讨论】:

    猜你喜欢
    • 2016-12-04
    • 1970-01-01
    • 1970-01-01
    • 2019-11-14
    • 2022-10-14
    • 1970-01-01
    • 2016-09-13
    • 2013-08-19
    • 2011-06-03
    相关资源
    最近更新 更多