中安拓也のブログ

プログラミングについて書くブログ

【JavaScript】Ionicで架空のECアプリを作成する #4 - カート画面を作る(前編)

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第4回目である今回は、ショッピングカートに商品を追加する処理の実装まで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10
  • Google Chrome バージョン: 60.0.3112.113

下準備

カート画面用のファイル作成

Ionic CLIのコマンドで、カート画面用のコンポーネントファイルなどをまず作る。

$ ionic generate page cart --no-module

コマンドの結果、下記の3ファイルが作成される

  • src/pages/cart/cart.ts
  • src/pages/cart/cart.html
  • src/pages/cart/cart.scss

ionic-storageのインストール

カート画面でストレージを使うので、下記のコマンドを実行してionic-storageをインストールしてください。

$ ionic cordova plugin add cordova-sqlite-storage
$ npm install --save @ionic/storage

NgModuleの設定

下記の対応を実行する。

  • @NgModuledeclarations: []entryComponents: []配列に、作成したCartPageを追加
  • @NgModuleimports: []配列にIonicStorageModule.forRoot()を追加

src/app/app.module.ts:

// ...省略

import { CartPage } from "../pages/cart/cart";
import { IonicStorageModule } from "@ionic/storage";

@NgModule({
  declarations: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    DetailPage,
    CartPage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    HttpClientModule,
    IonicStorageModule.forRoot()
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    DetailPage,
    CartPage
  ],
// ...省略

実装

タブ(共通ナビゲーション)を実装する

カート画面へのリンクがないので、タブにリンクをつけて、アイコンもショッピングカートぽいのに変える。

src/pages/tabs/tabs.html:

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="商品" tabIcon="home"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="カート" tabIcon="cart"></ion-tab>
</ion-tabs>

src/pages/tabs/tabs.ts:

import { Component } from "@angular/core";

import { HomePage } from "../home/home";
import { CartPage } from "../cart/cart";

@Component({
  templateUrl: "tabs.html"
})
export class TabsPage {
  tab1Root = HomePage;
  tab2Root = CartPage;

  constructor() {}
}

あとの回で、ショッピングカート内の商品数をBadgeで表現する実装も入れます

f:id:l08084:20180501164505p:plain
実装後のタブ

ショッピングカートに商品を追加する処理

商品詳細画面に「カートに1つ追加する」ボタンを押下すると、ストレージに商品情報を格納する処理を追加する。

src/pages/detail/detail.ts:

import { Component } from "@angular/core";
import { NavController, NavParams } from "ionic-angular";
import { Storage } from "@ionic/storage";
import { Product } from "../../model/product.model";

@Component({
  selector: "page-detail",
  templateUrl: "detail.html"
})
export class DetailPage {
  product: Product;

  constructor(
    private navCtrl: NavController,
    private navParams: NavParams,
    private storage: Storage
  ) {}

  ngOnInit() {
    // 商品リスト画面でタップした商品情報を受け取る
    this.product = this.navParams.get("product");
  }

  /**
   * ショッピングカートに商品を追加する
   *
   * @param {Product} product
   * @memberof DetailPage
   */
  addItem(product: Product) {
    let itemList = [];
    this.storage
      .length()
      .then(result => {
        // カート内の商品が0個だった場合
        if (result === 0) {
          itemList = [];
          itemList.push(product);
          // ストレージにボタンを押下した商品を追加する
          this.storage.set("items", itemList);
        } else {
          // カート内にすでに商品があった場合
          this.storage
            .get("items")
            .then(items => {
              items.push(product);
              const count = items.length;
              // ストレージにボタンを押下した商品を追加する
              this.storage.set("items", items);
            })
            .catch(err => console.log(`storage error: ${err}`));
        }
      })
      .catch(err => console.log(`storage error: ${err}`));
    // 商品リスト画面に戻る
    this.navCtrl.pop();
  }
}

src/pages/detail/detail.html:

  <!-- ... 長いので省略 -->

  <button ion-button full color="secondary" class="in-cart" (click)="addItem(product)">カートに1つ追加する</button>
</ion-content>

カートに追加ボタンの動作確認

Chromeのdevtool(デベロッパーツール)で確認すると、ボタンを押下した商品情報がionic-storageの中に格納されていることがわかる。

f:id:l08084:20180501173923p:plain


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #3 - 商品詳細画面を作る

f:id:l08084:20180430174028g:plain

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第3回目である今回は、商品詳細画面を作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

下準備

商品詳細画面用のファイルを作成

前回作成した商品リスト画面から遷移できる商品詳細画面を作成する。

まず、Ionic CLIの下記コマンドを実行して、必要なファイルのセットを作成する

$ ionic generate page detail --no-module

商品詳細画面のテンプレート、SCSS、コンポーネントファイルが作成される

f:id:l08084:20180430160627p:plain

NgModuleの設定

@NgModuledeclarations: []entryComponents: []配列に先ほど作成した商品詳細画面のクラスDetailPageを追加する。

src/app/app.module.ts

// ...省略

// add this!
import { DetailPage } from "../pages/detail/detail";

@NgModule({
  // add this!
  declarations: [MyApp, AboutPage, ContactPage, HomePage, TabsPage, DetailPage],
  imports: [BrowserModule, IonicModule.forRoot(MyApp), HttpClientModule],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    // add this!
    DetailPage
  ],
// ...省略

実装

商品リスト画面に画面遷移処理を追加

商品リスト画面で商品をタップした時に、商品詳細画面に遷移するようにする。

src/pages/home/home.ts:

// ...省略

// add this!
import { DetailPage } from "../detail/detail";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  // ...省略

  constructor(
    // add this!
    private navCtrl: NavController,
    private productProvider: ProductProvider
  ) {}
  
  // add this!
  goToDetail(product: Product) {
    this.navCtrl.push(DetailPage, { product: product });
  }
}

タップした商品を商品詳細画面に渡して遷移するメソッドgoToDetail()を追加した

src/pages/home/home.html:

    // ...省略
 // (click)="goToDetail(jacket)"を追加
    <button ion-item *ngFor="let tops of topsList" (click)="goToDetail(tops)">
      <ion-thumbnail item-start>
        <img src="{{tops.imagePath}}">
      </ion-thumbnail>
      <h2>{{tops.name}}</h2>
      <p>¥{{tops.price}}</p>
    </button>
    <ion-list-header>
      ジャケット/アウター
    </ion-list-header>
    // (click)="goToDetail(jacket)"を追加
    <button ion-item *ngFor="let jacket of jacketList" (click)="goToDetail(jacket)">
      <ion-thumbnail item-start>
        <img src="{{jacket.imagePath}}">
      </ion-thumbnail>
      <h2>{{jacket.name}}</h2>
      <p>¥{{jacket.price}}</p>
    </button>
  </ion-list>
</ion-content>

<button ion-item>(click)="goToDetail(tops)"を追加

商品詳細画面の作成

商品詳細画面の実装をしていく

src/pages/detail/detail.ts:

import { Component } from "@angular/core";
import { NavController, NavParams } from "ionic-angular";
import { Product } from "../../model/product.model";

@Component({
  selector: "page-detail",
  templateUrl: "detail.html"
})
export class DetailPage {
  product: Product;

  constructor(public navCtrl: NavController, public navParams: NavParams) {}

  ngOnInit() {
    this.product = this.navParams.get("product");
  }
}

this.product = this.navParams.get("product");の部分で、商品リスト画面からタップした商品データを受け取っている

src/pages/detail/detail.html:

<ion-header>
  <ion-navbar>
    <ion-title>
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <h2>{{product.name}}</h2>
  <div class="image-container">
    <img class="product-image" src="{{product.imagePath}}">
  </div>
  <div class="comment">
    <p>{{product.commentOne}}</p>
    <p>{{product.commentTwo}}</p>
  </div>
  <ion-list no-lines>
    <ion-item>
      <div>
        <p class="title">小計</p>
        <p class="price">¥{{product.price}}</p>
      </div>
    </ion-item>
    <ion-item></ion-item>
  </ion-list>
  <button ion-button full color="secondary" class="in-cart">カートに1つ追加する</button>
</ion-content>

商品リスト画面から渡された商品データの、名称、値段などを表示している

なお、「カートに1つ追加する」ボタンの処理ついては、今回は書かない

src/pages/detail/detail.scss:

page-detail {
  .image-container {
    display: flex;
    justify-content: center;
  }
  .product-image {
    width: 60%;
    height: 60%;
  }
  .comment {
    margin: 5%;
  }
  .in-cart {
    position: fixed;
    bottom: 60px;
    left: 0;
    margin: 0;
  }
  .title {
    float: left;
    font-weight: bold;
    font-size: 1.7rem;
  }
  .price {
    float: right;
    font-weight: bold;
    font-size: 1.9rem;
    color: #3d9970;
  }
}

画像のサイズが思ったより大きかったので、少し小さくして、flexboxで中央寄せしたりしてる

動作確認

ここまで実装した段階で動作確認をする。

f:id:l08084:20180430172818p:plain
商品詳細画面

ちゃんと商品詳細画面が表示されたので、今回はここまで。


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #2 - 商品リスト画面を作る

f:id:l08084:20180429155759p:plain
今回作成する画面

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第2回目である今回は、商品リスト(ホーム)画面を作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

JSONにアクセスするサービスクラスを作る

第1回目の記事で商品データ(JSON)を作成したので、それにHTTPアクセスするサービスクラスを作る。

サービスクラス作成

ターミナルを立ち上げて、下記のIonic CLIコマンドを実行

$ ionic generate provider product

Angular CLIのng generate serviceにあたる上記のコマンドで、src/providers/product/product.tsファイルが作成される

NgModuleの設定

HttpClientModule@NgModule.importsの配列に追加する

src/app/app.module.ts:

// ...省略

// add this!
import { HttpClientModule } from "@angular/common/http";

@NgModule({
  declarations: [MyApp, AboutPage, ContactPage, HomePage, TabsPage],
  // imports:[]にHttpClientModuleを追加
  imports: [BrowserModule, IonicModule.forRoot(MyApp), HttpClientModule],
  // ...省略
})
export class AppModule {}

サービスクラスの実装

作成したサービスクラスにローカルのJSONにHTTPアクセスするコードを書く

src/providers/product/product.ts:

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Product } from "../../model/product.model";

