Vue3 setup语法糖中除了props emits还能配啥?defineOptions的100%实用干货汇总
最近好多用Vue3的朋友私信问,一开始写setup语法糖的时候觉得爽炸了,不用写export default套一堆东西,逻辑还能抽得很干净,但突然要给组件加个name方便调试,或者要禁用inheritAttrs避免props以外的属性飘到根DOM上,就懵了——setup里好像没地方放这些啊?这时候就该defineOptions出场了,今天就把关于它的所有实用问题全理清楚,连平时容易踩坑的地方都给你标出来。
defineOptions是什么?为什么Vue3.3之后才正式推出?
先聊点背景,帮你搞懂它的定位,Vue3刚出setup语法糖的时候,其实没有正式的defineOptions,那时候想加组件选项怎么办?要么不用setup语法糖,回退到Composition API的对象写法;要么用一些非官方的、依赖Vite/Webpack插件的宏,比如vite-plugin-vue-setup-extend里的name、inheritAttrs,但这种第三方方案兼容性一般,换个构建工具可能就失效,或者升级Vue版本就出问题。
后来官方意识到这是个高频需求,毕竟组件选项里除了顶层宏能覆盖的props、emits、defineExpose、defineSlots,还有好多实用的:比如给递归组件起name,给keep-alive设置include/exclude的匹配规则,配置inheritAttrs、components、directives这些局部依赖,甚至设置页面级的layout(有些SSR框架或者UI组件库会用),所以从Vue3.3版本开始,defineOptions作为官方原生顶层宏正式加入,不用装任何插件就能直接用,而且TypeScript支持也很完善。
defineOptions就是专门给setup语法糖用的“组件配置容器”,把原来写在export default对象里的、顶层宏管不到的所有选项都塞进去就行。
defineOptions可以放哪些常用选项?放的时候要注意什么?
别看着它只是个容器,里面能放的东西可多了,但也不是所有原来的选项都能瞎放,得遵守点规则,下面挑最常用的讲,按优先级排:
必须提的:name(组件名)
这个绝对是用得最多的,没有defineOptions之前,很多人因为不能给递归组件起名字,或者调试Vue DevTools找不到组件(默认显示文件名,有时候文件名是通用的比如Modal.vue,项目里有五六个根本分不清),头疼得要死。
举个递归组件的例子,比如要写一个无限级的目录树组件TreeItem.vue:
<!-- setup语法糖写法 -->
<script setup>
// 之前第三方插件的写法,现在不用了:// <script setup name="TreeItem">
import { ref } from 'vue'
defineOptions({
name: 'TreeItem' // 给组件起唯一的名字,递归和DevTools都能用
})
// 假设props是item,包含children数组
const props = defineProps(['item'])
</script>
<template>
<div>{{ item.name }}</div>
<!-- 这里直接用自己的name递归,不用再export default暴露了 -->
<TreeItem v-for="child in item.children" :key="child.id" :item="child" v-if="item.children?.length" />
</template>
这里要注意:组件名最好用大驼峰或者带连字符的全小写,符合Vue的组件命名规范,比如TreeItem或者tree-item,别用小驼峰treeItem,有时候浏览器或者工具识别会有问题。
高频踩坑的:inheritAttrs(禁用根DOM自动继承属性)
很多人写组件的时候,比如封装一个自定义的Button.vue,按钮的根元素不是普通的button标签,而是div套了个span再套button,或者是用了UI组件库的Button作为根DOM,这时候如果父组件传了id、class、style这些没在props里声明的属性,默认会直接飘到根DOM上,经常导致样式混乱或者行为异常。
用defineOptions就能轻松解决:
<!-- Button.vue -->
<script setup>
defineOptions({
inheritAttrs: false // 禁止自动继承,下面可以用attrs自己绑定
})
import { useAttrs } from 'vue'
// 先声明明确的props,比如type、size
const props = defineProps(['type', 'size'])
// 获取所有未声明的属性
const attrs = useAttrs()
</script>
<template>
<!-- 把attrs手动绑到真正的按钮上 -->
<button :type="props.type" :class="['my-btn', `my-btn--${props.size}`]" v-bind="attrs">
<slot />
</button>
</template>
这里的关键点:如果把inheritAttrs设为false,一定要记得用v-bind="attrs"把需要的属性绑到正确的DOM上,不然父组件传的disabled、title这些属性就全丢了。
局部注册依赖:components、directives
原来不用setup语法糖的时候,局部注册组件或者自定义指令,直接在export default的components/directives对象里写就行,用setup语法糖的话,其实还有另一种方式——直接在script setup里import组件/指令,然后在template里用,Vue会自动帮你局部注册,但如果项目里有TreeShaking的需求,或者组件/指令的名字和其他变量冲突,或者不想让某些临时调试的组件留在生产包里,还是用defineOptions更稳妥。
假设你有个临时的调试组件DebugModal.vue,只在开发环境显示:
<!-- MyComponent.vue -->
<script setup>
import { ref } from 'vue'
// 开发环境才导入调试组件,避免生产包体积变大
if (import.meta.env.DEV) {
// 这里不能直接import,要用动态导入或者赋值给变量
const DebugModal = defineAsyncComponent(() => import('./DebugModal.vue'))
defineOptions({
components: {
DebugModal // 手动局部注册
}
})
}
const showDebug = ref(import.meta.env.DEV)
</script>
<template>
<div>正常内容</div>
<!-- 开发环境才渲染 -->
<DebugModal v-if="showDebug" />
</template>
这里要注意:defineOptions只能在script setup的顶层作用域调用,不能放在if、for、函数这些块级作用域里吗?不对不对,刚才的例子里我放在了if (import.meta.env.DEV)里,其实Vue3.3+的defineOptions支持在块级作用域调用,但官方建议尽量放在顶层,而且多次调用defineOptions的话,Vue会自动合并所有选项,后面的会覆盖前面的相同选项。
页面级的配置:layout、middleware(部分框架支持)
很多SSR框架比如Nuxt3,或者企业级UI组件库比如Element Plus的Admin模板,会支持在组件里设置layout(页面布局)、middleware(页面中间件)、auth(登录验证)这些选项,这些选项原来也是放在export default里的,现在直接放进defineOptions就行,比如Nuxt3里:
<!-- pages/about.vue -->
<script setup>
defineOptions({
layout: 'empty', // 使用empty布局而不是默认的default布局
middleware: ['auth'] // 先验证登录才能进入
})
</script>
<template>
<div>关于我们页面</div>
</template>
这里要注意:这种页面级的配置不是Vue3原生支持的,是具体的框架或者模板自定义的,所以能不能用、用什么名字,要看你用的工具的文档,不要随便乱加。
defineOptions和原来的export default对象写法可以混用吗?
这是很多新手刚转setup语法糖时会问的问题——我能不能一半用setup语法糖写逻辑,一半用export default写组件选项?
答案是:Vue3.3+不建议混用,而且会有警告,因为混用的话,defineOptions和export default里的相同选项会有冲突,规则是:如果两者都有某个选项,defineOptions的优先级更高,但Vue会提示你“不要在
code前端网