はじめに
こんにちは、メルペイでフロントエンドエンジニアをしているnaughtLdyです。
メルペイではお客さまや加盟店さまからのお問い合わせに対応するために、専門のチームと専用のアプリケーション(以降、カスタマーサービスツール)があります。カスタマーサービスツールはメルペイの Frontend チームで開発しており、お客さまからのお問い合わせなどに対応するための機能(CS)と加盟店さま向けの機能(MS)を持っています。
当初はそのアプリケーションをお客さまと加盟店さまそれぞれの体験に責任を持つ2チームで開発をしていました。そのため、それぞれのリリーススケジュールを考慮しながら開発する必要がありました。
カスタマーサービスツールは、メルペイの機能追加に合わせて柔軟にオペレーターに機能を提供する必要があります。アプリケーションを分けた方が開発効率が上がり、安定して機能提供ができると判断しアプリケーションを分割することにしました。
アプリケーションの分割に際して、次のような前提条件がありました。
- この作業の進行状況にかかわらず分割のリリース日は決まっている
- この先のタスクでデザインの全体的な変更がある
- オペレーターの運用を止めることはできないのでダウンタイムを短くする
また、今回のプロジェクトで達成したいことは以下です。
- アプリケーションは分割したいが、コンポーネントなど共通化できるものは極力共通化したい
- 今後サービスが成長しても柔軟に対応できる下地は作りたい
- 開発用に使用しているライブラリも共通化したい
メルペイのフロントエンドでは Nuxt.js を採用しています。この記事では Nuxt.js で構築したアプリを複数に分割するとき、コンポーネントをどのように共通化したかについて書きます。
コンポーネント分割で参考にしたもの
Vue.js のコンポーネントライブラリをいくつか見たところ Vuetify が提供している仕組みが自分たちのやりたいことに近かったので参考にすることにしました。
Vuetify は、ローダーやプラグインなどを提供していて導入が簡単なので、分割後に共通化したコンポーネントの読み込み部分の仕組み作りで参考にしました。Vuetify はES5へのトランスパイルをしていますが、内製ツールで使用者が限定されているため、共通化するコンポーネントはトランスパイルはせず、使用するアプリ側で最適化設定に任せるようにしました。
コンポーネント分割までにやること
コンポーネントの命名規則の見直し
Vuetify はコンポーネント名が v-
で始まる規則になっていて、 ローダーでは v-
で始まるコンポーネント名の場合は、 Vuetify のコンポーネントを適用するようにしています。
この部分は、ローダーを作るときに参考にしました。
分割後、ローダーを使用してコンポーネントを読み込むことを想定し、リファクタリングをしました。
最初はアプリ内で使用しているコンポーネントを見直して、 共通化できるコンポーネント
と 分割後のそれぞれのアプリ固有のコンポーネント
に分けました。
そして 共通化できるコンポーネント
は base-
で始めるという命名規則を作り、ソースコード内のコンポーネント名を変更していきました。
今回の分割に合わせて共通化するものは base-
という命名規則にし、今後のサービス成長に合わせてさらに分割したくなった場合は、 cs-
や ms-
など増やしていけたらいいねという程度の共通認識も持つようにしました。
コンポーネントの読み込み方法の変更
コンポーネント名を変更したら次は、各ファイルで import している 共通コンポーネントをプラグインから一括で読み込むようにし、各ファイルの import 文を削除しました。
plugins/component.ts
import Vue from 'vue'; import BaseButton from '~/components/base/BaseButton.vue'; Vue.component('BaseButton', BaseButton);
のようにして、あらかじめ共通コンポーネントを読み込むようにしました。
ここまで進むと、この状態でアプリを分割することになり、その後に共通コンポーネントを配信する仕組みができた時に、変更範囲は plugins/component.ts
の削除と新しいコンポーネントの読み込みだけで済みます。
これをしないで Fork する形でアプリを分割した場合、各ファイルでコンポーネントを読み込んでいる部分の記述を変更しなければならず分割後の修正が膨大になってしまいます。
コンポーネントで画像読み込む方法の変更
画像はパスで指定していましたが今後URLなどが変わったときに参照できなくなってしまう可能性があります。
そのため、画像はファイルはbase64データとして読み込む方法に変更しました。
分割前
<template> <img src="~/path/to/icon.svg"> </template> <script lang="ts"> import Vue from 'vue'; export default Vue.extend({}); </script>
画像を ~/path/to/icon.svg
で指定していました。
この場合 ~/
の部分は、アプリケーション側のnuxtの設定 build.publicPath
で解決されます。
分割後
<template> <img :src="icon"> </template> <script lang="ts"> import Vue from 'vue'; import icon from './path/to/icon.svg'; export default Vue.extend({ data() { return { icon, }, }, }); </script>
画像をファイルとして読み込みます。
この場合、画像はコンポーネントパッケージ内で完結するようになります。
こうすることで、使われる環境のURLなどを考慮せず汎用的にコンポーネントを使えるようにしました。
コンポーネント開発のための環境を用意する
コンポーネントを分割してしまったため、今までと同じ開発方法ではコンポーネントの変更を容易に確認できません。
コンポーネントの管理ツールとしてStorybookを使用することも考えましたが、コンポーネントをプレビューできれば良いのでcatalogというアプリケーションを作成しコンポーネントの確認できるようにしました。
コンポーネントやローダーなどは、lerna を使用してmonorepoで開発できるようにしました。lerna はひとつのリポジトリで複数の JavaScript プロジェクトを管理しやすくするツールです。
ディレクトリ構造は次のようになります。
app ├─ catalog // デザイン確認用のNuxt.jsで作成されたアプリ └─ pakages └─ @merpay-admin // この下でパッケージごとに管理する ├─ base-compoennt ├─ base-style └─ component-loader
また、配布するパッケージの他に、デザイン確認用のcatalogアプリケーションを作成しました。開発中はローカルでcatalogアプリケーションを起動しておくことでコンポーネントの見た目や動作を確認しながらライブコーディングできます。また、これをwebに公開することで、実装したデザインがどのように見えるかをデザイナーにも確認できるようになりました。
コンポーネント分割
メルペイでは、JFrog を活用して プライベートな npm パッケージを配信する仕組みがあります。今回は、共通化したコンポーネント、プラグイン、ローダー、CSSをパッケージ化して配布するようにしました。
配信されているものは nuxt.config.ts
で設定できるようにします。
nuxt.config.ts
import NuxtConfiguration from '@nuxt/config'; import MerpayAdminComponentPlugin from '@merpay-admin/component-loader/lib/plugin'; const config: NuxtConfiguration = { css: [ '/path/to/v1/css1.css', '/path/to/v1/css2.css', ], plugins: [ new MerpayAdminComponentPlugin({ version: 'v1', }), ], }; export default config;
このように plugins
に数行書くだけで、共通化したコンポーネントを読み込めるようにしています。
version
が指定されているのは、後に控える新デザインに対応するときに version: 'v2'
のような形で切り替えられるようにするためです。
こうすることで、分割した各アプリケーションでバージョンを指定してより柔軟にデザインを選択できるようになります。
まとめ
以上の方法で、アプリケーションに共通するコンポーネントを分離しつつも各アプリでデザインを共有できる状態を作りました。
現在は新デザインに合わせてコンポーネントのカタログを作成し、エンジニア – デザイナー間でのデザインに関するやりとりをできる状態を目指して改善に取り組んでいます。