This article was translated from Japanese by AI and may contain inaccuracies. For the most accurate content, please refer to the original Japanese version.
vite

【Vue.js】script setup 構文がすごくすごい

Vue.js 3.x から script setup 構文が使えるようになりました。これは単一ファイルコンポーネント(SFC)内で Composition API を使用している際に使える糖衣構文です。下記のようなメリットを得ることができ、公式からも使用が推奨されています。

<script setup> 構文とは

Vue.js 3.2 から <script setup> 構文が使えるようになりました。これは単一ファイルコンポーネント(SFC)内で Composition API を使用している際に使える糖衣構文です。下記のようなメリットを得ることができ、公式からも使用が推奨されています。

  • ポイラープレートが減りより簡潔になる
  • props と emit を定義する際に純粋な TypeScript の構文が使える
  • ランタイムのパフォーマンスが向上する
  • IDE のパフォーマンスが向上する

基本的な構文

<script setup> 構文をざっくりと説明すると、従来の Composition API における setup() 関数内部を <script> 直下に直接記述できるという構文です。

単一ファイルコンポーネントの <script> タグに setup 属性を付与することでこの糖衣構文を使用できます。

<script setup> 構文によってどれほど記述が簡潔になったかを従来の Options API と Composition API の書き方と比較してみましょう。

  • Options API
<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>
 
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});
</script>

簡単なカウンターアプリですが、おおむねこのような感じになるでしょう。 これを Composition API に置き換えたものが以下となります。

  • Composition API
<template>
  <h1>{{ count }}</h1>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
</template>
 
<script lang="ts">
import { defineComponent, ref } from "vue";
 
export default defineComponent({
  setup() {
    const count = ref(0);
 
    const increment = () => {
      count.value++;
    };
 
    const decrement = () => {
      count.value--;
    };
    return {
      count,
      increment,
      decrement,
    };
  },
});
</script>

最後にお待ちかねの <script setup> 構文です。

  • <script setup>
<script setup lang="ts">
import { ref } from "vue";
 
const count = ref(0);
 
const increment = () => {
  count.value++;
};
 
const decrement = () => {
  count.value--;
};
</script>
 
<template>
  <h1>{{ count }}</h1>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
</template>

これ Svelete じゃん!? 公式のサンプルコードを見ても、<template><script> の順番だったのが <script><template> の順番になっています。

Vue.js 特有の構文が少なくなって、純粋なの JavaScript の構文のように記述できているのが気持ちよいですね。

Composition API との比較をすると以下の点が異なっていることがわかります。

  • export default で Vue.js のオブジェクトを export する必要がなくなった
  • setup() 関数内で定義した変数や関数を return しないと <template> 内で使用できなかったが、 <script setup> 内で宣言した場合すべて使用可能となる

他にも従来の書き方といくつか異なる点があるので見ていきましょう。

VSCode 拡張機能

VSCode における .vue ファイルの拡張機能としてVeturがよく使われていたかと思われますが、 <script setup> 構文には対応していません。

代わりに、Volarを使うことが推奨されています。

コンポーネント

今まで components に import したコンポーネントの一覧を登録しなければいけなかったのですが、import するだけで直接使えるようになりました。

<script lang="ts" setup>
import TheHeader from "./components/TheHeader.vue";
import TheFooter from "./components/TheFooter.vue";
</script>
 
<template>
  <TheHeader />
  <main>main</main>
  <TheFooter />
</template>

コンポーネントにはケパブケース <the-header> も使えますが、公式ではパスカルケース <TheHeader> を推奨しているようです。

The kebab-case equivalent also works in the template - however PascalCase component tags are strongly recommended for consistency. It also helps differentiating from native custom elements.

ついでに auto import もいい感じになってます。

setup-component

Namespeced Components

React っぽいことができます。

  • Forms/Input.vue
<template>
  <input type="text" />
</template>
  • Forms/Checkbox.vue
<template>
  <input type="checkbox" />
</template>
  • Forms/index.ts
export { default as Input } from "./Input.vue";
export { default as Checkbox } from "./Checkbox.vue";
  • App.vue
<script lang="ts" setup>
import * as Form from "./components/Forms";
</script>
 
<template>
  <Form.Input />
  <Form.Checkbox />
</template>

しかしこれだとコンポーネントが any 型になってしまうので正しいやり方なのかどうかは不明です。

スクリーンショット 2021-09-21 21.26.07

Props と emit

個人的に一番嬉しかった点です。propsemit を純粋な TypeScript で定義できるようになりました。

