JSライブラリ >

Angular2

環境構築

準備

node 6.9.x かつ npm 3.x.x が必要なので、必要に応じてインストール、バージョンアップする。

nodejs のインストール、バージョンアップ

Angular CLI のインストール

npm install -g @angular/cli

アップデート

npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest

プロジェクトの作成

ng new my-app

ローカルPC上でサーバ起動

ng serve

ビルド

ビルドコマンド

ng build オプション

ビルドオプション

オプション使用例説明
--output-path=出力先ng build --output-path=/var/www/site1ファイルの出力先を指定する
--bh アプリケーションルートng build --bh /dir1/dir2/例えば http://example.com/dir1/dir2/ 配下で Angularアプリを動作させる場合
--prodng build --prod本番用に出力。各出力ファイルは minify され、ファイル名の末尾にランダムな文字列が付与される。
例) main.45f2a42eedaf6eab8df0.bundle.js
--env=環境名ng build --prod --env=prod環境名を指定してビルドする。(左記の場合、環境変数として environment.prod.ts が使用される)

Angular CLI メモ

ng new my-app  // my-appプロジェクトの作成
ng g component hoge // hogeコンポーネントの作成
ng g service hoge // hogeサービスの作成
ng test // jasmin + karmaテスト。
ng lint // Typescriptのtslintを実行。記法、エラーのチェック。
ng e2e  // e2e(End to End)テスト

Component

Angular2 では各ページをや部品を Component として定義する。
コンポーネントは html、css、TypeScript で構成され、各処理からそれらを利用する事ができる。

Componentの生成

ng generate component hello

Componentの配置

app.component.html などに以下の通り記載

<app-コンポーネント名></app-コンポーネント名>

もしくは PATH定義と共にリンクやコードから切り替える(後述を参照)

例)

<!-- headerコンポーネントを配置 -->
<app-header></app-header>

<!-- ルーターがコンポーネントを表示する場所 -->
<div id="content">
  <router-outlet></router-outlet>
</div>

<!-- footerコンポーネントを配置 -->
<app-footer></app-footer>

Component の継承

親コンポーネント ( base/base.component.ts )

  .
  .
export class BaseComponent implements OnInit {
  loading = false;
  .
  .

子コンポーネント( child1/child1_component.ts )

  .
  .
import { BaseComponent } from '../base/base.component';
  .
  .
export class Child1Component extends BaseComponent implements OnInit {

  books = [];

  ngOnInit() {
    this.loading = true;    //  ←  親コンポーネントで定義した変数にアクセス
    const params = { 'var1' : 'ABC' };
    this.api.getBooks(
      params,
      (results) => {
        if (results.items) {
          this.books = results.items;
        }
        this.loading = false;//  ←  親コンポーネントで定義した変数にアクセス
      },  
      (error) => {
        this.loading = false;//  ←  親コンポーネントで定義した変数にアクセス
      }   
    );  
  }
}

子コンポーネントのHTML ( child1/child1_component.html )

  .
  .
<!-- loading 中は loading コンポーネントを表示 -->
<app-loading *ngIf="loading"></app-loading>

環境変数を利用する

環境の定義

.angular-cli.json の app.environments 配下に環境名を追加し、ファイルを用意する。

{
  .
  .
  "apps": [
    {   
        .
        .
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        "test": "environments/environment.test.ts"       <--  test環境を追加
      }   
    }   
  ],  

環境変数の定義

environments 配下の environment.環境名.ts というファイルで環境毎の値を定義する。

例) src/environments/environment.test.ts

export const environment = { 
  production: false,
  apiUrl: "http://xxx.xxx.xxx/api/"
};

環境変数の利用

サービスからの利用例( src/app/api-base.service.ts )

  .
  .
import { environment } from '../environments/environment';
  .
  .
@Injectable()
export class ApiBaseService {
  
  doGet (url: string, params: Object, callback: Function, errorCallback: Function) {
    url = environment.apiUrl + url;
    .
    .
  }

環境を指定してビルド

ng build --env=test

環境を指定してローカルサーバ起動

ng serve -e test

データバインディング

app.module.ts

