reona.dev

Vue 3 の概要と Vue 2 との違いについて


前置き

2023 年 1 月 27 日に実施した社内勉強会でのポストを一部改変して転記しています。

概要

現在 Vue.js を使っている各プロジェクトにおいて、Vue 2 → Vue 3 へのアップグレードが進行しています。 このアップグレードにおける破壊的変更は非常に多く、今後フロントエンド開発を継続する上での学習コストは少なくないため、Vue 3 における主要機能やアップグレードすることによるメリットを Vue 2 までの機能と比較しながら解説していきます。

Vue 3について

Vue 3 は 2020 年 9 月 19 日にリリースされました。2023年01月27日現在の最新バージョンは v3.2.45です。 Vue 2 と比較したメリットはざっくり次の通りです。

  • Composition API などの主要機能追加によるコードの最適化
  • 全面的な TypeScript のサポート
  • ビルド後のバンドルサイズが大きく改善したことによるパフォーマンスの向上

コードの最適化

Vue 2 では Options API が主に利用されていましたが、Composition API に変更することで次のようなメリットがあります。

  • JavaScript らしい記法でコンポーネントを記述できる
  • フレームワークにそこまで依存しない記法なので、変数や関数の型定義がしやすい
  • リアクティブな値(ref, reactive, computed)やそれに関するロジックが UI コンポーネントに依存しない
    • 再利用性が向上し、Pure な JS/TS の関数になるためより testable になる
  • ライフサイクルフックが少し単純になった

また、Vue 3.2 で追加された <script setup> 記法を使うことでよりコードがシンプルに記述できるようになります。

JavaScript らしい記法でコンポーネントを記述できる

Options API では、ボイラープレート的なフレームワークに依存した記法を覚える必要がありました。

export default Vue.extend({
  data: () => ({
    name: "",
  }),
  methods: {
    input(value) {
      this.name = value;
    },
  },
  computed: {
    call() {
      return `${this.name} 様`;
    },
  },
});

Composition API を利用することで、フレームワーク独自の記法が少なくなるので、より JavaScript らしいコードでコンポーネントを記述できるようになります。

export default defineComponent({
  setup() {
    const name = ref("");
    const input = (value) => {
      name.value = value;
    };
    const call = computed(() => {
      return `${this.name} 様`;
    });
 
    return { name, input, call };
  },
});

const, let などの JavaScript のキーワードを用いて変数や関数を宣言できるので、setup 内のコードは非常に記述しやすくなります。

リアクティブな値やロジックが UI コンポーネントに依存しない

setup 内で宣言するリアクティブな値や関数は、UI コンポーネントから切り離すことができます。

export default const useForm = () => {
    const name = ref('')
    const input = value => {
      name.value = value
    }
    const call = computed(() => {
      return `${this.name} 様`
    })
 
    return { name, input, call }
}
 
// Vue コンポーネント側
export default defineComponent({
  setup() {
    const { name, input, call } = useForm()
 
    return { name, input, call }
  }
})

コンポーネントに関係する値やロジックを独立できるので、単体テストが書きやすくなります。 また、複数コンポーネント間で値やロジックの使いまわしができるようになるのもメリットです。

ライフサイクルフックが単純になった

Vue.js ライフサイクルフックの早見表

refs: ライフサイクルフック

beforeCreate, created がなくなり、本来これらのライフサイクルに書かれていたロジックは setup 内で直接記述するようになりました。

export default defineComponent({
  setup() {
    // 従来の created のタイミングで実行される
    console.log("Component is created!");
    // mounted
    onMounted(() => {
      console.log("Component is mounted!");
    });
  },
});

Vue 3.2 で追加された記法について

Vue 3.2 からは <script setup>というシンタックスシュガーが追加され、よりシンプルにコンポーネントを構成できるようになります。 これまでの記法と比較することで違いがわかりやすくなるので、順を追ってコードを比較していきましょう。 (それぞれのタイトルが Playground のリンクになっています)

Options API

