在 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>--><slot></slot><--</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。 <child>
是一些有用的子组件,我想将其包裹在事先不知道的 data.text
块中。 (input
仅用于演示。此 MCVE 与我正在构建的代码并不真正相似,它只是一个示例,显示了我遇到的那种情况。)
那么:我将如何更改output
函数或父组件的模板,以使input
中的HTML 和插入的<child>
模板都正确呈现?
我尝试过的
在 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);
,
)
我也无法使用内联模板来欺骗我:<foo inline-template>$parent.output</foo>
与普通的旧 output
完全相同。回想起来,这应该是显而易见的,但值得一试。
也许即时构建async component 就是答案?这可以清楚地生成一个带有任意模板的组件,但是我如何合理地从父组件调用它,并将output
提供给构造函数? (它需要可重复使用不同的输入,多个实例可能同时可见;没有全局变量或单例。)
我什至考虑过一些荒谬的事情,比如让output()
将输入拆分为一个数组,在它应该插入<child>
的点处,然后在主模板中执行类似的操作:
...
<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>--><slot></slot><--</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”,结果将不是您所期望的,并且某些组合会在编译时引发错误。
【讨论】:
完全可以,谢谢!我永远不会自己想出<component :is...>
构造,它比我尝试的子组件操作要简单得多。我对 '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)的主要内容,如果未能解决你的问题,请参考以下文章