19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularAdvent Calendar 2016

Day 15

ReactユーザーのためのAngular2ファーストステップ

Posted at

Reactはやったことある人向けに、ReactのアレはAngular2だとどうなってるの?というのをまとめていきます。Reactユーザーだった自分がAngular2始めるにあたりこういう情報あればなと思ってたやつです :joy:

ところでAngular2じゃなくてAngularと呼んでね!とのことなので、以下Angularと呼びますがAngular 2.xを指します。

アプリケーションのブートストラップ

React

index.html
<!doctype html>
<html lang="en">
  <head>
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
index.js
import React from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
  render() {
    return (
      <div>
        <p>hello world</p>
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

ReactではアプリケーションのルートとなるComponentと、対象となるDOM要素を指定して ReactDOM.render ですね。

Angular

index.html
<!doctype html>
<html>
<head>
  <title>Angular2</title>
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>
index.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <p>{{title}}</p>
    </div>
  `
})
export class AppComponent {
  title = 'hello world';
}

Angularでは、ブートストラップ対象になるのはModuleです。Moduleは、Component、Serviceなどからなるアプリケーションの機能の単位です。 @NgModule デコレータの bootstrap に指定したComponentが表示されます。
この例では platformBrowserDynamic().bootstrapModule(AppModule);AppModule が起動し、 bootstrap: [AppComponent] なので AppComponent 描画され、 selector: 'app-root' なので <app-root>Loading...</app-root><div><p>hello world</p></div> に書き換わるという感じです!

Component

React

App.js
class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: [
        'sleep',
        'do nothing'
      ]
    };
  }

  componentDidMount() {
    console.log('mounted :)');
  }

  render() {
    return (
      <div className="App">
        <h1>{this.props.title}</h1>
        <ul>
          {this.state.todos.map((todo, i) => <li key={i}><p>{todo}</p></li>)}
        </ul>
        <button type="button" onClick={this.props.onClick}> +1 :)</button>
        { this.props.children }
      </div>
    );
  }
}

React.Component を継承したクラスに render メソッドを実装しておけばReactのComponentですね!
その他にも componentDidMount などのライフサイクルメソッド、 props, props.children, state あたりがReact Componentの代表的な機能でしょうか。

Angular

先程の例にもあるように、AngularではReactで render メソッドに書いていたHTMLは @Component デコレータに書きます。 templateUrl: 'app.component.html' とすることでHTMLファイルを分離することもできます。CSSも同様です。
Reactだとクラス名がそのまま <MyClass /> のように使えましたが、Angularでは selector に指定した文字列がそのComponentで置き換えられます。
Reactの props, props.children, state, ライフサイクルメソッドに相当するような機能がAngularにもあるので、ひとつずつ見ていきましょう。

props

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <sample-child [title]="'hello'" (onHello)="handleHello($event)"></sample-child>
  `
})
export class AppComponent {
  handleHello(ev: any) {
    console.log(ev);
  }
}
sample-child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'sample-child',
  template: `
    <h1>{{title}}</h1>
    <button type="button" (click)="clicked()">say hello</button>
  `
})
export class SampleChildComponent {
  @Input() title: string;
  @Output() onHello = new EventEmitter<any>();

  clicked() {
    this.onHello.emit({ 'i said': 'hello' });
  }
}

親Component→子Component

親Componentから子Componentにデータを受け渡すには、子Componentに @Input アノテーションを付けたプロパティを用意します。親Componentは <child [title]="'hello'" /> のように [] で渡すプロパティを指定します。これがプロパティバインディングです。

<child [title]="'hello'" />"'hello'" という表記が気になったかと思いますが、これはReactでいう <Child title={'hello'} /> です。Reactだと {} の中にJSの式を書けるように、Angularのバインディングの右辺では "" の中はJSになります。 なのでただの文字列を与えたいときは "'hello'" となります。

子Component→親Component

子から親にイベントを伝えるには、子に @Output() onHello = new EventEmitter<any>(); のように @Output アノテーションをもった EventEmitter のプロパティを定義し、親では <child (onHello)="handleHello($event)" /> のように () で囲んで指定します。 handleHello は親Componentのクラスが持っているメソッドです。 $event は特別なシンボルで、イベントのメッセージというかペイロードが入っています。この例では { 'i said': 'hello' } というJSONが入っています。これがイベントバインディングです。
他にもフォームなどでよく使う <input [(ngModel)]="myName" /> とするTwo-wayデータバインディングなどがあります。親→子、子→親両方あるので [(ngModel)] ってことですかね。なかなか慣れてないとアレな気はします :sob:

props.children

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <my-frame>
      <p>in my frame :)</p>
    </my-frame>
  `
})
export class AppComponent {
}
my-frame.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-frame',
  template: `
    <div class="myframe">
      <ng-content></ng-content>
    </div>
  `,
  styles: [`
    .myframe {
      color: red;
    }
  `]
})
export class MyFrameComponent {
}

自分のタグ内に定義されたHTMLを出力するには <ng-content></ng-content> を使います。上記の例の出力は

<div class="myframe">
  <p>in my frame :)</p>
</div>

となり、 .myframe のCSSが効いて赤い文字になります。

state

Angularだと普通にクラス内のインスタンスをOne-wayなりTwo-wayバインディングする感じでしょうか。特に言うことないです。 :rolling_eyes:

ライフサイクルメソッド

.app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { SampleService } from './sample.service';

@Component({
  selector: 'app-root',
  template: `
    <p>{{message}}<p>
    <button (click)="fetchMessage()">fetch</button>
  `,
  providers: [
    SampleService
  ]
})
export class LifeCycleSampleComponent implements OnInit, OnDestroy {

  message: string = 'initial message';
  subscription: Subscription;

  constructor(private sampleService: SampleService) { }

  fetchMessage() {
    this.sampleService.nextMessage();
  }

  ngOnInit() {
    this.subscription = this.sampleService.message.subscribe(message => this.message = message);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Reactの componentDidMountComponentWillUnmount といったライフサイクルメソッドに相当するものがAngularにもあります。それらを実装する場合は class MyComponent extends OnInit のように該当するサイクルをミックスインし、 ngOnInit メソッドなどに実装を書きます。実装するライフサイクルメソッドがクラスを見たときに明示的なのがいいですね!
この例ではComponentの初期化時にServiceクラス(後述)が持っているObservableをsubscribeし、Componentが破棄されるタイミングでsubscriptionも破棄しています。こういったイベントリスナーの登録・破棄がまずは思いつくユースケースな気がします。

(Reactにはない)Service

sample.service.ts
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable()
export class SampleService {

  private _message: Subject<string>;
  message: Observable<string>;

  constructor() {
    this._message = new Subject<string>();
    this.message = this._message.asObservable();
  }

  nextMessage() {
    this._message.next('next message');
  }
}

Viewだけを扱うReactにはビジネスロジックや副作用などを書く場所がありません。そこでFluxアーキテクチャを導入することが多いと思います。Angularではそういったものを担当するクラスをServiceとして定義し、Componentから使うことができます。Serviceにデータを持たせて、取得や変更するメソッド、もしくはデータ変更で発火するイベントやObservableを生やしておく感じです。
Serviceのインスタンス化は自分でnewせずにAngularのも持つDIの仕組みに任せます。 @Injectable アノテーションを付け、Serviceを使うComponentやModuleから providers: [ MySerivce ] とします。そうしてコンストラクタの引数にServiceを引数として与えればServiceのインスタンスが得られます。
注意点としては providers に書くたびに別のServiceのインスタンスができるので、アプリケーションで共通のデータを持たせたいServiceはComponentではなくアプリケーションのModuleのprovidersに書いておかないといけません。

router

React

index.js
// github.com/ReactTraining/react-router/README.md から
render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
      <Route path="*" component={NoMatch}/>
    </Route>
  </Router>
), document.getElementById('root'))

React自体はルーティングの機能を持っていないので、react-routerといったライブラリを使用すると思います。使い方はシンプルで、 pathcomponent を列挙していく感じですね。

app.module.ts
+ import { RouterModule } from '@angular/router';
+ import { routes } from './app.routes';


@NgModule({
  ...
  imports: [
    HttpModule,
+   RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
app.routes.ts
import { Routes } from '@angular/router';

import { TodosComponent } from './todos/todos.component';
import { UsersComponent } from './users/users.component';

export const routes: Routes = [
  { path: 'todos', component: TodosComponent },
  { path: 'users', component: UsersComponent }
];
app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  template: `
     <h1>{{title}}</h1>
     <a routerLink="/todos">Todos</a>
     <a routerLink="/users">Users</a>
     <router-outlet></router-outlet>
   `
})
export class AppComponent {
  title = 'root component';
}

Angularはルーティング機能も持っており、 @angular/router からModuleをインポートして使います。ルーティングの定義はreact-routerと似た感じでパスをComponentを書いていきます。ルーティングにマッチしたComponentは <router-outlet></router-outlet> に描画されます。

12/9に東京で開催されたAngularのミートアップでは、 @angular/core が2.xなのに対し @angular/router が3.xとずれている問題があり、これを解消するために次期Angularのメジャーバージョンは3ではなく4になる話が出たみたいです(発表資料)。4...!

create-react-app

create-react-appと同様のツールとしてangular-cliがあります。Typescript, Webpack, karmaなどなどの設定を済ませたAngularアプリの雛形を生成してくれます。それだけでなく、ComponentやServiceの雛形とそのテストコードを生成し、NgModuleにインポートしておいてくれるという便利機能なども付いています。

$ npm i -g angular-cli # ng コマンドが使えるようになる
$ ng new my-angular-app # Angularアプリを生成する
$ cd my-angular-app
$ ng serve # webpack-dev-serverを起動
$ ng test
$ ng e2e
$ ng build
$ ng g component my-component # generateを省略してg
installing component
  create src/app/my-component/my-component.component.css
  create src/app/my-component/my-component.component.html
  create src/app/my-component/my-component.component.spec.ts
  create src/app/my-component/my-component.component.ts
$ ng g service my-service
installing service
  create src/app/my-service.service.spec.ts
  create src/app/my-service.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

個人的にWebpackとかの設定つらいので、そういったところをこういうツールで補ってもらえるのはとてもありがたいです :pray:

react-native

Reactに対するReact Nativeと同じような位置にionic2、OnsenUI、NativeScriptなどいくつかのライブラリがあります。angular/react-native-rendererというReact NativeのレンダラとAngularをつなぐ(!?)のもあるみたいです。
この中でionic2を少し触ったのですが、ionic2が提供するComponentがWeb/iOS/Androidで動作するようになっており、learn once write anywhereではなくwrite once run anywhereなとこが衝撃的でした。ジャバか...!

// どのプラットフォームでも動く
<ion-list>
  <button ion-item *ngFor="let item of items" (click)="itemSelected(item)">
    <my-item>{{ item }}</my-item>
  </button>  
</ion-list>

番外編

Angularへの苦手意識、解消します :muscle:

ng-xxxを無限に覚えるのが嫌だ

Angular 1.xをちょっとやってたときの個人的な記憶なのですが、何をするにもng-xxxを使わなければならず、ng-xxxを探して実装してバグったらエラーメッセージに出て来るng-xxxがどうしたというのをググる...という辛いものがあったのですが、Angular2ではそのへんかなり軽減されてる気がします。とりあえずの登場人物は ng-forng-if だけで、エラーメッセージも良くなっていて、だいたいは書いてあることを読めば修正できる感じです :thumbsup:

JSXをキモイと言うが、 クラスにくっついてるクッソでかいデコレータにしかもtemplateがただの文字列で入ってるとかそっちのほうが(略

テンプレートがただの文字列なのは仕方ないのですが、webstormやエクステンションを入れたVSCodeなどではシンタックスハイライトを提供してくれるのでかなり低減されます。

スクリーンショット 2016-12-14 23.36.01.png

もちろん templateUrl: 'foo.component.html' としてファイルを分けてしまうのもアリです。テンプレートがただの文字列のため、実行するまではテンプレートにミスがあるかわからないという問題があります。そのためコンポーネントが描画されるかの最低限のテストを書いていくか、AoTコンパイルを行って実行前にエラーを出すなどをやったほうがいいみたいです。(そもそもAoTは必須という話も)

まとめ

Reactから見たAngularの基礎要素をざっと紹介しました。Angular本体はstableになりましたがまだまだエコシステムがReactのように成熟していないので、いまのところは dependencies の中がbetaだらけになったりする難点がありますが、Webアプリを楽しく書けるフレームワークなので、まだ触っていない方は是非! :joy:

参考

公式ドキュメントをGoogle翻訳片手に読んでいくのがAngular入門には結局のところベストな気がします。
https://angular.io/docs/ts/latest/
Tour of heroesというチュートリアルにそって一通り学べます。Plunkerというブラウザ上でJSアプリが動作するWebサイトを使って各ステップごとのアプリが用意されているので、そこで簡単な動作確認をしつつ、ファイル一式をzipでダウンロードしローカルでいつものエディタでいじることも可能です。ダウンロードしたindex.htmlがあるディレクトリでhttp-serverなどを立ち上げればsystem.jsというすごいやつがなんやかんやしてAngularが動くぞ!
ちなみにバンドラーとしてWebpack, Browserify, System.jsなどありますが、WebpackがAngular界隈では一番使われてる気がします。System.jsはAngularのすごい人がまだ早いって言ってた!(参照: TechFeed Live#2レポート)

チートシートも用意されているので、あれどうするんだっけ?とか、これなんだ?→調べる→学びが捗ります :thumbsup:
https://angular.io/docs/ts/latest/guide/cheatsheet.html

19
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?