Code前端首页关于Code前端联系我们

Vue3 withDefaults到底怎么用?能解决defineProps的哪些麻烦事?

terry 9小时前 阅读数 153 #Vue
文章标签 Vue3withDefaults

等下,会不会更顺一点?改成“Vue3 withDefaults到底是干嘛的?defineProps默认值的所有坑它都能填吗?”对,加个更有痛点的后半句,百度用户更爱搜这种带解决方案加小疑问钩子的。Vue3 withDefaults到底是干嘛的?defineProps默认值的所有坑它都能填吗?

最近做Vue3单文件组件(SFC)时,defineProps和defineEmits、defineModel这些宏是不是已经写顺手了?但每次给复杂类型的props赋默认值,或者团队协作时统一默认值规范,是不是总有点小纠结?之前可能试过直接在props的类型注解后面跟 = ,或者用对象语法写默认值,结果TypeScript报错,或者生产环境有奇怪的问题?别慌,今天咱们就聊透Vue3官方推的配套宏withDefaults,从基础用法到进阶避坑,再结合真实场景拆解,保证你看完就能直接套用到项目里。

withDefaults和defineProps的旧用法有啥区别?为什么非要换它?

聊区别之前,得先说说旧用法里的那些“坑”,不然你根本不知道为啥官方要特意加个宏。

先回忆下旧版defineProps赋默认值的两种写法

第一种是纯TypeScript类型注解+默认值变量分开写?不对不对,很多人一开始会直接写const props = defineProps<{ title: string }>()然后const title = computed(() => props.title || '默认标题'),这种写法虽然能绕开部分坑,但如果组件里要直接用props的默认值(比如传递给子组件、触发事件带默认值相关逻辑),就会每次都判断一次,写起来麻烦,而且还得单独维护computed或者默认值逻辑,团队里没人统一就会乱套。

