やりたいこと
エンタープライズ向けのSaaSを構築する上で必ず必要になるマルチユーザー対応。
WEBアプリケーションにおいては、
利用するユーザー(会社)によって挙動・見た目を変える
がいつも課題になっている気がしています。
SPA(Angular)を使った場合、どうしたら開発者もユーザーもハッピーか考えてみる。
やりかた
以下のやり方が考えられるかなーと思います。
①クライアントに設定を持つ
②サーバに設定を持つ
①クライアントに設定ファイルを持つ
Angularの場合だとenvironment.tsのような設定ファイルやSCSSをユーザー別に用意しておき、APIの向き先・挙動などをずらっとかき、ビルド時にユーザーを指定する。
environment-userA.ts
export const environment = {
production: true,
user :"usera",
baseUrl: 'https://usera.application.jp/api/', // APIのベースURL的な
setteiA: 'A',
setteiB: 'B',
...
};
angular.json
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"usera": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.userA.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "sample:build"
},
"configurations": {
"production": {
"browserTarget": "sample:build:production"
},
"userA": {
"browserTarget": "sample:build:usera"
}
}
}
ng build -c usera
PROS
- クライアントサイドだけで完結できる
- 設定をクライアントソースに内包できる
CONS - ユーザーごとにoutputが増え、ビルドのコストが高い
- 設定変更だけでクライアントの修正&再リリースが必要
②サーバーに設定を持つ
初回リクエスト時に基底コンポーネントから初期サービスを同期でリクエスト。
FQDNに応じてユーザーをリクエストボディにセットした上で初期サービスを呼び出し、レスポンスに応じてCSSや設定を注入する。
app.component.ts
import { Component, OnInit, OnChanges } from '@angular/core';
import { SafeHtml, DomSanitizer } from '@angular/platform-browser';
import { InitService } from 'src/app/services/init.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnChanges {
public safeHtml: SafeHtml;
constructor(private domSanitizer: DomSanitizer, private initservice:InitService) {
}
async ngOnInit(): Promise<void> {
// 初期設定サービスを同期で呼び出し。
this.initialsetting = await this.initservice.init();
// 各ユーザー別のCSSを初回ロードして&inject。
this.safeHtml = this.domSanitizer.bypassSecurityTrustHtml("<link rel='stylesheet' type='text/css' href=" + this.initialsetting.cssurl + ">");
}
// 適当に設定を保持(storeなりsessionStorageなり)
}
initservice.ts
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { InitialSetting } from '../models/initialsetting';
@Injectable({
providedIn: 'root'
})
export class InitService {
constructor(
private http: HttpClient,
) { }
async init() {
// host名からユーザコードを持ってくる。mappingはenvironment.tsとかで。
const host = location.host;
const userCd = environment.hostToUserCd[host]
// なにも認証無いとあれなのでヘッダに認証キーを付けとく。
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'my-auth-token'
})
};
let body = {};
body['userCd'] = userCd
const url = 'https://' + host + '/api/initialservice';
return await this.http.post(url, body, httpOptions)
.toPromise()
.then(response => response as InitialSetting)
}
}
environment.ts
export const environment = {
production: false,
hostToUserCd: {
'usera.application.jp':'usera',
'userb.application.jp':'userb',
}
};
PROS
- どんなにユーザがいてもビルド一発で終わる
- 全ユーザが同じソースで動く(CSS以外)
CONS - サーバー側に考慮が必要
- 初期ロードが若干遅い
まとめ
ユーザが少なければサーバーとクライアントが疎な①型、ユーザが多い場合は②がよいかな。
他にいい案あれば教えてください。