Edited at

AngularからRedmineのチケット一覧を取得する

More than 1 year has passed since last update.


はじめに

この記事では、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』にしよう」

絵子「はーい」

樹里「絵子も、『オレンジジュースをあげるよ』と渡されたら美味しく飲めるが、『何かしらの汁をあげるよ』と渡されたら飲みづらかろう」

絵子「渡す側に悪意があるよね」


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の情報もあるが、これはプロジェクトのidnameが対になったオブジェクトとして格納されているので、それをFieldObjectという名前で定義している。他も同様だ」

絵子「うん。わかった気になった」


サービスのひな形を修正する

樹里「ファイルの作成は以上だ。あとは、出来上がったファイルを修正していく」

絵子「待ってました!」

樹里「まずはJSONを取得するサービスからだ。さっき作ったファイルのうち、今回はissues.service.spec.tsは使わない。issues.service.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を修正する」

絵子「一番キモの部分ね」


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に格納する。やっている事はこれだけだ」

絵子「ほう。じゃあ、取得したデータはどう使うの?」

樹里「それは、次に説明するテンプレートの中で使われる」


app.component.html

<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の修正だ。……最後というか、本来はこのファイルが最初に呼び出される。このアプリケーションではどのようなモジュールやサービスが使われるか、という事を最初に宣言するためだ」

絵子「登場人物紹介みたいなもんね」


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 { }


樹里「このように、JsonpModuleimportsの配列に、IssuesServiceprovidersの配列にそれぞれ追加する」

絵子「とりあえず言われた通りにやっておこう」


動作確認

樹里「これで完成だ。さっそく動かしてみよう」

> ng serve

絵子「……おー、チケットのIDと題名が表示されてる」

樹里「たったこれだけのコードで、しかもすべてJavaScript……まあ正確にはTypeScriptだが、ともかくこれだけでRedmineのデータを表示するWebアプリケーションを作ることが出来る。便利な時代になったものだ」

絵子「ところで……Redmineの画面を直接表示するのに比べて、どんなメリットがあるの?」

樹里「それを言われると言葉に詰まるが……せっかく便利なAPIが用意されてて、データの入出力が自由に出来るんだ。フロントエンドは自分の好きな言語で書いてみたいじゃないか」

絵子「確かに、JavaScriptでもフロントエンドが作れる!ってのは嬉しいよねー。私達からしたら」


おわりに

というわけで、AngularからRedmineのチケット一覧を取得する方法についてお話ししました。

ほぼ必要最小限の事にしか触れていませんが、個々のファイルやメソッド等の詳細については、Angularの公式ドキュメント等で調べてみてください。(私も勉強します……)