😎

【Vue.js 3.2】`<script setup>` 構文がすごくすごい

2021/09/24に公開

<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を使うことが推奨されています。

https://twitter.com/vuejs/status/1429195877365780486

コンポーネント

今まで 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 <my-component> also works in the template - however PascalCase component tags are strongly recommended for consistency. It also helps differentiating from native custom elements.

https://v3.vuejs.org/api/sfc-script-setup.html#using-components

ついでに 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 と同じです。

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

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> は一つの単一ファイルコンポーネントに同時に定義することができます。
これは以下の用途で使用されます。

  • inheritAttrs オプションなど <script setup> では設定できないオプションを定義するとき
  • 副作用のある操作を一度だけ実行したいとき ( <script setup> はコンポーネントが作成されるたびに実行されます)
  • named export をしたいとき
<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#他のコンポーネントとの組み合わせ

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

<template>
  <Suspense>
    <AsyncUserList />
  </Suspense>
</template>

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

感想

良くも悪くも Vuejs っぽい書き方を捨てて他のフレームワークの良いところを取り入れている感じですね。従来の書き方よりも簡潔になっているのは間違いないと思います。

個人的にやはり気に入っているのが props と emit の型定義ができるところですね。Vuejs での TypeScript の使いごこちも向上してきている感じですね

参考

https://v3.vuejs.org/api/sfc-script-setup.html

GitHubで編集を提案

Discussion