Vue3.2から追加された<script setup>記法をまとめてみた

この記事は、[ニフティグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nifty) 14日目の記事です。
Vue3.2から追加された`<script setup>`記法について紹介します。

# はじめに
Vue3.2からSFCを書く際に `<script setup>` というCompositionAPIのシンタックスシュガーが追加されました。
[公式ドキュメント](https://v3.ja.vuejs.org/api/sfc-script-setup.html#basic-syntax)には以下のように書いてあり、こちらが推奨される書き方のようです。

> It is the recommended syntax if you are using both SFCs and Composition API. It provides a number of advantages over the normal &lt;script&gt; syntax:

> これは、SFCとComposition APIの両方を使用している場合に推奨される構文です。通常の&lt;script&gt;構文に比べて、いくつかの利点があります。
翻訳 by DeepL


3.2から追加された記法のメリットは以下の通りです。

- More succinct code with less boilerplate
- Ability to declare props and emitted events using pure TypeScript
- Better runtime performance (the template is compiled into a render function in the same scope, without an intermediate proxy)
- Better IDE type-inference performance (less work for the language server to extract types from code)

翻訳 by DeepL

- ボイラープレートを減らし、より簡潔なコードに
- 純粋なTypeScriptを使用して、propsとemittedされたイベントを宣言することができます。
- ランタイムパフォーマンスの向上（テンプレートは、中間プロキシを介さず、同じスコープ内のレンダリング関数にコンパイルされる）
- IDEの型推論のパフォーマンスが向上（コードから型を抽出する言語サーバーの作業が減少）

とのことなので、今後は3.2から追加された記法を使っていくことになりそうです。

# 従来の書き方との違い
従来のCompositionAPIは次のように、`setup()`の中に記述します。

```vue:sample.vue
<template>
  <p>{{ msg }}</p>
</template>

<script lang="ts">
// vue
import { defineComponent } from 'vue';

export default defineComponent({
  setup() {
    const msg = 'Hello!'

    return {
      msg,
    };
  },
});
</script>
```

3.2から追加された新しい書き方は次のように書けます。

```vue:sample.vue
<script setup>
  const msg = 'Hello!'
</script>

<template>
  <p>{{ msg }}</p>
</template>
```
従来のCompositionAPIの書き方と比べて、とても~~Svelteっぽく~~書きやすくなりました。
`return`を書かなくてもtemplate内で変数が使えるようになりました。

これに伴い、VS Codeの拡張機能の推奨が[Vetur](https://vuejs.github.io/vetur/)から[Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)に変更されました。
参考）https://twitter.com/VueDose/status/1463169464451706897

# 新記法 &lt;script setup&gt;構文の書き方
従来のCompositionAPIでの書き方と比べて変更されたポイントを見ていきます。

## コンポーネントの使用
`<script setup>`内でインポートされたコンポーネントは、`<template>`内で直接利用できるようになりました。
`components`プロパティに登録しなくて良くなったのはとても嬉しいですね。

```vue:component-sample.vue
<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
  <my-component> <!-- ケバブケースも可 -->
</template>
```

### 動的コンポーネント
`<script setup>`構文で動的コンポーネントを利用する場合は、インポートしたコンポーネントを`v-bind`でバインドします。

```vue:component-sample2.vue
<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <component :is="MyComponent" />
</template>
```

### 名前空間コンポーネント
コンポーネントのエントリポイントを作成して、名前空間つきでインポートできるようになりました。

```vue:components/form-components/Input.vue
<template>
  <input type="text" />
</template>
```

```ts:components/form-components/index.ts
export { default as Input } from "./Input.vue";
```

```vue:components/form-components/Form.vue
<script setup>
import * as Form from "./form";
</script>

<template>
  <Form.Input />
</template>
```
## カスタムディレクティブ
グローバルに登録されているカスタムディレクティブはそのまま使えます。
ローカルのカスタムディレクティブを`<template>`で利用するには、`vディレクティブ名`という形式で命名する必要があります。

```vue:ローカルでカスタムディレクティブを利用
<script setup>
// directiveだとNG
const vDirective = {
  beforeMount: (el) => {
    el.style.color = "red";
  },
};
</script>
<template>
  <h1 v-directive>This is a Heading</h1>
</template>

```

## PropsとEmit
propsは`defineProps`を利用して宣言できます。
`defineProps`はpropsオプションと同じ値を取ります。

```vue:PropsJavaScriptSample.vue
<script setup>
const props = defineProps({
  text: String
})
</script>
```

TypeScriptの場合は次のようにして型定義ができます。
`type: String as PropType<SampleType>`のように書かなくてよくなったのは便利ですね。

```vue:PropsJavaScriptSample.vue
<script setup>
interface Props {
  text: string;
}

const props = defineProps<Props>();
console.log(props.text);
</script>
```

emitは`defineEmits`を利用して宣言できます。
`defineEmits`はemitsオプションと同じ値を取ります。

```vue:EmitsJavaScriptSample.vue
<script setup>
const emit = defineEmits(['change', 'close']);
</script>
```
Propsと同様に、TypeScriptの場合は次のようにして型定義ができます。

```vue:PropsJavaScriptSample.vue
<script setup>
interface Emits {
  (e: "change", value: string): void;
  (e: "close", value: string): void;
}

const emit = defineEmits<Emits>();

const handleChange = (value: string) => {
  emit("change", value);
};
</script>
```

`defineProps`および`defineEmits`はコンパイラマクロのため、`<script setup>`構文ではインポートせずに利用可能です。

## SlotとAttr
これまで`setup()`の引数で受け取っていたcontextの中身の残り2つはそれぞれヘルパーが用意されています。
`useSlots`および`useAttrs`は`<script setup>`構文に限らず、通常のCompositionAPIでも利用可能です。

```vue:SlotAndAttrSample.vue
<script setup>
import { useSlots, useAttrs } from 'vue';

const slots = useSlots();
const attrs = useAttrs();
</script>
```

## トップレベルawait
トップレベル`await`が利用できます。
内部的には`async setup()`と同じのようなので、トップレベル`await`を利用した場合は、コンポーネントを`Suspense`で受ける必要があります。

```vue:TopLevelAwaitSample.vue
<script setup>
const result = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const res = await result.json();
console.log(res);
</script>
```

# まとめ
以上が、`<script setup>`記法のまとめでした。
全体的により書きやすくなった印象で、とても良い感じです。
今後CompositionAPIを書く場合は`<script setup>`記法で書いていこうと思いました。