Vue 实现 dialog 组件以及碰到的问题

5,778 阅读1分钟

实现

<template>
  <div>
    <div
      v-show="isShow"
      ref="moveNodeRef"
      :class="curClass"
      :style="style"
    >
      <slot></slot>
    </div>
    <div
      ref="maskRef"
      :class="maskClass"
      :style="maskStyle"
      v-show="showMask&&isShow"
    ></div>
  </div>
</template>
<script>
  import { getMaxZIndex } from '../../../utils/dom';
  import { mixinData } from '../../../mixin';
  export default {
    name: 'YsDialog',
    mixins: [mixinData],
    data() {
      return {
        style: {
          'z-index': 10
        },
        maskStyle: {
          'z-index': 5
        },
        moveNode: null
      };
    },
    computed: {
      curClass() {
        let list = [this.classPrefix + 'kd-dialog-outer', this.customClass];
        return list;
      },
      maskClass() {
        let list = [this.classPrefix + 'kd-mask', this.customMaskClass];
        return list;
      }
    },
    props: {
      // 是否添加到body
      appendToBody: {
        type: Boolean,
        default: false
      },
      customClass: {
        type: String,
        default: ''
      },
      customMaskClass: {
        type: String,
        default: ''
      },
      // parentNode
      parentNode: {
        type: HTMLDivElement,
        default: null
      },
      showMask: {
        type: Boolean,
        default: false
      },
      styleData: {
        type: Object,
        default: () => { return {}; }
      },
      isShow: {
        type: Boolean,
        default: false
      }
    },
    mounted() {
      this.moveNode = this.$refs.moveNodeRef;
      this.maskNode = this.$refs.maskRef;
      this.updateDom();
    },
    destroyed() {
      if (this.moveNode && this.moveNode.parentNode) {
        this.moveNode.parentNode.removeChild(this.moveNode);
      }
      if (this.maskNode && this.maskNode.parentNode) {
        this.maskNode.parentNode.removeChild(this.maskNode);
      }
    },
    watch: {
      parentNode: {
        handler(val) {
          this.updateDom();
        }
      },
      appendToBody: {
        handler() {
          this.updateDom();
        }
      },
      styleData: {
        immediate: true,
        handler(val) {
          if (!val) {
            return;
          }
          this.style = {
            ...this.style,
            ...val
          };
        }
      },
      isShow: {
        handler() {
          this.updateStyle();
        }
      }
    },
    methods: {
      updateStyle() {
        let moveNodeRef = this.$refs.moveNodeRef;
        let maskRef = this.$refs.maskRef;
        if (maskRef) {
          let maxZIndex = getMaxZIndex(maskRef.parentNode);
          this.maskStyle = {
            'z-index': maxZIndex + 5
          };
        }
        if (moveNodeRef) {
          let maxZIndex = getMaxZIndex(moveNodeRef.parentNode);
          this.style = {
            ...this.styleData,
            'z-index': maxZIndex + 10
          };
        }
      },
      updateDom() {
        this.$nextTick(() => {
          let moveNodeRef = this.$refs.moveNodeRef;
          let maskRef = this.$refs.maskRef;

          if (maskRef) {
            if (this.parentNode) {
              this.parentNode.appendChild(maskRef);
            } else if (this.appendToBody) {
              document.body.appendChild(maskRef);
            }
          }
          if (moveNodeRef) {
            if (this.parentNode) {
              this.parentNode.appendChild(moveNodeRef);
            } else if (this.appendToBody) {
              document.body.appendChild(moveNodeRef);
            }
          }
          this.updateStyle();
        });
      }
    }
  };

</script>


问题

一开始,使用的是element-ui的dialog 但是在使用v-if 的时候会出现报错。 错误的代码也放上来吧。

<template>
	<div>
		<!-- <test-com v-if='a' :key="'a'" :parentNode="parentNode">123</test-com>
		<div>ad</div>
		<test-com v-if='b' :key="'b'" :parentNode="parentNode">456</test-com>
		<div>ck</div> -->
		<el-dialog v-if="a" :visible="true" :key="'cf'" :append-to-body="true">
			<span>这是一段信息</span>
		</el-dialog>
		<el-dialog v-if="b" :visible="true" :key="'cs'" :append-to-body="true">
			<span>222222</span>
		</el-dialog>
		<div @click="cc">dianji a </div>
	</div>