@Injectable()
export class ProductProvider {
  constructor(public http: HttpClient) {}

  retrieve(): Observable<Product[]> {
    // JSONにアクセスして、結果をProduct[]に変換
    return this.http.get<Product[]>("assets/data/products.json");
  }
}

商品リスト画面コンポーネントクラス作成

続いて、商品リスト画面のコンポーネントクラスであるhome.tsとそのテンプレートファイルであるhome.htmlの内容を更新する。

src/pages/home/home.ts:

import { Component, OnInit } from "@angular/core";
import { NavController } from "ionic-angular";
import { Product, PRODUCT_TYPE } from "../../model/product.model";
import { ProductProvider } from "../../providers/product/product";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  productList: Product[];
  topsList: Product[];
  jacketList: Product[];

  constructor(
    private navCtrl: NavController,
    private productProvider: ProductProvider
  ) {}

  ionViewDidLoad() {
    // 全商品リストを取得
    this.productProvider.retrieve().subscribe(products => {
      this.productList = products;
      // トップス のみのリストを取得
      this.topsList = products.filter(
        product => product.type === PRODUCT_TYPE.tops
      );
      // ジャケット/アウター のみのリストを取得
      this.jacketList = products.filter(
        product => product.type === PRODUCT_TYPE.jacket
      );
    });
  }
}

