こんにちは、駅メモ!でフロントエンドを良い感じにしたかったチームの id:yunagi_n です。
今回は、駅メモ!にて使用している Vue.js を 2 系から 3 系へあげて行くに当たって、採用した手法とマイグレーションプロセスについて紹介します。
今回、マイグレーションするに当たって、以下の要件がありました:
- 機能開発を止めてはいけない
- 駅メモ!では 6 月と 10 月に周年リリースがあり、それの開発を止めるわけにはいきませんでした
- もちろん、その間にあったイベントなどについても、開発は継続し続けています
- 多くのメンバーは割けない
- 基本はわたしが中心に、追加で 1 人〜2 人に手伝ってもらうことはありました
また、参考のため、駅メモ!のフロントエンドの規模感を紹介しておくと:
- Vue コンポーネント数は 1500 コンポーネント
fd --type file --extension vue | wc -l
にて算出 *1
- フロントエンドのコード全体は 4700 ファイル、16 万行
tokei .
にて算出 *2
といった感じでした。
規模感で言えば超大規模、またフロントエンドにおいてはテストなども一切存在していなかったため、かなり厳しい作業となることが予想されました。
ただ、駅メモ!では、歴史的な理由から外部の依存パッケージが少なく、サードパーティーパッケージの更新による影響は避けられました。
しかしながら、パッケージ数による影響は無いですが、 Babel や Webpack などの依存関係は導入当時以降アップデートされておらず (導入当時は、つまりサービス開始日より前です)、そのままでは 2023 年に開発されたパッケージの一部はバンドル出来ない状態でした。
また、今回は Vue.js 公式から提供されている Migration Build は使用していません。
これは、超大規模となった場合、一度入れてしまったライブラリは抜くことが困難であることや、かえって効率が低下する可能性があったことからです。
これらの状態から、 Vue 3 環境へとマイグレーションするため、以下のような手法を採用しました。
- 新しく Vue.js 3.x で起動可能なエントリーポイントを別パッケージとして作成
- 新しいエントリーポイントが起動でき次第、各種ロジックについて適切にパッケージとして切り出す (monorepo)
- 各パッケージは
main
とexports
フィールドを用いてビルド成果物を出し分けるmain
には古いエントリーポイントを対象としたコードを (Webpack が古すぎてexports
を認識できないため)exports
には新しいエントリーポイント、もしくは新しいパッケージを対象としたコードを
- 各パッケージは
- 徐々に現在のエントリーポイントに属する部分から新しいパッケージへと分離する
- 純粋なロジックは単純な JavaScript パッケージとして
- Vue に関わるものはコンポーネントライブラリとして
- 最終的には現在のエントリーポイントは廃止し、削除する
といった形です。
それぞれ、なぜこのような手法を取ったのか詳しく補足していきます。
新しく Vue.js 3.x で起動可能なエントリーポイントを別パッケージとして作成
これは、以下の理由からになります
- 現在のエントリーポイントにあたる Babel や Webpack では、まずアップグレードプロセスが必要になる
- ただ、アップグレードについては過去業務委託の方に調査をお願いしたが、現実的な時間では不可能という結論となった
- フロントエンドの開発体験が著しく悪い (初期開発ビルドまで数分、リロードまでも数十秒) という話があり、マイグレーションするにあたって障壁となる
- 時間的制約が厳しい中で、1 イテレーションに対して数十秒かかるのは進行スピードに大きな影響を与える
これらの問題を解決するため、既存のエントリーポイントに追加や置き換えをするのでは無く、新たにパッケージを切り出すことで、上記問題を解決することが可能でした (少々力尽くですが......)。
また、このタイミングで今までは超巨大な 1 パッケージだったのを monorepo として切り出すため、以下のような整備も行いました。
- Turborepo の導入
- Yarn Workspaces の導入
monorepo としては Yarn Workspaces を入れるのは良く聞きますが、 Turborepo を入れたのはなかなか当時としては珍しい構成だった記憶があります。
Turborepo を導入したきっかけとしては、依存グラフを用いて必要なパッケージだけでコマンドが実行できる点、キャッシュ機能により変化がないパッケージについてはビルドがされない点などです。
新しいエントリーポイントが起動でき次第、各種ロジックについて適切にパッケージとして切り出す
こちらは、以下の理由があります。
- 上記に繋がるが、 Babel が古すぎて ES2015 の一部文法のみが使用可能であり、生産性が低い
- 現状のエントリーポイントにあたる部分では、循環参照などが当たり前に起きており、バンドル時に明らかに不要なファイルなども含まれていた
これについては、パッケージを分けたことで、単純に新しい文法とスピード感あるイテレーションをやりたい、の他にも以下の利点がありました。
- Turborepo を採用したので、パッケージを適切に分けることで、ビルドキャッシュがうまく使える
- パッケージマネージャーレベルで循環参照を防げる
- 完全な形で TypeScript の導入が出来る
- 妥協しない形で ESLint や Stylelint 、 Prettier の導入が出来る
とくに、前者は今フロントエンドのビルドにおいて、プロダクションビルドを行った際 10 分程度かかっていた時間が大幅に削減できます。
そして、ここで分けたパッケージについては GitHub Package Registry に Publish しており、そちらを利用することであらかじめビルドしたパッケージも使えます。
また、いままで駅メモ!のフロントエンドでは、今年中頃まで ESLint も Stylelint もまともに運用されていませんでした (もちろん Prettier もありません)。
それらについて、さすがに無いのはコードクオリティ面で問題があるので導入はしたのですが、かなり妥協した設定です。
しかしながら、パッケージとして切り出した部分については、厳密な形で TypeScript や ESLint, Prettier ,必要であれば Stylelint も導入できます。
これによって、副作用的に新しく書かれた部分についてはコードクオリティ面での向上も図れました。
徐々に現在のエントリーポイントに属する部分から新しいパッケージへと分離する
これは開発が常に続いているといった前提によるものです。
例えば、いまわたしがこの記事を書いている時点でもフロントエンドを含む新規開発が行われており、それらを新しく Vue3 で書いて・書き直して欲しい、というのは納期や教育コストなどを考えた場合現実的ではありません。
そのため、現在のエントリーポイントについてはそのままで、 Vue3 で書かれたパッケージを vue-demi
や社内で新たに作成したマイグレーション用パッケージを用いて Vue 2/3 両対応としてビルド可能にし、それを上記パッケージ経由で使用することで、極力同じ使用感で Vue3 パッケージを使用できるようにしています。
例えば、以下のコードは書き換え前のコードです。
<template> <button v-se-player>...</button> </template> <script> import sePlayer from 'example/directives/v-se-player'; // Vue2 export { directives: { sePlayer } } </script>
これらは、次のように書き換えるだけで、 Vue3 にも対応したコードに出来ます。
<template> <button v-se-player>...</button> </template> <script> import { vSePlayer } from '@example-scope/directives'; // Vue3 export { directives: { sePlayer: vSePlayer } } </script>
インポート先を変えるだけですね。簡単です。
このような形で、既存モジュールをパッケージとして切り出し、かつ同じ形式で使えるようにすることで、学習コストがほぼ無い状態のままで移行ができます。
また、新規開発部分については、既存のモジュールを使いつつインポート先を変えるだけで Vue3 にも対応できるので、今後のコストが下げられます。
まとめ
ということで、現在対応している駅メモ!における Vue.js のマイグレーションについて採用した手法とそのプロセスについての紹介でした。
わたしはこの仕事を最後に退職してしまうので、続きは残った人にお願いする形ですが、また次回の id:yunagi_n の記事をお楽しみください。