太一生水,水反辅太一,是以成天; 天反辅太一,是以成地。 天地复相辅,是以成神明。
使用 Vue.extend 拓展组件
在 Vue 2 时代可以用 Vue.extend 来拓展一个组件,实现使用纯 js 来完成一些功能而不需要关心 Dom ,比如实现一个简单的 Toast。
到 Vue 3 时代,官网上明确表示,extends 是为选项式 API 设计的,不建议用于组合式 API。
这里是很多年前的一个解决方案,mm-toast ,通过 Vue.extend 来实现的 Toast。首先创建一个 mm-toast.vue 组件,然后通过 Vue.extend 来拓展一个组件,然后通过 Vue.prototype 来挂载到全局上。可以直接用 $msg 来在页面上弹出消息。
import TempToast from './mm-toast.vue'
let instance, showToast = false, time; // 存储toast显示状态
const mmToast = {
install(Vue, options = {}) {
let opt = TempToast.data();//获取组件中的默认配置
Object.assign(opt, options);//合并配置
Vue.prototype.$msg = (message, position) => {
if (showToast) {
clearTimeout(time);
instance.vm.visible = showToast = false;
document.body.removeChild(instance.vm.$el)
//return;// 如果toast还在,则不再执行
}
if (message) {
opt.message = message;//如果有传message,则使用所传的message
}
if (position) {
opt.position = position;//如果有传type,则使用所传的type
}
let TempToastConstructor = Vue.extend(TempToast);
instance = new TempToastConstructor({
data: opt
});
instance.vm = instance.$mount();
document.body.appendChild(instance.vm.$el);
instance.vm.visible = showToast = true;
time = setTimeout(function () {
instance.vm.visible = showToast = false;
document.body.removeChild(instance.vm.$el)
}, opt.duration)
}
}
};
export default mmToast
创建 VNode 虚拟节点
搜到一篇 Vue 插入组件到 Dom 的最佳实践 ,提供了两种思路:
- 使用
h()/createVNode()函数,将引入的组件转为虚拟节点,然后用render()挂在到 Dom 上 - 使用
createApp()创建一个应用实例,使用app.mount()挂载到 Dom 节点上
自己结合实际场景应用了一下,假设我需要创建很多迷你对话框,每一个都有关闭按钮,下面这样基本能实现需求。
首先定义一个容器,用于挂载组件,然后创建一个ref 来保存组件名称列表,然后通过 h() 函数创建一个虚拟节点,通过 render() 函数挂载到 mdContainer 上。每一个 MiniDialog 都有一个 onClose 事件,用于移除自身,然后重新渲染。
import MiniDialog from './components/MiniDialog.vue';
const mdContainer = ref()
const CompNameList = ref < string[] > ([])
const openMiniDialog = (val: string) => {
CompNameList.value.push(val)
const newList = h('div',
CompNameList.value.map(val => h(MiniDialog, {
data: { q: 23, val },
key: new Date().getTime(),
onClose: () => {
CompNameList.value.splice(CompNameList.value.indexOf(val), 1)
render(newList, mdContainer.value!)
}
}))
)
render(newList, mdContainer.value!)
}
// 调用方法
openMiniDialog(val)
由于 Vnodes 必须唯一 这里需要一个函数返回新的组件 VNode 列表,而且外面容器 mdContainer 也需要一个 div 来包裹,造成不必要的嵌套;然后关闭的时候需要再次渲染,会造成方法的重复引用,看起来不够优雅。
更加 Vue 的方式
目前需要的是动态渲染组件列表,搜索了一下,看到 这一个 答案,本质上还是用 <Component /> 渲染动态组件,然后直接循环一个列表,这样就可以实现动态插入多个弹窗了。
<Component :is="item.comp" :ckey="item.key" :key="item.key" :data="item.data" @close="closeMiniDialog" v-for="item in MdList"></Component>
const MdList = ref<{
comp:Component
key: string,
data: any
}[]>([])
const openMiniDialog = (data: any) => {
MdList.value.push({
comp: markRaw(MiniDialog),
key: new Date().getTime() + '',
data: data,
})
}
const closeMiniDialog = (key: string) => {
const index = MdList.value.findIndex(x => x.key === key)
MdList.value.splice(index, 1)
}
这里最开始没有加 :key ,会造成 关闭 A 弹窗,B 弹窗跑到了 A 弹窗的位置。排除了半天,最后通过 :key 来解决。