本文最后更新于:2023年2月22日 早上
场景
官方文档
在两年前,vue3 发布之后不久创建了一个 sfc 提案 ,它允许所有 script 中顶级变量默认绑定到模板,以消除和 react 的开发者体验的差距之一。
大概长这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script setup> // imported components are also directly usable in template import Foo from './Foo.vue' import { ref } from 'vue' // write Composition API code just like in a normal setup() // but no need to manually return everything const count = ref(0) const inc = () => { count.value++ } </script> <template> <Foo :count="count" @click="inc" /> </template>
第一感觉:现在真就 vue script 了 实际使用:真香
它确实解决了一些固有的问题
template 无法访问 js 值域
defineProps/defineEmits
无法复用类型定义
经过两年之后,它确实能够在生产中使用了,即便可能仍然不算完美,下面将描述它主要解决的两个问题以及与其他方法的差异
template 访问 js 值域 在 react 中,我们可以直接在 jsx 中访问任意 js 值域。例如 import 一个变量,并在 jsx 中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { List } from 'antd' import { uniq } from 'lodash-es' import { useState } from 'react' export function App ( ) { const [list] = useState ([1 , 2 , 1 ]) return ( <List dataSource ={uniq(list)} renderItem ={(item) => <List.Item.Meta key ={item} title ={item} /> } /> ) }
在 vue2 中,template 能访问的值必须注册到 vue 中,不管是 data/methods/components
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script lang="ts"> import { defineComponent } from 'vue' import { List, ListItemMeta } from 'ant-design-vue' import { uniq } from 'lodash-es' export default defineComponent({ components: { List, ListItemMeta }, data: () => ({ list: [1, 2, 1], }), methods: { uniq, }, }) </script> <template> <List :data-source="uniq(list)"> <template #renderItem="{ item }"> <ListItemMeta :title="item" /> </template> </List> </template>
在 vue3 中,虽然有了 hooks,但仍然未解决 js 值域的问题,只是可以通过 setup 统一暴露给 template
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script lang="ts"> import { defineComponent, ref } from 'vue' import { List, ListItemMeta } from 'ant-design-vue' import { uniq } from 'lodash-es' export default defineComponent({ components: { List, ListItemMeta }, setup() { const list = ref([1, 2, 1]) return { list, uniq } }, }) </script> <template> <List :data-source="uniq(list)"> <template #renderItem="{ item }"> <ListItemMeta :title="item" /> </template> </List> </template>
但 vue3 setup script 中,则部分解决了这个问题,所有在 script 中的值均可以在 template 中访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script lang="ts" setup> import { ref } from 'vue' import { List, ListItemMeta } from 'ant-design-vue' import { uniq } from 'lodash-es' const list = ref([1, 2, 1]) </script> <template> <List :data-source="uniq(list)"> <template #renderItem="{ item }"> <ListItemMeta :title="item" /> </template> </List> </template>
可以看到,setup script 已经接近 react jsx 的使用体验了
当然,你可以在 https://sfc.vuejs.org/ 中查看 setup script 的实际编译结果,例如上面的代码会被编译为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import { defineComponent as _defineComponent } from 'vue' import { unref as _unref, createVNode as _createVNode, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, } from 'vue' import { ref } from 'vue' import { List , ListItemMeta } from 'ant-design-vue' import { uniq } from 'lodash-es' const __sfc__ = _defineComponent ({ __name : 'App' , setup (__props ) { const list = ref ([1 , 2 , 1 ]) return (_ctx, _cache ) => { return ( _openBlock (), _createBlock ( _unref (List ), { 'data-source' : _unref (uniq)(list.value ), }, { renderItem : _withCtx (({ item } ) => [ _createVNode ( _unref (ListItemMeta ), { title : item }, null , 8 , ['title' ], ), ]), _ : 1 , }, 8 , ['data-source' ], ) ) } }, }) __sfc__.__file = 'App.vue' export default __sfc__
局限性
尽管有些改进,但它也确实引发了一些问题
无法在 setup 中使用 export,因为所有代码都会被编译到 setup 函数中
defineProps 复用类型定义 在 tsx 中,另一个很有用的功能是可以直接复用 ts 类型定义,而不需要定义单独的 PropType。实际上,在 react 早期,它也包含内置的 PropType 支持,但最终它们采用了 ts 的类型定义,而 vue 则一直保留了下来,并不得不忍受 ts 类型与 props 类型无法共享的问题,即便它们都在 ts 中。
例如在 tsx 中我们会这样编写类型
1 2 3 4 5 6 7 8 9 interface WindowMeta { id : string title : string url : string }export function Window (props: { meta: WindowMeta } ) { return <pre > {JSON.stringify(props.meta, null, 2)}</pre > }
在 vue3 没有 defineProps 之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script lang="ts"> import { defineComponent, PropType } from 'vue' interface WindowMeta { id: string title: string url: string } export default defineComponent({ props: { meta: { type: Object as PropType<WindowMeta>, required: true, }, }, }) </script> <template> <pre>{{ JSON.stringify($props.meta, null, 2) }}</pre> </template>
使用 defineProps 之后
1 2 3 4 5 6 7 8 9 10 11 <script lang="ts" setup> interface WindowMeta { id: string title: string url: string } defineProps<{ meta: WindowMeta }>() </script>
可以看到,可以直接复用 ts 类型了,不再需要单独定义 vue PropType。你可能发现 defineProps 没有被导入,是的,它们实际上是宏,实际上它们在编译后就不存在了,甚至不会被编译为 vue PropType。下面是编译结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { defineComponent as _defineComponent } from 'vue' const __sfc__ = _defineComponent ({ __name : 'App' , props : { meta : null , }, setup (__props ) { return () => {} }, }) __sfc__.__file = 'App.vue' export default __sfc__
局限性
虽然看起来不错,但它确实有局限性,例如
注意:即便使用了 vite-plugin-vue-type-imports,它还是有两个已知的局限性
无法导入 node_modules 的类型直接使用,例如
1 2 3 4 5 6 <script lang="ts" setup> import { defineProps } from 'vue' import { AppMetaData } from '@pinefield/ipc-main' defineProps<AppMetaData>() </script>
必须修改为
1 2 3 4 5 6 7 8 9 10 11 <script lang="ts" setup> import { defineProps } from 'vue' import { AppMetaData } from '@pinefield/ipc-main' defineProps<{ id: AppMetaData['id'] name: AppMetaData['name'] version: AppMetaData['version'] url: AppMetaData['url'] }>() </script>
不支持复杂类型,具体来说是 props 的类型必须是对象字面量,例如
1 2 3 4 5 6 7 <script lang="ts" setup> defineProps< Partial<{ name: string }> >() </script>
必须修改为
1 2 3 4 5 <script lang="ts" setup> defineProps<{ name?: string }>() </script>
defineEmit 使用类型 在 react 中不存在 emits/attrs/slots,它们都被整合到 props 中(这是一个非常优雅而强大的设计)。在 vue3 中,emits 可以定义类型,但在 defineEmits 之前,仍然要通过定义值来推导类型。
例如
1 2 3 4 5 6 7 interface WindowMeta { id : string title : string url : string }export function App (props: { onClick(item: WindowMeta): void } ) {}
在 vue3 之前一般要这样写,定义对象并由 vue 根据值推断类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script lang="ts"> import { defineComponent } from 'vue' interface WindowMeta { id: string title: string url: string } export default defineComponent({ // 注意,此处是一个 js 对象 emits: { onClick(item: WindowMeta): void {}, }, }) </script>
在使用 setup script 后,可以使用 ts 类型
1 2 3 4 5 6 7 8 9 10 11 12 <script lang="ts" setup> interface WindowMeta { id: string title: string url: string } defineEmits<{ // 这里则是一个类型 (type: 'onClick', item: WindowMeta): void }>() </script>
当然,它会在编译时全部删除掉,仅用于开发时的代码提示和校验
1 2 3 4 5 6 7 8 9 10 11 12 import { defineComponent as _defineComponent } from 'vue' const __sfc__ = _defineComponent ({ __name : 'App' , emits : ['onClick' ], setup (__props ) { return () => {} }, }) __sfc__.__file = 'App.vue' export default __sfc__
也可以使用使用一些工具类型编写更符合直觉的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script lang="ts" setup> import { UnionToIntersection } from 'utility-types' type ShortEmits<T> = UnionToIntersection< { [P in keyof T]: T[P] extends (...args: any[]) => any ? (type: P, ...args: Parameters<T[P]>) => ReturnType<T[P]> : never }[keyof T] > interface WindowMeta { id: string title: string url: string } defineEmits< ShortEmits<{ onClick(item: WindowMeta): void }> >() </script>
问题 它确实解决了一些开发上糟糕的体验,但也引发了一些问题
必须使用 @vue/compiler-dom 才能处理 vue script 的代码,因为 setup script 意味着它不再是真正的 ts 代码了
某些更激进的社区方案添加了各种各样的自定义宏,参考 unplugin-vue-macros