AngularでFeature togglesを実装する

こんにちは、久しぶりにブログ更新です。
書けそうなネタは色々ありましたがサボっていました。仕事では相変わらずAngularを書いてます。
今回はAngularでFeature togglesを実装する方法を紹介します。

Feature togglesの知識がある前提で解説します。まだ知らない方は参考に記載したブログを参照してください。
今回の実装はRelease togglesです。
サンプルリポジトリ

実装

ディレクトリ構成

Feature toggles用のディレクトリを作成してサービスクラスとディレクティブを作成します。

// ※ Feature togglesに関係のないファイルは省略しています。
src/
├── app
│   └── feature-toggles
│       ├── feature-toggles.service.ts
│       ├── feature-toggles.directive.ts
│       ├── feature-toggles.module.ts
├── environments
│   ├── environment.prod.ts
│   └── environment.ts

トグルの切り替え

今回はenvironment.tsにトグルのフラグを設定して、ビルド時に各環境用のファイルに書き換えます。
この方法はenvironmentの値を変更して再デプロイする必要があります。
デプロイ無しでトグルを切り替えたい場合は、APIを作成して判定する必要があります。

// environment.dev.ts
export const environment = {
  featureToggles: {
    foo: true
  }
};

// environment.prod.ts
export const environment = {
  featureToggles: {
    foo: false
  }
};

サービスクラスを作成する

各トグルが有効か判定するサービスクラスを作成します。
基本的には FeatureTogglesService.isFooFeatureEnabledのように機能単位でメソッドを生やして運用するのがいいと思います。 FeatureTogglesService.isFeatureEnabledは後述するディレクティブ用です。

// feature-toggle.service.ts
import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';

export type FeatureNames = keyof typeof environment.featureToggles;

@Injectable({ providedIn: 'root' })
export class FeatureTogglesService {
  constructor() {}

  isFeatureEnabled(featureName: FeatureNames) {
    return environment.featureToggles[featureName];
  }

  isFooFeatureEnabled() {
    return environment.featureToggles.foo
  }
}

ディレクティブを作成する

*ngIfなどで使用されている構造化ディレクティブを作成して、テンプレートから直接Feature togglesを使用できるようにします。
これにより、コンポーネントのプロパティにセットしたフラグをテンプレートで参照する必要がなくなります。

// feature-toggle.directive.ts
import { Directive, OnInit, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { FeatureTogglesService, FeatureNames } from './feature-toggle.service';

@Directive({
  selector: '[featureToggles]',
})
export class FeatureTogglesDirective implements OnInit {
  @Input()
  set featureToggles(featureName: FeatureNames) {
    this.featureName = featureName;
  }
  private featureName: FeatureNames;

  constructor(
    private vc: ViewContainerRef,
    private tpl: TemplateRef<any>,
    private featureTogglesService: FeatureTogglesService
  ) {}

  ngOnInit() {
    if (this.featureTogglesService.isFeatureEnabled(this.featureName)) {
      this.vc.createEmbeddedView(this.tpl);
    } else {
      this.vc.clear();
    }
  }
}

モジュールを作成する

ディレクティブをエクポートするモジュールを作成します。 FeatureTogglesServiceをシングルトンで作成しているのでモジュールには含めません。

// feature-toggle.module.ts
import { NgModule } from '@angular/core';
import { FeatureTogglesDirective } from './feature-toggles.directive';

@NgModule({
  declarations: [FeatureToggleDirective],
  exports: [FeatureToggleDirective],
})
export class FeatureTogglesModule {}

使い方

// サービスクラス
import { Component, OnInit } from '@angular/core';
import { FeatureTogglesService } from './feature-toggles/features-toggle.service';

@Component({
  selector: 'my-foo',
  template: `<div><div>`,
  styleUrls: ['./foo.component.css'],
})
export class FooComponent implements OnInit {
  constructor(private featureTogglesService: FeatureTogglesService) {}

  ngOnInit() {
    if(this.featureTogglesService.isFooFeatureEnabled()) {
      console.log('foo feature enabled');
    } else {
      console.log('foo feature disabled');
    }
  }
}
// ディレクティブ
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <div *featureToggle="'fooFeature'">
      <p>fooFeature enabled</p>
    </div>
  `,
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  constructor() {}

  ngOnInit() {}
}

参考