Vue 动态组件的使用场景| 8月更文挑战

374 阅读3分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

前言

最近写 Vue 遇到个需求,需要对原有的 Modal 进行改造以支持富文本内容。原 Modal 组件大致是:

<template>
    <div>
        <div>{title}</div>
        <div>{desc}</div>
        <button>确认</button>
    </div>
</template>
<script>
export default {
    props: ['title', 'desc'],
    
}
</script>

使用这个 Modal 组件:

<template>
    <Modal title="弹窗" desc="确认要关闭吗?" />
</template>

现在的需求要求 desc 的部分是动态的富文本, 形如:

this.desc = `确认要关闭吗?点击阅读<span style="color: blue;">用户协议</span>~`

并且,点击 <span>用户协议</span> 需要跳转到用户协议页面。

方案一

由于需求要求渲染富文本,最先想到的方案肯定是通过 v-html 来渲染富文本,至于点击事件,就需要自定义一个唯一的id或类名,然后添加原生点击事件,代码形如:

<!-- Modal 组件 -->

<template>
    <div>
        <div>{title}</div>
-        <div>{desc}</div>
+        <div v-html="desc"></div>
        <button>确认</button>
    </div>
</template>
<script>
export default {
    props: ['title', 'desc'],
    
}
</script>

修改后,Modal 已经支持了渲染富文本的元素,但是还没有处理点击事件的部分。 现在接着添加点击事件的逻辑。

this.desc = `确认要关闭吗?点击阅读<span id="user-rule" style="color: blue;">用户协议</span>~`
this.$nextTick(() => {
    document.querySelector('#user-rule').addEventListener('click', () => {
        // do something
    })
})

如上所见,确实实现解决这个需求,但是其中有些问题:

  • 怎么保证选择器不重复
  • 点击事件封装在 $nextTick 里,但是有时 this.desc 设置完毕并不能保证 span id="user-rule" 的节点一定添加到 dom 中。(比如使用 Modal 组件时并不是父子关系,而是嵌套了 n 层,且每层里有些延迟的处理)

对于第一个问题,比较容易解决,设置唯一选择器前先查看是否存在,存在就再创建一个新的。 但是对于第二个问题,就比较难解决了。

下面我们看看还有没有其他的方案。

方案二

因为使用的是 Vue ,所以是不是可以用 Vue 的思路去解决这个问题呢?

这里我们先写一段伪代码看一下具体的思路:

  • 首先修改 Modal 组件中渲染 desc 的部分:
<!-- Modal 组件 -->

<template>
    <div>
        <div>{title}</div>
-        <div>{desc}</div>
+        <component v-if="desc" :is="desc" />
        <button>确认</button>
    </div>
</template>
<script>
export default {
    props: ['title', 'desc'],
    
}
</script>

可以看到,现在 Modal 组件已经支持了 desc 可以传入组件类型。

  • 然后在渲染富文本的部分,使用 Vue 组件。

this.desc = {
    methods: {
        onUserRuleClick() {
            // do something
        }
    },
    render: () => <div>
        确认要关闭吗?点击阅读
        <span onClick={this.onUserRuleClick} style="color: blue;">
        用户协议</span>~
    </div>,
}

至此可以看到,我们创建了一个 Vue 组件并赋给了 desc,并且点击事件相关的处理也都是 Vue 组件的写法。

我们不需要再去处理:

  • span 标签选择器的唯一性
  • desc 相关的点击事件

Tips

方案二的代码可能并不一定在所有环境都能跑起来,因为在 render 函数的部分直接使用了 jsx 的语法,对于这个问题,有两个解决方案:

  • 修改 render 中 jsx 的写法
render(h) {
    return h(
        'div', {}, [
            '确认要关闭吗?点击阅读',
            h('span', {
                style: 'color: blue;',
                on: {
                    click: this.onUserRuleClick,
                }
            }, '用户协议')
            '~',
        ]
    )
}
  • 安装 @vue/babel-preset-jsx 插件

总结

可以看到,方案二使用了 Vue 自带的组件渲染能力,与方案一对比,dom 渲染后点击事件就已经被添加完毕,所以不需要开发者去考虑何时绑定事件,并且当渲染的富文本有很多的事件需要绑定或者不知道富文本何时被添加到 dom 中,方案一无法满足需求。

欢迎大家交流分享!