[Vue]class-componentをcomosition-apiへ

# はじめに

プロダクト改善の業務でVueのバージョンアップを行っている。
最終的には、vueのバージョンを2から3へアップし、UIライブラリも刷新する予定。
ただ、いきなりvue3に上げるとまともに動かないので、ひとまず、互換性のあるVue2.7に上げ、class-componentをcompositon-apiにする。

本記事はその作業の備忘。

# 今回の対応

前提としてTypescriptベースで、UIライブラリには`element-ui`を採用している。
vue3移行のタイミングで`elemnt-plus`に置き換える予定。

* vue2.6　→　vue2.7へバージョンアップ
* class-component　→　composition-apiへ書き換え

# vue2.7へバージョンアップ

* nodeのバージョンを14.21.3(LTS)にあげ、vueや関連ライブラリのバージョンを上げる

```json:package.json
~~~一部抜粋~~~
"vue": "^2.7.0",
"vue-router": "^3.6.5",
"eslint-plugin-vue": "^7",
"typescript": "~4.7.0",
"vue-cli-plugin-style-resources-loader": "~0.1.5",
"vue-eslint-parser": "^7",

"@vue/cli-plugin-babel": "~4.5.19",
※その他、vue/cli系ライブラリも同様に"~4.5.19"へ上げる

"vue-loader": "~15.10.0",

```

参考

https://github.com/vuejs/vue/blob/main/CHANGELOG.md



# coposition-apiへ


## data

* class-component
    * インスタンスフィールドで定義
* composition-api
    * refかractiveどちらでもよいのだが、refに統一した。
        * reactive
            * 型定義が変わらないので、途中でリアクティブになっているかわからなくなる。
            * 分割代入や再代入があるとリアクティブ性が失われてしまう
        * ref
            * script内でvalueアクセスが必要になるため、対応漏れをIDEエラーが教えてくれる。
            * `Ref<XXX>`で型定義されるため、リアクティブなのか迷わずわかる。

```diff_vue
- private name = ''
+ const name = ref('')
```

## method

* class-component
    * クラスベースのメソッドで作成する：`アクセッサ 戻り値 メソッド名(引数)`

* composition-api
    * 名前付きアロー関数で作成する

```diff_vue
- private int add(num1:number, num2:number) {
-     return num1 + num2
- }
+ const add = (num1:number, num2:number): number => {
+     return num1 + num2
+ }
```

## computed

* class-component
    * `getterメソッド`で作成する。

* composition-api
    * `computed`で作成する。

```diff_vue
- private get labelName() {
-     return `${this.class} : ${this.name}`
- }
+ const labelName = computed(() => `${this.class} : ${this.name}`)
```
   
## props

* class-component
    * `@Props`で定義する
* composition-api
    * `defineComponent`で定義する
    * 型定義で行う方法と引数で定義する方法があるが、簡潔に書ける型定義で統一した。
        * 型定義方法では、`validation`は書けないため、今回の対応で`validation`は削除した。
        * あくまで開発時のワーニング（`[vue warn]`）でしかないことと、
        特定文字列のみを許容するチェックなら、リテラル型で定義することで、　`Volar`のチェックで検知できるため
    * デフォルト値を設定する場合は、`withDefault`でラップする。

```diff_vue
-  @Prop({
-    default: 'txt',
-    validator: function (value: string) {
-      return ['pdf', 'txt'].indexOf(value) !== -1
-    }
-  })
-  private fileType!: string

+ interface IProps {
+   fileType?: 'pdf' | 'txt' // 必須の場合はオプショナル(?)を外す
+ }
+ const props = withDefaults(defineProps<IProps>(), {
+   fileType: 'txt', // 必須プロパティは設定不要
+ })

```

* `validation`の警告エラーはでないが、IDEのエラーで検知できる
![スクリーンショット 2023-06-01 23.46.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3414442/d1aba81c-a953-04fc-05ab-262f487c2df0.png)

## emit
* class-component
    * `@Emit`で定義する
* composition-api
    * `defineEmits`で定義する

```diff_vue
- // 定義
-  @Emit()
-  public input(value: string) {}
-  // 呼び出し
-  private callEmit(value: string) {
-    this.input(value)
-  }
+ // 定義 
+ interface IEmits {
+   (e: 'input', value: string): void
+ }
+ const emit = defineEmits<IEmits>()
+ // 呼び出し
+ const callEmit = (value: string) => {
+   emit('input', value)
+ }
```

補足）`vue3.3.0`から、emitの型定義がシンプルになった模様

■参考