HomePageクラスでは、下記のことを行なっている。

  • ProductProviderクラスのretrieve()メソッドを呼び出して、商品リストを取得
  • 商品リストから、トップス(PRODUCT_TYPE.tops)の商品のみをフィルタリング
  • トップスの商品リストを、プロパティtopsListにバインド
  • 商品リストから、ジャケット/アウター(PRODUCT_TYPE.jacket)の商品のみをフィルタリング
  • ジャケット/アウターの商品リストを、プロパティjacketListにバインド

src/pages/home/home.html:

<ion-header>
  <ion-navbar>
    <ion-title>Apparel</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-list-header>
      トップス
    </ion-list-header>
    <button ion-item *ngFor="let tops of topsList">
      <ion-thumbnail item-start>
        <img src="{{tops.imagePath}}">
      </ion-thumbnail>
      <h2>{{tops.name}}</h2>
      <p>¥{{tops.price}}</p>
    </button>
    <ion-list-header>
      ジャケット/アウター
    </ion-list-header>
    <button ion-item *ngFor="let jacket of jacketList">
      <ion-thumbnail item-start>
        <img src="{{jacket.imagePath}}">
      </ion-thumbnail>
      <h2>{{jacket.name}}</h2>
      <p>¥{{jacket.price}}</p>
    </button>
  </ion-list>
