<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用单文件组件与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
基本语法
要启用该语法,需要在 <script>
代码块上添加 setup
attribute:
js
<script setup>
console.log('hello script setup')
</script>
里面的代码会被编译成组件 setup()
函数的内容。这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。
用 <script setup>
快速创建一个简单的计数器组件:
// Counter.vue
html
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
console.log('counter loaded')
const count = ref(0)
function increment() {
count.value++
}
</script>
如果你在父组件里多次使用 <Counter />
:
// Parent.vue
html
<template>
<Counter />
<Counter />
<Counter />
</template>
在 console 控制台上 counter loaded 会出现三次。
顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用:
html
<template>
<button @click="log">{{ msg }}</button>
</template>
<script setup>
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
import
导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的函数,而不需要通过 methods
选项来暴露它。
html
<template>
<p>当前日期:{{ formatDate(currentDate) }}</p>
</template>
<script setup>
import { formatDate } from '@/utils/date'
const currentDate = new Date()
</script>
使用组件
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用,无需再通过 components
进行注册
js
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
动态组件
由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup>
中要使用动态组件的时候,应该使用动态的 :is
来绑定:
js
<template>
<component :is="someCondition ? Foo : Bar" />
</template>
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
递归组件
在实际项目中,可以用递归组件渲染树形结构。以下是一个具体的实现示例:
html
<template>
<TreeNode :node="treeData" />
</template>
<script setup>
import TreeNode from './TreeNode.vue'
const treeData = {
name: '根节点',
children: [
{ name: '子节点 1' },
{
name: '子节点 2',
children: [
{ name: '子节点 2.1' },
{ name: '子节点 2.2' }
]
}
]
}
</script>
TreeNode.vue
的实现如下:
html
<template>
<div class="tree-node">
<p>{{ node.name }}</p>
<div v-if="node.children" class="children">
<TreeNode
v-for="(child, index) in node.children"
:key="index"
:node="child"
/>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const { node } = defineProps({
node: {
type: Object,
required: true
}
})
</script>
<style scoped>
.tree-node {
margin-left: 20px;
}
.children {
margin-top: 10px;
}
</style>
在这个实现中,TreeNode
组件通过递归调用自身来渲染树形结构的每个节点及其子节点。node
对象包含了每个节点的 name
和可选的 children
数组,用于表示子节点。
命名空间组件
可以使用带 .
的组件标签,例如 <Foo.Bar>
来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
js
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
<script setup>
import * as Form from './form-components'
</script>
./form-components
在这个例子中是一个模块,它可以是一个包含多个组件的文件,也可以是一个目录。通过 import * as Form from './form-components'
的语法,Form
作为一个命名空间对象,包含了 ./form-components
中导出的所有内容。
以下是对这个例子的详细展开,包括 ./form-components
的可能实现方式:
./form-components 是一个文件
如果 ./form-components
是一个文件,那么它可能是一个 JavaScript 或 TypeScript 文件,导出多个组件。以下是一个可能的实现:
js
import { defineComponent, h } from 'vue';
export const Input = defineComponent({
name: 'Input',
render() {
return h('input', { type: 'text' });
},
});
export const Label = defineComponent({
name: 'Label',
render() {
return h('label', {}, this.$slots.default ? this.$slots.default() : 'label');
},
});
在这种情况下,import * as Form from './form-components'
会将 Input
和 Label
作为 Form
对象的属性导入,使用时可以通过 Form.Input
和 Form.Label
访问。
./form-components 是一个目录
如果 ./form-components
是一个目录,那么它通常包含一个 index.js
文件,用于汇总导出目录中的所有组件。目录结构可能如下:
css
form-components/
├── Input.vue
├── Label.vue
└── index.js
-
Input.vue
vue<!-- filepath: ./form-components/Input.vue --> <template> <input type="text" /> </template>
-
Label.vue
vue<!-- filepath: ./form-components/Label.vue --> <template> <label><slot /></label> </template>
-
index.js
jsimport Input from './Input.vue'; import Label from './Label.vue'; export { Input, Label };
在这种情况下,import * as Form from './form-components'
会从 index.js
中导入所有导出的组件。
使用说明
无论 ./form-components
是文件还是目录,最终在 Vue 组件中使用时,代码如下:
vue
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
<script setup>
import * as Form from './form-components';
</script>
总结
- 如果是文件:直接导出多个组件。
- 如果是目录:通过
index.js
汇总导出组件。 - 这种命名空间的方式非常适合组织多个相关组件,避免命名冲突,同时提高代码的可读性和可维护性。
使用自定义指令
全局注册的自定义指令将正常工作。本地的自定义指令在 <script setup>
中不需要显式注册,但他们必须遵循 vNameOfDirective
这样的命名规范:
html
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
js
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
如下用自定义指令实现拖拽功能:
html
<template>
<div v-draggable>拖动我</div>
</template>
<script setup>
const vDraggable = {
beforeMount(el) {
el.style.position = 'absolute'
el.onmousedown = (e) => {
const shiftX = e.clientX - el.getBoundingClientRect().left
const shiftY = e.clientY - el.getBoundingClientRect().top
const moveAt = (pageX, pageY) => {
el.style.left = pageX - shiftX + 'px'
el.style.top = pageY - shiftY + 'px'
}
const onMouseMove = (event) => moveAt(event.pageX, event.pageY)
document.addEventListener('mousemove', onMouseMove)
el.onmouseup = () => {
document.removeEventListener('mousemove', onMouseMove)
el.onmouseup = null
}
}
}
}
</script>
与普通的 <script>
一起使用
<script setup>
可以和普通的 <script>
一起使用。普通的 <script>
在有这些需要的情况下或许会被使用到:
- 声明无法在
<script setup>
中声明的选项,例如inheritAttrs
或插件的自定义选项 (在 3.3+ 中可以通过defineOptions
替代)。 - 声明模块的具名导出 (named exports)。
- 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
js
<script>
// 普通 <script>,在模块作用域下执行 (仅一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
在同一组件中将 <script setup>
与 <script>
结合使用的支持仅限于上述情况。具体来说:
- 不要为已经可以用
<script setup>
定义的选项使用单独的<script>
部分,如props
和emits
。 - 在
<script setup>
中创建的变量不会作为属性添加到组件实例中,这使得它们无法从选项式 API 中访问。我们强烈反对以这种方式混合 API。
如果你发现自己处于以上任一不被支持的场景中,那么你应该考虑切换到一个显式的 setup()
函数,而不是使用 <script setup>
。
defineOptions()
这个宏可以用来直接在 <script setup>
中声明组件选项,而不必使用单独的 <script>
块:
js
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
}
})
</script>
这是一个宏定义,选项将会被提升到模块作用域中,无法访问 <script setup>
中不是字面常数的局部变量。
在 Vue 3 的 <script setup>
语法里,默认组件名是由文件名决定的。不过你也能通过 defineOptions
宏来明确指定组件名。以下是一个示例:
js
<template>
<div>
<p>这是 {{ name }} 组件</p>
</div>
</template>
<script setup>
// 定义组件选项,指定组件名
defineOptions({
name: 'MyCustomComponent'
})
</script>
顶层 await
<script setup>
中可以使用顶层 await
。结果代码会被编译成 async setup()
:
js
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
在 <script setup>
中使用 await
时,Vue 会自动将代码编译为保留当前组件实例上下文的格式。这意味着在 await
表达式之后,代码仍然能够正确访问组件实例的相关数据和方法,而不会因为异步操作导致上下文丢失。例如:
js
<script setup>
const data = await fetchData(); // 在这里使用 await
console.log(data); // 仍然可以访问组件实例的上下文
</script>
这种处理方式确保了在异步操作完成后,组件的逻辑能够继续正常运行,而无需手动绑定上下文。
但 async setup()
必须与 Suspense
组合使用才能保证页面正常渲染,该特性目前仍处于实验阶段。
父组件中用 Suspense
包裹 <Child>
组件以避免警告和渲染错误:
vue
<template>
<Suspense>
<Child />
</Suspense>
</template>
<script setup>
import Child from './Child.vue'
</script>
导入语句
在代码实例中,展示了 Vue 3 中 <script setup>
语法下的几种导入方式。
以下是对每种导入方式的详细讲解:
- 标准相对路径导入
javascript
import { componentA } from './Components'
- 含义 : 使用相对路径
./Components
导入componentA
。 - 特点 :
./Components
表示当前文件所在目录。- 这种方式是标准的 ECMAScript 模块导入方式,适用于项目中明确的文件路径。
- 适用场景: 当文件路径明确且不需要跨目录时,推荐使用这种方式。
- 使用
@
别名导入
javascript
import { componentB } from '@/Components'
-
含义 : 使用
@
作为别名导入componentB
。 -
特点 :
-
@
通常被配置为项目的 src 目录的别名。 -
这种别名需要在构建工具(如 Vite 或 Webpack)的配置文件中定义。例如:
javascript// Vite 配置示例 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': '/src', // 定义 @ 为 src 目录 }, }, })
-
-
适用场景: 当项目结构复杂且需要频繁引用 src 目录下的文件时,使用别名可以提高代码可读性和维护性。
- 使用
~
别名导入
javascript
import { componentC } from '~/Components'
-
含义 : 使用
~
作为别名导入componentC
。 -
特点 :
-
~
是另一种常见的别名,具体含义取决于构建工具的配置。 -
在某些项目中,
~
可能被配置为项目根目录或其他特定目录。 -
例如,在 Vite 中可以这样配置:
javascriptresolve: { alias: { '~': '/project-root', // 定义 ~ 为项目根目录 }, }
-
-
适用场景 : 当需要引用项目根目录或其他特定目录的文件时,可以使用
~
别名。
总结:
- 相对路径导入: 标准且无需额外配置,但路径可能较长。
@
别名导入: 常用于引用 src 目录,简化路径。~
别名导入: 灵活性更高,具体含义取决于项目配置。
在实际项目中,推荐根据团队约定和项目需求选择合适的导入方式,并确保在构建工具中正确配置别名。