<template>
  <form method="post">
    <label for="name">Name</label>
    <input id="name" v-model="name" type="text" />
    <input type="submit" />
  </form>
</template>
 
<script lang="ts">
import Vue from 'vue'
 
export default Vue.extend({
  data: () => ({
    name: '',
  }),
  methods: {
    input(value) {
      this.name = value
    },
  },
  computed: {
    call() {
      return `${this.name} 様`
    },
  },
}
</script>

Composition API

<template>
  <form method="post">
    <label for="name">Name</label>
    <input id="name" v-model="name" type="text" />
    <input type="submit" />
  </form>
</template>
 
<script lang="ts">
import { defineComponent, ref, computed } from "vue";
 
export default defineComponent({
  setup() {
    const name = ref("");
    const input = (value) => {
      name.value = value;
    };
    const call = computed(() => {
      return `${this.name} 様`;
    });
 
    return { name, input, call };
  },
});
</script>

<script setup>

<script setup lang="ts">
import { ref, computed } from "vue";
 
const name = ref("");
const input = (value) => {
  name.value = value;
};
const call = computed(() => {
  return `${this.name} 様`;
});
</script>
 
<template>
  <form method="post">
    <label for="name">Name</label>
    <input id="name" v-model="name" type="text" />
    <input type="submit" />
  </form>
</template>

とてもシンプルになりました🙌

一度 Composition API の記法でコードを書けるようになれば、<script setup> 構文に全く違和感なく、むしろ気持ちよく移行できるはずです。 公式のサンプルコードを見てもわかると思いますが、scripttemplate の順序に変わっているので、本稿のサンプルコードもそちらに準拠しています。 ちなみに逆の順序でも問題なく動くので、Playground で試してみてください。

Language server

これまで Vue の Language server は vuejs / vetur でしたが、<script setup> 構文には対応していないため johnsoncodehk / volar の利用が推奨されています。

以下公式ツイート https://twitter.com/vuejs/status/1429195877365780486?s=20&t=s8ZKmwmrabCWU7hhsx0eZw

Components

これまではテンプレート内で import した components 使うには props などの並びで一度列挙して登録する必要がありましたが、 import するだけで直接利用できるようになりました。

<script>
import MyComponent from "./MyComponent.vue";
 
// これまでは登録する必要があった
export default {
  components: { MyComponent },
};
</script>
 
<template>
  <MyComponent />
</template>
<script setup>
// import のみでコンポーネントを使える
import MyComponent from "./MyComponent.vue";
</script>
 
<template>
  <MyComponent />
</template>

props, emit

propsemit については defineProps, defineEmits を利用することで <script setup> 構文内で宣言ができます。 これまでは props, emit の型定義や型推論や完全ではありませんでしたが、Pure な TypeScript の記法で宣言ができるようになるので自然に型を定義でき、型推論もされるようになります。

<script>
export default {
  props: {
    name: { type: String },
  },
  methods: {
    onInput(target) {
      this.$emit("input", target.value);
    },
  },
};
</script>
<script setup lang="ts">
const props = defineProps<{ name: string }>();
// デフォルト値を入れたい場合は withDefaults を使う
// const props = withDefaults(defineProps<{ name: string }>(), { name: '太郎' })
 
const emit = defineEmits<{ (e: "input", value: string): void }>();
 
const onInput = ({ target }: { target: HTMLInputElement }) => {
  emit("input", target.value);
};
</script>

definePropswithDefaultsdefineEmits<script setup> 内でのみ使用可能なコンパイラマクロなので、インポートせずに利用できます。

最後に

今回紹介できなかった機能もたくさんあるので、ぜひ公式ドキュメントなどをチェックしてみてください!

また、TypeScript サポートが手厚くなっていることで今後も TypeScript のコードは増えていくと思うので、ドキュメントを読んでみたり 実際に手を動かしてみる のもいいと思います👩‍💻

vuejs-challenges は Vue 3 対応しているので、課題にトライしていって今回紹介したような記法に慣れておくのもいいですね👍

参考資料