如何优雅地为你同事提供一个弹框组件?

305 阅读2分钟

何为优雅

面试时,我常常问候选人,你在封装组件时,会考虑哪些问题。我最希望听到答案里包括“从调用者的角度考虑,尽可能方便调用者”,其实这一句话括展开来也就是高内聚、低耦合那一套思想。 我认为,我们日常开发中封装组件,与Element UI这类通用型组件库开发,在意图有很大区别,其实可以不需要考虑那么多通用性扩展性,更重要的是方便性。

常规做法

如果现在有一个需求,需要你写一个弹框组件,你打算调用者如何使用?是不是下面这样

<template>
<main>
    <FilesUpload v-if="uploaderVisible" @success="handleSuccess"></FilesUpload>
</main>
</template>

<script setup>
import { ref } from 'vue'
import FilesUpload from '@/components/files-upload/FilesUpload.vue'
const uploaderVisible = ref(false)
</script>
  1. 上面的用法有点啰嗦,调用者需要在template中插入一个标签,其实这个标签插在哪里都可以。
  2. 调用都需要为此增加一个变量uploaderVisible来控制组件显示和隐藏
  3. 当需要关闭弹框时,需要通知父组件,让它把uploaderVisible置为false。(当然这也可以用v-model来避免)

使用函数式组件可以解决这个问题

对于调用者来说,像下面这样调用肯定是最方便的

<template>
<main>
    <div @click="upload"></div>
</main>
</template>

<script setup>
import { ref } from 'vue'
import {open as openUpload} from '@/components/files-upload/index.js'
function upload(){
    openUpload().then(url => {
        ... // 得到上传后的url,进行后续逻辑
    })
}
</script>

具体实现

下面关键地方已经加了注释

// @/components/files-upload/index.js

import { render, h } from 'vue'
import FilesUpload from './FilesUpload.vue'
let container = null
export function open() {
    // 1. 首先什么都不做,给调用者一个promise,
  let resolve = null
  const promise = new Promise((res) => {
    resolve = res
  })

  const vNode = h(FilesUpload, {
    onClose: () => {
      close()
      resolve(null)
    },
    onSuccess: (value) => { // 2. 当用户选择了文件,并上传成功了,兑现这个promise
      close()
      resolve(value)
    },
  })

  container = document.createElement('div') // 3. 创建一个div,用于挂载这个弹框
  document.body.appendChild(container)
  render(vNode, container) // 4.挂载
  document.body.style.overflow = 'hidden'

  return promise
}

export function close() {
  render(null, container) // 5. 卸载
  container.remove() // 6. 清理
  document.body.style.overflow = ''
}
// FilesUpload.vue

<template>
  <div class="uploader-box" v-show="visible">
    <div class="loading">
      <Circle :current-rate="progress" color="#1EB2A8" size="50px" :text="text"> </Circle>
    </div>
  </div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { Circle } from 'vant'
import { upload } from '@/api/biz'

const emit = defineEmits(['success'])
const progress = ref(0)
const visible = ref(false)
const text = computed(() => {
  return progress.value.toFixed(0) + '%'
})
function uploadVideo() {
  const fileInput = document.createElement('input')
  fileInput.type = 'file'
  fileInput.accept = 'image/*, video/*'
  fileInput.click()
  fileInput.addEventListener('change', () => {
    visible.value = true // 已选择文件,开始上传
    const file = fileInput.files[0]
    upload(file, (e) => {
      progress.value = (e.loaded / e.total) * 100
    }).then((res) => {
      if (file.type.startsWith('image')) {
        emit('success', {
          type: 'image',
          url: res.result
        })
      } else {
        emit('success', {
          type: 'video',
          url: res.result
        })
      }

      fileInput.remove()
    })
  })
}

onMounted(uploadVideo)
</script>