angular2 の RC もリリースされたことですし、とりあえずどんなワークフローになるか知りたかったので簡単な SPA を作ってみることにしました。せっかくなので Material Design for Angular2 も使ってみました(執筆時点でまだ 2.0.0-alpha.5 granite-gouda ですが)。作ってみて現時点で思ったことを記しておきます。 申し訳ないですがあまりまとまりのない文章です! 誤った記載などありましたらご指摘いただければすごくうれしいです
↓こんなの作りました(ソース)

概要
画面としては
- ログイン画面
- ユーザー登録画面
- ダッシュボード(投稿一覧)
- 新規投稿/編集画面
を作りました。サーバーサイドはMockの json-server を、特にカスタマイズせずに使ったので、ユーザー登録周りとかはただのハリボテです。 Material Design のコンポーネントがまだ無いもの(例えばダイアログとか)はいったんダサいですけど confirm とかで代用しました(どんな感じに使うのか知りたかっただけなので)。また、他の css フレームワークとかはややこしくなるので使ってません。
ざっくりとした動きは↓のような感じです。
所感
まだ本当に表面しか触ってないですけど、率直に楽しいです(これは個人的な意見すぎるかもしれませんが)。特に TypeScript 信者な自分としては、サクサクとコード補完の恩恵も受けながら ES2015 記法でコンポーネントを作っていくのは楽しいです。と、そんな偏った意見はさておき、こんなシンプルなSPAだけど気づいた点についてメモっておきます。
「良い」と思った
- 一つの
classでコンポーネントが完結するのはうれしい -
Viewにローカル変数作れるのうれしい(#) -
httpがデフォルトでObservableを提供してくれるのはうれしい - Style Guide があるのうれしい
-
Material Design使うの簡単。けど部品がまだまだ無い(alphaですしね)
「悪い」というよりは「気になった」(と、よくわかってない)
-
Angular2の公式ドキュメントが結構いろいろなところでコードが壊れている - コンポーネント内で使いたいコンポーネントは
directivesに都度指定する必要があるのがちょっとだけ手間 -
Router関連の仕様がまだ安定していない(CanActivateとかpathからname消えちゃったりとか、useAsDefaultなくなっちゃったりとか)
順番にもうちょっと詳しくまとめます
一つのclassでコンポーネントが完結するのはうれしい
Decorator が使えるおかげで一つの class でコンポーネントを定義できます。Angular1の場合、 component というものが1.5で追加されたものの、 component も directive も controller を別途指定しないといけないので、下のように2箇所で宣言する必要があります。また class はホイストされないので component の定義よりも先に宣言しておく必要があります(少なくとも私の認識では。。。)。 directive は function なので class を先に書いてもちゃんと動くんですけどね。
class HogeController {
// 実装省略
}
export const HogeComponent = {
template: 'hoge.html',
bindings: {
hoge: '<',
moge: '@',
piyo: '&'
},
controller: HogeController // ここに functionで直接実装してもいいですが、ちょっとそれは。。。
}
なので、 HogeComponent がコンポーネントの定義を説明しているのに、下までスクロール( class の定義の下にあるので)しないとその中身が確認できない状態です。超個人的ですが、宣言はやっぱり先に見えてる方がわかりやすいと思います。
Angular2は Decorator を使ってコンポーネントのメタ情報を定義できるので見やすいと感じました。宣言が先にあって、実装が後にあるイメージです。例えば今回作ったダッシュボードコンポーネントは↓のような感じになります。地味に selector も Decorator で指定できるようになったのもうれしいです。
@Component({
selector: 'mta-dashboard',
templateUrl: 'src/app/dashboard/dashboard.component.html',
directives: [
ROUTER_DIRECTIVES,
MD_PROGRESS_CIRCLE_DIRECTIVES,
MD_LIST_DIRECTIVES,
MD_CARD_DIRECTIVES,
MdButton
],
pipes: [TranslatePipe]
})
export class DashboardComponent implements OnInit {
posts: any[] = [];
constructor(
private router: Router,
private postsService: PostsService,
private authService: AuthService
) { }
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
this.postsService.getPosts()
.subscribe(posts => this.posts = posts);
}
onNewClicked() {
this.router.navigate(['/posts/new']);
}
onEdit(post: any) {
this.router.navigate([`/posts/${post.id}`])
}
onDelete(post: any) {
// TODO: replace when md-dialog is ready
if (window.confirm('削除してもいいですか?')) {
this.postsService.deletePost(post.id)
.subscribe(res => {
this.fetchPosts();
});
}
}
}
View にローカル変数作れるのうれしい(#)
Viewの中で DOM の参照をローカル変数として用意できる(#を使う)のも便利だと思いました。これのおかげでコンポーネント側に、Viewと連動する為のフィールドを別途用意する必要がなくなるケースが増えそうです。例えば今回作ったログイン画面とかで使ってます。
<!-- login.component.html -->
<form>
<div>
<md-input placeholder="{{'Username' | translate}}" #username></md-input>
</div>
<div>
<md-input placeholder="{{'Password' | translate}}" type="password" #password></md-input>
</div>
<button md-raised-button color="primary" (click)="onLogin(username.value, password.value)">{{'Login' | translate}}</button>
</form>
<a [routerLink]="['/signup']">{{'Signup' | translate}}</a>
#username と #password をそれぞれ input にくっつけることで、その DOM の参照を作れて、それをボタン押下時に onLogin に値を渡しています。渡ってきた値は LoginComponent で引数として使えます。
@Component({
selector: 'mta-login',
templateUrl: 'src/app/top/login/login.component.html',
directives: [ROUTER_DIRECTIVES, MdInput, MdButton],
pipes: [TranslatePipe]
})
export class LoginComponent {
constructor(
private router: Router,
private authService: AuthService,
private translater: TranslateService
) { }
onLogin(username: string, password: string) {
this.authService.login(username, password)
.subscribe(user => {
this.router.navigate(['/dashboard']);
});
}
}
Angular1 だったら input 毎に ng-model 指定して、 onLogin でその値を使うイメージになると思います。
<input type="text" ng-model="$ctrl.model.username" />
<input type="password" ng-model="$ctrl.model.password" />
<button type="buttoon" ng-click="$ctrl.onLogin()" />
class HogeController {
onLogin() {
this.authService.login(this.model.username, this.model.password)
// 以下略
}
}
http がデフォルトで Observable を提供してくれるのはうれしい
.NET 時代から Rx 大好き人間なので、 Angular2 の http が提供してくれるのがデフォルトで Promise じゃなくて Observable なのはテンションが上がります。なんでもかんでも Observable でやりたくなりますね。これを気に RxJS の知識をもっと増やしたいと思います。
また、 template 側でも asyncPipe を使えば Observable を template で直接使うことができます!どういうことかというと、従来(例えばAngular1とか)であれば↓のようなコードをよく書いていたと思います。
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
this.posts = this.postsService.getPosts()
// レスポンスをローカル変数に格納
.subscribe(posts => this.posts = posts);
}
取得してきたデータをローカル変数に格納して、それを template で↓使います。
<!-- postsをtemplateで使う -->
<md-card *ngFor="let post of posts">
<md-card-title>{{post.title}}</md-card-title>
<!-- 以下略 -->
</md-card>
これを、 asyncPipe を使えば template で直接 Observable を使うことができます。
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
// Observableをそのまま`posts`に入れる
this.posts = this.postsService.getPosts().do(() => this.isLoading = false);
}
<!-- postsをtemplateで使う -->
<md-card *ngFor="let post of posts | async">
<md-card-title>{{post.title}}</md-card-title>
<!-- 以下略 -->
</md-card>
一手間減っただけにしか見えないかもですが、 Observable を template で直接使えるのは便利!(なはず)
Style Guideがあるのうれしい
Angular1 時代からある @johnpapa 氏の(といっても Community Driven?)Style Guide。 まだ rc ですし、本格的なアプリケーションも世にほとんど出ていない状態で Style Guideも何もあったもんじゃない、って思われるかもしれませんが、 Angular1 からの流れでそのまま使える考えなどもあるのでやっぱりこういう指標があるのは使う側としてうれしいです。一人でやる分にも役に立ちますが、チームでコーディングする上でもガイドがあるのはうれしいですね。ガイドのカテゴリも
- Do: 常に守るべきルール
- Consider: 守ったほうが良いけど、自分なりに理由があるなら守らなくてOK
- Avoid: やっちゃダメなルール
のように分かれていてすごく見やすいです。
Material Design使うの簡単。けど部品がまだまだ無い(alphaですしね)
コンポーネントで用意されてるので、そのまま使うだけです。CSSほぼ意識せずにそれっぽい見た目のものができあがるのはうれしいです。まだまだ部品が揃っていないですけど、さくっと何か創りたいときとかにい重宝しそうです。これからのアップデートに期待です!
Angular2の公式ドキュメントが結構いろいろなところでコードが壊れている
まだ一応 rc といえど、公式ドキュメントのサンプルコードのリンクが割りとそこら中で壊れていて結構困りました。そういうときはもう Github を直接みて issue とかから現在の正解を見つけるしかない、って状態です。
Router関連の仕様がまだ安定していない
上にも関連していますが、中でも特に Router の仕様が安定していなくて困りました。「ログインしていなかったら見れない画面」を作りたかったのですが、現状の仕組みでは多分無理かなって結論に達しました。 CanActivate が無くなってしまっていますし、あったとしても Decorator 内にサービスを Inject できないので、ログイン状態を知る術がない。(ただし、厳密には injector を使えば取得することは可能)
あと path から name (ui-router みたいなやつ) 消えちゃったりとか、デフォルトの遷移先である useAsDefault なくなっちゃったりとかしてて、まだまだこれからって感じがします。
コンポーネント内で使いたいコンポーネントはdirectivesに都度指定する必要があるのがちょっとだけ手間
これは私の認識があっているのかちょっと自信が無いのですが、コンポーネント内で使いたい他のコンポーネントは全部 Decorator に書く必要ありそうです。なので上の例にも載せていたように、基本的に↓のような感じに directives 配列に使いたいコンポーネントを詰め込む感じです。
@Component({
selector: 'mta-dashboard',
templateUrl: 'src/app/dashboard/dashboard.component.html',
directives: [
ROUTER_DIRECTIVES,
MD_PROGRESS_CIRCLE_DIRECTIVES,
MD_LIST_DIRECTIVES,
MD_CARD_DIRECTIVES,
MdButton
],
pipes: [TranslatePipe]
})
export class DashboardComponent implements OnInit {
Angular1 は一つのグローバルスペースに全てが定義されていたので、とにかく angular.module で宣言しちゃえばどこででも使えたんですけど、 Angular2 には独自の module なんてものは無いですし、そもそも Angular1 でのグローバルスペースも、名前の競合とか起きると面倒くさいことになっていたので、これは改善になります。が、いざいろいろコンポーネント作ってたらこの directives を書くのが割りとめんどくさかったです。が、が、他のコンポーネントを Import してきて自分のコンポーネントで使うので、当たり前といえば当たり前ですね!
最後に
とにかく最初にも書きましたが、作るのすごく楽しいです。 TypeScript も ES2015 も RxJS も使えてワクワクしちゃいます。これにさらに ng2-redux も組み合わせたいですね。これから Material Design の部品も増えていくでしょうし、徐々にこのサンプルアプリもブラッシュアップしていけたらと思います。
まとまりのない記事ですがここまで読んでくださりありがとうございます!
ソースは Githubに上げているので、よろしければ遊んでみてください。