组件封装
为什么需要封装组件
显而易见,封装组件可以用于重用;统一的组件也方便管理与维护
那么一个页面如果有五个弹窗,功能各异、且与当前页面业务高度耦合,封装成组件后很难被其他页面重用。这时还要不要为了“重用”而把这些弹窗抽离出来?
对一个业务复杂的页面,把这些块拆成组件几乎是必然的。可见这时封装的目的并不主要是重用,而是为了降低单文件、单页面的复杂度——把一大团逻辑拆解,每块有名字、有边界,读起来和维护起来都更轻松
“理想化”的反面:一个看起来完整的组件
这里有一个组件,功能完整、逻辑清晰:展示用户,展示关注按钮,点击则进行关注
<script setup lang="ts">
import { useUserStore } from '@/store/user'
const props = defineProps<{
user: {
id: string
name: string
avatar: string
}
showFollow?: boolean
}>()
const userStore = useUserStore()
const isFollowing = computed(() =>
userStore.followingIds.includes(props.user.id)
)
async function handleFollow() {
await userStore.follow(props.user.id)
}
</script>
<template>
<div class="user-card">
<img :src="user.avatar">
<div>{{ user.name }}</div>
<button v-if="showFollow" @click="handleFollow">
{{ isFollowing ? '已关注' : '关注' }}
</button>
</div>
</template>
但它并不够好——问题不在组件是否简单易懂,而在边界:父组件从 props 的输入,并不是这个组件的全部“输入”;子组件产生的,也不只是 UI 和事件
- 输入不够明确。除了父组件传入的 props,它还依赖 useUserStore() 里的 followingIds。若只看 defineProps,无法得知完整输入
- 输出不清晰。点击后的结果写进了全局 Store,而不是通过返回值或事件让父组件显式处理。父组件无法通过props推断结果(为什么要推断结果,就好比debugger时,无法仅凭父组件传入的props判断子组件的结果)
- 职责不够单一。从 props 看,它只展示 user 与是否显示关注按钮;实际上“是否已关注”和“点击后的关注行为”都依赖 userStore 。如果修改关注的数据来源,子组件内部也需要改动;同时单元测试时也很难脱离 Store
理想情况下:组件对外主要是“给定 props,产出 UI;需要外界协作时,通过事件交出去”。若一个组件不涉及状态管理,就可以表示为
{ UI, events } ≈ f(props)
在组件里使用状态管理,f 就不再只是 props 的函数。封装组件与控制边界,就是方便通过组件的输入输出看出依赖关系。组件就不应该使用状态管理吗,在于我们如何权衡状态管理带来的隐式依赖关系与状态管道能够减轻的复杂度,比如登录状态仍需状态管理
理想化的组件
通过props输入、通过事件输出
<script setup lang="ts">
const props = defineProps<{
user: {
id: string
name: string
avatar: string
}
showFollow?: boolean
isFollowing: boolean
}>()
const emit = defineEmits<{
(e: 'follow'): void
}>()
</script>
<template>
<div class="user-card">
<img :src="props.user.avatar" :alt="props.user.name">
<div>{{ props.user.name }}</div>
<button v-if="props.showFollow" @click="emit('follow')">
{{ props.isFollowing ? '已关注' : '关注' }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/store/user'
import UserCard from './UserCard.vue'
const props = defineProps<{
user: {
id: string
name: string
avatar: string
}
}>()
const userStore = useUserStore()
const isFollowing = computed(() =>
userStore.followingIds.includes(props.user.id)
)
async function handleFollow() {
await userStore.follow(props.user.id)
}
</script>
<template>
<UserCard
:user="user"
:is-following="isFollowing"
show-follow
@follow="handleFollow"
/>
</template>
使用状态管理可能会让代码量减少,但代码量少并不意味着简单,上面之前的例子中,想要复用或修改功能就需要付出额外的代价