</ion-content>

home.htmlテンプレートでは、下記のことを行なっている。

  • 分類がトップスの商品配列topsListngForディレクティブを使って、リスト形式に表示
  • 分類がジャケット/アウターの商品配列jacketListngForディレクティブを使って、リスト形式に表示

動作確認

上記のコードを書いた段階でいったん動作確認をしてみる。

f:id:l08084:20180429155759p:plain
商品リスト(ホーム)画面

ちゃんとそれっぽい画面が表示されたので、今回はここまで。


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #1 - 商品データを作る

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第1回目である今回は、商品(洋服)データを作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

Ionicプロジェクト作成

Ionic CLIをPCに入れていない場合は、まずインストールする必要がある。

$ npm install -g ionic

タブのテンプレートを採用したプロジェクトを作成する。

$ ionic start ionic-sample-shopping-app tabs

プロジェクトがちゃんと作成されたかの、動作確認

$ cd ionic-sample-shopping-app
$ ionic serve

タブ型の初期プロジェクトがちゃんと作成されていることを確認

f:id:l08084:20180428155357p:plain
初期状態の表示

モデルクラスを作成する

商品を表すモデルクラスを作成する。enumPRODUCT_TYPEは、商品のジャンル(アウターかジャケットかなど)を判定するのに使う予定。

src/model/product.model.ts:

export interface Product {
  id: string;
  name: string;
  price: number;
  type: number;
  imagePath: string;
  commentOne: string;
  commentTwo: string;
  quantity: number;
}

export const enum PRODUCT_TYPE {
  tops = 0,
  jacket = 1
}

商品データを作成する

データを取得できるWebAPIが存在しないので、ローカルにJSONファイルを配置して、そこからデータを取得する方式にする。

画像ファイル取得

サムネイルに使う画像ファイルが欲しかったので、下記のサイトから洋服の画像ファイルを取得する。

https://www.photo-ac.com/

ダウンロードした画像ファイルは、src/assets/配下に配置する。

f:id:l08084:20180429003408p:plain

JSONファイル作成

続いて、架空のアパレルショップA店の商品データ8件を格納した、JSONファイルを作成する

src/assets/data/products.json:

