

实用 vue3 script setup - rxliuli blog
source link: https://blog.rxliuli.com/p/452a85351b7f4a31b56cf25eb9d3ba50/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

实用 vue3 script setup
本文最后更新于:2022年11月16日 下午
在两年前,vue3 发布之后不久创建了一个 sfc 提案,它允许所有 script 中顶级变量默认绑定到模板,以消除和 react 的开发者体验的差距之一。
大概长这样
<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 中使用。
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
<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
<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 中访问
<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 的实际编译结果,例如上面的代码会被编译为
/* Analyzed bindings: {
"ref": "setup-const",
"List": "setup-maybe-ref",
"ListItemMeta": "setup-maybe-ref",
"uniq": "setup-maybe-ref",
"list": "setup-ref"
} */
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__ = /*#__PURE__*/ _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 /* PROPS */,
['title'],
),
]),
_: 1 /* STABLE */,
},
8 /* PROPS */,
['data-source'],
)
)
}
},
})
__sfc__.__file = 'App.vue'
export default __sfc__
尽管有些改进,但它也确实引发了一些问题
- 无法在 setup 中使用 export,因为所有代码都会被编译到 setup 函数中
defineProps 复用类型定义
在 tsx 中,另一个很有用的功能是可以直接复用 ts 类型定义,而不需要定义单独的 PropType。实际上,在 react 早期,它也包含内置的 PropType 支持,但最终它们采用了 ts 的类型定义,而 vue 则一直保留了下来,并不得不忍受 ts 类型与 props 类型无法共享的问题,即便它们都在 ts 中。
例如在 tsx 中我们会这样编写类型
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 之前
<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 之后
<script lang="ts" setup>
interface WindowMeta {
id: string
title: string
url: string
}
defineProps<{
meta: WindowMeta
}>()
</script>
可以看到,可以直接复用 ts 类型了,不再需要单独定义 vue PropType。你可能发现 defineProps 没有被导入,是的,它们实际上是宏,实际上它们在编译后就不存在了,甚至不会被编译为 vue PropType。下面是编译结果
/* Analyzed bindings: {
"meta": "props"
} */
import { defineComponent as _defineComponent } from 'vue'
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: 'App',
props: {
meta: null,
},
setup(__props) {
return () => {}
},
})
__sfc__.__file = 'App.vue'
export default __sfc__
虽然看起来不错,但它确实有局限性,例如
defineProps/defineEmits
无法引用外部类型,需要使用额外的插件 vite-plugin-vue-type-imports
defineEmit 使用类型
在 react 中不存在 emits/attrs/slots,它们都被整合到 props 中(这是一个非常优雅而强大的设计)。在 vue3 中,emits 可以定义类型,但在 defineEmits 之前,仍然要通过定义值来推导类型。
interface WindowMeta {
id: string
title: string
url: string
}
export function App(props: { onClick(item: WindowMeta): void }) {}
在 vue3 之前一般要这样写,定义对象并由 vue 根据值推断类型
<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 类型
<script lang="ts" setup>
interface WindowMeta {
id: string
title: string
url: string
}
defineEmits<{
// 这里则是一个类型
(type: 'onClick', item: WindowMeta): void
}>()
</script>
当然,它会在编译时全部删除掉,仅用于开发时的代码提示和校验
/* Analyzed bindings: {} */
import { defineComponent as _defineComponent } from 'vue'
const __sfc__ = /*#__PURE__*/ _defineComponent({
__name: 'App',
emits: ['onClick'],
setup(__props) {
return () => {}
},
})
__sfc__.__file = 'App.vue'
export default __sfc__
也可以使用使用一些工具类型编写更符合直觉的接口
<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
</div
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK