<script setup>-组合式API编译时语法糖
单文件组件(SFC)中使用组合式API编译时语法糖,该语法在同时使用单文件组件与组合式API是默认推荐的。
主要优点:
更少的样板代码:无需手动export default,无需setup()函数包裹。
能够使用纯的ts声明和自定义事件。
更好的运行性能(其模板会被编译成同一作用域内的渲染函数,避免上下文代理对象)
主要特性:
顶层绑定自动暴露给模板:在<script setup>中声明的变量,函数,导入内容,无需return,可直接在模板中使用.
自动defineComponent:使用<script setup>的租价会自动包裹在defineComponent()中,无需手动调用。
简洁的props和emits定义:
javascript
<script setup>
const props = defineProps({
msg: String
})
const emit = defineEmits(['change'])
function handleChange() {
emit('change', 'new value')
}
</script>
异步支持直接await:
javascript
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>
<script setup>中编译器宏
在<script setup>中,编译器宏是一种特殊的函数,不需要导入,可以直接在<script setup>中使用,这些宏在编译阶段会被处理,不能在普通的逻辑代码如(if,console.log或普通函数内部)中调用,只能在<script setup>的顶层作用域使用。
defineProps()-声明组件接收的props
返回值:返回一个包含所有prop值得响应对象(只读)
javascript
<script setup>
// 选项式语法 (Options Syntax)
const props = defineProps({
msg: String,
list: {
type: Array,
required: true
}
})
// TypeScript 语法 (Type-Only Syntax) - 推荐 TS 用户使用
// 无需运行时验证,类型仅在编译时检查
const props = defineProps<{
msg?: string
list: number[]
}>()
</script>
defineEmits()-声明组件可以触发的事件
返回值:返回emit函数,用于触发事件
javascript
<script setup>
// 选项式语法
const emit = defineEmits(['change', 'update'])
// TypeScript 语法
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
function notify() {
emit('change', 123)
}
</script>
defineExpose()-对外暴露显式指定那些属性/方法可被父组件通过ref访问
默认情况下,<script setup>组件时关闭的,父组件通过模板引用(ref)访问子组件实例时,无法访问到子组件内部定义的变量或方法
子组件对外暴露属性和方法
javascript
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 显式暴露
defineExpose({
count,
increment
})
</script>
父组件进行访问
javascript
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const childRef = ref(null)
// 现在可以访问 childRef.value.count 和 childRef.value.increment()
</script>
defineModel()-简化组件间的双向数据绑定
以前实现父子组件双向绑定需要手动编写props,emit事件以及computed计算属性来实现,具体操作如下
子组件
javascript
<script setup>
// 第一个模型 (对应 v-model)
const name = defineModel('name', { type: String, default: 'Guest' })
// 第二个模型 (对应 v-model:age)
const age = defineModel('age', { type: Number, default: 0 })
function growUp() {
age.value++
}
</script>
<template>
<div>
<label>名字: <input v-model="name" /></label>
<br />
<label>年龄: <input v-model="age" /></label>
<button @click="growUp">长大一岁</button>
</div>
</template>
父组件
javascript
<template>
<!-- 使用参数化 v-model -->
<MultiModel
v-model:name="userName"
v-model:age="userAge"
/>
</template>
withDefaults()-仅配合TS使用
当使用ts语法定义defineProps时,无法直接设置默认值。withDefaults用于补充默认值。
javascript
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
// 设置默认值
const props = withDefaults(defineProps<Props>(), {
msg: 'Hello',
labels: () => ['new', 'feature'] // 对象/数组默认值需用工厂函数
})
</script>
组合式API-useSlots()和useAttrs()
对应选项式API中 this.slots this.attrs。组合式API需显式导入。
useAttrs()-属性透传,精细控制属性绑定
应用场景:封装原生的<input>组件。父组件可能会传递各种原生属性(比如placeholder,disabled,class,style)等
如果不使用useAttrs,需要在defineProps里把每个原生属性都定义一次.。
子组件
javascript
<template>
<!--
这是一个多根节点结构 (Fragment):
1. 外层有个 div 包裹
2. 内部才是 input
Vue 不知道把 class/style 给谁,所以默认会报警告或绑定到外层。
需要用 useAttrs 手动绑定到 input 上。
-->
<div class="input-wrapper">
<label v-if="label" class="label">{{ label }}</label>
<!-- 核心:将 attrs 全部绑定到 input 元素上 -->
<input
v-bind="attrs"
class="native-input"
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<span class="suffix">px</span>
</div>
</template>
<script setup lang="ts">
import { useAttrs, defineOptions } from 'vue'
// 1. 定义简单的 props (只定义业务相关的)
defineProps<{
modelValue: string | number
label?: string
}>()
// 2. 定义事件
defineEmits<{
'update:modelValue': [value: string]
}>()
// 3. 【关键】关闭自动继承
// 手动把 attrs 绑定到 input 上,如果不关闭,
// 属性会同时出现在外层的 div 和内部的 input 上(导致重复 class 等)
defineOptions({
inheritAttrs: false
})
// 4. 获取 attrs 对象
const attrs = useAttrs()
// 可以在逻辑中读取特定属性(例如用于调试或条件判断)
// 注意:attrs 是响应式的,如果父组件动态改变 class,这里也会变
console.log('当前绑定的额外属性:', attrs)
</script>
<style scoped>
.input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.native-input {
border: 1px solid #ccc;
padding: 4px 8px;
/* 父组件传入的 class 会合并到这里 */
}
</style>
父组件
javascript
<template>
<div>
<!--
注意:我们并没有在 SmartInput 的 props 里定义
placeholder, disabled, @focus, style, class
但它们都能完美工作!
-->
<SmartInput
v-model="searchText"
label="搜索"
placeholder="请输入关键词..."
disabled="false"
class="custom-highlight"
style="border-color: blue;"
@focus="handleFocus"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SmartInput from './SmartInput.vue'
const searchText = ref('')
const handleFocus = () => {
console.log('Input focused!')
}
</script>
useSlots()-动态渲染插槽
应用场景:权限控制组件,根据某些条件(用户权限,数据是否为空),动态决定是否渲染某个插槽,对插槽的内容进行包装。
子组件:接收一个role属性。当前用户有权限则渲染默认插槽,如果没有权限就渲染'无权限'提示,或者渲染特定的插槽。
javascript
<template>
<!-- 常规情况:直接渲染 -->
<div v-if="hasPermission" class="content-box">
<slot />
</div>
<!-- 无权限情况:动态决定渲染什么 -->
<div v-else class="error-box">
<!--
这里演示用 v-if 判断插槽是否存在。
虽然模板里可以直接写 <slot name="denied">,
但如果逻辑更复杂(比如要根据 slots.denied 的返回值类型做处理),
就需要用到 useSlots。
-->
<slot name="denied">
<p>🚫 您没有权限查看此内容。</p>
</slot>
</div>
</template>
<script setup lang="ts">
import { useSlots, computed } from 'vue'
const props = defineProps<{
requiredRole: string
currentRole: string
}>()
// 1. 获取所有插槽函数
const slots = useSlots()
// 2. 计算是否有权限
const hasPermission = computed(() => {
return props.currentRole === props.requiredRole
})
// 3. 【高级用法】在 JS 中检查插槽是否存在
// 有时候我们需要知道父组件是否传了特定的具名插槽,以便做不同的逻辑处理
const hasCustomDeniedSlot = computed(() => {
// slots.denied 是一个函数,如果父组件没传,它就是 undefined
return !!slots.denied
})
// 模拟一个场景:如果有自定义 denied 插槽,我们在控制台打个日志
if (hasCustomDeniedSlot.value && !hasPermission.value) {
console.log('检测到用户使用了自定义的无权限插槽内容')
}
// 4. 【极端高级用法】手动渲染插槽 (通常在 render 函数中用得多,模板中少见)
// 假设我们需要把插槽内容作为参数传给某个第三方 JS 库
function processSlotContent() {
if (slots.default) {
// 调用插槽函数,得到 VNode 数组
const vnodes = slots.default()
console.log('默认插槽生成的虚拟节点:', vnodes)
// 这里可以对 vnodes 进行修改、过滤或包装,然后再返回
return vnodes
}
return null
}
</script>
父组件
javascript
<template>
<!-- 场景 1: 有权限,显示正常内容 -->
<PermissionWrapper required-role="admin" current-role="admin">
<h1>管理员仪表盘</h1>
<p>这里是敏感数据...</p>
</PermissionWrapper>
<!-- 场景 2: 无权限,使用默认的拒绝提示 -->
<PermissionWrapper required-role="admin" current-role="user">
<h1>普通用户仪表盘</h1>
</PermissionWrapper>
<!-- 场景 3: 无权限,使用自定义的拒绝提示 (触发 denied 插槽) -->
<PermissionWrapper required-role="vip" current-role="user">
<template #denied>
<div class="custom-alert">
🔒 成为 VIP 才能解锁此功能!
<button>立即升级</button>
</div>
</template>
<h1>VIP 专属内容</h1>
</PermissionWrapper>
</template>