[
  {
    "id": "01",
    "name": "ボーダーシャツ長袖 ホワイト/ブラック",
    "price": 3500,
    "type": 0,
    "imagePath": "assets/imgs/black-white-border.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "02",
    "name": "ドット柄ブラウス ブルー",
    "price": 2400,
    "type": 0,
    "imagePath": "assets/imgs/blue-blouse.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "03",
    "name": "英字プリント半袖Tシャツ",
    "price": 2200,
    "type": 0,
    "imagePath": "assets/imgs/gray-t-shirt.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "04",
    "name": "ストライプ柄ブラウス ホワイト",
    "price": 3700,
    "type": 0,
    "imagePath": "assets/imgs/white-blouse.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "11",
    "name": "シワ加工ジャケット ベージュ",
    "price": 12000,
    "type": 1,
    "imagePath": "assets/imgs/beige-jacket.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "12",
    "name": "スクールダッフルコート ブルー",
    "price": 11000,
    "type": 1,
    "imagePath": "assets/imgs/blue-duffle-coat.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "13",
    "name": "スクールダッフルコート ホワイト",
    "price": 11000,
    "type": 1,
    "imagePath": "assets/imgs/white-duffle-coat.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "14",
    "name": "ナイロンパーカー パープル/ピンク",
    "price": 37000,
    "type": 1,
    "imagePath": "assets/imgs/nylon-parker.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  }
]

今回はここまで。


次回の記事はこちら

GitHubから通知が来た:依存関係の1つに潜在的なセキュリティ脆弱性があります

GitHubの通知欄にめずらしく(本当に)通知が来ていた。

Potential security vulnerability found in the ssri dependency

実際の通知画面はこんな感じ

f:id:l08084:20180309105303p:plain

通知をクリックして詳細を見ると、「./package-lock.jsonで定義されている依存関係には既知のセキュリティ脆弱性があり、更新する必要があります。」とのことでした。

We found a potential security vulnerability in one of your dependencies.
A dependency defined in ./package-lock.json has known security vulnerabilities and should be updated.

以前作ったAngular Tetris./package-lock.jsonで定義された依存関係の中に、脆弱性のあるモジュールが含まれているようです。

f:id:l08084:20180309105721p:plain

「Review vulnerable dependency」ボタンを押すと、問題のあったモジュール名とバージョンも教えてくれます。

f:id:l08084:20180309121114p:plain

ssriのバージョンを5.2.2以上にすれば、脆弱性は解決するとのこと。

脆弱性のあるモジュールのバージョンを上げる

package.jsonの定義しているライブラリのバージョンを最新にしてくれるnpm-check-updatesを使います。

# インストール
npm install -g npm-check-updates

npm-check-updatesをインストールした後、ncuコマンドでバージョンのアップデートが可能なライブラリを調べることができます。

$ ncu
/angular-tetris/package.json
⸨░░░░░░░░░░░░░░░░░░⸩ ⠦ :
 @angular/cli                  1.7.3  →   1.7.4
 karma-jasmine-html-reporter  ^0.2.2  →  ^1.0.0
 ts-node                      ~5.0.1  →  ~6.0.0
 typescript                   ~2.7.2  →  ~2.8.3

The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, run ncu -a.

 @angular/animations                 ^5.2.8  →  ^5.2.10
 @angular/common                     ^5.2.8  →  ^5.2.10
 @angular/compiler                   ^5.2.8  →  ^5.2.10
 @angular/core                       ^5.2.8  →  ^5.2.10
 @angular/forms                      ^5.2.8  →  ^5.2.10
 @angular/http                       ^5.2.8  →  ^5.2.10
 @angular/platform-browser           ^5.2.8  →  ^5.2.10
 @angular/platform-browser-dynamic   ^5.2.8  →  ^5.2.10
 @angular/router                     ^5.2.8  →  ^5.2.10
 core-js                             ^2.5.3  →   ^2.5.5
 rxjs                                ^5.5.6  →  ^5.5.10
 zone.js                            ^0.8.20  →  ^0.8.26
 @angular/compiler-cli               ^5.2.8  →  ^5.2.10
 @angular/language-service           ^5.2.8  →  ^5.2.10
 @types/jasminewd2                   ~2.0.2  →   ~2.0.3
 @types/node                         ^9.4.7  →   ^9.6.6
 codelyzer                           ^4.2.1  →   ^4.3.0
 karma                               ~2.0.0  →   ~2.0.2
 karma-jasmine                       ~1.1.0  →   ~1.1.1
 protractor                          ~5.3.0  →   ~5.3.1

Run ncu with -u to upgrade package.json

問題がなければ、ncu -uコマンドでpackage.jsonの更新を行います

$ ncu -u
/angular-tetris/package.json
⸨░░░░░░░░░░░░░░░░░░⸩ ⠇ :
 @angular/cli                  1.7.3  →   1.7.4
 karma-jasmine-html-reporter  ^0.2.2  →  ^1.0.0
 ts-node                      ~5.0.1  →  ~6.0.0
 typescript                   ~2.7.2  →  ~2.8.3
 @angular/animations                 ^5.2.8  →  ^5.2.10
 @angular/common                     ^5.2.8  →  ^5.2.10
 @angular/compiler                   ^5.2.8  →  ^5.2.10
 @angular/core                       ^5.2.8  →  ^5.2.10
 @angular/forms                      ^5.2.8  →  ^5.2.10
 @angular/http                       ^5.2.8  →  ^5.2.10
 @angular/platform-browser           ^5.2.8  →  ^5.2.10
 @angular/platform-browser-dynamic   ^5.2.8  →  ^5.2.10
 @angular/router                     ^5.2.8  →  ^5.2.10
 core-js                             ^2.5.3  →   ^2.5.5
 rxjs                                ^5.5.6  →  ^5.5.10
 zone.js                            ^0.8.20  →  ^0.8.26
 @angular/compiler-cli               ^5.2.8  →  ^5.2.10
 @angular/language-service           ^5.2.8  →  ^5.2.10
 @types/jasminewd2                   ~2.0.2  →   ~2.0.3
 @types/node                         ^9.4.7  →   ^9.6.6
 codelyzer                           ^4.2.1  →   ^4.3.0
 karma                               ~2.0.0  →   ~2.0.2
 karma-jasmine                       ~1.1.0  →   ~1.1.1
 protractor                          ~5.3.0  →   ~5.3.1

Upgraded /angular-tetris/package.json

ここで念のため、packacge-lock.jsonを一度削除します。

削除した後、npm updateコマンドで更新したライブラリをインストールします(削除したpackage-lock.jsonもここで再作成されます)

$ npm update

package-lock.jsonの中身を確認すると、脆弱性のあったssriのバージョンが上がっていることがわかります。

package-lock.json:

    "ssri": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
      "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==",
      "dev": true,
      "requires": {
        "safe-buffer": "5.1.1"
      }
    },