https://github.com/vuejs/core/pull/7992

## 複数のv-model

* v-bindに、`propName.sync`修飾子、emitに`update:propName`の組み合わせで双方向バインディングを行う。
* ※v-modelが一つの場合は、そのままなので割愛。

参考（`.sync`修飾子）

https://v2.ja.vuejs.org/v2/guide/components-custom-events#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90

* ちなみにvue3からは同様のことを`v-model:propName`で行う

参考（`v-model`の引数）

https://ja.vuejs.org/guide/components/v-model.html#v-model-arguments

* class-component
    * `getter`で`props`を返し、`setter`で`emit`する
* composition-api
    * `WritableComputedRef`内で、class-componentと同様のことを行う
    * `WritableComputedRef`：`computed`内に`getter`と`setter`を含めることで書込み可能となる`computed`のこと

```diff_vue
-  get currentPage() {
-    return this.page
-  }
-  set currentPage(value) {
-    this.$emit('update:page', value)
-  }
+  const currentPage = computed({
+   get() {
+     return props.page
+   },
+   set(value) {
+     emit('update:page', value)
+   }
+ })

<!-- 呼び出し元（親コンポーネント）の定義は一緒 -->
<Child
  :page.sync="pageSize"
/>

```

# thisのアクセス

* composition-apiになると、thisへアクセスできなくなるので、`this.$XXX`による、vueインスタンスプロパティにアクセスできなくなる。
* `nextTick`や`emit`などはvueからimportして直接使用できる。
* `this.$refs`は、`ref関数`に設定してから利用する

``` diff
<el-form
  ref="createFormRef"
....
- this.$nextTick(() => {...})
- this.$emit('input', value)
- this.$refs.createFormRef.validate(...)
+ import {ref, emit, nextTick} from 'vue'
+ nextTick(() => {...}) 
+ emit('input', value)
+ const createFormRef = ref<Form>(null)
+ createFormRef.value.validate(...)
```

* サードパーティライブラリはcomposition-api対応のバージョンに上げることでuse関数で利用できる
``` diff
- this.$route.params.name
- this.$router.push(...)
+ useRoute().params.name
+ useRouter().push(...)
```

* サードパーティライブラリがcomposition-api未対応の場合は、`getCurrentInstance`を使用することでthisのプロパティにアクセスできる
    * ただし、`getCurrentInstance`は非推奨。composition-api対応までの一時しのぎと考え、対応されたら使用しない。
    * 今回、`elemnt-ui`がvue2系であったため、一部使用箇所が生じた。`element-plus`に刷新したら修正する。

```javascript
getCurrentInstance().proxy.$notify(...) // elment-uiの通知コンポーネントを定義する
```

# その他注意事項

* thisのアクセスがなくなったことで、関数内ローカル変数とフィールド変数が競合する。注意してローカル変数をリネームする。
* ref変数に置き換えたらvalueアクセスが必要になるので、基本的にはIDEエラーで検知できるが、条件判定に利用している箇所は検知しないので注意。

```javascript
// isVisible.valueが正しいが、IDEエラーが出ないので見落としがち。
if(isVisible){}
```
* reactive変数へのオブジェクトコピーは元々のリアクティブが失われるので注意
    * オブジェクト内のプロパティ単位に値を設定する必要がある
    * ref変数なら、value経由でコピーオブジェクトを代入できる。（その点でもrefが優位！）
```javascript
const obj = reactive({...}) 
// NG 分割代入だと、リアクティブが解除されてしまう。
obj = reractive(Object.assign({}, obj, data))
// NG コピーしたractiveオブジェクトを代入すると、リアクティブが解除されてしまう。
obj = reractive(Object.assign({}, obj, data))
// OK オブジェクト内のプロパティを走査して個別に設定する。
Object.keys(data).forEach((key) => (obj[key] = data[key]))

// OK ref変数なら問題ない
const obj = ref({...}) 
obj.value = Object.assign({}, obj, data)
```

# 所感

* 修正自体はほぼ機械的に行え、ハマりポイントも上述した注意事項くらいで、割とすぐに解決した。
* ただ、レグレッションテストが大変だった。
    * cypresのE2Eテストがあったので機能の確認はできたのはよかったが、
    * UIテストがなかったので手作業で確認した。jestによるテストケースの重要性を痛感した。
* リファクタリングをする中で、class-componet、composition-apiの機能を確認することが増え、この点は勉強になった。
* コンポーネント分割や前述したテスト自動化の必要性も感じたため、今後も継続したリファクタリングをしていきたい。