</template>
<script>
	import TestCom from './view/test.vue';
	export default {
		name: 'app',
		components: {
			TestCom
		},
		data() {
			return {
				a: false,
				b: false
			};
		},
		created() {},
		mounted() {
			this.parentNode = document.getElementById('bbb');
			window.cc = this.cc;
		},
		destroyed() {

		},
		methods: {
			cc() {
				if (this.a) {
					this.a = false;
					this.b = true;

				} else {
					this.a = true;
					this.b = false;
				}
			}
		}
	};

</script>
<style lang='stylus' scoped>


</style>

当不加:key 的时候,如果两个弹出层不是同步显示出来的话,el-dialog的mounted只会触发一次。后面的不是新生成,而是更新之前的el-dialog。因此需要加上:key 将两个区别开来。但是在连续去切换的时候会出现报错。

// 报错信息
DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to 
be inserted is not a child of this node.

1547108057(1).jpg
从字面意思大概是insertBefore 选取node不是要插入的parentNode的子元素。查看element 的dialog的代码

visible(val) {
    if (val) {
          this.closed = false;
          this.$emit('open');
          this.$el.addEventListener('scroll', this.updatePopper);
          this.$nextTick(() => {
            this.$refs.dialog.scrollTop = 0;
          });
          if (this.appendToBody) {
            document.body.appendChild(this.$el);
          }
        } else {
          this.$el.removeEventListener('scroll', this.updatePopper);
          if (!this.closed) this.$emit('close');
        }
      }

他是使用的this.$el 将整个dom元素都插入body中。通过跟踪代码可以发现vue在插入第二个元素的时候,第一个dialog还未销毁,使用他去进行定位插入。而第一个dialog的dom被插入到了body下面,但是前面使用的parentNode还是vue结构树中的父节点,因此第一个dialog 并不在parentNode 下面,因此就出现了上面的报错信息。 这里是vue的insertbefore

1547108548(1).jpg

// 这里的referenceNode是第一个dialog的dom元素,已经被插入到了body下面了。

1547108533(1).jpg
而parentNode 却还是vue组件树结构的父元素,
1547108829(1).jpg
因此会爆出

[Vue warn]: Error in nextTick: "NotFoundError: Failed to execute 'insertBefore' on 'Node': 
The node before which the new node is to be inserted is not a child of this node."
vue.esm.js:1741 DOMException: Failed to execute 'insertBefore' on 'Node': 
The node before which the new node is to be inserted is not a child of this node.

这个错误。

解决

取巧的解决方案,不将整个dom全部插入body中,而是在外层再包一层div,将真正要插入的作为div的子元素。这样虽然modeNodeRef会被插入到body中,但是vue在使用inserBefore的时候只会去使用外层的div去定位。

<template>
  <div>
  <!-->这里才是真正要插入的dom<-->
    <div ref="moveNodeRef" class="dialog-outter" :style="style">
      <div class="mask"></div>
      <slot></slot>
    </div>
  </div>
</template>

注意点:需要在destoryed中自己去手动删除插入的dom

destroyed() {
    if (this.moveNode && this.moveNode.parentNode) {
      this.moveNode.parentNode.removeChild(this.moveNode);
    }
  },
  watch: {
    parentNode: {
      immediate: true,
      handler(val) {
        this.updateDom();
      }
    },
    appendToBody: {
      immediate: true,
      handler() {
        this.updateDom();
      }
    }
  },
  methods: {
    updateDom() {
      this.$nextTick(() => {
        let moveNodeRef = this.$refs.moveNodeRef;
        if (moveNodeRef) {
          if (this.parentNode) {
            this.parentNode.appendChild(moveNodeRef);
          } else if (this.appendToBody) {
            document.body.appendChild(moveNodeRef);
          }
        }
      });
    }
  }