# SFC <script setup>

<script setup> は単一ファイルコンポーネント(SFC)内で Composition API を使用するコンパイル時のシンタックスシュガー(糖衣構文)です。SFC と Composition API の両方を使うならば、おすすめの構文です。これは通常の <script> 構文よりも、多くの利点があります:

  • ボイラープレートが少なくて、より簡潔なコード
  • 純粋な TypeScript を使ってプロパティと発行されたイベントを宣言する機能
  • 実行時のパフォーマンスの向上(テンプレートは中間プロキシなしに同じスコープ内のレンダリング関数にコンパイルされます)
  • IDE で型推論のパフォーマンス向上(言語サーバがコードから型を抽出する作業が減ります)

# 基本の構文

この構文を導入するには、setup 属性を <script> ブロックに追加します:

<script setup>
console.log('hello script setup')
</script>
1
2
3

内部のコードは、コンポーネントの setup() 関数の内容としてコンパイルされます。これはつまり、通常の <script> とは違って、コンポーネントが最初にインポートされたときに一度だけ実行されるのではなく、<script setup> 内のコードは コンポーネントのインスタンスが作成されるたびに実行される ということです。

# トップレベルのバインディングはテンプレートに公開

<script setup> を使用する場合、<script setup> 内で宣言されたトップレベルのバインディング(変数、関数宣言、インポートを含む)は、テンプレートで直接使用できます:

<script setup>
// 変数
const msg = 'Hello!'

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

<template>
  <div @click="log">{{ msg }}</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13

インポートも同じように公開されます。これはつまり、インポートされたヘルパー関数を methods オプションで公開することなくテンプレート式で直接使用できます:

<script setup>
import { capitalize } from './helpers'
</script>

<template>
  <div>{{ capitalize('hello') }}</div>
</template>
1
2
3
4
5
6
7

# リアクティビティ

リアクティブな状態は リアクティビティ API を使って明示的に作成する必要があります。setup() 関数から返された値と同じように、テンプレート内で参照されるときに ref は自動的にアンラップされます:

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

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>
1
2
3
4
5
6
7
8
9

# コンポーネントの使用

<script setup> のスコープ内の値は、カスタムコンポーネントのタグ名としても直接使用できます:

<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>
1
2
3
4
5
6
7

MyComponent を変数として参照していると考えてください。JSX を使ったことがあれば、このメンタルモデルは似ています。ケバブケースの <my-component> も同じようにテンプレートで動作します。しかし、一貫性を保つために、パスカルケースのコンポーネントタグを強く推奨します。これはネイティブのカスタム要素と区別するのにも役立ちます。

# 動的コンポーネント

コンポーネントは、文字列キーで登録されるのではなく変数として参照されるため、<script setup> 内で動的コンポーネントを使う場合は、動的な :is バインディングを使う必要があります:

<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>
1
2
3
4
5
6
7
8
9

三項演算子で変数としてコンポーネントをどのように使うことができるかに注意してください。

# 再帰的コンポーネント

SFC はそのファイル名を介して、暗黙的に自身を参照できます。例えば、FooBar.vue というファイル名は、そのテンプレート内で <FooBar/> として自身を参照できます。

これはインポートされたコンポーネントよりも優先度が低いことに注意してください。コンポーネントの推論された名前と競合する名前付きインポートがある場合、インポートでエイリアスを作成できます:

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

# 名前空間付きコンポーネント

<Foo.Bar> のようにドット付きのコンポーネントタグを使って、オブジェクトプロパティの下にネストしたコンポーネントを参照することができます。これは単一のファイルから複数のコンポーネントをインポートするときに便利です:

<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

# カスタムディレクティブの使用

グローバルに登録されたカスタムディレクティブは期待通りに動作して、ローカルのカスタムディレクティブは前述のコンポーネントで説明したようにテンプレートで直接使用できます。

しかし、注意する制限が 1 つあります: ローカルのカスタムディレクティブをテンプレート内で直接使用できるようにするためには、以下のスキーマに従って名前をつける必要があります: vNameOfDirective

<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
4

# definePropsdefineEmits

<script setup> の中で propsemits を宣言するには、definePropsdefineEmits の API を使用する必要があります。これらは <script setup> の中で自動的に利用できるようになっており、完全な型推論をサポートしています:

<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// セットアップのコード
</script>
1
2
3
4
5
6
7
8
  • definePropsdefineEmits は、<script setup> 内でのみ使用可能なコンパイラマクロです。インポートする必要はなく、<script setup> が処理されるときにコンパイルされます。

  • definePropsprops オプションと同じ値を受け取り、defineEmitsemits オプションと同じ値を受け取ります。

  • definePropsdefineEmits は、渡されたオプションに基づいて、適切な型の推論を行います。

  • defineProps および defineEmits に渡されたオプションは、setup のスコープからモジュールのスコープに引き上げられます。そのため、オプションは setup のスコープで宣言されたローカル変数を参照できません。参照するとコンパイルエラーになります。しかし、インポートされたバインディングはモジュールのスコープに入っているので、参照できます。

TypeScript を使用している場合は、純粋な型アノテーションを使って props や emits を宣言することも可能です。

