在 vuejs2 数据中动态插入子组件(不使用 $compile 或滥用 v-html) [英] Dynamically insert child components inside vuejs2 data (without $compile or abusing v-html)

查看:20
本文介绍了在 vuejs2 数据中动态插入子组件(不使用 $compile 或滥用 v-html)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在一个不需要预定义的 HTML 块中的任意位置即时插入新的 vuejs 组件.

这是一个稍微做作的例子,演示了我正在尝试做的事情:

Vue.component('child', {//假装我做了一些有用的事情模板:'<span>--><slot></slot><--</span>'})Vue.component('父', {数据() {返回 {输入:'lorem',文本:'<p>Lorem ipsum dolor sat amet.</p><p><i>Lorem ipsum!</i></p>'}},模板:`

搜索:<input type='text' v-model="input"><br><小时>这会插入子组件但不渲染它或 HTML:<div>{{输出}}</div><小时>这会呈现 HTML,但当然会去掉子组件:<div v-html="输出"></div><小时>(这是子组件,只是为了表明它在这里可用:<孩子>你好</孩子>)<小时>这是目标:它同时呈现输入 html和插入的子组件:待办事项¯\_(ツ)_/¯</div>`,计算:{输出() {/* 这是错误的方法;我用什么代替它?*/var out = this.text;如果(this.input){this.input = this.input.replace(/[^a-zA-Z\s]/g,'');var regex = new RegExp(this.input, "gi");out = out.replace(regex, '<child><b>' + this.input + '</b></child>');}返回;}}});新的 Vue({el: '#app'})

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script><div id="应用程序"><父母></父母>

在上面的代码段中,假设 data.text 是经过清理的 HTML.<child> 是一些做一些有用的事情的子组件,我想用它来包装一些事先不知道的 data.text 块.(input 仅用于此处的演示.此 MCVE 与我正在构建的代码并不真正相似,它只是一个示例,显示了我遇到的情况.)

那么:我将如何更改 output 函数或父组件的模板,以便 input 中的 HTML 和插入的 <child> 模板是否正确呈现?

我的尝试

  • 在 Vue 1 中,这个问题的答案将是一个简单的 $compile.我正在使用 vuejs2,它删除了 $compile(出于合理的考虑,它使天真地引入 XSS 漏洞变得太容易了.)

  • v-html 对你提供的内容进行消毒,从而去除子组件.显然这是不是这样做的方式.(该页面建议改用部分,但我不确定如何将其应用于这种情况;无论如何,部分也已从 vue2 中删除.)

  • 我尝试将 output() 的结果传递给另一个组件,然后该组件将其用作模板.这似乎是一种很有前途的方法,但我不知道如何更改该辅助组件的模板.template 只接受一个字符串,而不是像许多其他组件属性那样的函数,所以我不能将模板 html 传递到 prop 中.像在 beforeMount()bind() 中重写 this.template 这样的事情本来很好,但也没有乐趣.是否有其他方法可以在安装之前替换组件的模板字符串?

  • template 不同,我 可以 将数据传递给组件的 render() 函数...不得不将该 html 字符串解析为嵌套的 createElement 函数.这正是 Vue 在内部首先要做的事情;除了自己重新发明之外,还有什么方法可以吸引到这里吗?

Vue.component('foo', {道具:['myInput'],渲染(创建元素){控制台日志(this.myInput);//这有效...//...但是如何将 this.myInput 中的 html 解析为可用的渲染函数?//return createElement('div', this.myInput);},})

  • 我也无法使用内联模板欺骗我:<foo inline-template>{{$parent.output}}</foo>与普通的旧 {{output}} 完全相同.回想起来,这应该是显而易见的,但值得一试.

  • 也许可以动态构建一个异步组件是答案吗?这可以清楚地生成一个带有任意模板的组件,但是我如何合理地从父组件调用它,并将 output 提供给构造函数?(它需要可重复使用不同的输入,多个实例可能同时可见;没有全局变量或单例.)

  • 我什至考虑过一些荒谬的事情,比如让 output() 在插入 的点将输入拆分成一个数组,然后在主模板中做这样的事情:

 ...<模板 v-for=输出中的块"><span v-html="chunk"></span><孩子>...</孩子></模板>....

如果费力的话,这将是可行的——我也必须将孩子插槽中的内容拆分成一个单独的数组,并在 v-for 期间通过索引获取它,但这可以完成... if input 是纯文本而不是 HTML.在拆分 HTML 时,我经常会在每个 chunk 中出现不平衡的标签,当 v-html 为我重新平衡时,这可能会弄乱格式.无论如何,整个策略感觉就像一个糟糕的黑客;一定有更好的方法.

  • 也许我只是将整个输入放到 v-html 中,然后(以某种方式)通过事后 DOM 操作将子组件插入到适当的位置?我还没有深入探讨这个选项,因为它也感觉像是一种黑客行为,并且与数据驱动策略相反,但如果其他方法都失败了,这也许是一种可行的方法?

一些先发制人的免责声明

  • 我非常清楚类似 $compile 的操作所涉及的 XSS 风险.请放心,我所做的一切都不会以任何方式涉及未经消毒的用户输入;用户不会插入任意组件代码,而是组件需要在用户定义的位置插入子组件.
  • 我有理由相信这不是 XY 问题,我确实需要即时插入组件.(我希望从失败的尝试次数和我跑过的死胡同中可以明显看出,我在这个问题上投入了更多的想法!)也就是说,如果有一种不同的方法会导致类似的结果,我'我的耳朵.重点是我知道我需要添加哪个组件,但我无法提前哪里添加它;该决定发生在运行时.
  • 如果相关,在现实生活中,我使用的是 vue-cli webpack 模板中的单文件组件结构,而不是上面示例中的 Vue.component().不会偏离该结构太远的答案是首选,但任何有效的方法都会有效.

进展!

@BertEvans 在评论中指出 Vue.compile() 是一个存在的东西,如果有的话,我不敢相信我错过了.

但是如果不使用该文档中的全局变量,我仍然无法使用它.这会呈现,但在全局中硬编码模板:

var precompiled = Vue.compile('<span><child>test</child></span>');Vue.component('测试', {渲染:预编译.render,staticRenderFns:预编译的.staticRenderFns});

但是各种尝试将其重新调整为可以接受输入属性的东西都没有成功(例如,以下内容抛出渲染函数中的错误:ReferenceError:_c 未定义",我认为是因为 staticRenderFns<当 render 需要时/code> 还没准备好?

Vue.component('test', {道具:['输入'],render() { return Vue.compile(this.input).render()},staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}});

(这不是因为有两个单独的 compile() - 在 beforeMount() 内部进行预编译,然后返回其渲染,而 staticRenderFns 会引发相同的错误.)

这真的感觉像是在正确的轨道上,但我只是被困在一个愚蠢的语法错误或类似的问题上......

解决方案

正如我上面的评论中提到的, $compile 被删除了,但 Vue.compile 是可用的在某些构建中.除非在几种情况下,否则按照您的意愿使用下面的内容.

Vue.component('child', {//假装我做了一些有用的事情模板:'<span>--><slot></slot><--</span>'})Vue.component('父', {数据() {返回 {输入:'lorem',文本:'<div><p>Lorem ipsum dolor sat amet.</p><p><i>Lorem ipsum!</i></p></div>'}},模板:`

搜索:<input type='text' v-model="input"><br><小时><div><component :is="output"></component></div></div>`,计算:{输出() {如果(!this.input)返回 Vue.compile(this.text)/* 这是错误的方法;我用什么代替它?*/var out = this.text;如果(this.input){this.input = this.input.replace(/[^a-zA-Z\s]/g,'');var regex = new RegExp(this.input, "gi");out = out.replace(regex, '<child><b>' + this.input + '</b></child>');out = Vue.compile(out)}返回;}}});新的 Vue({el: '#app'})

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script><div id="应用程序"><父母></父母>

您提到您正在使用 webpack 构建,我相信该构建的默认值是 Vue 没有编译器,因此您需要修改它以使用不同的构建.

我添加了一个动态component来接受编译输出的结果.

示例 text 不是有效模板,因为它有多个根.我添加了一个包装 div 以使其成为有效模板.

注意:如果搜索词与 text 中的任何 HTML 标记的全部或部分匹配,这将失败.例如,如果您输入i"、di"或p",结果将不是您所期望的,并且某些组合会在编译时引发错误.

I'd like to insert new vuejs components on the fly, at arbitrary points within a block of not-necessarily-predefined HTML.

Here's a slightly contrived example that demonstrates the sort of thing I'm trying to do:

Vue.component('child', {
  // pretend I do something useful
  template: '<span>--&gt;<slot></slot>&lt;--</span>'
})

Vue.component('parent', {
  data() {
    return {
      input: 'lorem',
      text: '<p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p>'
    }
  },
  template: `<div>
      Search: <input type='text' v-model="input"><br>
      <hr>
      This inserts the child component but doesn't render it 
      or the HTML:
      <div>{{output}}</div>
      <hr>
      This renders the HTML but of course strips out the child component:
      <div v-html="output"></div>
      <hr>
      (This is the child component, just to show that it's usable here: 
      <child>hello</child>)
      <hr>
      This is the goal: it renders both the input html 
      and the inserted child components:
      TODO ¯\_(ツ)_/¯
    </div>`,
  computed: {
    output() {
      /* This is the wrong approach; what do I replace it with? */
      var out = this.text;
      if (this.input) {
        this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
        var regex = new RegExp(this.input, "gi");
        out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
      }
      return out;
    }
  }
});

new Vue({
  el: '#app'
})

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
  <parent></parent>
</div>

In the above snippet, assume data.text is sanitized HTML. <child> is some sub-component that does something useful, which I want to wrap around chunks of data.text that aren't known ahead of time. (input is just for demo here. This MCVE doesn't really resemble the code I'm building, it's just an example that shows the sort of situation I'm stuck on.)

So: how would I change either the output function or the parent component's template, such that both the HTML from input and the inserted <child> templates are rendered properly?

What I've tried

  • In Vue 1, the answer to this would be a straightforward $compile. I'm using vuejs2 which removed $compile (out of justifiable concern that it made it too easy to naively introduce XSS vulnerabilities.)

  • v-html sanitizes what you feed it, which strips the child component out. Obviously this is not the way to do this. (That page suggests using partials instead, but I'm not sure how that could be applied to this situation; in any case partials have also been removed from vue2.)

  • I've tried passing the results of output() into another component which would then use it as its template. This seems like a promising approach, but I can't figure out how to change that secondary component's template. template only accepts a string, not a function like many of the other component properties, so I can't pass the template html in, say, a prop. Something like rewriting this.template inside beforeMount() or bind() would have been nice, but no joy there either. Is there some other way to replace a component's template string before it's mounted?

  • Unlike template, I can pass data to a component's render() function... but then I'm still stuck having to parse that html string into nested createElement functions. Which is exactly what Vue is doing internally in the first place; is there some way to hook into that here short of reinventing it myself?

Vue.component('foo', {
  props: ['myInput'],
  render(createElement) {
    console.log(this.myInput); // this works...
    // ...but how to parse the html in this.myInput into a usable render function?
    // return createElement('div', this.myInput);
  },
})

  • I wasn't able to cheat my around this with inline-template, either: <foo inline-template>{{$parent.output}}</foo> does exactly the same thing as a plain old {{output}}. In retrospect that should have been obvious, but it was worth a shot.

  • Maybe constructing an async component on the fly is the answer? This could clearly generate a component with an arbitrary template, but how would I reasonably call that from the parent component, and feed output to the constructor? (It would need to be reusable with different input, with multiple instances potentially visible simultaneously; no globals or singletons.)

  • I've even considered ridiculous stuff like having output() split the input into an array at the points where it would have inserted <child>, and then doing something like this in the main template:

  ...
  <template v-for="chunk in output">
      <span v-html="chunk"></span>
      <child>...</child>
  </template>
  ....

That would be doable, if laborious -- I'd have to split out what goes in the child's slot into a separate array too and get it by index during the v-for, but that could be done... if input were plain text instead of HTML. In splitting HTML I'll often wind up with unbalanced tags in each chunk, which can mess up the formatting when v-html rebalances it for me. And anyway this whole strategy feels like a bad hack; there must be a better way.

  • Maybe I just drop the whole input into a v-html and then (somehow) insert the child components at the proper positions through after-the-fact DOM manipulation? I haven't explored this option too deeply because it, too, feels like a hack, and the reverse of the data-driven strategy, but maybe it's a way to go if all else fails?

A couple of pre-emptive disclaimers

  • I'm very well aware of the XSS risks involved in $compile-like operations. Please be assured that none of what I'm doing involves unsanitized user input in any way; the user isn't inserting arbitrary component code, instead a component needs to insert child components at user-defined positions.
  • I'm reasonably confident that this is not an XY problem, that I really do need to insert components on the fly. (I hope it's obvious from the number of failed attempts and blind alleys I've run down that I've put more than a little thought into this one!) That said, if there's a different approach that leads to similar results, I'm all ears. The salient point is that I know which component I need to add, but I can't know ahead of time where to add it; that decision happens at run time.
  • If it's relevant, in real life I'm using the single-file component structure from vue-cli webpack template, not Vue.component() as in the samples above. Answers that don't stray too far from that structure are preferred, though anything that works will work.

Progress!

@BertEvans points out in comments that Vue.compile() is a thing that exists, which is an I-can't-believe-I-missed-that if ever there was one.

But I'm still having trouble using it without resorting to global variables as in that documentation. This renders, but hardcodes the template in a global:

var precompiled = Vue.compile('<span><child>test</child></span>');
Vue.component('test', {
  render: precompiled.render,
  staticRenderFns: precompiled.staticRenderFns
});

But various attempts to rejigger that into something that can accept an input property have been unsuccessful (the following for example throws "Error in render function: ReferenceError: _c is not defined", I assume because the staticRenderFns aren't ready to go when render needs them?

Vue.component('test', {
  props: ['input'],
  render() { return Vue.compile(this.input).render()},
  staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}
});

(It's not because there are two separate compile()s -- doing the precompile inside beforeMount() and then returning its render and staticRenderFns throws the same error.)

This really feels like it's on the right track but I'm just stuck on a dumb syntax error or the like...

解决方案

As mentioned in the my comment above, $compile was removed, but Vue.compile is available in certain builds. Using that below works as I believe you intend except in a couple cases.

Vue.component('child', {
  // pretend I do something useful
  template: '<span>--&gt;<slot></slot>&lt;--</span>'
})

Vue.component('parent', {
  data() {
    return {
      input: 'lorem',
      text: '<div><p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p></div>'
    }
  },
  template: `<div>
      Search: <input type='text' v-model="input"><br>
      <hr>
      <div><component :is="output"></component></div>
    </div>`,
  computed: {
    output() {
      if (!this.input)
         return Vue.compile(this.text)
      /* This is the wrong approach; what do I replace it with? */
      var out = this.text;
      if (this.input) {
        this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
        var regex = new RegExp(this.input, "gi");
        out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
        out = Vue.compile(out)
      }
      return out;
    }
  }
});

new Vue({
  el: '#app'
})

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
  <parent></parent>
</div>

You mentioned you are building with webpack and I believe the default for that build is Vue without the compiler, so you would need to modify it to use a different build.

I added a dynamic component to accept the results of the compiled output.

The sample text is not a valid template because it has more than one root. I added a wrapping div to make it a valid template.

One note: this will fail if the search term matches all or part of any of the HTML tags in the text. For example, if you enter "i", or "di" or "p" the results will not be what you expect and certain combinations will throw an error on compilation.

这篇关于在 vuejs2 数据中动态插入子组件(不使用 $compile 或滥用 v-html)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