はじめに
プロダクト改善の業務で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>
で型定義されるため、リアクティブなのか迷わずわかる。
- reactive
- refかractiveどちらでもよいのだが、refに統一した。
- 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', // 必須プロパティは設定不要
+ })
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
-
getter
でprops
を返し、setter
でemit
する
-
- composition-api
-
WritableComputedRef
内で、class-componentと同様のことを行う -
WritableComputedRef
:computed
内にgetter
とsetter
を含めることで書込み可能となる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インスタンスプロパティにアクセスできなくなる。 -
nextTick
やemit
などは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の機能を確認することが増え、この点は勉強になった。
- コンポーネント分割や前述したテスト自動化の必要性も感じたため、今後も継続したリファクタリングをしていきたい。