#はじめに
この記事では、AngularからRedmineのREST APIにアクセスして、チケット一覧を取得する方法について説明します。
(環境は、この記事の執筆時点の最新バージョンである Angular 4.3.4 と Redmine 3.4.2 です)
解説は、「恋に落ちるコード.js」に登場するJavaScript好きの女子高生、絵子と樹里がお送りします。
##人物紹介
瀬尾絵子:「恋に落ちるコード.js」の登場人物。JavaScript勉強中の女子高生。
篠宮樹里: 同じく「恋に落ちるコード.js」の登場人物。JavaScriptに詳しい女子高生。
藤倉紅子:「Redmineで始める異世界人心掌握術」の登場人物。自称美人SE。二人の高校の先輩でRedmineに詳しい。
#ひな形を作成する
樹里「ではさっそく、Angular CLIを使って、RedmineのREST APIにアクセスするWebアプリを作ってみよう」
絵子「ホントにいきなりだね。Angularとは?とかRedmineとは?とか、その辺は説明しないの?」
樹里「その辺は、Qiitaに有益な記事がすでにいくつも公開されているので、そちらを読んでほしい」
絵子「……確かに。お世話になってます」
樹里「とりあえずnode.jsとAngular CLIはセットアップしておこう。乙女の新常識だ」
絵子「そうなんだ。いつの間に」
樹里「あと、Redmineが同一のホスト上で稼働しているという前提で話を進めるが……Redmine側にも準備が必要だ」
絵子「どんな?」
樹里「まず、APIを有効にするため、設定画面で『RESTによるWebサービスを有効にする』と『JSONPを有効にする』の機能をオンにしておく必要がある。また、今回は公開されたプロジェクトにアクセスするが、認証が必要なプロジェクトのチケットにアクセスするには、事前に認証用のキーを発行してパラメータに付与する必要がある」
絵子「うーん、わかった。詳しいところはあとで紅子さんに聞いてみよう」
樹里「じゃ、準備も整ったところで、サクッとひな形を作るぞ」
絵子「らじゃー」
> ng new sample
絵子「……これでひな形が出来たの?便利だねー」
樹里「今回は『sample』という名前のアプリにしたが、名前はなんでも良い。では、これをベースに作っていくぞ」
#JSONデータを取得するサービスのひな形を作成する
樹里「では、出来上がったディレクトリに移動しよう」
> cd sample
絵子「樹里!なんだか山のようにファイルが出来てるんだけど!」
樹里「とりあえず必要なのはsrcディレクトリ、さらにその中のappディレクトリだ。それ以外のファイルの役割については徐々に覚えていけばいい」
絵子「……良かった。早々に挫折するところだった」
樹里「ではさっそく、出来上がったファイルを修正していくのだが……」
絵子「まだ準備がいるの?」
樹里「今出来上がったのは、コンポーネントやモジュールといった基本的な機能を持つファイルなのだが……JSONファイルを取得する、といった独立した機能については、『サービス』として分けて管理する方が都合が良い。今後のためにも分けておこう」
絵子「はーい」
樹里「ではサービスのひな形を作成する。これも便利なコマンドがある」
> ng g service issues
樹里「これで、appディレクトリ内に『issues.service.spec.ts』と『issues.service.ts』が、必要なコードがすでに記述された状態で作成される」
絵子「便利すぎて逆に不安になるね」
#データを格納するクラスを作る
樹里「それから、もう一つ……」
絵子「まだあるの?早く作ろうよ」
樹里「まあ聞きなさい。JSONを取得するリクエストを投げた時、返ってくるデータは?」
絵子「……JSON、だよね……?」
樹里「そう。JSON形式のデータが返ってくる。しかし、その構造は自由自在だし、どんなデータが返ってくるかわからないのでは不安だ。なので、ちゃんと事前にクラスを定義しておき、こういう構造のデータが返ってくるよ……という受け入れ態勢を作っておく方がWebアプリとしては望ましい」
絵子「確かに、事前の心構えは大事だよね」
樹里「なので、データの構造を定義するクラスファイルをappディレクトリ内に作成しておく。名前は『issues.ts』にしよう」
絵子「はーい」
樹里「絵子も、『オレンジジュースをあげるよ』と渡されたら美味しく飲めるが、『何かしらの汁をあげるよ』と渡されたら飲みづらかろう」
絵子「渡す側に悪意があるよね」
export class Issues {
issues: Issue[];
total_count: number;
offset: number;
limit: number;
}
export class Issue {
id: number;
project: FieldObject;
tracker: FieldObject;
status: FieldObject;
priority: FieldObject;
author: FieldObject;
assigned_to: FieldObject;
subject: string;
description: string;
due_date: string;
done_ratio: number;
custom_fields: CustomFieldObject[];
created_on: string;
updated_on: string;
}
export class FieldObject {
id: number;
name: string;
}
export class CustomFieldObject {
id: number;
name: string;
value: string;
}
樹里「細かい説明は省くが……Redmineチケット一覧をJSON形式で取得すると、こういう構造のデータが返ってくる」
絵子「これもあとで紅子さんに聞いとこう」
樹里「概要だけ言うと……issues
の中にチケットの情報が配列で入っている。そのチケットの構造がIssue
オブジェクトの通りだ。Issue
には、例えばid
の情報がnumber型で格納されている。他にはproject
の情報もあるが、これはプロジェクトのid
とname
が対になったオブジェクトとして格納されているので、それをFieldObject
という名前で定義している。他も同様だ」
絵子「うん。わかった気になった」
#サービスのひな形を修正する
樹里「ファイルの作成は以上だ。あとは、出来上がったファイルを修正していく」
絵子「待ってました!」
樹里「まずはJSONを取得するサービスからだ。さっき作ったファイルのうち、今回はissues.service.spec.tsは使わない。issues.service.tsを修正していく」
import { Injectable } from '@angular/core';
/* ↓(1) 必要なモジュール等のインポート */
import { Jsonp } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Issues } from './issues';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
/* ↑ */
@Injectable()
export class IssuesService {
/* ↓(2) JSONファイルのURLを定義 */
private issuesUrl = 'http://localhost/redmine/issues.json?callback=JSONP_CALLBACK';
/* ↑ */
/* ↓(3) コンストラクタを書き換え */
//constructor() { }
constructor(private jsonp: Jsonp) { }
/* ↑ */
/* ↓(4) JSONを取得する処理 */
getIssues(): Observable<Issues> {
return this.jsonp
.get(this.issuesUrl)
.map(response => response.json() as Issues)
.catch(this.handleError);
}
private handleError(error: any): Observable<any> {
console.error('An error occurred', error);
return Observable.throw(error.message || error);
}
/* ↑ */
}
##必要なモジュール等のインポート
樹里「ではさっそくコードの説明だが……最初は使用するモジュールや関数のインポートだ。コード内で使用するものは、最初にインポートする必要がある、とだけ覚えておけば良い。個々の役割についてはあとで説明する」
絵子「うん。あとで聞く」
##JSONファイルのURLを定義
樹里「続いて、RedmineのREST APIにアクセスするURLの定義だが……まず、『localhost/redmine/』というのがRedmineのURLだ。別ホストのRedmineにアクセスする場合には各自直すように」
絵子「おっけー」
樹里「続いて『issues.json』が、チケット一覧を取得するためのAPIだ。この後ろにパラメータを付与することにより、特定のプロジェクトやトラッカーのみの一覧を取得することも可能だ。詳しいことはRedmine APIの仕様を確認しておくように」
絵子「紅子さんに聞くこといっぱいあるな……」
樹里「最後に、『?callback=JSONP_CALLBACK』。これは、AngularからJSONP形式のデータを取得するための約束事だと思えばいい。詳しい事は割愛する」
絵子「あ、逃げた」
樹里「ちなみに、なぜJSONじゃなくてJSONP形式でデータを取得するのかと言うと……JSONはクロスドメインがどうとか、セキュリティ上の制約が厳しくて色々あるので……」
絵子「色々あるよ、こんな世の中だもの」
##コンストラクタを書き換え
樹里「これは、最初にJsonpモジュールを呼び出して変数に格納しているだけだ」
##JSONを取得する処理
樹里「このgetIssues()
関数でJSONデータを取得している。ちなみに戻り値のObservable
とは……JSONのようなHTTPリクエストは、このObservable
オブジェクトの形式で返ってくる……らしい」
絵子「あんまり深く聞くと可哀想だからやめておこう」
樹里「……処理の流れは、
-
get()
関数で指定したURLからデータを取得し、 -
map()
関数で実データ部分を先ほど定義したIssues
クラスにマッピングして返す。 - エラーが発生したら
catch()
関数でエラー処理に飛ぶ。
……と言った具合だ」
絵子「ま、これで意図したデータが取れるならそれでオッケーだね」
#コンポーネントを修正する
樹里「次は、ページの動作を定義するコンポーネント、app.component.tsを修正する」
絵子「一番キモの部分ね」
/* ↓(1) 必要なモジュール等のインポート */
// import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Issues } from './issues';
import { IssuesService } from './issues.service';
/* ↑ */
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
/*
export class AppComponent {
title = 'app';
}
*/
/* ↓(2) JSONを取得する処理 */
export class AppComponent implements OnInit {
issues: Issues;
constructor(private issuesService: IssuesService) { }
ngOnInit(): void {
this
.issuesService
.getIssues()
.subscribe( issues => this.issues = issues );
}
}
/* ↑ */
##必要なモジュール等のインポート
樹里「さっきと似たような感じだが……OnInitモジュール、さっき作ったIssuesクラスとIssuesServiceサービスをインポートしている」
##JSONを取得する処理
樹里「ここがメインとなる処理だが……まずIssuesクラスを変数に格納し、IssuesServiceサービスのインスタンスを生成する。それから、OnInitモジュールをインポートすることでngOnInit()
メソッドを使用できるようになる」
絵子「ngOnInit()
メソッドって?」
樹里「データバインド後に一度だけ呼び出される処理なのだが……初期化の処理はこういう風に書くことが推奨されてるらしいので、ここに書く」
絵子「はーい」
樹里「で、さっき作ったIssuesServiceのgetIssues()
を実行して、取得したデータを変数issues
に格納する。やっている事はこれだけだ」
絵子「ほう。じゃあ、取得したデータはどう使うの?」
樹里「それは、次に説明するテンプレートの中で使われる」
<h1>チケット一覧</h1>
<div *ngIf="issues">
<p>
total_count:<span>{{issues.total_count}}</span>
</p>
<ul>
<li *ngFor="let issue of issues.issues">
<span>{{issue.id}}</span> <span>{{issue.subject}}</span>
</li>
</ul>
</div>
樹里「これがテンプレートだ。見慣れたHTMLと、Angularで使用される構文が混在している」
絵子「ほんとだ」
樹里「この『{{』と『}}』に囲まれた部分に変数を記述すると、展開して表示してくれる」
絵子「『カーリーブラケット』だね。こないだ覚えた」
樹里「また、ngFor
は、いわゆるfor文と同じく反復処理だ。データの数だけ繰り返し表示される」
絵子「なるほどー。表示させるのもカンタンだね」
#モジュールを修正する
樹里「最後に、app.module.tsの修正だ。……最後というか、本来はこのファイルが最初に呼び出される。このアプリケーションではどのようなモジュールやサービスが使われるか、という事を最初に宣言するためだ」
絵子「登場人物紹介みたいなもんね」
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { JsonpModule } from '@angular/http'; /* 追加 */
import { IssuesService } from './issues.service'; /* 追加 */
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
JsonpModule /* 追加 */
],
providers: [
IssuesService /* 追加 */
],
bootstrap: [AppComponent]
})
export class AppModule { }
樹里「このように、JsonpModule
をimports
の配列に、IssuesService
をproviders
の配列にそれぞれ追加する」
絵子「とりあえず言われた通りにやっておこう」
#動作確認
樹里「これで完成だ。さっそく動かしてみよう」
> ng serve
絵子「……おー、チケットのIDと題名が表示されてる」
樹里「たったこれだけのコードで、しかもすべてJavaScript……まあ正確にはTypeScriptだが、ともかくこれだけでRedmineのデータを表示するWebアプリケーションを作ることが出来る。便利な時代になったものだ」
絵子「ところで……Redmineの画面を直接表示するのに比べて、どんなメリットがあるの?」
樹里「それを言われると言葉に詰まるが……せっかく便利なAPIが用意されてて、データの入出力が自由に出来るんだ。フロントエンドは自分の好きな言語で書いてみたいじゃないか」
絵子「確かに、JavaScriptでもフロントエンドが作れる!ってのは嬉しいよねー。私達からしたら」
#おわりに
というわけで、AngularからRedmineのチケット一覧を取得する方法についてお話ししました。
ほぼ必要最小限の事にしか触れていませんが、個々のファイルやメソッド等の詳細については、Angularの公式ドキュメント等で調べてみてください。(私も勉強します……)