SFC 概述

将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个.vue 文件里,即单文件组件( Single-File Components,缩写为 SFC)。

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">Count is: {{ count }}</button>
</template>

<style scoped>
button {
  font-weight: bold;
}
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

每一个 *.vue 文件都由三种顶层语言块构成:<template><script><style>

  • 最多可以包含一个顶层 <template> 块,其包裹的内容将会被提取、传递给 @vue/compiler-dom,预编译为 JavaScript 渲染函数,并附在导出的组件上作为其 render 选项。
  • 最多可以包含一个 <script> 块。(使用 <script setup> 的情况除外)
  • 最多可以包含一个 <script setup>。(不包括一般的 <script>) , 这个脚本块将被预处理为组件的 setup() 函数,这意味着它将为每一个组件实例都执行。<script setup> 中的顶层绑定都将自动暴露给模板。
  • 可以包含多个 <style> 标签,可以使用 scoped 或 module attribute 来帮助封装当前组件的样式。使用了不同封装模式的多个 <style> 标签可以被混合入同一个组件。
  • 语言块内的注释,使用各自语言对应的注释语法
// js 的注释
  • 1.
<!-- html 的注释 -->
  • 1.
/* css 的注释 */
  • 1.
  • 语言块外的注释,使用 HTML 的注释语法
<!-- 注释内容 -->
  • 1.

SFC 的优点

  • 使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件
  • 让本来就强相关的关注点自然内聚
  • 预编译模板,避免运行时的编译开销
  • 组件作用域的 CSS
  • 在使用组合式 API 时语法更简单
  • 通过交叉分析模板和逻辑代码能进行更多编译时优化
  • 更好的 IDE 支持,提供自动补全和对模板中表达式的类型检查
  • 开箱即用的模块热更新 (HMR) 支持

SFC 的缺点

必须使用构建工具

SFC 的使用场景

  • 单页面应用 (SPA)
  • 静态站点生成 (SSG)
  • 任何值得引入构建步骤以获得更好的开发体验 (DX) 的项目

SFC 的原理

SFC 会在打包构建过程中,通过@vue/compiler-sfc 编译为标准的 JavaScript 和 CSS。

  • 开发阶段:<style> 标签会注入成原生的 <style> 标签以支持热更新
  • 生产环境: <style> 标签会被抽取、合并成单独的 CSS 文件

使用预处理器

需在语言块标签的 lang 属性中声明预处理器

script 使用 TypeScript

<script lang="ts">
  // ts 代码
</script>
  • 1.
  • 2.
  • 3.

template 使用  Pug

<template lang="pug">
p {{ msg }}
</template>
  • 1.
  • 2.
  • 3.

style 使用 Sass

<style lang="scss">
  $primary-color: #333;
  body {
    color: $primary-color;
  }
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

<script setup>

SFC 中使用组合式 API 默认会推荐使用<script setup> 语法糖,相比于普通的 <script> ,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 TypeScript 声明 props 和自定义事件。
  • 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  • 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

运行机制

  • 普通的 <script> 只在组件被首次引入的时候执行一次
  • <script setup>中的代码会被编译成组件 setup() 函数的内容,在每次组件实例被创建的时候执行

顶层声明在模板中可直接使用

<script setup>
// 导入的函数
import { capitalize } from './helpers'
// 导入的组件(强烈建议使用 PascalCase 格式)
import MyComponent from './MyComponent.vue'

// 变量
const msg = 'Hello!'

// 函数
function log() {
  console.log(msg)
}
</script>

<template>
  <button @click="log">{{ msg }}</button>
  <div>{{ capitalize('hello') }}</div>
  <MyComponent />
</template>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

递归组件

名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。

这种方式相比于导入的组件优先级更低。

如果有具名的导入和组件自身推导的名字冲突了,可以为导入的组件添加别名

import { FooBar as FooBarChild } from './components'
  • 1.

从单文件中导入多个组件

将组件嵌套在对象属性中,使用带 . 的组件标签在模板中使用。

<script setup>
import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

使用自定义指令

自定义指令必须遵循 vNameOfDirective 命名规范(自定义指令名的首字母必须是 v ,后续跟首字母大写的自定义指令名称 )

