Node.js
AngularJS
SPA
SaaS
Angular7

複数ユーザー向けSaaSのベストプラクティスを考える feat.Angular


やりたいこと

エンタープライズ向けの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

1. クライアントサイドだけで完結できる

2. 設定をクライアントソースに内包できる

CONS

1. ユーザーごとにoutputが増え、ビルドのコストが高い

2. 設定変更だけでクライアントの修正&再リリースが必要


②サーバーに設定を持つ

初回リクエスト時に基底コンポーネントから初期サービスを同期でリクエスト。

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

1. どんなにユーザがいてもビルド一発で終わる

2. 全ユーザが同じソースで動く(CSS以外)

CONS

1. サーバー側に考慮が必要

2. 初期ロードが若干遅い


まとめ

ユーザが少なければサーバーとクライアントが疎な①型、ユーザが多い場合は②がよいかな。

他にいい案あれば教えてください。