AngularでFeature Flagを扱いたい
AngularでFeature Flagを扱いたいことがあったので、機能のオンオフ状態を簡単にUIに反映できるようにします。
準備
今回試したコードは以下のリポジトリにあります。
最終的に出来上がるプロジェクトの構成はざっくり以下のようになっています。
src/app/feature-flag
配下が、FeatureFlag管理機能のモジュール、src/app/features
が各機能を提供するモジュールになります。
src/app
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── feature-flag
│ ├── feature-flag.directive.spec.ts
│ ├── feature-flag.directive.ts
│ ├── feature-flag.module.ts
│ ├── feature-flag.service.spec.ts
│ └── feature-flag.service.ts
└── features
├── feature1
│ └── ...
├── feature2
│ └── ...
└── feature3
└── ...
今回は feature1 ~ feature3
の有効無効を管理できるように作ります。
まずは特定の機能が有効か無効か判定するサービスを作る
最初に、どの機能が有効でどの機能が無効かを判定するためのサービスを作ります。
とりあえず feature1とfeature3が有効、feature2が無効としました。
switch文で決めうち判定していますが、実際には設定ファイルをFetchして判定するようなケースが多いと思います。
このサービスを元に、各機能への動線を塞いだりルーティングを無効にしたりしていきます。
無効になっている機能への動線を塞ぐ
無効になっている機能への動線を塞ぐため、構造ディレクティブを使って無効な機能へのリンクを削除します。
構造ディレクティブとは
Angularには、コンポーネントと構造ディレクティブ、属性ディレクティブと三種類のディレクティブが存在します。
それぞれざっくり説明すると、コンポーネントはディレクティブに見た目がついたコンポーネント、構造ディレクティブはDOM要素を追加したり削除したりとDOMを変更するディレクティブ。属性ディレクティブはDOM要素の属性を変更することで、見た目を変更するディレクティブのことを指します。
今回は無効になっている機能への動線となるDOMを削除するための構造ディレクティブを作ります。
作る
Angularには元々NgIfという、DOMの表示非表示を切り替えるために構造ディレクティブが存在します。
これと合成して、表示非表示を切り替える属性ディレクティブを作ります。
hostDirectivesにNgIfを指定して、ディレクティブ内でinjectします。
FeatureFlagServiceで、指定した機能が有効かを判定して、その結果をNgIfDirectiveに渡すことで、そのDOMの表示非表示を管理できます。
このDirectiveをFeatureFlagModuleのproviderに含め、exportすることでアプリケーション内で使えるようになり、テンプレート内で以下のように使うことができます。
これで、 feature1, feature3は表示されて、feature2はリンクが表示されないようになりました。
機能を読み込めないようにガードしたい
ここまでで、リンクを非表示にすることはできたのですが、/feature1のような直アクセスは防げません。
特定の機能へのアクセスを完全に遮断したいので、Routerレベルでアクセスを防ぐようにします。
canMatchを使う
Angular RouterにはcanMatchという、特定のルートにアクセス可能かどうかを判定して制御するためのガードが存在します。
これを使って、機能が無効な場合、ページをルーティングレベルで開けなくします。
これにて、リンク直アクセスでも開くことができなくなったので、今回の目的達成です🙌
機能を入れ替えたい場合にも使える
今回機能のオンオフ判定に使ったcanMatchですが、他にも機能を入れ替えたいような場合にも使えます。
以下のように feature1というパスが複数ある場合、上から順番に、最初にcanMatchの条件に一致したモジュールが読み込まれます。
{
path: 'feature1',
loadChildren: () =>
import('./features/feature1/feature1.old.module').then(
(m) => m.OldFeature1Module
),
canMatch: [() => false],
},
{
path: 'feature1',
loadChildren: () =>
import('./features/feature1/feature1.new.module').then(
(m) => m.NewFeature1Module
),
canMatch: [() => true],
},
この場合は NewFeature1Module が読み込まれます。
長く運用しているサービスの場合は、機能をリニューアルしたり、ABテストで出し分けたりと、ページを入れ替えたくなる機会がたくさんあると思います。
そんなケースに柔軟に対応できるので、canMatchを活用できるとAngularがもっと好きになると思います。
canMatchについて、詳細はドキュメントを見てください。
最後に
今回はAngularを使って、FeatureFlagをいい感じに扱えるようにしてみました。
良いAngularライフを。
Discussion