はじめに
こんにちは。アーティサン株式会社の木戸です。
前編、後編と 2 回に渡って、Angular でのマルチテナント対応についてご紹介致します。
前編では、URL からテナントを判別し、対応したテナント情報を取得するまでを記載致します。
Angular でサブドメインを用いたマルチテナント対応を行いたい方に向けた記事となります。
マルチテナントとは
マルチテナントとは、同じシステムやサービスを、複数のユーザーが共有して利用する方式です。
SaaS などで用いられており、アカウント名や URL などからユーザーを区別し、それぞれの設定を用いて同じサービスを利用します。
環境
Node: 14.17.0
npm: 6.14.13
Angular: 12.1.1
TypeScript: 4.3.5
マルチテナント対応
フロントエンド側の Angular を用いてマルチテナントに対応するためには、接続した URL 等からテナントを判別し、各テナントに応じた設定情報を取得する必要があります。
今回の例ではサブドメインをテナント ID として利用し、各テナントの情報を取得します。
(テナント情報を取得する API は、別途バックエンドで用意されているものとします。)
例: https://tenant01.example.comの場合、テナント ID は tenant01 となります。
実装手順
マルチテナント機能用のモジュールを作成
マルチテナントを個別の機能として扱うため、モジュールを作成しメインのアプリケーションから分離します。
npx ng generate module features/tenant
テナント情報を表す型を作成
各テナント毎の設定情報を格納する型を定義します。
今回の例ではテーマカラーの情報を格納します。
// tenat-config.interface.ts
export interface TenantConfig {
readonly themeColors: {
readonly primary: string;
readonly accent: string;
readonly warn: string;
};
}
URL からテナント ID を取得するサービスを作成
location オブジェクトから現在接続している URL を読み取り、テナント ID を取得するサービスを作成します。
テナント ID を取得する、getTenant メソッドを定義します。(10 行目)
// tenant.service.ts
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
@Injectable()
export class TenantService {
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
public getTenant(): string {
return this.getTenantForHostname(this.document.location.hostname);
}
private getTenantForHostname(hostname: string): string {
return hostname.split(".")[0].toLowerCase();
}
}
テナント情報取得 API の URL を保持するクラスを作成
テンプレート文字列の形で、テナント情報取得 API の URL を保持するクラスを作成します。
テンプレートの書式をテナント ID に置き換えた URL を取得する、url プロパティを定義します。(10 行目)
今回の例では{tenantId}と書かれた文字列をテナント ID に入れ替えるシンプルな形としています。
例: テンプレート文字列をhttps://sample.example.com/{tenantId} 、テナント ID を testTenant とした場合、https://sample.example.com/testTenantとなります。
// config-url.ts
import { TenantService } from "tenant.service";
export class ConfigUrl {
constructor(
private readonly templateUrl: string,
private readonly tenantService: TenantService
) {}
public get url(): string {
return this.templateUrl.replace(
"{tenantId}",
this.tenantService.getTenant()
);
}
}
API の URL とテナント ID からテナント情報を取得するサービスを作成
API の URL とテナント ID からテナント情報(テーマカラー等)を取得する、load メソッドを定義します。(18 行目)
// tenant-config.service.ts
import { DOCUMENT } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable, Inject } from "@angular/core";
import { TenantConfig } from "tenant-config.interface";
import { ConfigUrl } from "config-url";
@Injectable()
export class TenantConfigService {
public config: TenantConfig | null = null;
constructor(
private readonly httpClient: HttpClient,
@Inject(DOCUMENT) private readonly document: Document,
private readonly configUrl: ConfigUrl
) {}
public async load(): Promise<void> {
try {
this.config = await this.httpClient
.get<TenantConfig>(this.configUrl.url)
.toPromise();
} catch {}
}
}
Angular アプリケーション起動時にテナント情報を取得するようにモジュールを設定
APP_INITIALIZERを用いて、Angular アプリケーション起動時にテナント情報を取得します。(17 行目)
forRoot関数を定義し、マルチテナントモジュールインポート時に API の URL を外部から設定できる形にします。(25 行目)
APP_INITIALIZER とは
設定した関数がアプリケーション初期化中に実行されます。
これらの関数がPromiseまたはObservableを返す場合、Promise が解決されるか、Observable が完了するまで初期化は完了しません。
そのため、APP_INITIALIZERで Promise を返す関数(テナント情報取得関数)を設定することにより、 初期化完了時に、テナント情報が利用できるようになります。
// tenant.module.ts
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from "@angular/core";
import { TenantConfigService } from "tenant-config.service";
import { ConfigUrl } from "config-url";
import { TenantService } from "tenant.service";
export const initConfig =
(tenantConfigService: TenantConfigService): (() => Promise<void>) =>
() =>
tenantConfigService.load();
@NgModule({
providers: [
TenantService,
TenantConfigService,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [TenantConfigService],
multi: true,
},
],
})
export class TenantModule {
static forRoot(templateUrl: string): ModuleWithProviders<TenantModule> {
return {
ngModule: TenantModule,
providers: [
{
provide: ConfigUrl,
useFactory: (tenantService: TenantService): ConfigUrl =>
new ConfigUrl(templateUrl, tenantService),
deps: [TenantService],
},
],
};
}
}
マルチテナントモジュールを Angular アプリケーション本体のモジュールにインポートする
マルチテナントモジュールインポート時に、forRoot メソッドを用いてテナント情報取得 API の URL を設定します。(14 行目)
APP_INITIALIZER により初期化時にテナント情報取得が完了しているため、アプリケーション起動後はTenantConfigService.configよりテナント情報を取得できます。
// app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { TenantModule } from "./features/tenant/tenant.module";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
TenantModule.forRoot("https://sample.example.com/{tenantId}"),
],
bootstrap: [AppComponent],
})
export class AppModule {}
あとがき
後編では、本記事で取得したテナント情報をもとに、tailwindcss でのテーマカラー変更、HTTP ヘッダへのテナント ID の付与をご紹介致します。
関連リンク
木戸裕貴
私は主にTypeScriptでのフロントエンド開発を担当しております。