読者です 読者をやめる 読者になる 読者になる

undefined

bokuweb.me

Angular2でReactのチュートリアルを試してみる


概要

一回触ってみたいと思っていたAngular2をようやく触ってみた。最近は新しいフレームワークやライブラリを触る場合はゲームを作ってみるか、Reactのチュートリアルをやるようにしていて、今回はReactのチュートリアル(コメントフォームのやつ)をAngular2でやってみることにした。

基本的には環境構築周りは以下のシェルスクリプトマガジンとAngularのSlackチームng-japanのビギナー用に紹介されていた手順を踏んでいる。

シェルスクリプトマガジン vol.37

シェルスクリプトマガジン vol.37

  • 作者: 當仲寛哲,岡田健,佐川夫美雄,大岩元,松浦智之,後藤大地,白羽玲子,水間丈博,濱口誠一,すずきひろのぶ,花川直己,しょっさん,法林浩之,熊野憲辰,桑原滝弥,USP研究所,ジーズバンク
  • 出版社/メーカー: USP研究所
  • 発売日: 2016/04/25
  • メディア: 雑誌
  • この商品を含むブログを見る

Join ng-japan on Slack!

なにか間違いやより良い方法などありましたらご指摘願います。

バージョン

package.jsonも最後にのせるが

Angular: 2.0.0-rc.3 Typescript: 1.8.10

で試している。

Repository

github.com

作業ログ

インストール

Angular2関連のパッケージは@angular/xxxxxという形式になっているよう。すこし前まではangular2/xxxxxだったのかな。 一点注意が必要なのはRx.jsのバージョンが上がって、symbol-observableが必要になった点でしょうか。その他はほぼ、slackで紹介されていた手順だと思う。

stackoverflow.com

$ npm init -y

$ npm i -S es6-shim systemjs symbol-observable zone.js reflect-metadata rxjs @angular/common @angular/compiler @angular/core @angular/platform-browser @angular/platform-browser-dynamic

$ npm i -D concurrently lite-server typescript typings

npm scriptの記述。

  • package.json
"scripts": {
  "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
  "tsc": "tsc",
  "tsc:w": "tsc -w",
  "lite": "lite-server",
  "typings": "typings",
  "postinstall": "typings install"
},

typescriptの設定。

  • tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

型情報のインストール

$ node_modules/.bin/typings init
node_modules/.bin/typings install -GS es6-shim jasmine --source dt
touch index.html
mkdir scripts
touch scripts/main.ts
mkdir -p components/home
touch components/home/home.ts
touch system-config.ts
touch bs-config.json
  • bs-config.json
{
  "port": 8000,
  "files": ["./**/*.{html,htm,css,js}"],
  "server": { "baseDir": "./" }
}

system.jsの設定。この辺りよく作法を理解していないので要復習。

  • system-config.js
System.config({
  map: {
    '@angular': 'node_modules/@angular',
    'rxjs': 'node_modules/rxjs',
    'scripts/main': 'app/main.js',
    'symbol-observable': 'node_modules/symbol-observable',
  },
  packages: {
    '@angular/core':  { main: 'index' },
    '@angular/common':  { main: 'index' },
    '@angular/compiler':  { main: 'index' },
    '@angular/http':  { main: 'index' },
    '@angular/router':  { main: 'index' },
    '@angular/platform-browser':  { main: 'index' },
    '@angular/platform-browser-dynamic':  { main: 'index' },
    'symbol-observable': { defaultExtension: 'js', main: 'index' },
    'rxjs':  { main: 'Rx' },
    'components':  { main: 'index' }
  }
});
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <base href="/">
        <title>ng2 comment system</title>
    </head>
    <body>
        <comment-box>Loading...</comment-box>
        <script src="node_modules/es6-shim/es6-shim.js"></script>
        <script src="node_modules/reflect-metadata/Reflect.js"></script>
        <script src="node_modules/systemjs/dist/system.src.js"></script>
        <script src="node_modules/zone.js/dist/zone.js"></script>
        <script>
         System.import('system-config.js')
               .then(function () {
                   System.import('app/main');
               })
               .catch(console.error.bind(console));
        </script>
    </body>
</html>
  • app/main.ts
