Play2でのSPAコンテンツの配信の仕方



## 概要

Webアプリを作るとき、最近は画面の構成はSPAでやるのが主流です。

SPAでのコンテンツの配置はこちらの記事で書きました。
[SPAの静的ファイルのデプロイの仕方](http://qiita.com/uryyyyyyy/items/a4b97709a019af1242be)

それらの用意されたコンテンツをplay2で配信する方法を記しておきます。

## 環境

- Play 2.5
- React.js 15.x
- react-router
  - `/page1/accoutId/1`のようなパスを扱う想定

ソースコードはこちら。

https://github.com/uryyyyyyy/play2sample/tree/play2-spa

## ゴール

- 静的コンテンツ（css/js/image）はplay2のpublicから配信できる
- フロント側でルーティングしたいので、サーバーはどこのパスにアクセスが来てもindex.htmlを返す
- API操作用のRouting設定を別に分けられる
  - いずれAPIが多くなることが予想されるため。
  - 管理者用コマンドや特定ページ用コマンドを分けて管理したいため。
- deploy（sbt dist）したときにも問題なくパッケージングされる

## 手順

- routesファイルを複数に分ける。
- 静的ファイル群をpublic以下に配置する
- どのパスにも引っかからなかったらindex.htmlを返すようにする
- （routes一覧を見れるようにする）

### routesファイルを複数に分ける。

ここでは、APIとそれ以外に分けます。（このへん、ドキュメントに書いてないっぽいので辛かった。。）

```test:routes
->  /api api.Routes
```

```test:api.routes
GET     /action              controllers.HomeController.apiAction()
```

上記のように定義すると、`/api` 以下のパスに対してはapt.routesの設定が適用されます。
ここでは、`/api/action` へGETリクエストを投げることができるようになります。

### 静的ファイル群をpublic以下に配置する

こちらの記事（[SPAの静的ファイルのデプロイの仕方](http://qiita.com/uryyyyyyy/items/a4b97709a019af1242be)）でindex.htmlとpublicフォルダができているので、それを利用して以下のように配置しなおすshellを用意します。

```bash:copyToPublic.sh
rm -r -f ./public/
cp -R ./front/public/ ./public/
cp ./front/index.html ./public/
```

要するに、静的ファイル全てをpublic以下に置いておくだけです。
このようにすると、sbt distでパッケージングした際も全てのファイルがjarの中に含まれてくれます。
（デフォルトではpublicというフォルダ以外に置かれたものはパッケージングされた際に無視されるので注意。）

あとはroutesの中で以下のように定義すれば静的ファイルが拾えるようになります。

```text:routes
GET     /public/*file               controllers.Assets.versioned(path="/public", file: Asset)
```

### どのパスにも引っかからなかったらindex.htmlを返すようにする

上記で、APIと静的ファイルの呼び出しはできるようになりました。
上述の通り、SPAでは、フロント側のルーティング機構を使って`/page1/accountId/3`のようなパスを扱うことができるようになります。
このときに`/page1/accountId/3.html`などを返そうとされても困るので、エントリポイントであるindex.htmlを返す必要があります。

これは、ちょっとトリッキーですが以下のように定義してあげます。

```test:routes
GET /         controllers.HomeController.index
GET /*path    controllers.HomeController.indexAll(path)

# このように定義していってもいいが、とてもめんどくさい。。。
# GET /page1/accountId/:id    controllers.HomeController.indexLong(id)
```

```scala:HomeController.scala
  def index = {
    loadIndex()
  }

  def indexAll(path: String) = {
    loadIndex()
  }

  private def loadIndex(): Action[AnyContent] = {
    controllers.Assets.at(path="/public", file="index.html")
  }
```

まず、`/`への問い合わせに対して`controllers.Assets.at(path="/public", file="index.html")`を返すのはわかりやすいと思います。public以下にindex.htmlがあるはずなのでそれを呼ぶだけです。

`/*path`という記述は、`/`以外の全てのパスを対象としています。この時に普通に`controllers.Assets.at(path="/public", file="index.html")`を書いてしまうと「pathって変数使ってないじゃねーかコンパイル通さねぇぞ！」と怒られてしまいます。。。
そこで、とても気持ち悪いですが、pathという変数を一度controllerで受け取って、それを無視することで、pathを使わなくてもコンパイルエラーにならずに実行することができます。
（特定のパスに来たら別の処理をする、のような特殊なケースにも対応できるので、これはこれで良いのかも？？？）

### routes一覧を見れるようにする

さて、これでどこにつなぎに行ってもroutesの方で404を返す代わりにindex.htmlを返すようになりました。
これでとりあえず問題ないのですが、開発時などで「今playで設定されてるroutes一覧が見たい」という要望があるかもしれません。404を返さないのでこの画面が出てこなくなってしまったので。。。

そこで、別途それ用のAPIを用意する方法も記しておきます。（普通は必要ないと思いますが、、、）

```test:routes
GET /routes    controllers.HomeController.routes
```

```scala:HomeController.scala
  //DIしたいけどRouterはController依存なので循環参照してしまう。
  def routes() = Action {
    val myRoutes = Play.current.routes.documentation map { r =>
      "%-10s %-50s %s".format(r._1, r._2, r._3)
    }
    Ok(myRoutes.mkString("\n"))
  }
```

説明は面倒なので省きます。。。


## まとめ

playでSPAを扱う方法について、これがベストだ！みたいな方法がなかったので色々調べてみました。
もしもっと良いやり方が有れば、都度修正していこうと思います。