  .
  .
import { FormsModule } from '@angular/forms'; // 追加
  .
  .
@NgModule({
  .
  .
  imports: [
    .
    .
    FormsModule,     // 追加
  .
  .

bind1.component.html

<div style="padding:10px;">
  片方向データバインディング
  <span>value1 : {{value1}}</span>
</div>

<div style="padding:10px;">
  双方向データバインディング  ※厳密には プロパティ/イベントの片方向バインディングを同時に指定する事により実現。
  <input type="text" [(ngModel)]="value1" >
</div>

<div style="padding:10px;">
  イベントバインディング
  <button (click)="clickButton1()">テスト</button>
</div>

<div style="padding:10px;">
  プロパティバインディング
  <button [disabled]="isDisabled">テスト</button>
</div>

bind1.component.ts

export class Bind1Component implements OnInit {

  isDisabled = false;
  value1 = 'Test';

  clickButton1(){
    this.value1 = 'TEST2';
    this.isDisabled = !this.isDisabled;
  }

ヘッダとフッタ

<p>ヘッダー</p>
<router-outlet></router-outlet>
<p>フッター</p>

※ router-outlet の箇所にコンポーネントが埋め込まれる

ログイン認証などがあるケース等でヘッダやメニューの内容を動的に変更したい場合は、コンポーネント化する方が良い。

<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>

ページ切り替え

コンポーネントの作成

ng g component page1
ng g component page2

URLとコンポーネントの関連を定義

app.module.ts

  .
import { RouterModule } from '@angular/router';
  .
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
  .
  .
@NgModule({
  declarations: [
    AppComponent,
     .
     .
  ],  
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: 'page1', component: Page1Component },
      { path: 'page2', component: Page2Component }
    ], { useHash: true })
  ],  
  .
  .
})

リンクによる切り替え

<a routerLink="/page1" routerLinkActive="active">ページ1に切り替え</a>
<a routerLink="/page2" routerLinkActive="active">ページ2に切り替え</a>

コードから切り替え

https://codezine.jp/article/detail/9887
https://angular.io/api/router/Router#navigate

page1.component.html

<button (click)="changePage()">ページ2に切り替え</button>

page1.component.ts

import { Router } from '@angular/router';

constructor(private router:Router) {
}

changePage() {
  this.router.navigate(['./page2']);
}

画面遷移時のパラメータ

PATH定義

app.module.ts

  .
  .
@NgModule({
  declarations: [
    AppComponent,
     .
     .
    Page3Component,
  ],  
  imports: [
    BrowserModule,
    HttpModule,
    RouterModule.forRoot([
       .
       .
      { path: 'page3/:id', component: Page3Component }
    ], { useHash: true })
  ],  
  providers: [], 
  bootstrap: [AppComponent]
})

パラメータ付きで画面切り替え

page1.component.ts

import { Router } from '@angular/router';

  constructor(private router:Router) {
  }

  changePage() {
    this.router.navigate(['./page3', 1234, {'msg': 'Hello!'} ]);    // page3/1234 に遷移
  }

パラメータの受け取り

page3.component.ts

import { ActivatedRoute } from '@angular/router';
import { Params } from '@angular/router';
  .
  .
export class Page3Component implements OnInit {

  constructor(private route:ActivatedRoute) {
  }

  ngOnInit() {
    console.log('page3 init');
    this.route.params.forEach((params: Params) => {
      console.log(params['id']);        // 1234 
      console.log(params['msg']);    // Hello!
    }); 
  }
}

非同期通信

参考: https://codezine.jp/article/detail/10009

素でComponentに実装する場合

list1/list1.component.html

  <table id="books" class="books table" style="width:100%;">
    <thead>
      <th>No</th>
      <th>Isbn</th>
      <th>Title</th>
      <th>Price</th>
      <th>Date</th>
    </thead>
    <tbody>
    <tr *ngFor="let book of books; let i = index;" (click)="onClick(i)" (dblclick)="onDblClick(i)">
      <td>{{i+1}}</td>
      <td>{{book.isbn}}</td>
      <td>{{book.title}}</td>
      <td>{{book.price}}</td>
      <td>{{book.date}}</td>
    </tr>
    <tr *ngIf="nodata">
      <td colspan="5" style="text-align:center;">データはありません</td>
    </tr>
    </tbody>
  </table>

list1/list1.component.ts

// import { Http, Response, Headers, RequestOptions } from '@angular/http';  // 非推奨
import { HttpClient } from '@angular/common/http';
  .
  .
export class List1Component implements OnInit {

  nodata = false;
  loading = false;

  books = [];

