在 vuejs2 数据中动态插入子组件(无需 $compile 或滥用 v-html)

Posted

技术标签:

【中文标题】在 vuejs2 数据中动态插入子组件(无需 $compile 或滥用 v-html)【英文标题】:Dynamically insert child components inside vuejs2 data (without $compile or abusing v-html) 【发布时间】:2017-11-06 06:43:07 【问题描述】:

我想动态插入新的 vuejs 组件,在一个不必要的预定义 html 块中的任意点。

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

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>

在上面的 sn-p 中,假设 data.text 是经过净化的 HTML。 &lt;child&gt; 是一些有用的子组件,我想将其包裹在事先不知道的 data.text 块中。 (input 仅用于演示。此 MCVE 与我正在构建的代码并不真正相似,它只是一个示例,显示了我遇到的那种情况。)

那么:我将如何更改output 函数或父组件的模板,以使input 中的HTML 和插入的&lt;child&gt; 模板都正确呈现?

我尝试过的

在 Vue 1 中,这个问题的答案是简单的$compile。我正在使用 vuejs2,它删除了 $compile(出于合理的担忧,因为它很容易天真地引入 XSS 漏洞。)

v-html 会清理你提供给它的内容,从而将子组件剥离出来。显然这是not the way to do this。 (该页面建议改用 partials,但我不确定如何将其应用于这种情况;无论如何,partials 也已从 vue2 中删除。)

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

template 不同,我可以将数据传递给组件的render() 函数...但是我仍然不得不将该html 字符串解析为嵌套的createElement 函数。这正是Vue is doing internally in the first place;除了我自己重新发明之外,还有什么方法可以解决这个问题吗?

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);
  ,
)

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

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

我什至考虑过一些荒谬的事情,比如让output() 将输入拆分为一个数组,在它应该插入&lt;child&gt; 的点处,然后在主模板中执行类似的操作:

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

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

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

一些先发制人的免责声明

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

进展!

@BertEvans 在 cmets 中指出,Vue.compile() 是一个存在的东西,如果曾经有的话,这是一个我无法相信我错过的东西。

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

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

但各种尝试将其重新调整为可以接受输入属性的东西都没有成功(以下示例抛出“渲染函数中的错误:ReferenceError:_c 未定义”,我认为是因为staticRenderFns 不是当render 需要它们时准备好了吗?

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

(这不是因为有两个单独的compile()s -- 在beforeMount() 中进行预编译,然后返回它的渲染和 staticRenderFns 会引发相同的错误。)

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

【问题讨论】:

$compile 已删除,但 Vue.compile 可能可用。 【参考方案1】:

正如我在上面的评论中提到的,$compile 已被删除,但 Vue.compile 在某些版本中可用。我相信您可以按照以下方式使用,但在少数情况下除外。

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>

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

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

样本text 不是一个有效的模板,因为它有多个根。我添加了一个包装 div 以使其成为有效的模板。

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

【讨论】:

完全可以,谢谢!我永远不会自己想出&lt;component :is...&gt; 构造,它比我尝试的子组件操作要简单得多。我对 'd' 和 'i' 的失败很着迷,我必须在那里进行实验,但这至少让我走上了正确的轨道...... @DanielBeck 我想通了。失败是搜索文本与文本中的全部或部分 HTML 标记匹配。 啊啊啊当然,是的;这很容易解决。非常感谢!【参考方案2】:

我将其发布为对 Bert Evans 答案的补充,以便希望使用 .vue 文件而不是 Vue.component() 的 vue-cli webpack 用户受益。 (也就是说,我主要是发布这个,所以当我不可避免地忘记它时,我将能够找到这些信息......)

获得正确的 Vue 构建

在 vue-cli 2(也可能是 1?)中,为确保 Vue.compile 在分发版本中可用,请确认 webpack.base.conf.js 包含此行:

'vue$': 'vue/dist/vue.esm.js' // or vue/dist/vue.common.js for webpack1

而不是'vue/dist/vue.runtime.esm.js'.(如果您在运行vue init webpack 时接受默认值,您将已经拥有完整的独立构建。“webpack-simple”模板也设置了完整的独立构建。)

Vue-cli 3 的工作方式有所不同,并且 not 默认情况下有 Vue.compile 可用;在这里您需要将runtimeCompiler 规则添加到vue.config.js

module.exports = 
    /* (other config here) */
    runtimeCompiler: true
;

组件

“子”组件可以是一个普通的 .vue 文件,没什么特别的。

“父”组件的基本版本是:

<template>
    <component :is="output"></component>
</template>
<script>
import Vue from 'vue';
import Child from './Child'; // normal .vue component import

export default 
  name: 'Parent',
  computed: 
    output() 
      var input = "<span>Arbitrary single-root HTML string that depends on <child></child>.  This can come from anywhere; don't use unsanitized user input though...</span>";
      var ret = Vue.compile(input);
      ret.components =  Child ;  // add any other necessary properties similarly
      ret.methods =  /* ... */   // like so
      return ret;
    
  
;
</script>

(此版本与非 webpack 版本的唯一显着区别是导入 child,然后在返回之前将组件依赖项声明为 ret.components: Child。)

【讨论】:

我无法在子组件的插槽中运行任何代码。 IE。每次我处理一个方法时,我都会得到一个“未定义属性或方法“myfunction”” @MeLight 可能它正在父组件上寻找那些方法。 Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope. @DanielBeck 无法在父子节点上调用方法。如要点所示:gist.github.com/yuri-wisestamp/e9ff386c93483a04365cd628b103e873 啊,你没有使用插槽。这可能是它自己的单独问题的要点,但看起来您需要将这些方法附加到子组件(与将 helloBar 附加到 ret.components 的方式相同,在从输出返回之前定义 ret.methods = numUp() ..., yell() ...( )

以上是关于在 vuejs2 数据中动态插入子组件(无需 $compile 或滥用 v-html)的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 vuejs2 中我的默认道具没有传递给我的子组件?

vuejs2:我怎样才能摧毁一个观察者?

Vuejs 2将道具对象传递给子组件并检索

vuejs2在父子之间传递数据正在擦除子值

vuejs2:如何将 vuex 存储传递给 vue-router 组件

vue js 2:在挂载函数中访问道具