问题
在思维导图组件尝鲜——OpenTiny的MindMap组件这篇文章里,我们使用了第三方组件库绘制了一个思维导图,现在我想在某些节点后面加一个按钮。如下图所示:
这个思维导图都是封装在组件中的,我们要想在某些节点后面加一个按钮,要么去修改组件的源码,使其支持这个功能,要么自己写一些额外的逻辑去实现这个功能。显然这种比较复杂的业务逻辑,我们改源码基本上是改不动的,因此只能自己写额外的逻辑去实现。
困境在于:我们追加的DOM元素,没有办法按照正常的Vue的写法去实现,无法在template里写DOM结构。
我想到的第一个方案,就是最笨的方法:用原生JS或者jQuery,手动修改指定节点的DOM结构。
原生JS方案
接口返回的数据里,需要加上按钮的节点,会给一个状态值。我们把这些节点先找出来,存在一个数组中。
findEle是组件库提供的方法,可以通过节点id拿到对应的节点DOM对象;
afterImport是组件库中提供的生命周期函数,当数据导入成功之后,再去渲染删除按钮;
用createElement方法创建按钮,并且使用事件委托的方式去绑定点击事件。
完整代码如下:
const onAfterImport = () => {
for (let i = 0; i < deletedArr.length; i++) {
createOptBtn(deletedArr[i]);
}
};
// 根据id 新增dom结构
function createOptBtn({ id }) {
let dom = D.value.findEle(id, D.value);
let btn = document.querySelector('btn-' + id);
// 如果已经有这个节点,就直接修改节点的内容
if (btn) {
btn.setAttribute('class', 'btn-del');
btn.innerText = '删除'
} else {
// 否则,就新增一个节点
btn = document.createElement('div');
btn.setAttribute('id', 'btn-' + id);
btn.setAttribute('class', 'btn-del');
btn.innerText = '删除'
dom.appendChild(btn);
}
}
// 按钮绑定点击事件的处理方式
function bindBtnClick(e) {
e.preventDefault()
if (e.target.className == 'btn-del') {
let id = e.target.getAttribute('id')
doDel(id)
}
}
onMounted(() => {
document.addEventListener('click', bindBtnClick);
});
onUnmounted(() => {
document.removeEventListener('click', bindBtnClick);
});
通过上面的代码我们看到,回归原生JS之后,我们又要写一堆逻辑。实际上在Vue的环境下,我们也有Vue的写法,去生成并渲染DOM结构。
熟悉又陌生的createApp方法
一般我们都会在main.js中用这个方法:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
createApp方法一般只会使用一次。在这个业务下,我们可以使用这个方法来给指定的DOM下挂载一个Vue组件,因此,我们可以用这个方法来在指定位置生成一个按钮。
h函数——vue3中的createElement
我们的组件需要接受节点的数据,以及点击按钮成功之后需要通知父组件。因此我们需要用Vue的h函数对组件进行处理。
export declare function h<K extends keyof HTMLElementTagNameMap>(type: K, children?: RawChildren): VNode;
export declare function h<K extends keyof HTMLElementTagNameMap>(type: K, props?: (RawProps & HTMLElementEventHandler) | null, children?: RawChildren | RawSlots): VNode;
export declare function h(type: string, children?: RawChildren): VNode;
export declare function h(type: string, props?: RawProps | null, children?: RawChildren | RawSlots): VNode;
export declare function h(type: typeof Text | typeof Comment, children?: string | number | boolean): VNode;
export declare function h(type: typeof Text | typeof Comment, props?: null, children?: string | number | boolean): VNode;
export declare function h(type: typeof Fragment, children?: VNodeArrayChildren): VNode;
export declare function h(type: typeof Fragment, props?: RawProps | null, children?: VNodeArrayChildren): VNode;
export declare function h(type: typeof Teleport, props: RawProps & TeleportProps, children: RawChildren | RawSlots): VNode;
export declare function h(type: typeof Suspense, children?: RawChildren): VNode;
export declare function h(type: typeof Suspense, props?: (RawProps & SuspenseProps) | null, children?: RawChildren | RawSlots): VNode;
export declare function h<P, E extends EmitsOptions = {}, S extends Record<string, any> = any>(type: FunctionalComponent<P, any, S, any>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | IfAny<S, RawSlots, S>): VNode;
export declare function h(type: Component, children?: RawChildren): VNode;
export declare function h<P>(type: ConcreteComponent | string, children?: RawChildren): VNode;
export declare function h<P>(type: ConcreteComponent<P> | string, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren): VNode;
export declare function h<P>(type: Component<P>, props?: (RawProps & P) | null, children?: RawChildren | RawSlots): VNode;
export declare function h<P>(type: ComponentOptions<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots): VNode;
export declare function h(type: Constructor, children?: RawChildren): VNode;
export declare function h<P>(type: Constructor<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots): VNode;
export declare function h(type: DefineComponent, children?: RawChildren): VNode;
export declare function h<P>(type: DefineComponent<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots): VNode;
export declare function h(type: string | Component, children?: RawChildren): VNode;
export declare function h<P>(type: string | Component<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots): VNode;
从上面的TS声明可以看出,h函数可以将Vue组件和DOM转换成VNode。Vue和一些Vue组件库经常会将参数设计成VNode类型,例如Vue的render函数,ant-design-vue等等。
let ids = []
const onAfterImport = () => {
ids = []
for (let j = 0; j < editArr.length; j++) {
createVueOptBtn(editArr[j]);
}
};
function createVueOptBtn(dataObj) {
let dom = D.value.findEle(dataObj.id, D.value)
const comp = h(OptBtn, {
ref: dataObj.id,
dataObj: dataObj,
onSuccess: handleSuccess // 不加on就是属性,加on就是事件
})
// 不能重复mount 这里只mount一次
if (!ids.includes(dataObj.id)) {
createApp(comp).mount(dom)
ids.push(dataObj.id)
}
}
<template>
<span class="text">
{{ dataObj.topic }}
<a-button class="btn" type="link" @click="doEdit">编辑</a-button>
</span>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import { Button } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const AButton = Button
const emit = defineEmits(['success'])
const props = defineProps({
dataObj: {
type: Object,
default: () => { }
}
})
function doEdit() {
createMessage.success('编辑按钮被点击了,打印一下传过来的属性' + JSON.stringify(props.dataObj))
// console.log('编辑按钮被点击了,打印一下传过来的属性', props.dataObj)
emit('success')
}
</script>
<style scoped lang="less">
.btn {
position: absolute;
right: -60px;
top: 50%;
transform: translateY(-50%);
pointer-events: auto;
}
</style>
注意上方的mount操作,会清除掉作为root的所有内容,因此我们在按钮组件内部把内容也回显出来了。
优缺点对比
显然,原生JS的写法会更冗长,没有Vue简洁;
原生JS可以允许根节点有其他元素,Vue的方法,必须要覆盖掉根节点内原本的内容;
原生JS的逻辑可以复用于非Vue项目,如jQuery项目、React项目。
Demo地址
Demo地址:github.com/beat-the-bu…