変更をコミットしてプッシュすると、GitHub上の警告メッセージも無事消えます。

仮想通貨取引所のAPIを使ってみる #3 - Private API にアクセスする

前回記事はこちら

仮想通貨取引所のbitFlyerのHTTP Private APIにアクセスして、自分の資産残高を取得する。

バージョン情報

JavaScriptのフレームワークであるAngularを使用する

  • Angular: 5.2.9

  • crypto-js: 3.1.9-1

今回アクセスするAPIは認証が必要

第1回目第2回目でアクセスした、認証が不要なPublic APIではなく、認証が必要なPrivate APIにアクセスする

Private APIの認証

https://lightning.bitflyer.jp/docs

Private APIの認証では、下記の情報をHTTPリクエストヘッダーに含める必要がある。

  • ACCESS-KEY: 開発者ページで発行した API key

  • ACCESS-TIMESTAMP: リクエスト時の Unix Timestamp

  • ACCESS-SIGN: ACCESS-TIMESTAMP, HTTP メソッド, リクエストのパス, リクエストボディ を文字列として連結したものを、 API secret で HMAC-SHA256 署名を行った結果

下準備

HMAC-SHA256 署名を実施するのに必要なライブラリ、crypto-jsをインストールする。

$ npm i crypto-js

コード

Private API の一つにアクセスして、自分の資産残高を取得してみる

src/app/services/bitflyer.service.ts

// ...省略
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { PrivateAPIKeySet } from '../private/private.component';
import * as crypto from 'crypto-js';

const URLS = {
  BASE: 'https://api.bitflyer.jp',
  GET_BALANCE: '/v1/me/getbalance'
};

@Injectable()
export class BitflyerService {
  constructor(
    private http: HttpClient,
    private action: BitflyerTickerActions
  ) {}

  getBalance = (keySet: PrivateAPIKeySet): void => {
    const timestamp = Date.now().toString();
    const method = 'GET';
    // ACCESS-TIMESTAMP, HTTP メソッド, リクエストのパス,
    // リクエストボディ を文字列として連結したもの
    const text = timestamp + method + URLS.GET_BALANCE;

    // HMAC-SHA-256で署名を作成する
    // keySet.secretはAPI secret
    const hmac = crypto.HmacSHA256(text, keySet.secret);
    // 署名をHEXでエンコード
    const sign = crypto.enc.Hex.stringify(hmac);

    const httpOptions = {
      headers: new HttpHeaders({
        // keySet.keyはAPIキー
        'ACCESS-KEY': keySet.key,
        'ACCESS-TIMESTAMP': timestamp,
        'ACCESS-SIGN': sign
      })
    };
    this.http
      .get(`${URLS.BASE}${URLS.GET_BALANCE}`, httpOptions)
      .subscribe(console.log);
  };
}

動作確認

上記のコードを実行すると、下記の結果(自分の資産残高)が帰ってくる。

[{
    "currency_code": "JPY",
    "amount": 531.0,
    "available": 531.000000000000
}, {
    "currency_code": "BTC",
    "amount": 0.0000648,
    "available": 0.000064800000
}, {
    "currency_code": "BCH",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "ETH",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "ETC",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "LTC",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "MONA",
    "amount": 0.0,
    "available": 0.0
}, {
    "currency_code": "LSK",
    "amount": 0.0,
    "available": 0.0
}]

参考サイト

HMAC-SHA256 署名 プログラム・ソース・ソフトウェアの配布 PJC

base64ってなんぞ??理解のために実装してみた - Qiita

GolangでHMAC-SHA256署名する - 逆さまにした

HMAC - Wikipedia

Angular Materialでログインフォームを作る

f:id:l08084:20180420160959g:plain

Angular Materialを使って、ログイン用のフォームを作ります

Angular Materialのインストールから解説している記事はこちら

バージョン情報

  • Angular: 5.2.9

  • Node: 8.1.4

  • @angular/material: 5.2.4

テンプレート駆動型とモデル駆動型