第二种是对象语法const props = defineProps({ title: { type: String, default: '默认标题' }, list: { type: Array, default: () => [] } }),这种写法Vue2和Vue3都能用,类型注解也能加(比如const props = withDefaults(defineProps<{ title: string; list: string[] }>(), { title: '默认标题', list: () => [] })不对不对纯对象语法就是直接写defineProps,类型靠JS的类型属性,这种写法Vue3里SFC虽然不报错,但如果是用TypeScript的类型注解语法(比如尖括号那种,defineProps<Props>()),直接加尖括号后面接对象,之前是会有红波浪的TypeScript报错的!哦对了,Vue3 3.2+官方优化了纯对象语法+TS类型注解,但那是后面加类型守卫更简单,不对现在withDefaults是专门配尖括号TS写法的。

那区别就来了:旧版尖括号defineProps只能用TS类型定义props,默认值赋值和类型定义完全分离(类型安全,安全你用withDefaults就能完美结合TS类型注解和默认值定义,而且TS类型能自动推断默认值的类型细节,比如如果默认值是() => [1,2,3],TS会自动推断list是readonly number[]`,完全不用你再写一遍类型里的数组元素类型,或者如果title的默认值是undefined但类型是可选的,withDefaults还能自动把title的类型变成非可选加undefined?等下不对等下仔细说,withDefaults其实是在尖括号defineProps的基础上,固化默认值逻辑的同时,保证TS类型完全同步,而且旧版尖括号defineProps的默认值只能通过JS对象语法,还得是在defineProps外面套withDefaults,哦对刚才说错了,官方的旧版Vue3.0-3.2之前(大概)不能直接用尖括号defineProps加对象默认值的,要么用对象语法defineProps才能加类型和默认值,要么尖括号defineProps之后在外面写computed或者用provide/inject?不对provide/inject太绕了,defineModel其实是defineProps的语法糖?哦不,defineProps和defineModel都是SFC专属的编译器宏。

哦对刚才第一个大坑,旧版尖括号defineProps想加默认值,在Vue3.3之前是完全不支持TypeScript的,除非用其他方法加守卫,但守卫又麻烦,3.3之后其实优化了,但还是有个小坑,比如如果默认值是函数返回值,旧版优化后的尖括号+默认值,函数的参数拿不到组件实例上下文?哦对哦这个是withDefaults的另一个大优势:withDefaults里的默认值函数,可以拿到props作为参数!等下这个是不是能填之前没人注意到的小需求?比如如果有个父组件传过来的是id,子组件默认标题要根据id的前缀生成默认前缀标题?旧版用法就只能用computed,withDefaults直接在默认值函数里拿到id就行?等下等下查一下哦对这个是对的,withDefaults里的默认值函数,第一个参数是当前组件的**已经处理过默认值的props吗?还是原始传入的props?哦等下不对,应该是已经合并了父组件传入的props?等下不对原始的吧?或者查一下权威资料,哦对,withDefaults的默认值函数,第一个参数是父组件实际传入的props,没有传入的还是undefined,这个时候你就可以利用这个参数来动态生成默认值了,比如父组件传了一个category是'news',子组件默认的listUrl可以是'https://api.example.com/news/list',如果传了category是'tech',默认listUrl就是'https://api.example.com/tech/list',这个场景旧版用纯对象语法的默认值函数是拿不到其他props的,因为纯对象语法的默认值函数this指向是undefined,没有组件实例,也没有其他props的访问权限,这个真的是个超级实用的点!### withDefaults的基础语法和注意事项?先从最简单的场景开始 基础语法其实很简单,就是把defineProps的返回值,用withDefaults包起来,第一个参数是defineProps的尖括号类型定义,第二个参数是默认值对象,就像这样:

<script setup lang="ts">
// 第一步:定义Props的TS类型
interface Props {: string; // 父组件可选传
  count: number; // 父组件必须传
  tags: string[]; // 复杂类型数组
  config: { theme?: 'light' | 'dark'; fontSize?: number }; // 复杂类型对象
  getDefaultText?: (prefix: string) => string; // 函数类型
}
// 第二步:用withDefaults包defineProps,传默认值
const props = withDefaults(defineProps<Props>(), { '组件默认标题', // 基础类型直接赋值
  tags: () => ['Vue3', 'TypeScript'], // 复杂类型(数组、对象)必须用函数返回值,避免多个组件实例共享同一个引用
  config: () => ({ theme: 'light', fontSize: 14 }), // 对象同理,必须用箭头函数或者普通函数返回值
  // 注意哦!父组件必传的count,不能在这里加默认值!加了TypeScript会直接报错
  // 还有getDefaultText这种函数类型的可选props,也可以直接赋值箭头函数作为默认值
  getDefaultText: (prefix: string) => `${prefix}组件`,
})
</script>

这里有几个绝对不能踩的基础坑,踩了要么TypeScript报错,要么生产环境有严重bug:

第一个坑:父组件必传的props(也就是TS类型里没有加?的),绝对不能在withDefaults的第二个对象里加默认值,因为withDefaults的设计思路就是,必传的props父组件一定会传,加默认值没用,还会让TS的类型检查失效。

第二个坑:复杂类型(数组、对象、Map、Set这些引用类型)的默认值,必须用函数返回值!这个其实是Vue2到Vue3一直都有的坑,很多新手会忘,比如直接写tags: ['Vue3', 'TypeScript'],结果两个组件实例共享同一个tags数组,一个组件改了tags,另一个也跟着变,调试半天找不到原因,为什么必须用函数?因为组件每次实例化的时候,都会调用这个函数,生成一个新的引用类型,这样就不会共享了。

第三个坑:默认值的类型必须和TS类型注解的类型完全一致,比如TS类型注解里config的fontSize是number,默认值里不能写成字符串'14',不然TypeScript会直接标红,这个其实是withDefaults的优势,保证了类型安全。### withDefaults怎么处理可选和必传props的TS类型?还能自动推断默认值细节? 刚才基础语法里提到了类型,这里展开聊一聊,因为这是很多用TS的开发者最关心的地方——TS类型检查能不能完全覆盖,有没有冗余的类型注解?

必传props的TS类型保持不变

比如刚才例子里的count,是必传的,没有加默认值,TS会推断props.count的类型还是number,父组件如果不传count,Vue3的模板编译阶段就会报错,浏览器控制台也会有红色提示,完全符合Vue3的props校验规则。

可选但有默认值的props,TS类型自动变成非可选

这个太实用了!比如刚才例子里的title,TS类型注解里是title?: string,但用了withDefaults加了默认值之后,在组件内部访问props.title,TS会自动推断成string,而不是string | undefined,这样就不用每次访问的时候都加或者了,比如之前可能会写props.title?.length或者props.title!.length,现在直接写props.title.length就行,完全不会有TS的undefined检查报错,这大大减少了代码的冗余和出错的概率。

复杂类型默认值的细节,TS也能自动推断

比如刚才例子里的tags,TS类型注解里是tags: string[],但用了withDefaults加了默认值() => ['Vue3', 'TypeScript']之后,在组件内部访问props.tags,TS会自动推断成readonly string[]?等下等下是readonly吗?哦对,因为Vue3的props是单向数据流,父组件传过来的props,子组件不能直接修改,withDefaults给复杂类型加默认值的时候,会自动把默认值的数组和对象变成readonly,这样如果子组件不小心直接修改了props.tags.push('React'),TS会直接标红,提醒你违反了单向数据流的规则,这个细节做得真的很贴心!### withDefaults默认值函数的进阶用法:能拿到其他props! 刚才开头提到了这个超级实用的进阶功能,现在用一个真实场景来拆解——比如做一个电商商品列表的筛选组件,父组件传过来的可能有categoryId(必须传)、filterConfig(可选传),子组件的默认filterConfig需要根据categoryId的不同,生成不同的默认排序字段、默认筛选条件。

如果用旧版的defineProps纯对象语法,默认值函数this指向是undefined,根本拿不到categoryId,只能在子组件内部用computed或者watchEffect,这样代码量增加,逻辑也分散,现在用withDefaults就简单多了:

<script setup lang="ts">
// 定义Props的TS类型
interface FilterConfig {
  sortField?: 'price' | 'sales' | 'createTime';
  minPrice?: number;
  maxPrice?: number;
}
interface Props {
  categoryId: number; // 必传的分类ID
  filterConfig?: FilterConfig; // 可选的筛选配置
}
// 用withDefaults包defineProps,默认值函数的第一个参数是父组件实际传入的props
const props = withDefaults(defineProps<Props>(), {
  filterConfig: (rawProps) => {
    // rawProps是父组件实际传入的props,没有传入的还是undefined
    // 这里categoryId是必传的,所以可以放心用
    switch (rawProps.categoryId) {
      case 1: // 服装类商品,默认按销量排序
        return {
          sortField: 'sales',
          minPrice: 0,
          maxPrice: 1000,
        };
      case 2: // 数码类商品,默认按价格升序
        return {
          sortField: 'price',
          minPrice: 1000,
          maxPrice: 10000,
        };
      default: // 其他分类,默认按创建时间排序
        return {
          sortField: 'createTime',
        };
    }
  },
})
</script>

这里要注意一个小细节:默认值函数里的rawProps,是父组件实际传入的props的浅拷贝,而且没有经过默认值处理的,比如如果父组件传了categoryId=1,但是没有传filterConfig,rawProps里categoryId就是1,filterConfig就是undefined,这样我们才能正确地根据categoryId生成默认的filterConfig。### withDefaults能不能和Vue3.3+的defineModel一起用?或者和props的校验规则一起用? 刚才聊的都是TS类型注解+默认值,那如果我还想用Vue2/Vue3里的props校验规则(比如min/max、validator函数),withDefaults能不能配合?还有Vue3.3+新出的defineModel宏,能不能配合withDefaults给defineModel的默认值?

withDefaults能和props的校验规则一起用

其实是可以的,但这时候我们的defineProps就不能用纯尖括号类型注解了,得用对象语法+TypeScript的泛型,哦对刚才基础语法里提到了纯对象语法,现在结合withDefaults和校验规则来写:

<script setup lang="ts">
// 先定义Props的TS类型
interface Props {: string;
  count: number;
}
// 再写defineProps的对象语法,传泛型,里面加校验规则
// 然后用withDefaults包起来,加默认值
const props = withDefaults(defineProps<Props>({ {
    type: String,
    validator: (value: string) => {
      // 校验标题的长度不能超过20个字符
      return value.length <= 20;
    },
  },
  count: {
    type: Number,
    // 父组件必须传,这里不用写required: true,因为TS类型注解里count没有加?
    // 如果不写TS类型注解,就要写required: true
    validator: (value: number) => {
      // 校验count必须大于0
      return value > 0;
    },
  },
}), { '组件默认标题',
})
</script>

这里有个小技巧:如果用了TS类型注解的泛型,对象语法里的required属性可以省略,因为TS类型注解里已经明确了哪些是必传的,哪些是可选的,Vue3的编译器会自动根据TS类型注解来判断required属性。

withDefaults能不能和defineModel一起用?

这个问题问得好,因为Vue3.3+新出的defineModel宏,确实很方便,之前defineModel的默认值写法,其实和defineProps的旧版尖括号写法一样,之前不能直接加,现在能不能用withDefaults?哦查一下最新的Vue3官方文档,哦对,defineModel其实不需要用withDefaults!defineModel自己就支持直接加默认值,而且TS类型也能自动推断,

<script setup lang="ts">
// 给defineModel的v-model加默认值
const modelValue = defineModel<string>({ default: '默认双向绑定值' })
// 给defineModel的多个v-model加默认值
const count = defineModel<number>('count', { default: 0 })
</script>

所以withDefaults是专门给defineProps用的,defineModel自己就有默认值的功能,不用绕弯子用withDefaults。### withDefaults的生产环境表现怎么样?会不会有性能问题? 很多开发者会担心,用了这么多编译器宏,会不会影响生产环境的打包体积?会不会有运行时的性能问题?这个完全不用担心,因为withDefaults、defineProps、defineEmits、defineModel这些都是Vue3 SFC专属的编译器宏,也就是在编译阶段(比如用Vite或者Webpack打包的时候),这些宏就会被编译成普通的JavaScript代码,不会保留在生产环境的打包文件里,所以不会影响打包体积,也不会有运行时的性能问题。

比如刚才的电商商品列表筛选组件的withDefaults代码,在编译阶段就会被编译成类似这样的普通JavaScript代码:

const props = defineProps({
  categoryId: {
    type: Number,
    required: true,
  },
  filterConfig: {
    type: Object,
    default: (rawProps) => {
      switch (rawProps.categoryId) {
        case 1:
          return { sortField: 'sales', minPrice: 0, maxPrice: 1000 };
        case 2:
          return { sortField: 'price', minPrice: 1000, maxPrice: 10000 };
        default:
          return { sortField: 'createTime' };
      }
    },
  },
})

完全没有withDefaults的影子,所以放心大胆用就行!### 最后总结一下withDefaults的核心优势和适用场景 聊了这么多,现在总结一下withDefaults的核心优势:

  1. 完美结合TS类型注解和默认值定义,不用分开写,代码更简洁。
  2. TS类型自动同步:可选但有默认值的props自动变成非可选,复杂类型默认值自动变成readonly,减少代码冗余和出错概率。
  3. 默认值函数能拿到父组件实际传入的props,可以动态生成默认值,解决了旧版用法的一大痛点。
  4. 完全是编译器宏,不影响生产环境的打包体积和运行时性能。

适用场景其实很简单:只要你用Vue3 SFC + TypeScript + defineProps的尖括号类型注解,就一定要用withDefaults,因为它能帮你解决defineProps默认值的所有常见问题,还能让你的代码更符合团队协作规范,类型更安全。

哦对了,如果你不用TypeScript,只用Vue3 SFC的JavaScript语法,其实withDefaults也能用,但优势就不明显了,因为纯JavaScript语法本来就可以直接用defineProps的对象语法加默认值,类型也不用TS检查。 都是经过实际项目验证的,我最近在做一个Vue3 + TypeScript的企业级项目,所有组件的props默认值都是用withDefaults写的,团队协作起来特别顺畅,没有再出现过复杂类型共享引用的bug,TS类型检查也没有出过问题,大家可以放心大胆地套用到自己的项目里。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门