# defineExpose

<script setup> を使用したコンポーネントは、デフォルトで閉じられています。つまり、テンプレート参照や $parent チェーンを介して取得されるコンポーネントのパブリックインスタンスは、<script setup> 内で宣言されたバインディングを公開しません

<script setup> コンポーネントのプロパティを明示的に公開するには、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 } という形状になります(ref は通常のインスタンスと同様、自動的にアンラップされます)。

# useSlotsuseAttrs

<script setup> 内で slotsattrs を使用することは比較的少ないはずです。なぜなら、テンプレート内で $slots$attrs として直接アクセスできるからです。万が一、必要になった場合には、それぞれ useSlotsuseAttrs ヘルパーを使用してください:

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>
1
2
3
4
5
6

useSlotsuseAttrs は、setupContext.slotssetupContext.attrs と同等のものを返す実際のランタイム関数です。これらは通常の Composition API の関数内でも使用できます。

# 通常の <script> との併用

<script setup> は、通常の <script> と一緒に使うことができます。次のことが必要な場合は、通常の <script> が必要になることがあります:

  • inheritAttrs や、プラグインで有効になるカスタムオプションなど、<script setup> では表現できないオプションを宣言する
  • 名前付きのエクスポートを宣言する (TypeScript の型を含む)
  • 副作用を実行したり、一度しか実行してはいけないオブジェクトを作成する
<script>
// 通常の <script>、モジュールのスコープで実行される(1 回だけ)
runSideEffectOnce()

// 追加のオプションを宣言
export default {
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
// setup() のスコープで実行される(インスタンスごとに)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

WARNING

このケースでは render 関数はサポートされていません。代わりに、通常の <script>setup オプションを使用してください。

# トップレベルの await

<script setup> の中ではトップレベルの await を使うことができます。その結果、コードは async setup() としてコンパイルされます:

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

さらに、await の対象の式は、await 後の現在のコンポーネントのインスタンスのコンテキストを保持する形式で自動的にコンパイルされます。

Note

async setup() は、現在まだ実験的な機能である Suspense と組み合わせて使用する必要があります。将来のリリースで完成させてドキュメント化する予定ですが、もし今興味があるのであれば、そのテスト (opens new window)を参照することで、どのように動作するかを確認できます。

# TypeScript のみの機能

# 追加タイプのエクスポート

上記のように、SFC から追加タイプをエクスポートするには、それらを <script setup> ブロックの隣の追加 <script> ブロックに移動する必要があります。

例:

<script lang="ts">
export type SizeOptions = 'small' | 'medium' | 'large';
</script>

<script lang="ts" setup>
defineProps({
  size: { type: String as PropType<SizeOptions> },
})
</script>
1
2
3
4
5
6
7
8
9

# 型のみの props/emit 宣言

definePropsdefineEmits にリテラル型の引数を渡すことで、純粋な型の構文を使って props や emits を宣言することもできます:

const props = defineProps<{
  foo: string
  bar?: number
}>()

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
1
2
3
4
5
6
7
8
9
  • defineProps または defineEmits は、実行時の宣言か型宣言のどちらかしか使用できません。両方を同時に使用すると、コンパイルエラーになります。

  • 型宣言を使用する場合は、同等の実行時宣言が静的解析から自動的に生成されるため、二重の宣言を行う必要がなくなり、実行時の正しい動作が保証されます。

    • 開発モードでは、コンパイラは型から対応する実行時バリデーションを推測しようとします。上記の例では foo: string という型からは foo: String が推測されます。もし型がインポートされた型への参照である場合、コンパイラは外部ファイルの情報を持っていないので、推測される結果は foo: nullany 型と同じ)になります。

    • プロダクションモードでは、バンドルサイズを小さくするために、コンパイラが配列形式の宣言を生成します(上記の props は ['foo', 'bar'] にコンパイルされます)。

    • 生成されるコードは有効な型付けがなされた TypeScript であり、他のツールを使ってさらに処理できます。

  • 現在のところ、正しく静的解析を行うためには、型宣言の引数は以下のいずれかでなければなりません:

    • 型リテラル
    • 同一ファイル内のインタフェースか型リテラルへの参照

    現在、複雑な型や他のファイルからの型のインポートはサポートされていません。将来的には型のインポートをサポートすることも理論的には可能です。

# 型宣言を使用時のデフォルトの props 値

型のみの defineProps 宣言の欠点は、props のデフォルト値を提供する方法がないことです。この問題を解決するために、withDefaults コンパイラマクロも用意されています:

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

これは、同等な実行時の props の default オプションにコンパイルされます。さらに、withDefaults ヘルパーは、デフォルト値の型チェックを行います。また、返される props の型が、デフォルト値が宣言されているプロパティに対して、省略可能フラグが削除されていることを保証します。

# 制限: src インポートの禁止

モジュールの実行セマンティクスの違いにより、<script setup> 内のコードは、SFC のコンテキストに依存しています。外部の .js.ts ファイルに移動すると、開発者とツールの両方に混乱を招く可能性があります。そのため、<script setup> は、src 属性と一緒に使うことはできません。