はじめに
前回に引き続き、Angular でのマルチテナント対応についてご紹介致します。
前編では、URL からテナントを判別し、対応したテナント情報を取得するまでを記載致しました。
本記事では、取得したテナント情報をもとに、tailwindcss を用いたテーマカラーの変更、HTTP ヘッダへのテナント ID の付与を記載致します。
Angular でサブドメインを用いたマルチテナント対応を行いたい方に向けた記事となります。
tailwindcss とは
ユーティリティファーストな CSS フレームワークです。
ユーティリティファーストとは
html の class 属性に CSS プロパティと似たクラス名(ユーティリティクラス)を指定して、デザインする手法です。
例: class 属性にp-4を指定すると、padding: 1rem が実際に指定されます。
環境
Node: 14.17.0
npm: 6.14.13
Angular: 12.1.1
TypeScript: 4.3.5
tailwindcss: 2.2.4
テナント情報と tailwindcss を用いたテーマカラーの変更
tailwindcssでは設定を拡張し、自身で指定した名前と値の CSS クラスを設定できます。
設定を拡張する際、値に CSS カスタムプロパティを使用するよう設定し、Angular 側で取得したテナントのカラー情報から CSS カスタムプロパティを定義します。
CSS カスタムプロパティを通して、値を動的に設定する事により、テーマカラーを変更する事ができます。
tailwindcss の設定例
下記では primary をクラス名として、CSS カスタムプロパティの”–primary-theme-color”値を使用するよう設定を拡張しています。 詳細についてはCustomizing Colorsをご覧下さい。
module.exports = {
theme: {
extend: {
colors: {
primary: "var(--primary-theme-color)",
},
},
},
important: true,
};
CSS カスタムプロパティとは
再利用可能な値を定義できる、CSS の変数のようなものです。
ハイフン 2つ– –で始まるカスタムプロパティ名と、何らかの有効な CSS の値になるプロパティ値を使用することで定義できます。
var()関数の中でカスタムプロパティ名を使用することで、カスタムプロパティの値を使用することができます。
:root {
--main-bg-color: #123456;
}
element {
background-color: var(--main-bg-color);
}
実装手順
tailwindcss の設定を拡張
テナント情報の型(tenant-config.interface.ts)より、primary、accent、warn の 3 色を定義し、CSS カスタムプロパティを使用するようにします。
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: "var(--primary-theme-color)",
accent: "var(--accent-theme-color)",
warn: "var(--warn-theme-color)",
},
},
},
important: true,
};
CSS カスタムプロパティにテナントのテーマカラーを設定する関数を定義
CSS カスタムプロパティに、取得したテナントのテーマカラーを設定する関数を定義します。(2 行目)
// update-theme-variables.ts
export const updateThemeVariables = (
config: TenantConfig,
document: Document
): void => {
for (const [name, color] of Object.entries(config.themeColors)) {
document.documentElement.style.setProperty(`--${name}-theme-color`, color);
}
};
テナント情報を取得後、テーマカラーを設定する関数を実行
テナント情報を取得するサービスに、テナント情報を設定する関数を追加します。(33 行目)
テナント情報取得後、updateThemeVariables関数を呼び出し、テーマカラーを設定します。(35 行目)
// 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";
import { updateThemeVariables } from "update-theme-variables";
@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> {
let config: TenantConfig;
try {
config = await this.httpClient
.get<TenantConfig>(this.configUrl.url)
.toPromise();
} catch {
return;
}
this.set(config);
}
private set(config: TenantConfig): void {
this.config = config;
updateThemeVariables(config, this.document);
}
}
ユーティリティクラスを用いて HTML の class 属性を指定
tailwindcss 設定の拡張により、text-primary, text-accent, text-warn 等のテーマカラーを使用した class 属性が利用可能となっています。
拡張した class 属性を使用して、デザインを構成します。
// sample.component.html
<p class="text-primary">test</p>
<div class="bg-accent"></div>
HTTP ヘッダにテナント ID を付与
Angular ではインターセプターと呼ばれる、HttpClientのリクエスト、レスポンスの処理に割り込むための機能があります。
その機能を用いて、HTTP リクエストを行う際、常にテナント ID を HTTP ヘッダに付与します。
実装手順
HTTP ヘッダにテナント ID を付与する関数を TenantService に定義
HTTP ヘッダにテナント ID を付与する関数を定義します。(14 行目)
// tenant.service.ts
import { DOCUMENT } from "@angular/common";
import { HttpHeaders } from "@angular/common/http";
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);
}
public addTenantToHeaders(headers: HttpHeaders): HttpHeaders {
return headers.append("X-Tenant-ID", this.getTenant());
}
private getTenantForHostname(hostname: string): string {
return hostname.split(".")[0].toLowerCase();
}
}
HTTP ヘッダを付与する、HttpInterceptor を定義
HttpInterceptorインターフェイスで定義される、intercept メソッドを実装し、HTTP リクエストの処理に割り込みます。(15 行目)
intercept メソッド内で TenantService.addTenanttoHeadersを用いて、テナント ID をヘッダに付与します。(19 行目)
import { Injectable } from "@angular/core";
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
} from "@angular/common/http";
import { Observable } from "rxjs";
import { TenantService } from "tenant.service";
@Injectable()
export class TenantInterceptor implements HttpInterceptor {
constructor(private readonly tenantService: TenantService) {}
public intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const headers = this.tenantService.addTenantToHeaders(request.headers);
const newReq = request.clone({ headers });
return next.handle(newReq);
}
}
定義した HttpInterceptor を登録
TenantModuleにHttpInterceptorを登録し、HttpClient が利用できるようにします。(24 行目) 登録後、HTTP リクエストが行われる度、割り込み処理が入りテナント ID が HTTP ヘッダに付与されます。
// 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";
import { HTTP_INTERCEPTORS } from "@angular/common/http";
import { TenantInterceptor } from "tenant.interceptor";
export const initConfig =
(tenantConfigService: TenantConfigService): (() => Promise<void>) =>
() =>
tenantConfigService.load();
@NgModule({
providers: [
TenantService,
TenantConfigService,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [TenantConfigService],
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: TenantInterceptor,
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],
},
],
};
}
}
あとがき
今回ご紹介した方法では、サブドメインをテナント ID として利用していますが、TenantServiceの処理を変更することにより、クエリストリングからテナント ID を取得する事等も可能です、試してみて下さい。
関連リンク
- Angular でマルチテナント対応(1)
- Building a Skinnable, Multi-Tenant Angular Application
- Using Tailwind to Theme Your Angular App
- Customizing Colors
木戸裕貴
私は主にTypeScriptでのフロントエンド開発を担当しております。