GAE と Angular で普通の SPA の作り方 〜 導入編

# はじめに

* Google App Engine をバックエンドサーバーとして動く普通の Angular アプリケーションの作り方を紹介していく。
* 今回は導入編としてアプリケーションのひな形を作って通信ができるだけのところまでやってみる。

# 準備

* Go, Node.js のインストール

* Google Cloud SDK のインストール
  * GAE のローカル開発サーバーなどが入っているので[公式ドキュメント](https://cloud.google.com/sdk/downloads?hl=ja)の通りにインストールする

* Angular CLI のインストール
  * Angular CLI を使って Angular プロジェクトの初期化や管理ができるので[README.md](https://github.com/angular/angular-cli#installation)に従ってインストールする

# プロジェクトルートの作成

* $GOPATH の通っているディレクトリにプロジェクトルートを作成する
* 以下は github.com で公開し、ユーザー名が you で、プロジェクト名が gae-angular-sample の場合のプロジェクトルートディレクトリの作成

```
mkdir -p $GOPATH/src/github.com/you/gae-angular-sample/{server,client}
```

# GAE アプリケーションのひな形作成

* GAE アプリのひな形としては、HTTP リクエストを処理するメインの go ファイルと、設定用の yaml が必要になる

* メインの Go ファイルとして "/" へのリクエストで "hello world" と出力するだけの HTTP ハンドラの定義された `server/gae/app.go` を作成する

```go:server/gae/app.go
package main

import (
    "net/http"
    "fmt"
)

func init() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "hello world")
    })
}
```

* 設定用の Yaml として全リクエストを前記の Go ファイルで処理するように定義した `server/gae/app.yaml` を作成する

```yaml:server/gae/app.yaml
runtime: go
api_version: go1.8

handlers:
- url: /.*
  script: _go_app
```

# 開発サーバーを起動する

* Cloud SDK に入っている dev_appserver.py コマンドで開発サーバーを立ち上げる

```
cd server/gae
dev_appserver.py app.yaml
```

* 起動ログに表示される通り `http://localhost:8080` で立ち上げたアプリケーションへアクセスできる
* これも起動ログにあるが `http://localhost:8000` で管理用のサーバーにアクセスすることができ、開発用にエミュレートされた Google Datastore などを操作することができる


# Angular アプリケーションのひな形の作成

* Angular CLI を使ってひな形を作成する

```
# プロジェクトルートヘ
cd $PROJECT_ROOT
# client という名前でひな形の作成
ng new client
# 依存パッケージのインストール
cd client && npm install
```

# Angular アプリの開発サーバーを起動する

```
npm start
```

* `http://localhost:4200` でクライアントの開発サーバーへアクセス

# GAE アプリと Angular アプリを同一ホストで提供する

* GAE と Angular でそれぞれ開発サーバーを立ち上げると、それぞれ別ドメインとして動作してしまい Cookie などを共有することができずに面倒な場合がある
* ここでは Angular アプリのソースをビルドして GAE アプリで提供することで同一ドメインで動作させる

## Angular アプリのビルド

* 下記の通り `client/package.json` に GAE アプリ用のビルドスクリプトを追加する

```yaml:client/package.json
{
  ...
  "scripts": {
    ...
    "build:gae": "ng build --prod --output-path=../server/gae/client/ --base-href=/client/"
```

* ビルドスクリプトを実行して `server/gae/client` にビルド後のソースがコピーされていることを確認する

```
npm run build:gae
```

## GAE アプリで Angular アプリのファイルを提供する

* GAE アプリの設定ファイルを変更してビルドした Angular アプリのファイルを静的ファイルとして提供する
* 以下のように "handlers" 要素に "/client/" 以下の静的ファイルを提供する設定と、それ以外の "/client/.*" で "/client/index.html" を提供する設定を追加する

```yaml:server/gae/app.yaml
runtime: go
api_version: go1.8

handlers:
- url: /client/(.*\.(gif|png|jpeg|jpg|css|js|ico))$
  static_files: client/\1
  upload: client/(.*)
- url: /client/(.*)
  static_files: client/index.html
  upload: client/index.html
- url: /.*
  script: _go_app
```

* GAE 開発サーバーを起動し `http://localhost:8080/client/` で Angular アプリが立ち上がるのを確認する

## 自動でリビルドする

* ビルドコマンドにオプションを足すとファイル変更を検知してリビルドしてくれる

```
npm run build:gae -- --watch
```

# Angular アプリから GAE アプリへ通信する

## GAE アプリに通信の受け口を追加する

* パラメータを受けて動的な結果を返す HTTP ハンドラを定義する。
* パラメータの bind と JSON のレスポンスなどウェブサーバーではおなじみの処理を手軽に実装したいのでウェブアプリケーションフレームワーク echo を利用する

```go:server/gae/app.go
package main

import (
	"github.com/labstack/echo"
	"net/http"
)

func init() {
    // 軽量なウェブアプリケーションフレームワーク echo を使う
    // 素の net/http に比べてパラメータの bind や Json の出力を便利になる
	e := echo.New()

	// ルート定義
	e.GET("/hello", helloHandler)

	// 全リクエストを echo で処理する
	http.Handle("/", e)
}

// helloHandler のリクエスト
type helloRequest struct {
    // クエリストリングの name 要素を受け取る
	Name string `query:"name"`
}

// helloHandler のレスポンス
type helloResponse struct {
	Message string
}

// echo のハンドラ型を定義
func helloHandler(c echo.Context) error {
    // 入力を受け取って構造体に入れる
	req := new(helloRequest)
	c.Bind(req)

	// ステータスコード 200 で JSON を返す
	return c.JSON(http.StatusOK, helloResponse{
		Message: "hello " + req.Name,
	})
}
```

* 追加したライブラリをインストールして開発サーバーを起動する

```bash
go get github.com/labstack/echo
cd server/gae
dev_appserver.py app.yaml
```

## Angular アプリから GAE アプリに通信する

* Angular で HTTP 通信をする場合は @angular/common/http の HttpClient を利用する

* HttpClient を DI できるように AppModule に HttpClientModule を追加する

```diff:client/src/app/app.module.ts
 import { AppComponent } from './app.component';
+import {HttpClientModule} from "@angular/common/http";

 @NgModule({
   declarations: [
     AppComponent
   ],
   imports: [
+    HttpClientModule,
     BrowserModule
   ],
```

* AppComponent 初期化時に通信を行う
  * コンストラクタで HttpClient を DI で初期化し GET リクエストを送信、console.log で結果を表示する
  * レスポンスの型 helloResponse は事前に定義する

```ts:client/src/app/app.module.ts
interface helloResonse {
  Message: string
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  constructor(private http: HttpClient) {
    this.http.get<helloResonse>("/hello?name=bob").subscribe(
      res => console.log(res)
    );
  }
}
```

# おわりに

* 実コードは[リポジトリ](https://github.com/nirasan/gae-angular-sample/tree/qiita.com/nirasan/items/d877a75902498a717eb8)を参照のこと