Angularのフォームには、テンプレート駆動型とモデル駆動型があります

  • テンプレート駆動型のフォーム

    • 検証ルールをテンプレート側に記述する
    • モデル駆動型よりも手軽に実装できる
    • 複雑な要件には向いていない
    • NgModuleFormsModuleのインポートが必要
  • モデル駆動型のフォーム

    • 検証ルールをコンポーネント側に記述する
    • コードが冗長になりやすい
    • 複雑な要件でも表現できる
    • NgModuleReactiveFormsModuleのインポートが必要

今回は、簡単に書けそうなテンプレート駆動型のフォームを採用します。ただ、Angular Materialで作られたフォームのサンプルを見ると、モデル駆動型のフォームで記述されているものが多い気がするので、後々問題が発生したら、モデル駆動型に切り替えます。

NgModuleの設定

ログインフォーム作成のために、下記のAngular Materialコンポーネントを使ったので、NgModuleに設定していきます。

  • MatButtonModule

  • MatCardModule

  • MatInputModule

  • MatFormFieldModule

  • MatIconModule

また、今回はAngularのテンプレート駆動型のフォームを使うので、FormsModuleの設定も必要です。

src/app/app.module.ts:

import { FormsModule } from '@angular/forms';

import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';


@NgModule({
  declarations: [AppComponent, ExchangeListComponent, PrivateComponent],
  imports: [
    // add this!
    MatButtonModule,
    MatCardModule,
    MatInputModule,
    MatFormFieldModule,
    MatIconModule,
    FormsModule,
    // ...省略
  ],
// ...省略

下記の手順を実行してください

  1. 上記のモジュール(MatFormFieldModule他6つ)をインポート

  2. @NgModuleデコレータで定義されているimports:[]リストに上記の6つのモジュールを追加する

index.htmlにアイコンのCDNを追加

マテリアルアイコンをパスワード欄で使うので、下記のCDNをindex.htmlに追加する必要があります。

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

src/app/index.html:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>CriptocurrencyApiCall</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>

<body>
  <app-root></app-root>
</body>

</html>

ログインフォームを作る

f:id:l08084:20180420161351p:plain

下記の要件に従って、ログインフォームを作ります。

  • ログインフォームの要件
    • API Key欄とAPI Secret欄がある
    • API Secret欄は、目のアイコンをクリックすると、type=passwordが解除される
    • バリデーションは、API Key欄とAPI Secret欄の必須チェックのみ
    • バリデーションのチェックが全部OKにならないと、Loginボタンはdisable
    • 必須チェックを満たしていない欄には、<mat-error>によるエラーメッセージが表示される

src/app/private/private.component.html:

<mat-card class="login-card">
  <mat-card-header>
    <mat-card-title class="login-title">bitFlyer API</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <form (ngSubmit)="onSubmit()" class="login-form" #loginForm="ngForm">
      <mat-form-field>
        <input matInput placeholder="API Key" id="key" [(ngModel)]="keySet.key" name="key" #key="ngModel" required>
        <mat-error *ngIf="key.errors?.required">You must enter a value</mat-error>
      </mat-form-field>

      <mat-form-field>
        <input [type]="hide ? 'password' : 'text'" matInput placeholder="API Secret" id="secret" [(ngModel)]="keySet.secret" name="secret"
          #secret="ngModel" required>
        <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
        <mat-error *ngIf="secret.errors?.required">You must enter a value</mat-error>
      </mat-form-field>
      <button type="submit" mat-raised-button color="primary" [disabled]="loginForm.invalid">Login</button>
    </form>
  </mat-card-content>
</mat-card>

src/app/private/private.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

export class PrivateAPIKeySet {
  constructor(public key: string, public secret: string) {}
}

@Component({
  selector: 'app-private',
  templateUrl: './private.component.html',
  styleUrls: ['./private.component.scss']
})
export class PrivateComponent implements OnInit {
  keySet = new PrivateAPIKeySet('', '');
  hide = true;
  constructor() {}

  ngOnInit() {}

  onSubmit() {
    console.log(this.keySet);
  }
}

src/app/private/private.component.scss:

.login-card {
  max-width: 400px;
}

.login-form {
  display: flex;
  flex-direction: column;
}

.login-title {
  font-weight: bold;
  font-size: 22px;
  color: #3f51b5;
}

参考サイト

https://material.angular.io/components/form-field/overview