1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Vue]class-componentをcomosition-apiへ

Last updated at Posted at 2023-06-01

はじめに

プロダクト改善の業務で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や関連ライブラリのバージョンを上げる
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",

参考

coposition-apiへ

data

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

method

  • class-component

    • クラスベースのメソッドで作成する:アクセッサ 戻り値 メソッド名(引数)
  • composition-api

    • 名前付きアロー関数で作成する
- 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で作成する。
- 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でラップする。
-  @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

emit

  • class-component
    • @Emitで定義する
  • composition-api
    • defineEmitsで定義する
- // 定義
-  @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の型定義がシンプルになった模様

■参考

複数のv-model

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

参考(.sync修飾子)

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

参考(v-modelの引数)

  • class-component
    • getterpropsを返し、setteremitする
  • composition-api
    • WritableComputedRef内で、class-componentと同様のことを行う
    • WritableComputedRefcomputed内にgettersetterを含めることで書込み可能となるcomputedのこと
-  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インスタンスプロパティにアクセスできなくなる。
  • nextTickemitなどはvueからimportして直接使用できる。
  • this.$refsは、ref関数に設定してから利用する
<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関数で利用できる
- this.$route.params.name
- this.$router.push(...)
+ useRoute().params.name
+ useRouter().push(...)
  • サードパーティライブラリがcomposition-api未対応の場合は、getCurrentInstanceを使用することでthisのプロパティにアクセスできる
    • ただし、getCurrentInstanceは非推奨。composition-api対応までの一時しのぎと考え、対応されたら使用しない。
    • 今回、elemnt-uiがvue2系であったため、一部使用箇所が生じた。element-plusに刷新したら修正する。
getCurrentInstance().proxy.$notify(...) // elment-uiの通知コンポーネントを定義する

その他注意事項

  • thisのアクセスがなくなったことで、関数内ローカル変数とフィールド変数が競合する。注意してローカル変数をリネームする。
  • ref変数に置き換えたらvalueアクセスが必要になるので、基本的にはIDEエラーで検知できるが、条件判定に利用している箇所は検知しないので注意。
// isVisible.valueが正しいが、IDEエラーが出ないので見落としがち。
if(isVisible){}
  • reactive変数へのオブジェクトコピーは元々のリアクティブが失われるので注意
    • オブジェクト内のプロパティ単位に値を設定する必要がある
    • ref変数なら、value経由でコピーオブジェクトを代入できる。(その点でもrefが優位!)
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の機能を確認することが増え、この点は勉強になった。
  • コンポーネント分割や前述したテスト自動化の必要性も感じたため、今後も継続したリファクタリングをしていきたい。
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?