何为优雅
面试时,我常常问候选人,你在封装组件时,会考虑哪些问题。我最希望听到答案里包括“从调用者的角度考虑,尽可能方便调用者”,其实这一句话括展开来也就是高内聚、低耦合那一套思想。 我认为,我们日常开发中封装组件,与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>
- 上面的用法有点啰嗦,调用者需要在template中插入一个标签,其实这个标签插在哪里都可以。
- 调用都需要为此增加一个变量
uploaderVisible
来控制组件显示和隐藏 - 当需要关闭弹框时,需要通知父组件,让它把
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>