import {bootstrap} from '@angular/platform-browser-dynamic'
import {AppComponent} from '../components/home/home'

bootstrap(AppComponent);
  • components/home.ts
import {Component} from '@angular/core'
@Component({
  selector: 'my-app',
  template: `
    <h1>Hello ng2</h1>
    `
})
export class AppComponent {
}

これでnpm startするとブラウザが開いて、Hello ng2 と表示されるはず。

Commentボックスの実装

app/main.tsから呼ぶのをcomponents/comment-boxに修正。

  • scripts/main.ts
import { bootstrap } from '@angular/platform-browser-dynamic';
import { CommentBox } from '../components/comment-box';

bootstrap(CommentBox);

index.htmlから呼ぶコンポーネント名を修正

<ng-app>を<comment-box>に変更

 <body>
   <comment-box>Loading...</comment-box>
          ...

components/home.tscomponents/comment-box.txに修正して、以下のように修正した。

comment-box

  • components/comment-box.ts
import { Component, OnInit } from '@angular/core';
import { CommentList } from './comment-list';
import { CommentForm } from './comment-form';
import { CommentService } from '../service/comment';
import Comment from '../interfaces/comment';

@Component({
  selector: 'comment-box',
  providers: [CommentService],
  directives: [CommentList, CommentForm],
  template: `
    <div class="commentBox">
      <h1>Comments</h1>
      <comment-list [comments]="comments"></comment-list>
      <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form>
    </div>
  `,
})
export class CommentBox implements OnInit {

  comments: Comment[];

  constructor(private commentService: CommentService) {
  }

  ngOnInit() {
    this.commentService
      .startIntervalFetch()
      .subscribe(comments => this.comments = comments);
  }

  handleCommentSubmit(comment) {
    comment.id = this.comments.length;
    this.commentService
      .add(comment)
      .subscribe(res => this.comments.push(res));
  }
}

こうなるまで結構右往左往したんだけど、ひとまず結果だけを載せておく。 また、ポイントとなりそうな箇所を以下に記載しておく。

コンポーネント

Angular2ではコンポーネントはDecoratorを使用して以下のような形で記述するらしい。

@Component({
  selector: 'comment-box',
  providers: [CommentService],
  directives: [CommentList, CommentForm],
  template: `
    <div class="commentBox">
      <h1>Comments</h1>
      <comment-list [comments]="comments"></comment-list>
      <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form>
    </div>
  `,
})
export class CommentBox implements OnInit {
 ...省略
selector

他のコンポーネントからこのコンポーネントを呼ぶ場合はこの名前を使って呼ぶ。上記の場合であれば<comment-box></comment-box>とする。(<comment-box />とは書けないよう。)また、カスタムディレクティブの場合は使用するディレクティブを次のdirectivesで指定する必要があるっぽい。

directives

使用するカスタムディレクティブをここで指定する。この場合はCommentList, CommentFormというディレクティブを使用する。

providers

Injectするサービスを指定する。Viewに関心のない処理などはサービスとして切り出してInjectするらしい。このコメントシステムであればサーバとAjax通信する処理をサービス(CommentService)として切り出して、Injectしている。Injectされる側(CommentService)は後述する。

templete

テンプレートストリングを使用して、以下のように書く。

  template: `
    <div>Hello</div>
  `

また、templateUrlを使って次のように別ファイルのhtmlを使用するこもできるっぽい。

templateUrl: 'foo/bar.html'

どっちが主流なのだろうか。デザイナーとの協業であれば後者のほうがやりやすいだろうし、チームの体制に左右されるだろうか。テンプレートストリングだとエディタのシンタックスハイライトとかインデントが死んでてみんなどうしているのって感じ。

class

デコレートされる側のclassは以下のようになっている。

export class CommentBox implements OnInit {

  comments: Comment[];

  constructor(private commentService: CommentService) {
  }

  ngOnInit() {
    this.commentService
      .startIntervalFetch()
      .subscribe(comments => this.comments = comments);
  }