  constructor(protected http: HttpClient) {
  }

  ngOnInit() {

    this.loading = true;
    // this.nodata = false;

    const httpGetObservable = this.http.get('https://xxx.xxx.xxx/api/books/');
    httpGetObservable.subscribe(
      res => {
        this.nodata = true;

        /* @angular/http を使用する場合(非推奨)
        if (res.text()) {
          var results = JSON.parse(res.text());
          if (results.items) {
            this.books = results.items;
          }
          if (this.books.length == 0) {
            this.nodata = true;
          }
        }
        */

        const result: any = res;
        if (result.items) {
          this.books = result.items;
          this.loading = false;
          if (this.books.length > 0) {
            this.nodata = false;
          }
        }
      },  
      error => {
        console.error(error.status + ':' + error.statusText);
      }   
    );  
  }

}

通信処理をサービスに分離する

TODO: RxJS Observable

environments/environment.ts

export const environment = { 
  production: false,
  apiUrl: "http://xxx.xxx.xxx/api/"
};

api-base.service.ts

import { Injectable } from '@angular/core';
// import { Http, Response, Headers, RequestOptions } from '@angular/http'; // 非推奨
import { HttpClient } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { environment } from '../environments/environment';

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class ApiBaseService {

  vars = {}; 

  // constructor (private http: Http) {
  constructor (private http: HttpClient) {
    console.log('ApiBaseService constructor!');
  }

  doGet (url: string, params: Object, callback: Function, errorCallback: Function) {

    let apiUrl = environment.apiUrl + url;

    // クエリストリングの組み立て
    let delimiter = '?';
    for (const key in params) {
      if (typeof(key) === 'string' && params[key]) {
        apiUrl = apiUrl + delimiter + key + '=' + params[key];
        delimiter = '&';
      }   
    }   

    // 追加ヘッダ
    const options = { 
        headers: {
            'x-text-header1': 'test123'
        }
    };

    // const httpGetObservable = this.http.get(apiUrl, params);
    const httpGetObservable = this.http.request('get', apiUrl, options);
    httpGetObservable.subscribe(
      res => {
        if (callback) {
          callback(res);
        }
      },  
      error => {
        if (errorCallback) {
          errorCallback(error);
        }
      }
    );
  }

  doPost (url: string, params: Object, callback: Function, errorCallback: Function) {

    url = environment.apiUrl + url;

    // 追加ヘッダ、ボディ部のデータ指定
    const options = {
        headers: {
            'x-text-header1': 'test123'
        },
        body: JSON.stringify(params)
    };

    // const httpGetObservable = this.http.post(url, params);
    const httpGetObservable = this.http.request('post', url, options);
    httpGetObservable.subscribe(
      res => {
        if (callback) {
          callback(res);
        }
      },
      error => {
        if (errorCallback) {
          errorCallback(error);
        }
      }
    );
  }

}

api.service.ts

import { Injectable } from '@angular/core';
import { ApiBaseService } from './api-base.service';

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class ApiService extends ApiBaseService {

  getBooks(params:Object, callback:Function, errorCallback:Function) {
    this.doGet('books/', params, callback, errorCallback);
  }

}

app.module.ts

  .
  .
import { ApiService } from './api.service';
  .
  .
@NgModule({
  .
  .
  providers: [ ApiService ],
  .
  .
})

list2/list2.component.ts

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../api.service';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-list2',
  templateUrl: './list2.component.html',
  styleUrls: ['./list2.component.css']
})
export class List2Component extends BaseComponent implements OnInit {

  books = []; 

  constructor(private api: ApiService) {
  }

  ngOnInit() {
    this.loading = true;
    var params = { 'var1' : 'ABC' };
    this.api.getBooks(
      params,
      (results)=>{
        if (results.items) {
          this.books = results.items;
        }   
        this.loading = false;
      },  
      (error)=>{
        this.loading = false;
      }   
    );  
  }

}

リロード対応

その1 ... useHash を使用する