Props

<script setup lang="ts">
import { computed } from "@vue/reactivity";
 
interface Props {
  value: string;
  label?: string;
  type?: "text" | "password" | "email" | "number";
  placeholder?: string;
  disabled?: boolean;
}
 
const props = defineProps<Props>();
 
const disabledClass = computed(() => {
  props.disabled ? "bg-gray-200" : "";
});
</script>
<template>
  <label>
    {{ label }}
    <input
      :class="disabledClass"
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
    />
  </label>
</template>

props は defineProps() で定義されます。 defindProps と後述する defineEmitswidhDefaults<script setup> 内で使用できるコンパイラマクロであり、どこかから import する必要はありません。

TypeScript を使用している場合には、 defineProps() は型引数を受け取ることができ、渡した型定義は従来のプロパティのバリデーションの代わりに使用できます。

stringboolean などのはそのまま従来の typeStringBoolean のように定義していたところに対応します。

プロパティをオプショナルにした場合には従来の required: false と同じであり、オプショナルでない場合には required: true と同じです。

ここの型定義はコンポーネントを利用するときもタイプヒントが効いています。

スクリーンショット 2021-09-21 21.26.07

props のデフォルト値を定義する場合には、 withDefaults を使用します。

const props = withDefaults(defineProps<Props>(), {
  label: "",
  type: "text",
  placeholder: "",
  disabled: false,
});

emit

emit も props と同様に、TypeScript により型定義をすることが可能です。

 <script setup lang="ts">
interface Emits {
  (e: "input", value: string): void;
  (e: "update:value", value: string): void;
}
 
const emit = defineEmits<Emits>();
 
const handleInput = ({ target }: { target: HTMLInputElement }) => {
  emit("input", target.value);
  emit("update:value", target.value);
};
</script>
 
<template>
  <label>
    {{ label }}
    <input
      :class="disabledClass"
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
      @input="handleInput"
    />
  </label>
</template>

もちろんしっかりと型が効いています。

スクリーンショット 2021-09-21 21.28.04

スクリーンショット 2021-09-21 21.41.06

useSlots と useAttrs

Composition API では、 setup() 関数の第 2 引数から $slots$attrs を取得していましたが、 <script setup> では代わりに useSlots()useAttrs() を使用します。

<script setup>
import { useSlots, useAttrs } from 'vue'
 
const slots = useSlots()
const attrs = useAttrs()
</script>

通常の <script> と使う

<script setup> と通常の <script> は 1 つの単一ファイルコンポーネントに同時に定義できます。 これは以下の用途で使用されます。

  • inferitAttrs オプションなど <script setup> では設定できないオプションを定義するとき
  • 副作用のある操作を一度だけ実行したいとき(<script setup> はコンポーネントが作成されるたびに実行されます)
<script setup lang="ts">
interface Props {
  value: string;
  label?: string;
  type?: "text" | "password" | "email" | "number";
  placeholder?: string;
  disabled?: boolean;
}
 
const props = withDefaults(defineProps<Props>(), {
  label: "",
  type: "text",
  placeholder: "",
  disabled: false,
});
</script>
 
<script lang="ts">
import { defineComponent } from "@vue/runtime-core";
 
export default defineComponent({
  inheritAttrs: false,
});
</script>
 
<template>
  <label>
    {{ label }}
    <input
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
    />
  </label>
</template>

トップレベル await

<script setup> の中ではトップレベル await が使えます。

  • AsyncUserList.vue
<script setup lang="ts">
import { ref } from "@vue/reactivity";
 
interface User {
  id: number;
  username: string;
}
 
const result = await fetch("https://jsonplaceholder.typicode.com/users");
const json = await result.json();
 
const users = ref<User[]>(json);
</script>
 
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.username }}
    </li>
  </ul>
</template>

注意点として、 await を使用した時点でそのコンポーネントは Promiseを返す非同期コンポーネントとして扱われます。

つまりは、必ず <Suspense> と組み合わせ使う必要があるということです。

詳しくは以下を参照してください。

https://v3.ja.vuejs.org/guide/migration/suspense.html#%E4%BB%96%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9B%E3%82%9A%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A8%E3%81%AE%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B

  • App.vue
<script lang="ts" setup>
import AsyncUserList from "./components/AsyncUserList.vue";
</script>
 
<template>
  <Suspense>
    <AsyncUserList />
  </Suspense>
</template>

紹介しておいてなんですが <Suspense> 自体が実験的な機能であるので、トップレベル await の使用は控えたほうがよいでしょう。