  handleCommentSubmit(comment) {
    comment.id = this.comments.length;
    this.commentService
      .add(comment)
      .subscribe(res => this.comments.push(res));
  }
}

OnInitってのを継承していて、これはLifecycle Hooksを使うためのもの。今回はReactcomponentWillMount相当する(?あってる?)タイミングでサーバへリクエストを投げたかったので、OnInitを使用している。以下によくまとまっていた。

blog.yuhiisk.com

constructorでは後述するcommentService(主にサーバとのAjax通信するサービス)を受け取って、ngOnInitでポーリングする処理を呼んでいる。handleCommentSubmitでは子コンポーネントのcomment-formからのデータを受け取りcommentServiceに渡すことで、コメントを投稿している。startIntervalFetch()add()ともにobservableが返ってくるのでsubscribeしてcommentsを書き換えたり、追加したりしている。

comment-list

基本的にはcomment-boxでの知識があればほぼほぼ書けた。

  • components/comment-list.ts
import { Component, Input } from '@angular/core';
import { CommentItem } from './comment-item';

@Component({
  selector: 'comment-list',
  directives: [CommentItem],
  template: `
    <div class="comment-list">
      <div *ngFor="let comment of comments">
        <comment-item [author]="comment.author" [text]="comment.text"></comment-item>
      </div>
    </div>
    `
})

export class CommentList {
  @Input() comments;
}

新しいポイントは以下。

@Input()

親コンポーネントからディレクティブに対する入力は@input()で定義する必要がある。

*ngFor

みたまんま繰り返しを行う記述でstructural directivesと呼ぶらしい。他には、*ngIf="condition"等が用意されている。ちょっと前まではletではなく#と書いていたようで、#を使うと現在では動作はするが、#" inside of expressions is deprecated. Use "let" instead!という警告がでる。

ここではcommentsの要素数分繰り返し<comment-item></comment-item>に渡している。

詳細は以下を見ると良さそう。

angular.io

comment-form

フォーム部分については以下のようになった。

  • components/comment-form.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { CommentList } from './comment-list';
import { CommentService } from '../service/comment';
import Comment from '../interfaces/comment';

@Component({
  selector: 'comment-form',
  template: `
    <div class="comment-form">
      <input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" />
      <input type="text" value={{text}} (keyup)="onTextChange($event)" placeholder="Say something..." />
      <input type="submit" value="Post" (click)="handleSubmit()" />
    </div>
    `,
  styles: [`
    .comment-form {
      margin-top: 50px;
    }
  `]
})
export class CommentForm {
  @Output() onCommentSubmit: EventEmitter<any> = new EventEmitter();

  public author: string
  public text: string

  onAuthorChange(e: KeyboardEvent): void {
    this.author = (<HTMLInputElement>event.target).value;
  }

  onTextChange(e: KeyboardEvent): void {
    this.text = (<HTMLInputElement>event.target).value;
  }

  handleSubmit(): void {
    const author = this.author.trim();
    const text = this.text.trim();
    if (!text || !author) return;
    this.onCommentSubmit.emit({ author, text });
    this.text = '';
    this.author = '';
  }
}

新しい要素は以下。

styles

コンポーネントのスタイルはstyleで指定できる。

  styles: [`
    .comment-form {
      margin-top: 50px;
      }
  `]

Angular2Scoped CSSを実現しているという話しをちらっと聞いていたんだけどどのように実現しているか見たら以下のようになっていた。

スコープはどのように実現しているか確認すると以下のようになっていた。attributeを使用して擬似的にスコープを実現しているように見える。

  • html
<comment-form _nghost-dkv-3="">
    <div _ngcontent-dkv-3="" class="comment-form">
      <input _ngcontent-dkv-3="" placeholder="Your name" type="text" ng-reflect-value="">
      <input _ngcontent-dkv-3="" placeholder="Say something..." type="text" ng-reflect-value="">
      <input _ngcontent-dkv-3="" type="submit" value="Post">
    </div>
</comment-form>
  • css
.comment-form[_ngcontent-dkv-3] {
    margin-top: 50px;
}

またstyleUrlsによりcssファイルを使用することもできるっぽい。

styleUrls: ['app/style.css'],

バインディング

<input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" />のようにしてkeyupイベントで値をセットし、反映している。この辺りは以下のように書くことで1way2wayか選択できるっぽい。

