Vue2 の公式サポート終了に伴い、Options API から Composition API に書き換えてみる。

Vue2 の公式サポート終了に伴い、Options API から Composition API に書き換えてみる
Vue2 の公式サポート終了に伴い、Options API から Composition API に書き換えてみる

Web illustrations by Storyset

こんにちは、開発チームでエンジニアをしている和田です。

最近は、Vue2 で記述されたプロジェクトを Vue3 および Nuxt3 に書き換える毎日です👨‍💻

ということで今回は、Options API で記述された Vue2 のコンポーネントを Composition API で記述する過程について記事にしたいと思います。

Vue2からVue3・Nuxt3 へ移行する背景

Vue2 のプロジェクトを Nuxt3 へと移行するに至った背景としては、主に Vue2 の公式サポートが終了することにあります。

2023年12月に Vue2 の公式サポートが終了することが発表されています。このままプロジェクトを放置すると、Vue2 に依存するライブラリなどもアップデートできず脆弱性を抱え続けることになるため、一念発起してプロジェクト全体の変更をすることになりました🤓

  1. Options API で記述された Vue2 コンポーネントを Composition API に書き換える
  2. Composition API で記述されたプロジェクト全体を Vue3 に引き上げる
  3. Vue3 のプロジェクトをベースに Nuxt3 に移行する

このような流れで作業をしていきます。

この記事の対象読者: Vue2からVue3への乗り換えを検討している人

今回の記事では、Vue2 の Options API で記述されたコンポーネントを Composition API に書き換えるまでを解説します。

Composition API で記述されたコンポーネントを Nuxt3 のプロジェクトで使えるようにする方法については、今後の記事で解説させていただきます🙇‍♂️


Vue2とVue3、Options APIとComposition APIの関係について

前提知識として Vue2, Vue3, Options API, Composition API の関係性について述べておきます。

Options API について

Options API とは、Vue2 の代表的な記法であり下記のようなサンプルコードで書けます。

<script>
export default {
    name: 'Counter',
    data: () => {
        return {
            count: 0,
        };
    },

    methods: {
        countUp() {
            this.count++;
        },
        reset() {
            this.count = 0;
        },
    },
};
</script>

data の中でリアクティブな値 count を管理し、リアクティブに値を操作するための処理を methods 内に記述するという形式です。

この記法の問題として、this を使ってリアクティブに値を操作することになるため、View と処理を分離することができないということが挙げられます。

Composition API について

Composition API とは、Vue3 の代表的な記法です。
Vue2 のプロジェクトにも導入することができ、リアクティブな値の操作と View を切り離せるのが特徴です。

import { ref } from "vue";

export const useCounter = () => {
    const count = ref(0);

    const addCounter = () => {
        count.value++;
    };
    const resetCounter = () => {
        count.value = 0;
    };

    return {
        count,
        addCounter,
        resetCounter,
    };
};
import { useCounter } from '@/ ... /useCounter'

export default defineComponent({
    name: 'Counter'
    setup() {
        const { count, addCounter, resetCounter } = useCounter();
    }
});

composables というディレクトリに、リアクティブな値の処理を記述しておけば、どのコンポーネントからでも簡単に呼び出すことができます。


Vue2 Options API で記述されたコンポーネント

今回は Options API で記述された下記のコンポーネントについて見ていきます。
説明の都合上、<style /> の記述は省略します。

<template>
  <button
    :class="{ [`btn-${buttonType}`]: true }"
    :disabled="hasPermission === false || isDisabled === true"
  >
    <span>{{ buttonText }}</span>
  </button>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  name: 'ButtonBase',
  props: {
    buttonText: { type: String, required: true },
    buttonType: { type: String, default: 'primary' },
    isDisabled: { type: Boolean, default: false },
    rejectedRoleIds: { type: Array, default: () => [] },
  },
  computed: {
    ...mapGetters('staff', ['staff']),
    hasPermission() {
      return this.judgeHasPermission(
        this.staff,
        this.rejectedRoleIds,
      );
    },
  },
  methods: {
    judgeHasPermission(staffObj, rejectedRoleIds) {
      const staffObject = staffObj;
      if (typeof staffObject !== 'object') return false;

      if (rejectedRoleIds.length === 0) return true;

      return (
        staffObject.role &&
        rejectedRoleIds.includes(staffObject.role.id) === false
      );
    },
  },
};
</script>

それぞれの props の説明は下記のようになります。

props 用途
buttonText ボタン中央に表示するテキスト
buttonType ボタンの class を利用するための文字列
isDisabled ボタンの非活性フラグ
rejectedRoleIds ボタンの押下制限を行うユーザーの role ID の配列

Composition API に書き換える

先程のコードの仕様をほとんど変更せずに Composition API に書き換えると次のようになります。

<template>
  <button
    :class="{ [`btn-${buttonType}`]: true }"
    :disabled="hasPermission === false || isDisabled === true"
  >
    <span>{{ buttonText }}</span>
  </button>
</template>

<script>
import { computed, defineComponent } from '@vue/composition-api';

export default defineComponent({
  name: 'ButtonBase',
  props: {
    buttonText: { type: String, required: true },
    buttonType: { type: String, default: 'primary' },
    isDisabled: { type: Boolean, default: false },
    rejectedRoleIds: { type: Array, default: () => [] },
  },

  setup(props, context) {
    const store = context.root.$store;
    const staff = store.getters['staff/staff'];

    const hasPermission = computed(() =>
      judgeHasPermission(staff, props.rejectedRoleIds),
    );

    const judgeHasPermission = (staffObj, rejectedRoleIds) => {
      const staffObject = staffObj;
      if (typeof staffObject !== 'object') return false;

      if (rejectedRoleIds.length === 0) return true;

      return (
        staffObject.role &&
        rejectedRoleIds.includes(staffObject.role.id) === false
      );
    };

    return {
      hasPermission,
    };
  },
});
</script>

変更前は、this.staff としてユーザーの情報を取得していたところを const staff = store.getters['staff/staff']; として this を使わないので簡単です。
setup の中に関数をシンプルに記述できる点でも便利ですね。

今回の変更について

今回、変更したのはシンプルなコンポーネントだったため、劇的に変わった点はありませんでした😅

defineComponent() を使った記法ではなく、<script setup /> を使った setup 構文というものを使えば、もっとシンプルな記述ができるようなので次回以降の記事で解説していきたいと思います!