こんにちは。LayerX バクラク事業部 バクラク請求書受取・仕訳チーム エンジニアの coco です。最近人生で初めてクリスマスにプレゼントをするということをしました。喜んでもらえると嬉しいものですね。
この記事は LayerXテックアドカレ2023 47日目の記事です。 前回は同じく バクラク請求書受取・仕訳チーム の noritama さんが 「BurpSuiteを使ってサクっとWebアプリケーションの脆弱性診断を実施する」を書いてくださいました。 次回も同チームの wataru さんが書いてくれる予定なのでご期待ください!
今回は バクラク請求書受取・仕訳チーム で行った Vue3 移行作業のうちの VeeValidate v4 の破壊的変更を互換コンポーネントを用意して乗り切った方法についてご紹介します。全体的な状態と進行は主に tatane さんの 「バクラクの Vue3 移行戦略と詰まったポイント」 を参考にしていただければと思います。
Vue3 化の対応方針
Vue3 化の作業は2023年の7月からスタートしました。
Vue2 の EOL(end of life) は2023年の末と半年ほど残っている状態でしたが、バクラク請求書受取・仕訳 はバクラクシリーズの中でも最も歴史が長いこともあり、Nuxt や使用しているライブラリのバージョンが最も古く、不確実性が高いことから最速で開始できたこの時期に開始しました。
当時バクラク請求書受取・仕訳 ではインボイス制度対応の機能が次々とリリースされ、主にそちらにチームのリソースを割いていたため新機能開発を止めて Vue3 化に注力するという選択肢は取れない状態でした。またQAチームのリソースも限られているため慎重に検証し、なるべく影響範囲が小さくなるようにリリースを重ねていきました。
VeeValidate v4の破壊的変更
VeeValidate は vue の form validation 用のライブラリです。VeeValidate は Vue3 化に伴って v3 から v4 にアップデートする必要があり、VeeValidate の v3 と v4 の IF と機能の違い の違いから バクラク請求書受取・仕訳 で多くの破壊的変更が必要でした。
具体的には機能の違いによって全ての VeeValidate の使用箇所のロジックの変更。さらに IF の違いからコードの変更箇所も多くなります。
バクラク請求書受取・仕訳 ではほぼ全てのフォームで VeeValidate を使用しており、以下の点で全てのフォームに破壊的変更を加えるのは現実的ではないという判断になりました。
- フォーム数が多く、全箇所を変更した場合の検証に要するQA工数を確保できない
- ユーザー影響があった場合の深刻度が大きい仕訳画面で使用されている
Vue2, Vue3 で破壊的変更がない validation ライブラリがあればそちらへの移行も検討したかったのですが、そういったライブラリは見つかりませんでした。
そもそも VeeValidate が v3 で実装していた IFを維持できずに破壊的な変更をせざるを得なかった理由を調査したところ Vue 内部の VNode の API が大きく変わったことによって v-model が適用されている場所を検知できなくなっていることが主な原因だということがわかりました。
v-model が適用されている場所を検知して、ユーザーの入力による inputイベント等をトリガーにvalidationを実行していたため、validationの実行タイミングが掴めなくなっていたわけです。
しかし VeeValidate v3 のソースコードを読んだところ以下の点から validation の実行タイミングを自前で用意することで VeeValidate v3 の使用箇所を部分的に移行していくことができるのではないかと考えました。
- validation 用の関数は切り出されており、vue の reactive system に依存していない → 好きな場所から好きなタイミングで validation ロジックを実行できる
- ValidationObserver と ValidationProvider の連携がVeeValidateを識別する文字列を provide/inject することで行われている → 同じ文字列を provide/inject することで VeeValidate と連携することができる
VeeValidate 互換コンポーネント
前提として バクラク請求書受取・仕訳 のユースケースにおいて互換性を持たせているのでVeeValidateの全ての機能で互換性があるわけではないです。
validation 実行タイミング
ユーザーが入力を行ったタイミングで validation の実行を行いたいので form に bind している値をwatchして、変化が起こったタイミングで validation を実行することにしました。
注意する点としては inputイベント等の入力を検知しているわけではなく、reactive な値の変更を検知しているため、補完機能等によって値が上書きされたタイミングでも validation が実行されてしまいます。
バクラク請求書受取・仕訳では補完したタイミングで validation の実行結果をリセットするような仕様になっていたので実行結果をresetする関数を提供し、補完後にそれを呼び出す形で対応しました。
export const useLxValidationProvider = <T extends string | number | boolean | string[] | number[] | null>( value: ComputedRef<T>, ... ): LxProvider => { ... const reset = async () => { // validation 状態の reset } const lxProvider: LxProvider = { ... reset, } // valueの変更を監視し、validationを実行する watch(value, () => validate()) return lxProvider }
VeeValidate との連携
前述した通り VeeValidate が ValidationObserver, ValidationProvider の連携に使用してる provide/inject 用の文字列を使用してオリジナルのコンポーネント(LxValidationObserver, LxValidationProvider)の連携を行っていきます。
provide/inject で使用されている文字列は非公開の内部で使用されているものですが、VeeValidate v3 は2022年の7月から更新されておらず、今後も更新がされない可能性が高いことから使用しても問題ないと判断しました。
export const useLxValidationObserver = (): LxObserver => { ... // VeeValidate の Provider を拾えるように VeeValidate の InjectionKey を使用する const providedVee = inject<VeeLikeObserver | null>(VeeValidateInjectionKey, null) if (!providedVee) { provide<VeeLikeObserver>(VeeValidateInjectionKey, veeLikeObserver) } } export const useLxValidationProvider = (...): LxProvider => { ... // Subscribe 処理 const $veeObserver = inject<VeeLikeObserver | null>(LxValidateInjectionKey, null) if ($veeObserver) { $veeObserver.observe(lxProvider, LxObserverSubscriberType.LxProvider) onBeforeUnmount(() => { $veeObserver.unobserve(lxProvider.id, LxObserverSubscriberType.LxProvider) }) } }
これで VeeValidate と連携することができるようになったので影響範囲を小さくするために段階的に移行することが可能になります。
置き換えを行う順番として以下の二つが考えられます
- ValidationObserver を LxValidationObserver に置き換えたのちに ValidationProvider を LxValidationProvider に置き換える
- ValidationProvider を LxValidationProvider に置き換えたのちに ValidationObserver を LxValidationObserver に置き換える
ValidationProvider から置き換えてしまった場合 ValidationObserver が ValidationProvider に期待している全ての振る舞いに対応させる必要が出てきます。
全ての機能に互換性を持たせたいわけではなく、バクラク請求書受取・仕訳 におけるユースケースで互換性を持たせるので十分であったため、後者の方法で置き換えることで最低限の機能の置き換えで済ませました。
互換性の対応表は以下のようになってます。
この表から分かるように ValidationObserver を一括して LxValidationObserver に変更した後に段階的に ValidationProvider を LxValidationProvider に変更することができます。
ValidationObserver を一括して変更するため影響範囲は大きくなりますが、Observer が主に提供している機能はネストしている Provider の status の集約と reset 機能の提供です。それらの機能に関して VeeVlidate が行っているテストの内容と同じものをオリジナルのコンポーネントでも用意することで QA が全てのフォームを事細かに検証せずとも、form の status が UI に表示されていることを確認するだけで済むようにしました。
ValidationObserver の置き換えが完了すれば、あとは ValidationProvider → LxValidationProvider を段階的に移行することができるので影響範囲を小さく、リリースを分割して行えます。
結果
結果としてオリジナルのコンポーネントを利用したことで影響範囲を小さくでき、インシデントや hotfix につながることなく安全に移行することができました。 現在もプロダクションで元気に動いてくれています。
インシデントの発生を一番の課題と捉えていたので嬉しい限りです。
また、実装詳細について触れることはできませんでしたが、コンポーネントの IF を VeeValidate とほぼ同じ形にするということもしていました。 その結果、コードの変更箇所が少なくなり、レビューもしやすく、ミスが起こりづらかったように思います。
さらにチーム内で作業を分担することでインボイス制度の新機能にも対応しながら2回のリリース(約1ヶ月)で移行を完了させることができました。
振返り
破壊的変更における影響範囲を小さくするためにオリジナルのコンポーネントを作成し対応するということをしました。結果でも振り返ったようにインシデントにならずに短期間で置き換えることができました。
ですが、オリジナルコンポーネントの開発、テスト実装、動作確認、メンバー共有のためのドキュメント作成など想像以上にやることも多かったため正直もうやりたくはないですね。。。w
今後としては、UXの向上やメンテナンスコストの観点でオリジナルのコンポーネントを別のライブラリに置き換える対応が必要になるかと思います。こちらに関しては移行先のライブラリと共存させつつフォーム毎に段階的に移行させれると思うので時期を見てやっていきたいと思います。
おわりに
今回は バクラク請求書受取・仕訳 で行った VeeValidate の移行についてご紹介させていただきました。
Vue3化だけに留まらず保守性やパフォーマンスの観点での改善など課題は山積みです。。。
バクラク事業部ではフロントエンドのカイゼンをこれまで以上に進めていこうと思っており、プロダクトエンジニアに加えて「フロントエンドEnabling」といったロールの採用も始めています。
ハタラクをバクラクにするプロダクトを一緒に開発していきたい人、スペシャリストとして事業部全体のフロントエンドをカイゼンしていきたい人を大募集中です!もしご興味ありましたら、ぜひカジュアル面談からお話させてください!