はじめに
フロントエンドエンジニアの @mk0812 です。自分は普段BackOfficeというチームで新規機能開発を担当しています。
この記事ではBASEのフロントエンド周りの事例として「Monorepo」を紹介します。 エンジニアの皆さんなら1度は聞いたことあるかもしれませんが、BASEではここ最近Monorepoにしていきました。 具体的にどこをMonorepoにしてるかというとBASEの管理画面にある基本機能とBASE Appsです(下図)。 前者はBASE管理画面で使用する機能を指しており、後者はショップにより充実した設定を追加したり、新しい機能が必要となる時に追加できる機能で、数多く提供されていて、その機能単位もしくはApp単位でMonorepo化されています。
Monorepo とは
「Monorepo」とは単一のリポジトリである特定のプロジェクトコードを全て管理する方法を指します。下図はMonorepo構成であり、リポジトリに対してBASE App1
,BASE App2
...とApp単位、機能単位でディレクトリがあり、その単位でビルド等ができるようになっています。
Monorepoのメリットは下記になります。
- 依存管理がわかりやすくなる
- Monorepo単位で依存パッケージ(dependencies)が選択できる
- 関心ごとを分けられる
Monorepoにした背景
元々フロントエンドの構成としてはマルチエントリーポイントであり全てのコードは1つに合わせて扱われます。個別のビルドはできず毎回全てをビルドする必要がありました。当たり前ではありますがコードが増えていくとビルド時間も増えていきます。
上記で説明したマルチエントリーポイントの構成は下図のようになります。BASE Appごとのディレクトリ(appディレクトリ)にそれぞれのAppや基本機能があります(図でいうApp1、App2、基本機能1)。そのディレクトリに必要なcomponents
,domain
,store
などのそれぞれの役割に応じたディレクトリにわけてエントリーポイントを配置しています。結果的にそれらをまとめてビルドしているので当然ビルド時間はファイル数の増加に伴い増えていきます。
基本的に右に向かって依存していきます。図は一部の構成にすぎないもののどうしても良くない依存関係が発生したりします(例: domainなどのコードはその特定のAppのみで参照するべきで上図の構造だとどのAppでも参照可能である)。
この上図の状態に対して、整理を行い下図のようなMonorepo構成にしました。BASEで使用する基本機能およびappはfrontend/shopadmin
配下に配置し、その機能およびapp単位で特有のcomponents
,domain
,store
を配置しました。また、app共通で使うcomponents
やapi-client
は共通ライブラリとしてfrontend/package
に置きました。こうすることでMonorepo前の依存関係を整理し、マルチエントリーポイントを剥がして単体ビルドが可能となります。これらのことを踏まえてフロントエンドの構成をMonorepoへと移していきました。
Monorepoツールの選定
次はMonorepoをする上でのツールのお話です。様々なMonorepoツールがある中でBASEではturborepoを採用しました。
turborepoを採用した理由は下記になります。
- ローカルキャッシュ、リモートキャッシュの生成・利用
- turborepoでは
node_modules/.cache
のキャッシュを見て、次のビルド時の復元をすることでキャッシュが利用され、不要なビルドがスキップされる(下図)
- turborepoでは
- ビルドパイプラインの実行
- 依存しているタスクを並列実行し、処理の最適化が可能
- パッケージ間の依存関係を考慮した再ビルド
- パッケージごとに設定されたビルドコマンドを呼び出すことで個別のパッケージでビルドが可能
キャッシュなし | キャッシュあり |
---|---|
Monorepo化してみた
実際にMonorepo化の手順を以下に示します。
scaffdogでプロジェクトの雛形を作成
monorepo用のディレクトリを作成するのですが、scaffdogを使用してプロジェクトの雛形を作成します。ここでは仮のAppとして「app-sample-a」を作ります。
? Please select the output destination directory. shopadmin/ ? アプリの名前を設定してください。App管理画面では app- プレフィックスを使用してください。 app-sample-a 🐶 Generated files! ✔ shopadmin/app-sample-a/package.json ✔ shopadmin/app-sample-a/tsconfig.json ✔ shopadmin/app-sample-a/webpack.config.js ✔ shopadmin/app-sample-a/src/index.tsx ✔ shopadmin/app-sample-a/src/components/xxx.vue ✔ shopadmin/app-sample-a/src/store/index.ts
これでMonorepoの雛形が完成しました。
引越し作業
「Monorepoにした背景」で記載した部分の対応です。下図はイメージ図でありBeforeにあるcomponents
,domain
,api-client
を目的に沿ってmonorepo単体のパッケージ(app-sample-a
)もしくは共通パッケージ(frontend/package
)に配置して、Afterにあるような形にもっていきます。配置変換に応じてimport先の修正等もやります(後述するeslintでもimport先が間違っている場合はエラーとして検出されます)。
足りない依存パッケージがないか検出する
上記の引越し作業を行うとビルドは可能となります。しかし、turborepoに依存関係を認識させるために使用しているパッケージを package.json に記述する必要があります。依存パッケージがないと判定されると本来依存してるパッケージより先にビルドが走ってしまい、ビルドが失敗します。
turborepoにあるeslintコマンド(turbo run eslint
)を行うと、足りないパッケージを検出できます。その検出されたパッケージを追加することでエラー周りを解消していきます。(下記はエラーが出た時のイメージ)
# app-sample-a配下でeslint $ turbo run eslint
エラーが検出されると下記のように足りないパッケージを指摘します。
6:1 error 'date-fns' should be listed in the project's dependencies. Run 'npm i -S date-fns' to add it import/no-extraneous-dependencies 7:1 error 'lodash' should be listed in the project's dependencies. Run 'npm i -S lodash' to add it import/no-extraneous-dependencies
上記のエラーが解消されて、動作確認完了したらMonorepo移行は完了です。
結果はどうなったか
先日あったVue Fes Japan Online 2022にて弊社のスポンサーセッションとして「BASEのフロントエンド組織の人数が2.5倍になって起きた変化」という発表がありました。こちらを引用して結果的にどうなったかというと
- turborepoを導入したことによってローカル環境・デプロイサーバー共に 高速化
- フルビルドの時間はそれなりにかかるが、フルキャッシュ、一部だけshopadminの変更があった時のビルド時にかかる時間を削減した。
フルビルド | フルキャッシュ時 | 一部だけshopadminの変更があった時 |
---|---|---|
567sec | 12sec | 85sec |
まとめ
以上、Monorepoの紹介をしました。Monorepoツールは様々あってBASEのフロントエンドはturborepoを採用しましたが、他ツールも検証しながらプロジェクトにあったMonorepoツールを採用してみてください。
またMonorepo化は対応しましたが、まだまだやることはたくさんあります。Monorepo化が完了したこの先としてはパッケージ単体での技術選定などが可能になったのでパッケージ毎で検証しやすくなったと思います。例えば特定のライブラリのアップデート(例: Vueのアップデート)もしくはそのライブラリの代役を探して検証したり(例: Vueのアップデートによって動かないライブラリがあるので代役ライブラリを探す)と、エンジニア目線で色々試行がしやすくなったのではないかと思います。
引き続きBASEではこれからのBASEを一緒に盛り上げて行ってくれる方を随時募集中です!カジュアル面談も実施しておりますので、お気軽にお問い合わせください。
以上、最後までお読みいただきありがとうございました。