@NgModule({
  imports: [ RouterModule.forRoot(routes, { useHash: true }) ],
  .
  .

URL が http://xxx.xxx.xxx/#/page1 のようになる為、リロードしても index.html がロードされる。

jQueryの使用

npm install jquery --save
npm install @types/jquery --save-dev

xxxx.component.ts

import * as $ from 'jquery';
  .
  .

以上で $('xxxx') 等で普通に使用できる。

bootstrapの使用

普通に npm install して使おうとしたらエラーになった。

失敗手順

npm install bootstrap --save

.angular-cli.json

       "styles": [
+      "../node_modules/bootstrap/dist/css/bootstrap.min.css",
         "styles.css"
       ],
       "scripts": [
        "../node_modules/jquery/dist/jquery.min.js",
+      "../node_modules/bootstrap/dist/js/bootstrap.min.js"
       ],

で ng serve すると以下のエラーになる。

ng serve
.
.
ERROR in ./node_modules/css-loader?{"sourceMap":false,"importLoaders":1}!./node_modules/postcss-loader/lib?{"ident":"postcss","sourceMap":false}!./node_modules/bootstrap/dist/css/bootstrap.min.css
Module build failed: BrowserslistError: Unknown browser major

対応

bootstrap 4.0.0-beta.2 だとOKらしいので、4.0.0-beta.2 に変更して npm install し直せばOK。

package.json を修正

-    "bootstrap": "^4.0.0",
+   "bootstrap": "4.0.0-beta.2",

再インストール

npm install

でOK。

対応その2

bootstrap をAngularのコンポーネントから触る事ないなら(というか触る事ないので)、assets に放り込んでしまえば早い。

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MyApp2</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
+ <link rel="stylesheet" type="text/css" href="assets/css/bootstrap.min.css">
</head>
<body>
  <app-root></app-root>

+ <script src="assets/js/jquery-3.3.1.min.js"></script>
+ <script src="assets/js/bootstrap.min.js"></script>
</body>
</html>

で、bootstrap の npm モジュールは削除してもOK。( .angular-cli.json からも消してOK )

対応その3

jQuery だけは Angularで使う。という場合は、こんな感じ。

・上記の index.html の追記は削除して。
・bootstrap の npm モジュールも削除。( package.json からbootstrapの行を削除して npm install )

jQueryのインストール

npm install jquery -save

bootstrap を assets とは別のディレクトリにダウンロードする。
※angular-cli.jsonで読み込むので assets にはいらないんで。(後述)

mkdir -p src/assets-min/css
mkdir -p src/assets-min/js
wget https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css -O src/assets-min/css/bootstrap.min.css
wget https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js -O src/assets-min/js/popper.min.js
wget https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js -O src/assets-min/js/bootstrap.min.js

.angular-cli.json

       "styles": [
+        "assets-min/css/bootstrap.min.css",
         "styles.css"
       ],
       "scripts": [
+        "../node_modules/jquery/dist/jquery.min.js",
+        "assets-min/js/popper.min.js",
+        "assets-min/js/bootstrap.min.js"
       ],

コーディングルールなど

Typescript の記法チェック( ng lint ) まで行うなら、以下のルールは守っておいた方が良い。

文字列の括りはシングルクォーテーション

import { ActivatedRoute } from "@angular/router";    // NG
import { ActivatedRoute } from '@angular/router';     // OK

コロンの後はスペース1個分あける

constructor(private http:Http, private router:Router, private route:ActivatedRoute) {      // NG
constructor(private http: Http, private router: Router, private route: ActivatedRoute) {   // OK

不要なスペースは削除(スペースは1個)

import { ActivatedRoute }  from '@angular/router'; // NG ... from の前のスペースが2個ある
import { ActivatedRoute } from '@angular/router';

コメント行の最初の1文字目はスペースにする

//NG
// OK

行末に不要なスペースを記述しない

  let var1 = '';

値の変更を行わない変数は const とする

  for (const key in params) {
     .
     .
  }

演算子の間などにはスペースを1個入れる

const var1 = "ABC";
const var2 = var1+'001';       // NG
const var3 = var1 + '001';     // OK

参考

Angular2
https://angular.io/

CodeZine「次世代Webアプリケーションフレームワーク「Angular」の活用」連載一覧
https://codezine.jp/article/corner/653

ルーター(URLとリンクでコンポーネント表示を切り替える)
https://codezine.jp/article/detail/9700

コンポーネントのライフサイクル
https://codezine.jp/article/detail/10046

コンポーネントとモジュール
https://codezine.jp/article/detail/9700

サービスと依存性注入
https://codezine.jp/article/detail/9779

非同期HTTP通信
https://codezine.jp/article/detail/10009


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-02-24 (土) 18:54:03 (288d)