- 投稿日:2019-02-27T18:15:15+09:00
データファーストなAngularプログラミング
この記事の対象者はこんな人
- Angularを使っていて、ElementRefとかNativeElementを駆使する人
- Angularを学び始めたばかり
- jQueryなどのライブラリでゴリゴリ書いているが、Angularでアプリを作ろうとしている
- AngularでTDDをやろうとしているとかテストコードを書こうと思ってる人
特に最初に該当する人は、基礎は読み飛ばして下の方を読んでみてください。
社内向けに書いていたんですが、割としっかりした内容になってきたので公開することにしました。データファーストの基礎
Angularでのアプリ開発を理解する上で、データファーストな考え方はとても重要です。
考え方を徹底してコーディングしていれば、データの状態がHTMLを作り変化させているという状態になっていると思います。これは、Angularでテストコードを書く際にも、効率の良いテストを行えるようにするためにとても大事なことだと思います。
DOMを操作するという考え方からの脱却
例えば「ボタンを押すと、カウントが増える」というような単純な機能を考えたときに「ボタン」と「カウント表示」の2つの要素があるとします。
sample.html<span id="counter">1</count> <button id="count-button">Count Up</button>Angularとして良くない作り方
DOM操作に特化したライブラリに慣れていると、以下のような作り方になってしまうかもしれません。
- ボタンをクリックしたときに実行される関数Aを設定
- 関数Aを叩くとカウント表示の数字を取得し(または変数を参照する)、1を足す
- 結果をカウント表示のHTMLに追加する
例えばjQueryのコードで書くと、以下のようになっているはずです。
sample.js$(function(){ $('#count-button').click(function(){ var count = +$('#counter').html(); $('#counter').html(count + 1); }); });これをAngularのコードで書いてみましょう。
app.component.html<span #counter>1</span> <button (click)="countUp()">Count Up</button>app.component.tsimport { Component, ElementRef, ViewChild } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { @ViewChild('counter') counter: ElementRef; countUp() { const count = this.counter.nativeElement.innerHTML; this.counter.nativeElement.innerHTML = +count + 1; } }こういう作り方になっているとGUIのカウント表示の部分が何であるかというところまで、プロダクトコードにて記述しているため、間違いが起こる箇所、テストをすべき箇所が増えて行ってしまいます。
HTML側に実行する関数が指定されているため、幾分jQueryコードよりはわかりやすくはありますが…これではまだ不完全です。Angularとしてあるべき姿
- ボタンをクリックした時に、(click)で設定したComponentの関数Aを実行する
- 関数Aを叩くと、カウント表示のメンバ変数に+1をする
- 結果として、バインドされたカウント表示が更新される
Angular的に正しいとされる作り方ではこうなります。
app.component.html{{count}} <button (click)="countUp()">Count Up</button>app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public count = 0; countUp() { this.count++; } }つまりカウント表示がデータバインドされているので、プログラム上ではメンバ変数である
count
の数字のことを考えるだけでよくなります。
このようにして、データありきの構成を作っていくことが望ましいとされています。データファーストの応用
基礎の例はデータバインドの基礎中の基礎なので、いちいちそんな回りくどいやり方をしないよ、という人も多いと思います。
なので、Angularのプログラミングに慣れていても、ついついやってしまいがちな例を挙げていきたいと思います。
メンバーのコードをレビューしている時に、たまに遭遇するんですが(実際はもっと複雑な状況ですが)、意外と気づきにくこともあるのかなと思い、簡単な例を挙げて見ていきたいと思います。そのCSS操作、elementRefが必要ですか?
CSSのプロパティを操作しようとする時に、データの状態がHTMLを作り変化させているという原則を忘れてしまいがちです。
例えばボタンを押すと、div要素が動くとうような例を見てみましょう。app.component.scss.awesome-box { width: 100px; height: 100px; background: #eee; margin-bottom: 10px; position: absolute; }app.component.html<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button> <div #myAwesomeBox class="awesome-box"> This is my awesome BOX!!!! </div>app.component.tsimport { Component, ViewChild, ElementRef } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { @ViewChild('myAwesomeBox') myAwesomeBox: ElementRef; public boxPositionLeft = 0; enlargeBox() { this.boxPositionLeft += 10; this.myAwesomeBox.nativeElement.style.left = this.boxPositionLeft + 'px'; } }ボタンをクリックする毎にdivが動くというような処理を書いてみました。
基本の指摘を元に、クリックして操作するのは、ts側でのboxPositionLeftという変数です。
その値を足していって、div要素のstyleを指定して動かしていく、ということをやっています。ただ、この例は、Angularのデータの状態がHTMLを作り変化させているという鉄則から、少しだけそれていることがわかります。
Angular Way
Angular的な表現を目指すため、リファクタリングしていきます。
まず、Angularでは、HTMLの要素に対して、プロパティに変数を指定する機能があります。app.component.html<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button> <div class="awesome-box" [style.left.px]="boxPositionLeft"> This is my awesome BOX!!!! </div>なのでts側でDOM要素にアクセスして、styleを変更する必要がなくなりました。
さらにpxという文字を指定しなくても、[style.left.px]
として、入力する値はpxだよ、という指定ができるため、いちいち文字列を追加しなくても良くなりましたね。app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public boxPositionLeft = 0; enlargeBox() { this.boxPositionLeft += 10; } }ngClass, ngStyleを使う
他にも、ngStyleやngClassを使ってデータの状態からDOMを変えていくようにすることも、忘れないようにしましょう。
先程のBoxが100px以上へ移動しようとした場合にBoxを赤くしたいとします。
ElementRefでstyleを変更しに行くというのは良くない例だということはもうおわかりかと思いますのでAngularらしくない例はもう省きます。
app.component.html<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button> <div class="awesome-box" [ngStyle]="getBoxStyle()" [style.left.px]="boxPositionLeft"> This is my awesome BOX!!!! </div>ngStyleでgetBoxStyleという関数を叩きに行きます。
app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public boxPositionLeft = 0; enlargeBox() { this.boxPositionLeft += 10; } getBoxStyle() { if (this.boxPositionLeft > 100) { return { background: 'red' } } return { background: '#eee' }; } }getBoxStyleでは、boxPositionLeftの値を見に行って、100以上であればbackground: 'red'を返しているだけです。
ngStyleでは、状態を常に見に行っているので、多用して複雑な計算をするのは考慮すべきですが、動くたびにDOMを参照して状態を把握する必要がないことがわかります。データを操作して、DOMを操作するような考え方でやるよりも、幾らかソースコードがシンプルになっているかと思います。
まとめ
音声ファイルの編集ソフトウェアのGUI部分を最近開発案件で作ったんですが(Garage BandみたいなGUIを想像してもらえればわかりやすいと思います)、表示しているDOMと音声のレンダリングに使う情報との乖離があってはいけないという状況がありました。非常にGUIのテストもし辛いですよね。
その点でAngularのような、データモデルありきで考えるフレームワークは強さを発揮します。
Angularにおいては操作によってDOMを動かすという考え方ではなく、データによってDOMが変化するように作るというのがポイントだと思います。
これを徹底していくことで、テストコードがデータの正しさを担保すると、GUIのレンダリングも正しさがおおよそ担保されることにもつながると思いますので、フロントのテストに困っている人がいたら参考になればな、と思いました。
- 投稿日:2019-02-27T00:40:00+09:00
ANGULAR.JS公式チュートリアルのPhoneCat Tutorial Appはじめました。
ANGULAR.JS公式チュートリアルのPhoneCat Tutorial Appはじめました。
なぜ始めたのか?
業務でJavascriptのwindowオブジェクトの操作とJasmineのUTをやる機会が
あったので、これを機会にJavaScriptの知識を深めたいと思い、テーマを探していました。目に留まったのがGoogleなどが開発を手掛けたAngularJSが有名!などという情報が入ってきたので、公式ページにあったチュートリアルを試してみることにしました。
電話のカタログページを使ったチュートリアルみたいですね。参考:https://docs.angularjs.org/tutorial
ひとまず、今回は環境構築から始めたいと思いまっす。
前提
・Gitがインストールされていることを前提として進めます。
環境構築
繰り返しになりますが、今回は環境構築がメインです。
まずはangular-phoncatのリポジトリをクローンして
クローンしたフォルダをカレントディレクトリにします。git clone --depth=16 https://github.com/angular/angular-phonecat.git cd angular-phonecat--depth=16のオプションは最新16件分のコミットのみ抽出するらしいです。
ダウンロード速度を考慮して設定してるみたいですね。次に依存関係を解決するためにNode.jsをインストールします。
これは事前設定されているローカルWEBサーバの実施に使うみたいです。インストーラはOS毎に分かれているので公式ページから適切なものをダウンロードしてインストールしてください。
参考:https://nodejs.org/en/download/
Node.JSについてよくわからなかったので調べてみたところ、サーバでJavaScriptを実行できるようにするための
プラットフォームらしいです。
まずは使ってみて雰囲気つかんでみて、気になるとこがあったら詳細に調べてみたいと思います。インストールが完了したらコマンドラインで次のコマンドを叩いて正常にインストールされた事を確認します。
ちなみに、2019年2月26日現在ではv10.15.1が推奨される最新のNode.jsです。
11.10.0も公開されていましたが、安定している方を選びます。node --versionNode.jsをインストールしたらnpmも取得されます。
npmはパッケージ管理ツールでNode.jsのパッケージを管理するのに使用します。
まずは依存関係をインストールしてみます。npm install完了したら実際にWEBサーバを起動してみましょう!
デフォルトの設定ではポート8000をリスナーとするlocalhostが作成されるはずです。npm start起動に成功したらhttp://localhost:8000/index.htmlにアクセスしてみましょう。
こ、これがAngular.jsを使ったページか!めっちゃカッコいい画面やんけ!って画面が表示されれば成功です。さて、まだ環境構築は終わりではありません。
次は単体テストの機能が正常に動作するか確認します。angular.jsはKarmaを使って単体テストするように構築されています。
さっそくKarmaを起動してみます。
起動はnpmの以下コマンドでできます。npm testChromeのブラウザが起動してKarmaの画面が表示されます。
今回はKarumaの勉強に焦点を当てるわけではないので、いったん此処まで。次にテストツールのE2E(エンドツーエンド)の確認です。
このプロダクトはユーザとブラウザの対話をシュミレートすることでテストします。WEBサーバが起動した状態で、別のコマンドラインを起動して、次のコマンドを実行します。
npm run protractorすると、ブラウザが起動してアプリケーションを自動でテストし始めます。
検索用テキストボックスに勝手に文字が入力されて検索されたりと少し驚きますが、これによりE2Eのテストが自動化できる仕組みっぽいです。
たぶん別途specを用意する必要があるんだと思いますが、これは別の機会に勉強します。今回はここまで!
参考:https://docs.angularjs.org/tutorial/step_01
マークダウンは後程なおします。
おやすみなさい。