  • 2way binding
<input type="text" [(ngModel)]=”author” placeholder=”Your name” />
  • 1way binding
<input type="text" value={{author}} placeholder="Your name" />

また、changeだとハンドラが呼ばれるのはblurのタイミングっぽくて、ReactでいうonChangeのタイミングで呼びたかったらkeyupイベントを使うのが良さそうに見える。

angular.io

@output

@input()があったように出力は@output()を使用するっぽい。ここではフォームの内容をEventEmitterを使って<comment-box></comment-box>まで引き上げている。多分、<comment-form></comment-form>に直接CommentServiceInjectすることもできるんだろうけど、Dumb ComponentSmat Componentを意識するとこういうことになるんじゃないかと思う。

comment-item

特筆すべき箇所はないが、この時点でmarkedを追加した。 あと、タグを使用する場合は[innerHTML]を使用するっぽい。

  • components/comment-item.ts
import { Component, Input } from '@angular/core';
import { parse } from 'marked';

@Component({
  selector: 'comment-item',
  template: `
    <div class="comment">
      <h2 class="comment-author">
        {{author}}
      </h2>
      <span [innerHTML]="rawMarkup()"></span>
    </div>
  `
})

export class CommentItem {
  @Input() author: string
  @Input() text: string

  rawMarkup():string {
    return parse(this.text, { sanitize: true });
  }

markedの追加

以下の手順で行った。

npm i -S marked
node_modules/.bin/typings -GS install marked --source dt

system-config.jsに追加

System.config({
  map: {
    ...省略
    'marked': 'node_modules/marked',
  },
  packages: {
    ...省略
    'marked': { main: 'index' },
  }

comment-service

CommentServiceに使用するHttpモジュールについては以下を参考にした。

qiita.com

angular.io

  • service/comment.ts
import { Injectable } from '@angular/core';
import Comment from '../interfaces/comment';
import { Http, Request, Response } from '@angular/http';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';

@Injectable()
export class CommentService {

  constructor(private http: Http) {
  }

  startIntervalFetch(): Observable<Comment[]> {
    return Observable.interval(1000)
      .flatMap(() => this.http.get("http://localhost:3001/comments"))
      .map(res => res.json() as Comment[]);
  }

  add(comment: Comment): Observable<Comment> {
    return this.http
      .post("http://localhost:3001/comments", comment)
      .map(res => res.json() as Comment);
  }
}

startIntervalFetch()では1秒ごとにサーバにコメントを取りに行って、add()でコメントを追加している。 この時点でjson-serverをインストールしてやり取りはそっちと行っている。

@Injectable()

@Injectable()DIを可能にする。

使う側(components/comment-box.ts)は以下のようになる。

@Component({
  providers: [CommentService],
})
export class CommentForm {
  constructor(private commentService: CommentService) {
  }

package.json

package.jsonは最終的に以下。

{
  "name": "ng2-comment-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\" \"npm run json\"",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "typings": "typings",
    "json": "json-server --watch db.json --port 3001",
    "postinstall": "typings install"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@angular/common": "^2.0.0-rc.3",
    "@angular/compiler": "^2.0.0-rc.3",
    "@angular/core": "^2.0.0-rc.3",
    "@angular/http": "^2.0.0-rc.3",
    "@angular/platform-browser": "^2.0.0-rc.3",
    "@angular/platform-browser-dynamic": "^2.0.0-rc.3",
    "es6-shim": "^0.35.1",
    "marked": "^0.3.5",
    "reflect-metadata": "^0.1.3",
    "rxjs": "^5.0.0-beta.9",
    "symbol-observable": "^1.0.1",
    "systemjs": "^0.19.31",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "concurrently": "^2.1.0",
    "json-server": "^0.8.14",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings": "^1.3.0"
  }
}

 まとめ

  • 1.xのときよりシンプルになったような印象を受ける
  • 二の足を踏んでいたけど、導入障壁も高くないように感じた
  • ただ、逐一追っているわけではないがまだ破壊的な変更が入っているような話しを耳にする
    • 次に触るのはもう少し安定してからでいいかな、と感じた
    • その際はなにか独立したコンポーネント、例えばreact-resizable-and-movableあたりを移植してみたい
  • Singleton store脳な自分には状態を一元管理するような方法があるとありがたい
    • ng-reduxとか?あまりいい評判は聞かない。。。
  • テスト周りがわからん。rc.4からjasmineに限らずmochaも使用できるようになった?