<script setup>
const vMyDirective = {
  beforeMount: (el) => {
    // 在元素上做些操作
  }
}
</script>
<template>
  <h1 v-my-directive>This is a Heading</h1>
</template>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

导入的自定义指令,可以通过重命名来使其符合命名规范:

<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
  • 1.
  • 2.
  • 3.

defineProps

无需导入,可直接使用,参数与 props 选项相同

<script setup>
const props = defineProps({
  foo: String,
  bar?: Number
})
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

TS 中的写法

const props = defineProps<{
  foo: string,
  bar?: number
}>()
  • 1.
  • 2.
  • 3.
  • 4.

添加默认值需使用 withDefaults

export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

defineEmits

无需导入,可直接使用,参数与 emits 选项相同

const emit = defineEmits(['change', 'delete'])
  • 1.

TS 中的写法

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+:另一种更简洁的语法
const emit = defineEmits<{
  change: [id: number] // 具名元组语法
  update: [value: string]
}>()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

defineModel

vue3.4 新增 无需导入,可直接使用,用来声明一个双向绑定 prop,详细用法见
 https://cn.vuejs.org/guide/components/v-model.html

解构 defineModel() 的返回值可以获取 v-model 指令使用的修饰符

const [modelValue, modelModifiers] = defineModel()

// 对应 v-model.trim
if (modelModifiers.trim) {
  // ...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

通过 get 和 set 转换器选项可以在同步回父组件时对其值进行转换

const [modelValue, modelModifiers] = defineModel({
  // get() 省略了,因为这里不需要它
  set(value) {
    // 如果使用了 .trim 修饰符,则返回裁剪过后的值
    if (modelModifiers.trim) {
      return value.trim()
    }
    // 否则,原样返回
    return value
  }
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

TS 中

const modelValue = defineModel<string>()
// ^? Ref<string | undefined>

// 用带有选项的默认 model,设置 required 去掉了可能的 undefined 值
const modelValue = defineModel<string>({ required: true })
// ^? Ref<string>

const [modelValue, modifiers] = defineModel<string, "trim" | "uppercase">()
// ^? Record<'trim' | 'uppercase', true | undefined>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

defineExpose

使用 <script setup> 的组件默认是关闭的(通过模板引用或者 $parent 链无法获取到组件中的绑定,如声明的变量,导入的函数等)

需通过 defineExpose 来显式指定对外暴露的属性:

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

此时,父组件通过模板引用的方式获取到的当前组件的实例为 { a: number, b: number }

defineOptions

Vue 3.3 新增 用于声明组件选项,避免使用单独的 <script>

<script setup>
defineOptions({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  }
})
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

选项中无法访问 <script setup> 中不是字面常数的局部变量。

defineSlots

Vue 3.3 新增 用于为 IDE 提供插槽名称和 props 类型检查的类型提示。

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { msg: string }): any
}>()
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

defineSlots() 只接受类型参数,没有运行时参数。类型参数应该是一个类型字面量,其中属性键是插槽名称,值类型是插槽函数。函数的第一个参数是插槽期望接收的 props,其类型将用于模板中的插槽 props。返回类型目前被忽略,可以是 any,但我们将来可能会利用它来检查插槽内容。

它还返回 slots 对象,该对象等同于在 setup 上下文中暴露或由 useSlots() 返回的 slots 对象。

与普通的 <script> 一起使用

仅在有下述需求时使用:

  • 声明模块的具名导出 (named exports)。
  • 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
<script>
// 普通 <script>,在模块作用域下执行 (仅一次)
runSideEffectOnce()
</script>

<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

在顶层可以直接使用 await

因为代码会被编译成 async setup()

<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
  • 1.
  • 2.
  • 3.

获取组件实例

通过 getCurrentInstance 获取

资源拆分 src

如果不喜欢单文件组件这样的形式,可以按下方代码拆分单独的 HTML、JavaScript 和 CSS 文件

<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>
  • 1.
  • 2.
  • 3.

由于模块执行语义的差异,<script setup> 中的代码依赖单文件组件的上下文。当将其移动到外部的 .js 或者 .ts 文件中的时候,对于开发者和工具来说都会感到混乱。因此,<script setup> 内的 JS 代码无法使用 src 拆分。