你都使用Vue了,为什么还要用原生JS新建元素呢?

1,236 阅读4分钟

问题

思维导图组件尝鲜——OpenTiny的MindMap组件这篇文章里,我们使用了第三方组件库绘制了一个思维导图,现在我想在某些节点后面加一个按钮。如下图所示:

截屏2024-06-17 23.22.48.png

这个思维导图都是封装在组件中的,我们要想在某些节点后面加一个按钮,要么去修改组件的源码,使其支持这个功能,要么自己写一些额外的逻辑去实现这个功能。显然这种比较复杂的业务逻辑,我们改源码基本上是改不动的,因此只能自己写额外的逻辑去实现。

困境在于:我们追加的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);
});

截屏2024-06-17 23.22.48.png

通过上面的代码我们看到,回归原生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的所有内容,因此我们在按钮组件内部把内容也回显出来了。

截屏2024-06-19 00.25.29.png

优缺点对比

显然,原生JS的写法会更冗长,没有Vue简洁;

原生JS可以允许根节点有其他元素,Vue的方法,必须要覆盖掉根节点内原本的内容;

原生JS的逻辑可以复用于非Vue项目,如jQuery项目、React项目。

Demo地址

Demo地址:github.com/beat-the-bu…

效果预览地址:beat-the-buzzer.github.io/vue3-demo/#…