保姆级教程:在Vue3的TSX组件里搞定父子传值(Props & Emit)
·
Vue3 TSX组件通信实战:类型安全的Props与Emit全解析
在Vue3生态中,TSX正逐渐成为类型敏感开发者的首选方案。不同于模板语法,TSX需要开发者直面JavaScript的灵活性,同时又要兼顾Vue的响应式特性。本文将聚焦父子组件通信这一核心场景,通过类型定义、Props验证和Emit事件三个维度,构建完整的TSX组件通信知识体系。
1. TSX环境下的Props类型系统
1.1 定义严格的Props接口
在TSX中,类型系统不是可选项而是必选项。我们需要通过TypeScript接口明确定义Props的契约:
interface UserProfileProps {
userId: number
userName: string
isVerified?: boolean
onFollow?: (userId: number) => void
}
这种定义方式相比Vue模板的 defineProps 更加直观,且能获得完整的IDE类型提示。注意可选参数( ? 修饰符)的合理使用,它们能有效降低组件间的强耦合。
1.2 Props的默认值策略
在函数式组件中设置默认值有两种推荐方式:
方法一:解构时直接赋值
const UserCard = ({ isVerified = false, ...props }: UserProfileProps) => {
return <div>{props.userName}{isVerified && <VerifiedBadge />}</div>
}
方法二:使用withDefaults工具
import { withDefaults } from 'vue'
const UserCard = withDefaults(
(props: UserProfileProps) => {
return <div>{props.userName}{props.isVerified && <VerifiedBadge />}</div>
},
{ isVerified: false }
)
提示:复杂对象的默认值建议使用工厂函数,避免引用类型共享问题
2. TSX特有的事件派发机制
2.1 类型化的事件发射器
不同于模板中的 defineEmits ,TSX中需要通过 ctx 参数访问emit方法。我们可以通过泛型约束事件类型:
type EmitEvents = {
(e: 'follow', userId: number): void
(e: 'message', payload: { text: string; timestamp: Date }): void
}
const SocialButton = (props: {}, ctx: { emit: EmitEvents }) => {
const handleClick = () => {
ctx.emit('follow', 123) // 类型检查生效
ctx.emit('message', {
text: 'Hello',
timestamp: new Date() // 自动类型推导
})
}
return <button onClick={handleClick}>互动</button>
}
2.2 事件与Props的联合类型
当组件需要同时处理传入的回调函数和内部事件时,可以创建联合类型:
type CommentComponentProps = {
comments: Array<{ id: number; text: string }>
onReply?: (commentId: number) => void
} & {
onVote?: (direction: 'up' | 'down') => void
}
const CommentList = (props: CommentComponentProps, ctx: { emit: any }) => {
const handleVote = (dir: 'up' | 'down') => {
props.onReply?.(123) // 调用可选Prop
ctx.emit('vote', dir) // 派发内部事件
}
return (
<div>
{props.comments.map(comment => (
<div key={comment.id}>
<p>{comment.text}</p>
<button onClick={() => handleVote('up')}>赞同</button>
</div>
))}
</div>
)
}
3. 条件渲染与列表渲染的TSX实践
3.1 类型安全的条件分支
TSX中不能使用 v-if 指令,但可以利用TypeScript的类型收窄特性实现更安全的条件渲染:
type Notification = {
type: 'message' | 'alert'
content: string
read?: boolean
}
const NotificationItem = ({ notification }: { notification: Notification }) => {
return (
<div class={notification.read ? 'read' : 'unread'}>
{notification.type === 'message' ? (
<MessageBubble content={notification.content} />
) : (
<AlertPopup content={notification.content} />
)}
</div>
)
}
3.2 带类型的列表渲染
结合泛型可以实现完全类型安全的列表渲染:
interface DataTableProps<T> {
data: T[]
renderRow: (item: T) => JSX.Element
}
function DataTable<T>(props: DataTableProps<T>) {
return (
<table>
<tbody>
{props.data.map((item, index) => (
<tr key={index}>{props.renderRow(item)}</tr>
))}
</tbody>
</table>
)
}
// 使用示例
interface Product {
id: number
name: string
price: number
}
const ProductList = ({ products }: { products: Product[] }) => (
<DataTable
data={products}
renderRow={(product) => (
<>
<td>{product.name}</td>
<td>{product.price.toFixed(2)}</td>
</>
)}
/>
)
4. 高级模式:双向绑定的TSX实现
4.1 受控组件模式
通过Props和Emit实现类似 v-model 的双向绑定:
interface InputFieldProps {
modelValue: string
placeholder?: string
}
const InputField = (
props: InputFieldProps,
ctx: { emit: (event: 'update:modelValue', value: string) => void }
) => {
const handleInput = (e: Event) => {
ctx.emit('update:modelValue', (e.target as HTMLInputElement).value)
}
return (
<input
type="text"
value={props.modelValue}
onInput={handleInput}
placeholder={props.placeholder}
/>
)
}
4.2 使用自定义hook抽象逻辑
将通用逻辑提取为可复用的hook:
import { ref, watch } from 'vue'
export function useModelWrapper<T>(
props: { modelValue: T },
emit: (event: 'update:modelValue', value: T) => void
) {
const value = ref(props.modelValue) as Ref<T>
watch(
() => props.modelValue,
(newVal) => (value.value = newVal)
)
watch(value, (newVal) => {
emit('update:modelValue', newVal)
})
return value
}
// 在组件中使用
const MyComponent = (props: { modelValue: string }, ctx: any) => {
const value = useModelWrapper(props, ctx.emit)
return <input v-model={value.value} />
}
5. 实战:构建类型安全的表单系统
5.1 表单上下文传递
通过provide/inject实现跨组件类型安全:
// types.ts
interface FormField {
name: string
value: any
valid: boolean
}
interface FormContext {
fields: Ref<Record<string, FormField>>
updateField: (name: string, value: any) => void
}
const formKey = Symbol() as InjectionKey<FormContext>
// FormProvider.tsx
const FormProvider = (props: { initialValues: Record<string, any> }, { slots }) => {
const fields = ref<Record<string, FormField>>(
Object.entries(props.initialValues).reduce((acc, [name, value]) => {
acc[name] = { name, value, valid: true }
return acc
}, {})
)
const updateField = (name: string, value: any) => {
fields.value[name].value = value
// 这里可以添加验证逻辑
}
provide(formKey, { fields, updateField })
return () => slots.default?.()
}
// FormInput.tsx
const FormInput = (props: { name: string }) => {
const form = inject(formKey)
if (!form) throw new Error('必须在FormProvider内使用')
const field = computed(() => form.fields.value[props.name])
const handleInput = (e: Event) => {
form.updateField(props.name, (e.target as HTMLInputElement).value)
}
return (
<div>
<input value={field.value.value} onInput={handleInput} />
{!field.value.valid && <span class="error">验证失败</span>}
</div>
)
}
5.2 表单提交与验证
整合所有技术点实现完整表单流程:
const LoginForm = () => {
const formData = reactive({
email: '',
password: '',
remember: false
})
const rules = {
email: (v: string) => /.+@.+\..+/.test(v),
password: (v: string) => v.length >= 8
}
const submit = (ctx: { emit: any }) => {
const isValid = Object.entries(formData).every(([key, value]) => {
return rules[key as keyof typeof rules]?.(value) ?? true
})
if (isValid) {
ctx.emit('submit', { ...formData })
} else {
ctx.emit('validation-failed')
}
}
return (
<FormProvider initialValues={formData}>
<form onSubmit={(e) => e.preventDefault()}>
<FormInput name="email" />
<FormInput name="password" type="password" />
<button onClick={() => submit}>登录</button>
</form>
</FormProvider>
)
}
更多推荐
所有评论(0)