MSAL.js+Azure AD v2を触ってみる
はじめに
使用ライブラリは下記のバージョンでお送ります。
msal.js: 1.0.1
SPAっぽいなにかを作りたかったのでAngularも使用しています。
@angular/cli: 8.0.0
ただ、記述するソースコードの殆どはmsal.jsのヴァニラなものを使用しているので
他のフレームワークやバニラJSでも問題なく動作すると思います。
Azureは2019/6時点の画面をキャプチャしています。
それぞれの内容について記事をご覧になるタイミングによっては、画面、仕様が変更されている可能性があるのでご留意くださいませ。
あらまし
マイクロソフトのアカウント認証に便利なJavascriptのライブラリ、msal.js
長らく0.x.xとβ?みたいな扱いだったのですが、いつの間にか(5/4)に1.0.0とメジャーバージョンアップしていたので改めて使い方を復習してみようと思います。
この記事ではmsal.jsの使い方にのみフォーカスを当てます。
Azure AD アプリケーションの作り方とかADのスコープの設定とか
その周りの話については割愛していきます。
Azure AD v2については流れ上触る機会があったので簡単に。。。
単純にサインイン
msal.jsのREADMEにある通り、まずはClientIdのみを指定してログイン処理を実行してみます。
import * as msal from 'msal'; export class SampleAuth { private msalClient: msal.UserAgentApplication; login() { const con: msal.Configuration = { auth: { clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b' } }; this.msalClient = new msal.UserAgentApplication(con); this.msalClient.loginPopup({scopes: ['openid']}).then( res => { console.log(res); } ); } }
単純に書くとこんなところですね。
何はともあれ実行してみましょう。
いつものログイン画面が表示されました。
ログイン後、バッチリアカウント情報も取得できているようです。
注意点-LoginPopup
LoginPopupを行う場合、ポップアップされた画面で認証画面からのリダイレクトが発生します。
しかし、リダイレクト先でUserAgentApplication
インスタンスが生成されていないと
ポップアップ画面が閉じられず残りっぱなしになってしまうようです。
ログイン用のボタン押下でインスタンス生成する処理になっている場合はこの部分で嵌りそうです(ハマった)。
リダイレクトページのコンストラクタなどでインスタンスを生成するようにしておく必要があります。
トークンを取得する
何はともあれトークンを取得しないことには始まりません。
ログインができたのであれば、トークンを取得してみましょう。
acquireTokenSilent
を使用し、openid
のスコープでトークンを取得してみます。
login() { const con: msal.Configuration = { auth: { clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b' } }; this.msalClient = new msal.UserAgentApplication(con); this.msalClient.loginPopup({scopes: ['openid']}).then( res => { console.log(res); this.msalClient.acquireTokenSilent({ scopes: ['openid' ] }).then(token => { console.log('get token'); console.log(token); }); } ); }
トークンが取得できました。
The provided value for the input parameter 'response_type' is not allowed for this client....
上記のようなエラーが出る場合は認証に使用しているADアプリケーションの暗黙的フローの許可がされていない場合があるので確認してみてください。
生成されたトークンがどのようなものか
JSON Web Tokens - jwt.io で確認してみます。
iss
でADのテナントID、aud
で使用したADアプリケーションIDが設定されていることが確認できます。
取得したトークンで、ASP.net Coreで構築した認証付きのWebAPIにアクセスしてみます。
しかし、APIにアクセスはできませんでした。
それは当然で、生成されたJWTのaud
は認証用に使用しているAzure ADアプリケーションのClientIdで
APIの認証で使用されているAzure ADアプリケーションのClientIdとは異なるからです。
WebAPIのClientIdは "ClientId": "792dd3ef-a306-4288-b12d-5aff71f16193"
が指定されています。
WebAPI側のClientIdを変更してもいいのですが、それも芸がないので、別の方法を使用してみます。
AzureADアプリケーションの設定変更
認証で使用しているAzureADアプリケーションで、作成したWebAPIのPermissionを通しておきます。
こんな状態ですね。
スコープの変更
追加されたAPI認証にしているアプリケーションのスコープは下図で取得できます。
早速、スコープを指定し、トークンを取得してみます。
this.msalClient.acquireTokenSilent({ scopes: [ 'user.read' ] })
となっていたものを
this.msalClient.acquireTokenSilent({ scopes: [ '<APIのスコープ>' ] })
のように変更してみます。
これでトークンが取得できました。
注意点-Authorityの指定
Authorityを未指定のままでトークン取得処理を実行すると下記のようなエラーが発生しました。
AADSTS501941: Resource '792dd3ef-a306-4288-b12d-5aff71f16193'(WebApplication1) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant.
標準で使用されている認証のURL https://login.microsoft.com/common
は使用できず、テナントの指定を促されました。
その場合は、msal.jsのインスタンス生成時にauthority
を指定することで回避できます。
const con: msal.Configuration = { auth: { clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b', authority: 'https://login.microsoftonline.com/<テナントのGUID>' } }; this.msalClient = new msal.UserAgentApplication(con);
注意点 - トークンの指定
ADのトークンは仕様として、複数のリソースのトークンを一挙に取得できないようです。
複数のリソースの承認を取得する (Microsoft Authentication Library for .NET) | Microsoft Docs
なので、スコープ指定時に、 scopes: [ 'user.read', '<APIのスコープURI>' ]
のようにリソースをまたがる設定にすると
AADSTS28000: Provided value for the input parameter scope is not valid because it contains more than one resource.
上記のようなエラーとなります。トークンの取得は単一リソースごとに取得が基本ということですね。
Azure AD アプリケーションのエンドポイントを v2の設定に
さて、取得できたトークンを解析してみると、aud
は下図のような状態でした。
ほしいのはClientIdであって、スコープではありません。
Azure AD v2であればClientIDを取得できそうなので(情報元は失念。。。)Azure AD アプリケーションの設定を変更します。
accessTokenAcceptedVersion
の null
を 2
に変更しました。
これで再度トークンを取得してみます。
お望みのトークンを取得できました。
ASP.net Core WebAPIの実装
さて、トークンは帰ってきましたが、Azure AD v2を使用することでissのエンドポイントが変わってしまいました。
(URLの末尾に/v2.0
がついた)
GitHubにサンプルが転がっていたのでそれと同じように実装してみましょう。
と、いってもやっていることは非常に単純で 上記GitHubで管理されているMicrosoft.Identity.Web
をAPIプロジェクトで参照して
startup.cs
の認証部分を少し書き換えるだけですね。appsettings.json
も書き換え必要ありません。
(Microsoft.Identity.Web
はNuGetで公開されていないのですね…)
startup.cs
- services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
- .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
+ services.AddProtectWebApiWithMicrosoftIdentityPlatformV2(Configuration);
msal.jsで取得したトークンで、APIを使用できるようになりました!
まとめ
msal.jsの基本的な使用方法と派生してAzure AD v2の認証方法を抑えることができました。
ただ、Azure ADv2は各所みるにまだまだ開発中の匂いがプンプンしますし
AzureADアプリケーションを使用して認証を行う場合は
v1エンドポイントを使用して、adal.jsを使用するほうがまだまだ無難なようです。
力尽きたので、Azure AD B2Cとmsal.jsの認証は別の機会に記事にしようと思います。。。
Azure Front Doorを使ってみる
はじめに
2019/6/2時点のAzureを使用しています。
記事をご覧頂いているタイミングによっては
記事内に出てくる画像、設定内容などが変更されている可能性があることをご留意くださいませ。
あらまし
先日のde:code19でデプロイ王子のアンプラグドイベントに参加しました。
API Managementが死んだ際の対応についてお話を聞いたところ
AzureFrontDoorもイイよ。と教えていただきました。
が、触ったことないので触ってみよう!というのが今回のあらましです。
この記事では、基本的な設定内容と使い方、キャッシュ機能の検証について記載しています。
なにはともあれ価格帯
個人で触るか会社で触るかの分水嶺ですね。Azure死するのも嫌ですし。
価格 - Front Door | Microsoft Azure
油断してたら結構持ってかれそうですが、時間単位で見ればまぁ普通にお安め。
作ってすぐ潰す検証環境であれば1,000円以内で済みそうです。
作ってみる
Azure クイック スタート - アプリケーションの高可用性を実現するフロント ドア プロファイルを Azure portal を使って作成する | Microsoft Docs
新しいサービスを使うときは基本ドキュメントに則るのが一番かなと思うので
上記のドキュメント通りに設定していきます。
結果非常に簡単に作成&使用できました。
FrontDoor作成
Configurationタブで下図のような設定画面が表示されるのでそれぞれ「+」で設定していきます。
FrontEndHost設定
SESSION AFFINITYはバックエンドなんかでCookieを使用した処理や認証を使用している際に使うものでしょうか。
ひとまず現状はセッション情報は使用しないので Disabled
に設定しておきます。
BackEndPool設定
Backendの登録は悩まずに設定することができました。
HEALTH PROBES
Azure Front Door Service - バックエンドの正常性監視 | Microsoft Docs
上記ドキュメントによると正常性の特定は200コードで判定されるようです。
なので、バックエンドで認証を設けている場合等は疎通試験用の無認証のAPIを用意したほうが良さそうです。
今回自分が作ったWebAPIは認証機構を設けておりませんが、上図のPathの/
は404になるので
使用できるAPIエンドポイントを指定するのが良さそうです。
今回はテストで使用する /api/values
を指定しました。
ヘルスチェックを行うインターバルも指定できるようです。
頻繁にアクセスされるようなリソースはインターバルを短めに設定しておいたほうが良さそうです。
後述しますが、停止~ヘルスチェックのインターバル間にアクセスした場合、停止したエンドポイントにアクセスすることになりそうです。
LOAD BALANCING
Azure Front Door Service のバックエンドとバックエンド プール | Microsoft Docs
負荷分散の設定については今回特に留意することはなさそうです。
Ruleの設定
PATTERNS TO MATCH
パターンについては今回特に指定していないですが 特定のAPIについてのみのアクセス。とかBackendPool/Ruleが複数設定される場合に重宝しそうです。
RouteType
後の検証の関係でRouteTypeはForward
を指定しています。
キャッシュがある場合はキャッシュを使用する。という設定のようですね。Redirect
はキャッシュ関係なしにAPIに直接アクセスする設定のようです。
頻繁に変更があるAPI/めったに変更がないAPIという感じにRule分離させているといいのかもしれません。
Forwardung Protocol/URL Rewriteはがっつり使う段階からになりそうなので割愛
Cachingは後の検証で使用しますが、ひとまず今はDisabled
にして終わらせます。
できあがり
大凡1分くらいでリソースが作成されました。
検証
みんなだいすきPostmanを使って検証してみます。
WebAPIは下記のようにASP.net Coreで雑に文字列が返却されるものをデプロイしています。
[HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "This is", "API One" }; // バックエンド1/2で返却される内容は変えている }
まずは普通に疎通試験してみます。
問題なく使用できてそうです。
優先的にアクセスされるAPIエンドポイントを停止させました。
これで接続されるエンドポイントが変更されるはずです。
設定通り同一のエンドポイントURLで別リソースを参照していることが確認できました!
注意!!
先で設定した通りエンドポイントのヘルスチェックは30秒のインターバルで行われます。
なので、停止後即アクセスすると403が返却されます(されました。焦った…)。
停止直後に403等が返却された場合は時間をおいて再度アクセスしてみると良いかもしれません。
キャッシュ機能の検証
de:code19で王子とお話した際に「キャッシュ機能もあるよ」とお聞きしたので、キャッシュ機能も試してみます。
「まずはキャッシュデータが参照される」というのがデータ取得時の動きなのだと思われます。
以下設定と検証。
まずは、FrontDoorの設定を変更します。
Cachingを有効に
Query string caching behavior
「クエリ文字列を無視」「一意なURLをすべてキャッシュ」の2種があります。
クエリ文字列を使用するURLが指定された場合のキャッシュ動作のようですね。
クエリ文字列を無視してキャッシュするか、クエリ文字列含めたURLでキャッシュするか。といった設定のようです。
ここはひとまずCatche every Unique URL
でクエリ文字列含めたURLでキャッシュするようにしておきます。
検証
クエリ文字列の検証も含めるたいので、Controllerの内容もちょっと変更しました。
[HttpGet(Name = "Get Single Data")] public ActionResult<string> GetSingleData([FromQuery]int id) { return $"This is API One And QuerId={id}!"; }
クエリ文字列で指定された文字列含め返却するよう変更しています。
まずは普通に実行した結果です。
queryで指定された文字列が返却されていることが確認できます。
次にバックエンドのAPIに使用しているWebAppsを停止したうえで実行してみます。
もう一方のバックエンドは参照せずに、キャッシュされたデータが返却されているようです。
query文字列を変更した状態で実行してみます。
こちらはデータがキャッシュされていないので停止されていないAPIの方を参照していることが確認できます。
Ignore Query Strings
の設定にした場合
queryでid=300
を指定して、実行してみました。結果は下図のとおりです。
では、idを変更して実行してみます。
idを変更しても返却される値は変更されず、queryの文字列を無視してキャッシュを行っていることが確認できました。
おわりに
Azure Front Doorを触ってみましたが、簡単に設定/使用することができることがわかりました。
キャッシュ機能については「はじめにキャッシュ」という動き方であるが故に
頻繁に更新が発生するデータの場合などには、すこし気をつけて使う必要がありそうです。
キャッシュが有効な期間とか、リソース使用の優先順位とか、キャッシュについて色々設定できると嬉しいかもですね。
ReactiveFormsで使用できるカスタムコンポーネントを作成する
はじめに
本記事ではAngularは下記Versionを使用しています。
- AngularCLI: 7.3.6
- Angular: 7.2.10
ゴール
Inputコントロールなんかと同様に formControlName
を指定できるような
ReactiveFormsで使用できるComponentを作成します。
今回は「ダイアログ表示ボタン+入力ダイアログ」を一つのコンポーネントとし
そのコンポーネントをReactiveFormsで使用します。
操作イメージは下のGifのような動作イメージです。
「Result」内にあるように、ダイアログの入力内容が反映されます。
ControlValueAccessor
Angularの公式リファレンスによると、ControlValueAccessorを使用して、Valueとコントロールのリンクを行っていると記載されています。
ControlValueAccessorは、下記4つの機能のインタフェースを持つようです。
- writeValue(obj: any) →ModelからViewへの値の書き込み
- registerOnChange(fn: any) →View変更時のCallback
- registerOnTouched(fn: any) → ViewTouch時のCallback
- setDisabledState(isDisabled: boolean) → disabled/enabledをViewに反映する
全容は下記のソースです。
@Component({ selector: 'app-address-input-dialog', templateUrl: './address-input-dialog.component.html', styleUrls: ['./address-input-dialog.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressInputDialogComponent), multi: true } ] }) export class AddressInputDialogComponent implements ControlValueAccessor { private addressData: AddressModel; private isDisabled = false; private onChange: any = () => {}; private onTouched: any = () => {}; constructor( private dialog: MatDialog ) { } openDiaog() { const dialog = this.dialog.open(DialogComponent, { data: this.addressData }); dialog.afterClosed().subscribe(data => { this.addressData = data; this.onChange(data); this.onTouched(); }); } // ↓ControlValueAccessorのInterface群 writeValue(obj: any): void { this.addressData = obj; } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.isDisabled = isDisabled; } }
コントロールの動きとして、ボタン押下時にダイアログを表示
ダイアログが確定されたらデータを反映。という流れです。
そのため、dialog.afterClosed()
でダイアログが閉じられた際に、formcontrolの値変更時のイベントを実行しています。
ダイアログコンポーネント側はオーソドックスなReactiveFormなので割愛します。
何が嬉しい?
下記は作成したカスタムコンポーネントを使用しているコンポーネントのソースです。
export class MyFormGroupFieldComponent implements OnInit { public fg: FormGroup; constructor( private fb: FormBuilder ) { } get formResult() { if (typeof(this.fg) !== 'undefined') { return JSON.stringify(this.fg.value); } else { return ''; } } ngOnInit() { const initAddressData: AddressModel = { city: '', prefecture: '', zipCode: ''}; this.fg = this.fb.group({ userId: ['', Validators.required], familyName: ['', Validators.required], lastName: ['', Validators.required], address: [initAddressData] }); } patchData() { const initAddressData: AddressModel = { city: '港区', prefecture: '東京都', zipCode: '000-0000'}; const data: MyFormGroupModel = { userId: 'devtakas', familyName: 'familyname', lastName: 'lastname', address: initAddressData }; this.fg.patchValue(data); } }
色々と書かれているように見えますが
初期値を設定したり、固定データを反映したり、画面上に表示する文字列を作成しているだけです。
なので、「ダイアログ開いたときは値を反映して~」とか「閉じたときは変数に値を格納~」みたいな処理は記述しておりません。
@ViewChild
で子コンポーネントのプロパティにアクセスする。なんてこともしていないです。
コンポーネントの処理記述が非常に簡潔になります。
またformControlNameをカスタムコンポーネントに指定することができるため
Htmlテンプレートも下記のようにシンプルに記述できるようになります。
<form [formGroup]="fg"> ... <div> <app-address-input-dialog formControlName="address"></app-address-input-dialog> </div> ... </form>
formに対して構造体の値そのままでやり取りできるのも良いポイントですね。楽。
参考ページ
Angularでカスタムフォーム(CustomFormControls)を作る(1/2) - Qiita
最後に
今回作成したデモは下記に格納しています。
デモソースのごった煮ですがご容赦ください ><
NLogを.Net Standard/Coreで使用する
はじめに
.Net Core等は下記バージョンでお送りします。
- netstandard: 2.0
- netcoreapp: 2.2
- AspNetCore: 2.2.0
- NLog: 4.6.3
あらまし
NLogを使うとかは今更ではあるのですが
.Net CoreのExeアプリケーションからASP.net WebAPIアプリケーションで
NLogを使用する機会があったので、学習ついでの備忘録な感じのトピックです。
NLog.configの設定を外に出す
基本的な使い方は本家のGitHubやググったら山程でてくるので割愛…
NLogのログ出力の設定は、nlog.config
のXML形式ファイルに記述していく感じです。
が、これはいまいち好きじゃない。
と、いうのもASPにしてもCoreの普通のアプリにしてもjsonファイルにアプリケーション設定を記述していますし
あちこちのファイルに設定内容が散らばっているのも少し邪魔くさい。
ASP.netに関してはWebAppsがもっているアプリケーション設定で出力先をいろいろ設定できるようにすれば
CI/CD側の負担も軽くなるんでは?と思ったわけです。
(nlog.configのようなXMLファイルの設定もWebAppsの設定上でできるのであればいいのですが…ない?ですよね?)
NLog.configの構成
まずはXMLで設定されるNLogの構成を見てみます。
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="console" xsi:type="Console" /> </targets> <rules> <logger name="*" minlevel="Info" writeTo="console" /> </rules> </nlog>
これはLogの内容をConsoleに吐き出す設定ですが、target
で「何に対して」ログ出力するか指定し
rule
で 'target' に対して出力するログの内容を指定しています。
この場合、「Info
レベルからのログを Console
に出力する」という設定になっているわけですね。
NLogのTarget/Ruleの関係は、下記サイトが参考になりました。
NLogの設定を動的に指定する
こちらのサイトを参考にしました。
NLogをプログラマブルに初期化し動的に構成変更する - M12i.
NLog.Target
名前空間に ConsoleTarget
というClassが存在します。
NLog/ConsoleTarget.cs at dev · NLog/NLog · GitHub
他にも FileTarget
や DatabaseTarget
が存在します。
Classにあるプロパティを見てみると、Layout
やConnectionString
などXMLで設定するプロパティが存在するのが確認できます。
XMLの設定を確認しながら、TargetClassの同名プロパティに値を設定していく…という方法でNLogの設定ができそうです。
で、出来上がったのが下記です。
public static void ConsoleLogInit() { var conf = LogManager.Configuration; var console = new ConsoleTarget("console"); // consoleターゲットを生成 console.Layout = LogLayout; // アウトプットフォーマットレイアウトを設定 conf.AddTarget(console); // NLogの設定に生成したターゲット情報を追加 conf.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, console)); // consoleターゲットを使用するルールを追加 LogManager.Configuration = conf; // NLogの設定に反映 }
あとは同じように設定するだけ
TargetのClassにたいしてどのようなAPIが存在してどのような引数が求められているのかは
上記のGitHubのソースコードやドキュメントから抑えることができるので
ファイルに出力する場合やDBに出力する場合も同じように設定していくだけです。
ファイルに出力する場合はファイル名称やエンコードを指定するAPIが追加されていたりします。
public static void WriteLogToFileInit(string filePath, LogLevel targetLogLevel) { var conf = LogManager.Configuration; var file = new FileTarget("file"); file.Encoding = Encoding.UTF8; file.FileName = filePath; file.Layout = LogLayout; conf.AddTarget(file); conf.LoggingRules.Add(new LoggingRule("*", targetLogLevel, file)); LogManager.Configuration = conf; }
Databaseはちょっと変わり種でDatabaseParameterInfoのインスタンスにLayoutを設定しないといけません。
public static void WriteLogToSqlDatabaseInit(string connectionString, LogLevel targetLogLevel) { var conf = LogManager.Configuration; var dbtarget = new DatabaseTarget(); dbtarget.ConnectionString = connectionString; dbtarget.Name = "dbtarget"; dbtarget.DBProvider = "System.Data.SqlClient"; dbtarget.CommandText = "Insert Into LoggingTable(" + "Logged," + ") values (" + "@logged," + ")"; var loggedParam = new DatabaseParameterInfo(); loggedParam.Name = "@logged"; loggedParam.Layout = "${date}"; dbtarget.Parameters.Add(loggedParam); conf.AddTarget(dbtarget); conf.LoggingRules.Add(new LoggingRule("*", targetLogLevel, dbtarget)); LogManager.Configuration = conf; }
外部からNLogの設定を行う!
と、いうわけでソースコード上でNLogの設定が十二分に行えることがわかりました。
あとは、ASP.net WebAPIなどのappsettings.jsonなどに適当な設定を作ってあげればいいだけです。
適当に↓な感じでaapsettings.jsonを作って
{ "Logging": { "LogLevel": { "Default": "Warning" }, "OutputToLogFile": { "FilePath": "logfile.txt", "LogLevel": "Error" }, "OutputToDatabase": { "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Logging;Integrated Security=True;", "LogLevel": "Info" } }, "AllowedHosts": "*" }
Startup.cs
あたりで設定ファイルを読み込んでNlogを設定すればいい感じかなと思います。
private void NLogSettings() { Logging.LoggingSettings.ConsoleLogInit(); var fileName = this.Configuration["Logging:OutputToLogFile:FilePath"]; var logLevel = this.Configuration["Logging:OutputToLogFile:LogLevel"]; // NLogの設定を行う~~ }
これでWebAppsのアプリケーション設定への設定のみでログ出力先の挿げ替えなどが簡単に行えるようになりました。
終わりに
今回作成したモロモロのデモは下記リポジトリになります。
ASP.net WebAPIの ActionFilterAttribute
や ExceptionFilterAttribute
使ったり
EntityFrameworkの実行SQLをログ出力したりする実験コードも含んでたりします。
「簡単な」入力状態復帰機能をAngularのReactiveFormsで実装した話
はじめに
Angularは下記のバージョンでお送りします。
@angular/cli: 7.3.9
なにがしたかったか
- 認証にAzure ADを使用している関係で、定期的に(1時間立ち上げっぱなしで1回程度)ページがリフレッシュされる
- 入力内容が消えちゃうのもアレなので入力状態を保存しておいて復帰させたい。
- 巷の入力状態復帰ライブラリはCookieやlocalstorageに保存するものがほとんど
- じゃあライブラリ使わなくてもReactiveForms使ってたら余裕で実装できるかも?
というわけで実装してみよう…というあらましです。
行うこと
- ReactiveFormsの
valueChanges
イベントをサブスクライブします。 - ReactiveFormsはデータを構造体でもっているので、そのデータをJSONにしてlocalstorageに保存します。
- ComponentがInitializeされた段階でlocalstorageにデータが存在する場合は
patchValue
で入力されていたデータを復帰します。
以上。
極端な話、下記ソースコードだけで実現できるということですね。
(もちろん格納データの存在チェックやらは必要なのでこれ+もろもろは必要ですが!)
ngOnInit() { // 起動時にlocalstorageの値をFormsに反映 const stVal = JSON.parse(localStorage.getItem('hogehoge')); this.sampleInputFormGroup.patchValue(stVal); this.sampleInputFormGroup.valueChanges.subscribe(value => { // 変更内容をlocalstorageに保存 const valSt = JSON.stringify(value); localStorage.setItem('hogehoge', valSt) }); }
結果↓のGifのように動きます。
タブを閉じて再度開いても
入力状態が保持されている事がわかると思います。
まとめ
ReactiveFormsは構造体の状態で値の受け渡しができるので
ライブラリなしでも入力状態復帰処理は簡単に実装できますよ!ということがわかりました。
上の図で動いているソースのコードは下記になります。
Component - Service間をRxJSでつなげたりとか
ほんのちょっとだけ複雑になっています。
ごった煮のソースの一部で申し訳ないですが。。。 ><
KarmaやJestのカバレッジをAzureDevOpsパイプラインで収集してみた
昨日、Azure DevOps Tokyo, Japan 2nd impactに参加しました。
その時にAzure DevOps パイプラインでのカバレッジレポートの収集の話になり
自分が今使っているAngularのプロジェクトでも、カバレッジレポートが収集できるか試してみました。
現在、自分がAngularで行っているユニットテストは、KarmaとJestの2パターンあるので
その両方で試してみた結果となります。
はじめに
ちと古いですが、昔JestのテストとCIを組んだ構成を利用したので
Angular Cli:7.0.5で試した結果となります。
パイプラインでのカバレッジレポートのPublish
ユニットテストの結果出力同様、テストバレッジの結果出力も、専用のタスクがあるようです。
タスク名は「Publish Code Coverage Results」となっています。
パイプライン構築GUIで見てみると、下図のような設定画面となっているようです。
YAMLの設定については下記サイトに記載されています。
Publish Code Coverage Results task - Azure Pipelines | Microsoft Docs
ユニットテスト同様、結果出力されたXMLファイル指定するようです。
XMLのフォーマットとしてCobertura か JaCoCoのどちらかが指定されています。
ざっと調べてみたところ、JestもKarmaもCobertura フォーマットのXMLを吐けるようなので
Cobertura フォーマットのカバレッジレポートをパイプラインでPublishしようと思います。
Jest
Jest自身がCoberturaフォーマットのカバレッジレポートを出力する機能を持っているようなので必要最低限の変更で済みます。
AzureDevOps + Angular + Jestのテストパイプラインについては、以前の記事を参考にしてください
package.json
"test:watch": "jest --watch", - "test:ci": "jest --reporters=jest-junit", + "test:ci": "jest --reporters=jest-junit --coverage",
"reporters": [ "default", "jest-junit" ], + "coverageReporters": [ + "text", + "html", + "cobertura" + ]
上記の設定で、npm run test:ci
を実行すると、テスト実行時にcoverageディレクトリにカバレッジレポートが出力されます。
Karma
Karmaも簡単な変更でカバレッジレポートをcoberturaフォーマットで出力できるようになります。
angular.json
"karmaConfig": "src/karma.conf.js", + "codeCoverage": true, "styles": [ "./node_modules/@angular/material/prebuilt-themes/purple-green.css", "src/styles.scss"
karma.conf.js
coverageIstanbulReporter: { dir: require('path').join(__dirname, '../coverage'), - reports: ['html', 'lcovonly', 'text-summary'], + reports: ['html', 'lcovonly', 'text-summary', 'cobertura'], fixWebpackSourcePaths: true },
karma側も同様の構成でカバレッジの結果が出力されます。
Azure DevOpsパイプライン
Azure DevOps側に戻ってきました。
出力したカバレッジレポートのXMLとレポートHTMLを取得するタスクを作成します。
KarmaもJestも、同じ名前のディレクトリに同じ名前のファイルでXML出力されているので同様の設定となります。
タスクのyamlは下記の通りです。
yaml
- task: PublishCodeCoverageResults@1 displayName: 'Publish code coverage from $(System.DefaultWorkingDirectory)/**/cobertura-coverage.xml' inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/cobertura-coverage.xml' reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'
- codeCoverageTool : 今回Coberturaフォーマットを使用したので
Cobertura
を指定します。 - summaryFileLocation : 出力されたXMLファイルを指定します。
- reportDirectory : カバレッジのHTMLレポートのディレクトリを指定できます。
結果
CIログのSummaryタブで全体の結果を確認できます。
CodeCoverageタブでは出力されたHTMLレポートが確認できます
デザインが統一されていないため、すこぶる見にくいです…
ダークテーマで見にくささらにドン。といった感じですね…
最後に
Angularに限らずKarmaやJestのテストフレームワークを使用している場合
コードカバレッジの結果をAzureDevOpsのCIでレポート出力するのはとても簡単だということが分かりました。
これでより良いユニットテストCIライフが送れそうですね!
AngularでQR/バーコードを使用するInputコントロールを作る
12/1 修正しました。
はじめに
今回のお話は、下記バージョンでお送りします
AngularCLI : 7.0.4
↓みたいなコントロールを作成していきます。
テキストボックスがあり
フォーカスが当たった際に、ソフトウェアキーボードではなくQRを読み込むカメラが起動し
カメラでQRが読み込めた場合
フォーカスが当たっていたテキストボックスに、QRから読み取った値を投入する。
といった動作です。
QR/バーコード読み込み
以前の記事でQRコードを読み取るために、@zxing/ngx-scanner
を使用しました。
今回は、そのライブラリからForkされた、@innotec/ngx-scanner
を使用します。
npm install @innotec/ngx-scanner --save
@zxing/ngx-scanner
の@Inputにはformats
が追加されており、CODE39等のバーコードのFormatを指定することで
バーコードのデータも読み込むことができるようになります。
Formatの指定は、@zxing/library
の Enumで管理されている、BarcodeFormat
の配列を使用します。
Selectorはzxingのngx-scannerと同様なので、下記のようにFormatを指定する部分のみが変わる感じです
<zxing-scanner #scanner [formats]="[BarcodeFormat.QR_CODE, BarcodeFormat.CODE_39]"> </zxing-scanner>
ngx-scannerの使い方は、以前書いた記事を参考にしていただければと思います。
AngularでQRする - はまったりひらめいたり…とか…
Angular Material BottomSheet
画面下からにゅっと飛び出てくるコントロールを作るために
AngularMaterialのBottomSheetコンポーネントを使用しました。
準備
まずは、Materialを使用するために、お決まりの
ng add @angular/material
を行っておきます。
@NgModuleでBottomSheeetのモジュールMatBottomSheetModule
をインポートしておきます。
親コンポーネントからの使用
constructorでMatBottomSheetのDIを行うことで、BottomSheetの処理が使用可能になります。
親コンポーネントから、BottomSheetのOpen()
を実行すればBottomSheetが表示されます。
その際、BottomSheet内に表示するComponentを指定することが可能です。
≪MatBottomSheet≫.open(SheetBodyComponent);
この時BottomSheetに表示されるComponentは、表示されるタイミングでインスタンス化されるようなので
LazyLoadするComponentの時同様、NgModuleのEntryComponentに加えてあげる必要があるようです。
@NgModule({ ... entryComponents: [ SheetBodyComponent ], ... }) export class AppModule { }
サービスの注入
QR/バーコードが読み込まれたタイミングで、親コンポーネントがその結果を引っ掛けたいです
ただ、BottomSheet内のComponentは、EntryComponentなので、@Output
あたりで引っ掛けるのも難しそうです。
なのでDIするClassを作成し、その中のSubjectなプロパティを介して
BottomSheet内Componentと、親Componentでデータのやり取りを行う方法をとろうと思います。
親コンポーネント
// BottomSheetをOpen const res = ≪MatBottomSheet≫.open(SheetBodyComponent); ≪DI Class≫.≪Subjectプロパティ≫.subscribe(x => { // QR/バーコードがScan出来た場合 // InputElementに対して取得したQR結果を突っ込む // ElementRefを使用しInputコントロールを取得する const con = <HTMLInputElement>≪ElementRef≫.nativeElement.querySelector('input'); con.value = x; // BottomSheetを閉じる res.dismiss(); });
BottomSheet内コンポーネント
handleQrCodeResult(resultString: string) { // QRが読み込めた時の処理。サービスのSubjectプロパティに値を流す ≪DI Class≫.scanSuccess.next(resultString); }
スクリプト内で特に指定しない限り、BottomSheetはEscキーなり、BottomSheet外をClickするなりしないと閉じないので
値が読み込めた段階で、bottomsheetのdismiss()
を使用して用がすんだら閉じてあげるのが良いかと思います。
bottomsheetのafterDismissed().subscribe()
で、BottomSheetが閉じた時を引っ掛けれるので
BottomSheet閉じた際に何か処理をしたい場合は、そいつを使うと楽にいろいろできそうです。
これで、親ComponentとBottomSheetのComponentでデータのやり取りが可能になりました。
結果
冒頭のGifの通り、QR/バーコードを使用したInputコントロールが作成できました。
おわりに
ソフトウェアキーボードみたいな、フォーカスあてると下からニュッと入力インターフェースが出てくるコントロールを作りたいという思いでBottomSheetを使用しました。
今回はQR/バーコードの読み取りで使用しましたが
Canvasあたりを使用すれば、手書きの入力コントロールも、OSのインターフェースっぽく表現できるかもしれませんね。
今回作成したものは、下記リポジトリで管理しています。
サンプルで作ったもののごった煮ですがご容赦ください。
バーコードとQRの生成は下記のサイトを使用しました。