本記事は Angular Advent Calendar 2021 12日目の記事になります。
Advent Calendar参加2年目ですが、フロントエンドの話をするのは超絶久しぶりです。
6年ぶりくらいにフロントエンドの話をQiitaに投稿します。
はじめに
私事ではございますが、最近転職をいたしました。
入社して最初にDB操作研修とAngular研修があったのですが、私はインフラ要員として入社したので、Angular研修はありませんでした。
Angular研修用の資料等を覗き見して、だいたいのことは今ある知識だけできそうだなと思いました。
ですのでAdvent Calendarに合わせてAngularでアプリを作って、勝手に一人研修を実施することにしました。
そこで、本記事ではAngularアプリ製作の上で苦労した話や気をつけるべきだと感じたことを初心者の皆様に広められるように記載しようと思います。
因みに、Go言語になりますが、バックエンド側の話も記事化しております(読んでくださると嬉しいです)。
筆者環境
- OS: Arch Linux
- Editor: code-server 3.12.0 / vim 8.2
今年の初めまでは、Google Cloud Shell Editorを使っていたのですが、デバッグがローカル環境でできないのは難点でした。
(もちろん、Googleの力を使ってローカル環境と遜色なくデバッグできるようになっていますよ。)
そこで、ブラウザで実行できるOSSなクラウドIDEであるcode-serverに乗り換えました。
自宅にいるときは実質ローカル環境ですし、外出先でもVPN越しで簡単にローカル環境のようにデバッグができますし。
もはやローカルPCにインストールするVS Codeには戻れないですね。
▼Angular関連
Package名称 | バージョン |
---|---|
Node.js | 16.13.1 |
npm | 8.1.2 |
Angular CLI | 13.0.3 |
Angular | 13.0.2 |
typescript | 4.4.4 |
rxjs | 7.4.0 |
IPアドレス計算について
前提としてIPアドレス計算とはなんぞや?という部分について解説しておきます。
フロントエンドエンジニアの方には普段馴染みが無いであろう、ネットワークの世界のお話です。
因みに、IPアドレスの話を始めると1つどころか複数の記事になるので、本記事では一切触れません。
クラウド化されたシステムが当たり前になった現代においても重要なポジションを占めるネットワークエンジニア。
なぜなら、ネットワークというインフラが途切れればそもそもクラウドもクソもないので、その領域を一元的に管理・運用、もしくはもっと上流の企画・設計・構築をしている人たちだからです。
私も開発もできますが、どちらかというと気分的にはインフラよりの人間ですので、過去にはCCNAを取得したこともあります。
CCNAというのはネットワーク機器販売大手のCisco社が独自に行なっているベンダー資格試験です。
AWSの認定資格のようなものだとお考えください。
CCNAの資格試験の中で、IPアドレスが当然たくさん出てきます。
IPアドレスによってネットワークが分割されるやされないやの話を瞬時に理解するために、IPアドレス計算という工程が必要になります。
例えるなら、微分積分を解くために文字列の式変形や素因数分解といった基礎的な数学知識がなければそもそも微分積分は解けないよ、というような話です。
IPアドレス計算は文字列の式変形や素因数分解に該当します。
やり方については以下の記事に譲るとします。
また、本記事をご覧になってネットワークに興味を持たれた方は本を買って勉強してみてください。
ネットワークの話は体系的に記載されている書籍のほうが入門には向いていると個人的には思っておりますので。
折角なので、1冊だけリンクを置いておきます。
小悪魔女子大生のサーバエンジニア日記 - インターネットやサーバのしくみが楽しくわかる
この本は概念や概要だけをふわっと知るために、何も知らない人におすすめできる本だと思っています。
なんでこのアプリを作ったの?
実はこのアプリは6年前に完成していました。
CCNAを勉強しているとき、個人的に練習するために作ったアプリでした。
しかし、ソースコードは散逸してしまい、手元に残っているのは画面設計時に取得したスクショだけになってしまいました。
初期画面 | 問題解答画面 | 結果表示画面 |
---|---|---|
当時はバックエンドはPHP5.6、フロントエンドはjQueryで製作していました。
スクショではすべての画面が見えていますが、jQueryで <div style="display:none">
か <div style="display:inline">
で出したり隠したりして制御していました。
折角なので、このアプリをバックエンドはGo言語、フロントエンドはAngularで再び蘇らせようと思いました。
設計とAngular全体俯瞰
初めに、設計関連の話を記載します。
画面について
最近のフロントエンドの動向はそれとなく追っていたので、画面から作り始めることにしました。
まずは画面設計をして、画面を構成するHTMLを書いて、scssでいい感じにして・・・
本筋ではないのでデザインの話は少しだけ後述することにします。
どういう方針でいくか?
画面が出来上がったところで、Angularでヌルヌル動くようにしていきます。
ここで、どうやって動かくすかについて考えます。
ベストプラクティスが何かはよくわからなかった(初心者ですし)ので、とりあえず昔のアプリと同じく css の display
プロパティを none
か inline
にすることで表示を切り替えようと考えました。
そして、作り始めたのが10月末だったので、調査や学習期間を含めるとかなりギリギリでした。
なので、ルータやモジュールの使用、サブモジュールからのコンポーネント階層構造化等の時間のかかりそうな技術は使用しない方針で設計しました。
結局、Rootの AppComponent
から複数の子コンポーネントが横並びに連なっている構造を採用しました。
方針が決まれば使用する周辺技術の選定です。
Angularのどういうツールを使うか?
最後に、どういったツールを使ってコーディングしていくかを決めます。
・HttpClientModule
これがないとバックエンドとは通信ができないので、使わざるを得ません。
なんか大変そうですけど、腹を決めます。
使ったことのある axios を考えましたが、RxJSとの連携を考えれば HttpClientModule
1択になりました。
・RxJS
当初から最大の鬼門だと思っていたものです。
そして、今回の開発の中で多分一番時間を取られました。
時間泥棒です。
私の過去の経験上、非同期システムは絶対にどこかでハマります。
構造だったり、コーディングスタイルだったり、非同期シグナルの待ち方だったり。
RxJSは折角なので使ってみたかったという理由もありました。
詳しくはハマったほうで記載します。
・固定値
いい感じに、安全に、グローバルで使い回せる定数をどこかに持っておきたいと考えることは、悪いことではないと思います。
ただ、コンポーネント間でも使いまわせる定数を定義できるのか?と思っていました。
探してみると、できるみたいですね。
定数値を書き連ねることはしたくなかったので、 export class
にて用途ごとに管理することにしました。1
・コンポーネント間での値の共有
この選定はRxJSに次ぐ鬼門でした。
難しさというよりも単純に選定に時間がかかりました。
Angularにはコンポーネント間で値を共有する手法は複数あることがわかりました。
-
@Input
,@Output
を使用する -
@ViewChild
を使用する -
EventEmitter
を使用する
弊社の研修資料をチラ見したところ、「 @ViewChild
を使ってね」 とあったので@ViewChild
を使おうかと考えました。
しかし、@ViewChild
は子コンポーネントから親コンポーネントに値を伝達する機能しかないことがわかりました。
今回は子コンポーネントと子コンポーネント間の値の共有が主になるので、これはいただけません。
何か良い方法がないか探し回っていると、公式にも記載のある「Service
を作って、その Service
内部の値を各コンポーネントに依存注入する」というものを見つけました。
これならコンポーネントの親子関係や階層に関係なく値を共有できそうです。
※注意※
今回の私の実装(詳しくは AppSharedService 参照 )はかなり気軽に値の書き換え・出し入れが可能になっています。
なにせ public
で get / set 自由なプロパティを開放しているのですから。
これは私自身ベストプラクティスだとは思っておりません。
大規模開発になればなるほど、この書き方はバグの温床になる可能性を孕んでいます。
なので、本番環境等で使用する際はコードレビュワーや先輩と相談して使用してください。
ただ、個人開発程度の小規模な開発ならこの気軽さは全然ありです。
苦労したところ
設計や技術選定、検証も終わりましたので、コードをガリガリ書いていきます。
が、困ったことになるのは初心者の常です。
(個人的にはこれが一番楽しいんですけどね。。。)
ngコマンドはいずこにおられる?
早速、盛大にわかりません。
さっきインストールしたngコマンドがいません!!
$ npm install @angular/cli@latest
$ ng version
zsh: command not found: ng
原因は?
なぜこの現象が起こるのか、いい加減エンジニア歴7年目になるのでわからなくはないです。
PATHが通っていないからです。
でも、Angularの入門記事等を読んでもどこにも「開発環境に作成した node_module
ディレクトリにPATHを通します」とは記載されていません。
仕方ないので、そのときはグローバルにインストールすることで逃げました。
$ sudo npm install -g @angular/cli@latest
ただ、これだとどう頑張っても開発環境ごとにAngularのバージョンを分けるということができません。
(Dockerや仮想環境等で分けられないわけではないですが、面倒です。)
灯台下暗し
そんなこんなでもやもやしていた数日後。
天啓は突然に降りてきます。
全く別の理由で Zenn.dev さん向けの記事をローカルで執筆していました。
Zenn.dev さんの記事を執筆するときは 📘 Zenn CLI を使っているのですが、こんなコマンドを打ちました。
$ npx zenn new:article --slug test
「待てよ、 📘 Zenn CLI をグローバルにnpmでインストールした記憶はないのになんで実行できるんだ?」
「そうか、これか!!」
$ npx ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 13.0.3
(以下省略)
「あー、ずっと気にしてなかったけど、npx
コマンドってそういう役割があったんだ・・・」
普段何気なく利用している技術でも、よく調べもせずに使用するのは極力避けようと強く決心した瞬間でした。
※おさらい npxコマンドとは? 何ができるのか?
バックエンドとのDebugはどうするの?
最初、バックエンドとの通信についてどうすればよいか全くわからず困り果てていました。
バックエンドのアプリが待ち受けているポートは当然違うポートなので、SOP(Same-Origin Policy)の制約が働き通信させてくれません。
まさかCORS対応を今からする時間もないし、どうすれば・・・
と思っていました。
でも、当時はまだ公式ドキュメントに1ミリも目を通していなかった時期だったので、公式ドキュメントを読みすすめることにしました。
まぁ、当然書いていますよね。
なんとも高機能なことに、デバッグ用のサーバ機能にリバースプロキシなんてついていると。
それはすごい!
なので早速設定してみます。
バックエンド向けの通信をProxyする
まずはProxy設定用のjsonファイルを package.json
等と同じ階層に新規作成します。
名称は何でも良いようです。
ここでは proxy.config.json
としましょう。
{
"/backend": {
"target": "http://localhost:8504",
"secure": false,
"pathRewrite": {
"^/backend": ""
}
}
}
こんな感じです。
これで、 http://localhost:4200/backend/
に接続する通信は http://localhost:8504/
へプロキシされます。
PATHについてカスタマイズしたければ、 pathRewrite
の項目をイジってください。
次に、 angular.json
に設定を行います。
...
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "ip-calc:build:production"
},
"development": {
"browserTarget": "ip-calc:build:development",
"proxyConfig": "proxy.config.json" ←これを追加
}
},
...
上下の行は省略しています。
serve
-> configurations
-> development
に proxyConfig
を追加します。
この設定項目は先程新規作成したProxy用のjsonファイル名です。
もし、違うディレクトリ階層に作成した場合は他の設定と同様、相対PATHで指定可能です。
これで npm start
を実行すると、Proxyされたバックエンドへアクセス可能になります。
ただ、個人的にもう一つ設定しておいたほうが良いと思っている項目があります。
バックエンドへの通信は開発環境と本番環境はおそらく違うものであると思われます。
ですので、environment
ファイルにも記載しておくべきかと思います。
例えば以下のように。
export const environment = {
production: false,
backend: '/backend/v1',
};
export const environment = {
production: true,
backend: '/api/v1',
};
そして、使用時は定数か何かに埋め込んでおきます。
import { environment } from './../environments/environment';
export class Const {
static readonly BACKEND_HOST: string = environment.backend;
}
こうすることで、productionの設定でビルドしたときは勝手に本番環境の値へと差し替わります。
このあたりの「勝手に本番環境の値へと差し替わる」理由や仕組みについては先程掲載した「Angularアプリケーションのビルドとサーブ」に記載されています。
一読してみてください。
Content-Type は誰が設定するの?
さて、ここから先は実装に関する話です。
正直なところ、Angularにおいて構造ディレクティブの話等でわからなかったことはほとんどありませんでした。
(基本的なところで、という限定になりますが。)
なぜなら、公式ドキュメントを読みつつ手を動かせば比較的簡単に理解できるからです。
そういう意味で、ファイル変更監視型のリアルタイムコンパイルは初心者にとって非常にありがたい存在です。
例えば、 [(ngModel)]=
を [ngModel]=
に変化させたらどうなるのか?といったことも直感的に理解できます。
なぜなら、目に見える形で即座にブラウザ上に変化が発生するからです。
しかし、やはりHttpClientModuleはそうはいきませんでいた。
POSTメソッドの罠
POSTメソッド(HTMLのメソッド)を処理してくれるpostメソッド(typescriptのメソッド)ですが、とりあえず公式のAPIリファレンスを確認しておきましょう。
(以降、ややこしいので単に「POST」すると表現します。)
post(url: string, body: any, options: { headers?: HttpHeaders | { [header: string]: string | string[]; }; context?: HttpContext; observe?: "body"; params?: HttpParams | { [param: string]: string | number | boolean | ReadonlyArray; }; reportProgress?: boolean; responseType: "arraybuffer"; withCredentials?: boolean; }): Observable
ふむふむ。
url
と body
が必須値で、 options
にオプションを詰め込んで呼び出せば Observable
何某がもらえる、と。
(オーバーロードについても見てはいますが、ここでは省略します。)
そして、公式チュートリアルで使い方を確認すると、どうやら body
には自分で定義したデータクラスなどを投げ込んでも、よしなに処理してくれる模様。
(チュートリアルでは put
で説明していますが、ほぼ変わりません。)
これは使い勝手が良いですね。
バックエンド側は multipart/form-data
もしくは application/x-www-form-urlencoded
のデータを待ち受けるように設計していたので、私はオプションのヘッダーを以下のように変更しました。
...
const h = new HTTP.HttpHeaders().set('Content-Type', 'multipart/form-data');
return this.http.post<MDL.NextType>(host, body, { headers: h }).pipe(
...
これで、値を投げて実行するだけです。
デバッグ実行してみると特にエラーもなく動いていたので、ほっと胸をなでおろしました。
しかし、DBのデータを見て愕然とします。
なんと、正しく解答したデータをPOSTで送っているはずなのに、エラーが起こった時に処理を止めないための ゼロ値 が保存されていました。
しかも、全てのデータでです。
これはどう考えても通信時に投げているデータが悪いとしか考えられません。
(バックエンド側の事前デバッグとテストはすでにパスしており、正常です。)
投げられているデータが何もない
調査のためにとりあえずPOSTで送る直前と、バックエンド側で値を送られてきた直後のデータを比較してみます。
▼Angular側の console.log
{
"nwaddr_1st": "1",
"nwaddr_2nd": "1",
"nwaddr_3rd": "1",
"nwaddr_4th": "1",
"bcaddr_1st": "1",
"bcaddr_2nd": "1",
"bcaddr_3rd": "1",
"bcaddr_4th": "1",
"elapsed": "20"
}
▼バックエンド側の fmt.Println
{ }
▼Chrome Debuggerのネットワークコンソール(リクエスト側)
content-type: multipart/form-data
うーん・・・
なんで、値、消えんの?
ここから半日を費やして調査してみましたが、結果は全てダメでした。
何も手がかりがありません。
そんなことどこかに書いてあったっけ?
最終手段であるパケットキャプチャを実施する前に、Googleで冗談半分に**「Angular post form multipart form-data」**と検索してみました。
するとそれらしいstack overflowの記事が出てきました。
Angular8 http client form data upload
まさか、な、と思って読んでみると症状は確かに同じようです。
Content-Type: multipart/form-data
でデータを送ると、バックエンド側は常にバッドレスポンスを返しているようです。
回答者の答えはなんと、「 Content-Type: multipart/form-data
を消してみるといいよ」というもの。
そして、どうやらリクエストは Content-Type: application/json
で飛んでくるらしいです。
そんなバカなと思いつつ、Headerオプションを消してからバックエンド側をJSONデータも受けられるように改修して、実行してみました。
▼Angular側の console.log
{
"nwaddr_1st": "1",
"nwaddr_2nd": "1",
"nwaddr_3rd": "1",
"nwaddr_4th": "1",
"bcaddr_1st": "1",
"bcaddr_2nd": "1",
"bcaddr_3rd": "1",
"bcaddr_4th": "1",
"elapsed": "20"
}
▼バックエンド側の fmt.Println
{"1", "1", "1", "1", "1", "1", "1", "1", "20"}
▼Chrome Debuggerのネットワークコンソール(リクエスト側)
content-type: application/json
え、何、お前、jsonしか投げないの?
確かにチュートリアルには options
の値として 'Content-Type': 'application/json'
を設定するように書いてありました。
しかし、それしかダメです、とはどこにも書いていなかったように思います。
なんか、ドッと疲れる幕引きでした。
Angular使いの間では常識なのでしょうかね。
覚えておきます。
追記(2021/12/13)
コメントで情報をいただいたのですが、どうやら body
に与えられた型によって Content-Type
は自動判定されるみたいです。
正確な情報が分かり次第改めて記事を修正したいと思います。
Observerがわからない
Observerがわからないよ
あの変数は何回やってもundefined
awaitで止めようとしても、書き方が違うのか止まらない
subscribeも試してみたけど、undefined相手じゃ意味がない
だから次は絶対勝つために僕はObserverパターンを理解する
例のメロディに乗せて頭の中でぐるぐるしていた時期がありました。
先に結論を述べてしまうと、.NETの Task
のようなものですね。
Rx自体が、元々Microsoftが .NET向けに開発したものなので、さもありなんという感じです。
( .NETに非同期関連の技術が充実した2012~2013年頃よりも前の、2009年からRxの研究が始められ2011年にRxは正式リリース2されています。)
callback地獄だけは嫌なのじゃ
とりあえずネットにある記事をボーッと眺めながら見様見真似でこんなコードを書いていました。
この時は検証段階で、公式のチュートリアルはまだ見ていませんでした。
▼バックエンドとのアクセス側
public getInit(total: number, callback: (p: Models.InitType) => void): void {
const host = this.appConst.BACKEND_HOST + `/v1/init/${total}`;
this.http.get(host).subscribe({
next: (res) => {
callback(res as Models.QuestionSet);
},
error: (error) => {
callback(error as Models.ErrorMessage);
}
});
}
▼コンポーネントからの呼び出し側
import { BackendService } from './backend/backend.service';
import { InitType } from './backend/backend.models';
@Component({
selector: 'app-choice',
templateUrl: './choice.component.html',
styleUrls: ['./choice.component.scss']
})
export class ChoiceComponent {
q_number: number;
onStartClick() {
this.backend.getInit(10, this.callbackInitial);
}
callbackInitial(res: InitType): void {
if (res == null) { return }
if ('id' in res) {
console.log(res.id);
} else {
console.log(res.error);
}
}
}
動くには動いたのですが、これはなんというか、スッキリしない書き方ですね。
Promiseも使っていなければ、async / await も使っていません。
後でこのコードはcallback地獄になることは目に見えていたので、却下しました。
(この段階では、subscribe内で既に非同期処理が行われていることはまだ理解していません。)
そして、この書き方にはもう一つ罠が潜んでいます。
最初は良かれと思って、callback用のメソッドを外出しにしていたのですが、外出しにしたcallbackメソッドの中でクラス内に定義したプロパティ等にアクセスしようとすると、スレッド違いのせいなのかアクセスすることができません。
つまり、こういうことです。
callbackInitial(res: InitType): void {
if (res == null) { return }
if ('id' in res) {
this.q_number = res.id;
// このアクセスはエラーになります
// 確かundefinedかなんかのエラーだったと思います
// なので、this.q_number の値が変更されることはありません
} else {
console.log(res.error);
}
}
ですので、書くのであればcallbackは冗長になりますが、引数として直接記載するほうが良いです。
ちょっと修行してくる
こういう時は素直に公式ドキュメントを読み漁ることが近道だと思っています。
わからないことがたくさんあるけど、実現したい処理がある。
ここで、ネットにある合っているかわからない情報に騙され続けていたら時間が経つばかりだということを、経験上知っています。
なので、RxJSの公式ドキュメントや学習サイトを読み漁りました。
それと、ここでようやくAngularのHttp通信周りのドキュメントも読みました。
結果的にObserverパターンは .NETの Task
のようなものだという知見を得ることができました。
多言語を習得していると、こういうときに思わぬ横のつながりを見つけることがあります。
書き直した結果
Observerパターンにおいては
- subscribeメソッドを呼び出すと非同期処理が動き出す
-
Observable
型の何某を生成したらだいたい勝てる
ということがわかりました。
(超ざっくりですが笑)
数日で勉強した成果をもって書き直します。
▼バックエンドとのアクセス側
public getInit(total: number): Observable<MDL.InitType> {
// 接続先生成
const host = Const.BACKEND_HOST + `/init/${total}`;
return this._http.get<MDL.InitType>(host).pipe(
map((res) => {
// resがundefinedかnullならcatchError側へ
if (res == null) {
throw new Error('undefined');
}
return res as MDL.QuestionSet;
}),
// ※catchErrorについて後述
catchError((err): Observable<MDL.ErrorMessage> => {
if (err == null || 'message' in err || 'status' in err) {
// バックエンド側からエラーメッセージのJSONを受け取れなかったときの処理
const tmp: MDL.ErrorMessage = {
error: 'E999',
message: Err.UNEXPECTED,
};
return of(tmp);
}
return of(err as MDL.ErrorMessage);
})
);
}
▼コンポーネントからの呼び出し側
import { BackendService } from './backend/backend.service';
import { InitType } from './backend/backend.models';
@Component({
selector: 'app-choice',
templateUrl: './choice.component.html',
styleUrls: ['./choice.component.scss']
})
export class ChoiceComponent {
public async onStartClick(): Promise<boolean> {
const init$ = this._backend.getInit(this.q_number);
const params: InitType = await lastValueFrom(init$);
console.log(params);
return false;
}
}
かなりスッキリしましたね。
今回は画面の表示を非同期的に触りたくはなかったので、非同期処理はやめてawaitを用いて同期処理に持ち込みました。
これで同期的に params
で値が拾えます。
非同期にしたければ、この状態で init$
に対して以下のように subscribe
を実行すると非同期に処理が走ります。
init$.scbscribe({
next: (res) => //何か処理,
error: (e) => //エラー時の処理,
complete: () => //完了時の処理,
});
subscribe
についてはこんな感じです。
catchError について
実は catchError
について書き直しているときに少し困ったことが起こっていました。
catchError((err) => {
if (err == null || 'message' in err || 'status' in err) {
const tmp //(省略)
return tmp;
}
return err as MDL.ErrorMessage;
})
最初はこのように書いて、型推論をtypescriptに任せていたのですが、これではエラーとなってしまいコンパイルできませんでした。
なぜなのかよくよく考えてみると、catchError
の中で Observable
を返していないからです。
どういうこと?と思っているかもしれませんので順に説明していきましょう。
catchError
が期待する返さなければならない型は OperatorFunction<T, T | ObservedValueOf<O>>
という型です。
この OperatorFunction という型は Observable
型を継承したインターフェースになっていますので、おそらく型推論ができていないのだろうとあたりを付けてみます。
(結論から述べるとこの推理は実際には間違っています。)
そこで、私が期待する返すべき型を直接書いて教えてあげてみます。
catchError((err): Observable<MDL.ErrorMessage> => {
すると、この様になります。
しかし、まだエラーのままです。
ここで、本質が見えました。
型推論ができていないのだと思っていたら、実際には返すべき型が正しくなかったのですから。
返す値の型が Observable<MDL.ErrorMessage>
ではなく、ただの MDL.ErrorMessage
になっていることがエラーの原因だったのです。
ならば、 Observable
を生み出すように変換してあげましょう。
幸いにも、 RxJSには of
という専用の Observable
を生み出すメソッドが備わっています。
catchError((err): Observable<MDL.ErrorMessage> => {
if (err == null || 'message' in err || 'status' in err) {
const tmp //(省略)
return of(tmp);
}
return of(err as MDL.ErrorMessage);
})
そうして上記のようになります。
型システムのエラー解消方法が少しでも伝わっていれば、幸いです。
非同期で動作し続けるカウントアップタイマー
今回の要件の中には解答時間を測るためのカウントアップタイマーを実装する必要がありました。
自分がどれだけの時間をかけて問題を解いているかやっぱり知りたいですからね。
これも非同期なのでRxJSで実装していくことにします。
もうここまで来たら簡単です。
まずは、RxJS単体で概念実装をしてみます。
ものの30分くらいで完成しました。
やはり概念を理解していると簡単に手が動きます。
コメントマシマシでstackblitzに共有状態で放置しておきますので、良ければ参考にしてください。
(Qiitaでもstackblitzの埋め込みに対応してほしいな・・・)
これをあとはAngularのコードの中に落とし込んでいきます。
これは、Service
化するのが妥当でしょうか?
import { Injectable } from '@angular/core';
import { map, interval, Subscription } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AppTimerService {
private _timer: Subscription;
private _elapsed: number;
// ストップしているかどうかの判定用
private _isStopped: boolean;
// 外部からの値取得用
get elapsed(): number {
return this._elapsed;
};
constructor() {
// 値を初期化
this._timer = new Subscription();
this._elapsed = 0;
this._isStopped = false;
}
public start(addition: number = 0): void {
// ここでObservable(何をするかの中身)を生み出す
const t = interval(1000).pipe(
map(x => x + 1 + addition)
);
// 生み出したObservableを購読する
this._timer = t.subscribe({
next: (x) => this._elapsed = x
});
}
public stop(): void {
if (!this._isStopped) {
this._timer.unsubscribe();
this._isStopped = true;
}
}
}
こんな感じになりました。
外部からは start()
, stop()
を呼び出すだけで簡単に動作させたり、停止させたりできます。
そして、外部で elapsed
プロパティを呼び出せば現在の経過時間が取れ続けるような仕組みになっています。
RxJSは難しかったですが、理解してしまえばそれほど難しいものではない気がします。
.NETの Task
の方が大変だった気がしています。
デザインのお話
本筋ではありませんが、少しだけデザインのお話もしておきます。
私はデザイナーではないですしデザインセンスがゴミなので、昔からデザインは本当にダメダメです。
なので、bootstrapを初めて知ったときには衝撃を受けました。
bootstrapのクラスを書くだけで簡単にきれいな見た目になるからです。
それから結構時が経ちましたが、今は何やらデザインフレームワークやら手法が色々あるようですね。
CSS in JSやらなんやらと。
よくわからないので深く語りませんが、とりあえず今回デザイン面をどうするかにあたり考えたことを少しだけお話します。
Bulma か Tailwind CSS か
大規模な開発については本職のデザイナーさんにおまかせするとして、個人開発の話に限定してお話していきます。
いろいろ調べてみましたが、個人的にはこの2つのCSSフレームワークが良いのかなと思いました。
どちらも簡単に扱えそうで、なおかつシンプルさが売りのようです。
結局触ってみて私はBulmaを選択しました。
Tailwind CSSは自分でcssを書かないといけないような気がしました。
あと、mdやらsmやら略記がとにかく多いので扱えるようになるまで時間がかかりそうだったので、今回はパスしました。
Bulmaを触ってみましたが、ドキュメントもしっかり整備されていて触りやすかったです。
あと、今回は自分でほぼ全くcssを書いていませんが、それっぽいサイトを作ることができました。
▼ギャラリー
1.初期画面 | 2.問題数選択画面 |
---|---|
3.解答画面 | 4.結果表示画面 |
---|---|
全体を通しての総括
最後に、今回実装してみての全体的な総括を書いていこうかなと思います。
全体を通して、個人的にはAngularは嫌いではないかなと思っています。
Vueを少し触ったことがありますが、あれはあれで面白いと思っています。
Reactは触ったことはないですが、Vueと同系列のタイプだと認識しています。
Angularは全部入りという名に恥じないものだと思っております。
まだ触っていない機能もいくつもありますし。
ReactやVueのようにデザイン面は面倒見るけど、それ以外は好きなツールを選定しろというスタンスも良いですが、Googleという巨人の肩に乗って、よしなにやってもらうことも悪くないかなと思います。
おんぶにだっこは疲れませんから。
ただ、Angularのメンテナンスが終了にならないのかと心配する人もいるかも知れません。
それは杞憂だと思います。
なにせ、 AngularはGoogle社内でも1000を超えるプロジェクトで利用されているという話ですから、Googleが「来年にメンテナンスすることをやめます」というようなことを簡単には言えないと思っていますので。
というわけで、好きなところでも書いて〆ようかなと思います。
好きなところ
制約が強い
とにかく制約が強い印象を受けました。
制約と申しますのは値の共有や、名称等のスコープのことです。
コンポーネントからは一歩も外には出さないぞ!という強い意志のようなものを感じます。
そして、こうした疎結合な部分は私は好きです。
密に繋がっていてズブズブになると、どうしてもバグが蔓延ってしまいますからね。
html + typescript + css はセット
この3点セットでコンポーネントと呼びます!というスタンスも良かったです。
スコープの制約が強いおかげで、他のコンポーネントで使っているcssの名称を使いまわしても怒られませんし。
cssは、あるいはtypescriptはまとめて書きましょう!となるとどうしても神クラスみたいなものが誕生して永遠に終わらない聖戦(バグとの)が始まってしまうのです。
そう考えると、Angularはできるだけバグらない構造を提供しているのかなと感じました。
最後に
大変な長文を最後までお読みいただき、ありがとうございました&お疲れさまでした。
今年の11月は土日の全てをひたすらコーディングに捧げました。
まぁ、楽しかったので良いですが。
今回製作したアプリはこちらから参照できます。
デモサイトを公開しようかなとも考えたのですが、セキュリティのことは1ミリも考えて作っていないので、やめました。
皆様個々人で使ってみてください。
初心者向けの記事になるように、できるだけ経過と丁寧さを重視して記載したのですが、伝わっておりますでしょうか?
皆様の学習に役立てることができるなら幸いです。
Angularを知ることができて良かったかなと思っております。
また機会があれば、フロントエンドはAngularで書こうかなと思っております。
それでは、良いエンジニアライフを!
参考文献
▼書籍
洋書です。
ですが、GoogleのAngular開発チームの人が書いた公式技術本のようなものです。
英語が読めなくても、Angularの基礎がわかっていればサンプルコードを眺めて写経するだけでも勉強になります。
因みに、日本のAmazonでも販売されています。
なぜ、和書を選択しなかったかというと、日本に私のお眼鏡にかなう最新のAngular本がなかったからです。。。
和書の入門本で基礎的なことを学習してから写経するのがおすすめです。
(英語が読めるのなら、普通に読んでみると良いと思います。)
▼WEBページ
- Angularのコンポーネント間の値受け渡しについて
- How to call another components function in angular2
- Angular2のHttpモジュールを眺めてベストプラクティスを考える
- RxJS heads up: toPromise is being deprecated
- RxJS の Operators (6) - Observable のエラーハンドリング
参照