Play FrameworkのアセットコンパイルをGulpにすべておまかせ

![play-gulp.png](https://qiita-image-store.s3.amazonaws.com/0/38958/1fbff73c-f169-d73a-4b7d-3f0cd39d72e3.png)

**追記：Herokuに簡単にデプロイできるようにしました。**(2015/9/28)
https://github.com/mmizutani/play-gulp-standalone
https://play-gulp-standalone.herokuapp.com

# sbt-webからGulpへ

Play Frameworkでは、スタティックアセットはapp/assetsフォルダから[sbt-web](https://github.com/sbt/sbt-web)プラグインでコンパイルされたものとpublic/フォルダにあるファイルがjarにまとめられてクライアントに送るかたちになっています。Play Frameworkを使い始めてしばらくの間はsbt-webのアセットコンパイルで充分で、bower/npmのパッケージを取りこめるようになったWebJarのエコシステムにも満足していたのですが、やれTypeScriptだやれReactだなどといってフロントエンドがどんどん複雑化してくると、sbt-webのプラグイン群(sbt-rjs, autoprefixerなど)の中には長期間更新のないものも散見される現状では心許なくなってきました。

ということで、今回、Playアプリのアセットコンパイルを人気のJavaScriptタスクランナーGulpにまるごと乗り換えられるようにしてみました。基本的には、sbtからGruntを呼び出しつつスタティックアセットへのアクセスをyeomanのジェネレータで作成したhtmlプロジェクトにリダイレクトする方法をとっているPlayプラグイン[play-yeoman](https://github.com/tuplejump/play-yeoman)のやり方に倣いつつ、GruntをGulpに置き換え、かつプラグインもサブプロジェクトも不要な形に簡素化しました。プロジェクトの細かい微調整は必要ですが、新規に作成する必要があるScalaファイルは２つのみです。

ささっと使ってみたい方は[こちらのサンプルプロジェクト https://github.com/mmizutani/play-gulp-standalone をcloneして、普通のPlayプロジェクト同様に`sbt run`, `sbt ~run`, `sbt test`, `sbt testProd`, `sbt ";stage;dist"`(Typesafe Activatorなら`activator run`, `activator ~run`, `activator test`, `activator testProd`, `activator ";stage;dist"`) などをしてみてください。

# 設定
## 0. 前提
Scala, sbt（またはTypesafe Activator）, Node.js (node & npm)をインストール済。

## 1. プロジェクト作成

まず[generator-gulp-angular](https://github.com/Swiip/generator-gulp-angular)をインストールし、

```bash
$ npm install -g yo gulp bower
$ npm install -g generator-gulp-angular
```

テンプレートからプレーンなPlayプロジェクトを作成します。プロジェクトの名称はここではとりあえずplay-scalaとしておきます。

```bash
$ activator new play-gulp
Choose from these featured templates or enter a template name:
  1) minimal-akka-java-seed
  2) minimal-akka-scala-seed
  3) minimal-java
  4) minimal-scala
  5) play-java
  6) play-scala
(hit tab to see a list of all templates)
> 6
OK, application "play-gulp" is being created using the "play-scala" template.
```

そして、`rm -rf public`で、プロジェクトルートのpublicフォルダとはおさらばします。代わりにプロジェクトルートにuiフォルダを作成して、この中にさきほどインストールしたyeomanのジェネレータの1つ、gulp-angularでプロジェクト雛形を作成します。（gulpのプロジェクト雛形であれば他のyeomanジェネレータでも以降の設定方法はほぼ同じなはずです。）

```bash
$ cd play-gulp
$ rm -rf public
$ mkdir ui && cd $_
$ yo gulp-angular
```

ディレクトリ構成はこうなります。
![Selection_012.png](https://qiita-image-store.s3.amazonaws.com/0/38958/418c8418-be9f-d05d-3ece-d846b1147075.png)

## 2. project/PlayGulp.scalaの作成

projectフォルダ内にPlayGulp.scalaファイルを新規作成し、Gulpのタスクをsbtのタスクに紐付けてsbtコンソールから呼び出せるようにするための設定をします。

```scala:project/PlayGulp.scala
import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport._
import com.typesafe.sbt.web.Import._
import play.sbt.PlayImport.PlayKeys._
import play.sbt.PlayRunHook
import play.twirl.sbt.Import.TwirlKeys
import sbt.Keys._
import sbt._

object PlayGulp {

  lazy val gulpDirectory = SettingKey[File]("gulp-directory", "gulp directory")
  lazy val gulpFile = SettingKey[String]("gulp-file", "gulpfile")
  lazy val gulpExcludes = SettingKey[Seq[String]]("gulp-excludes")
  lazy val gulp = InputKey[Unit]("gulp", "Task to run gulp")
  lazy val gulpBuild = TaskKey[Int]("gulp-dist", "Task to run dist gulp")
  lazy val gulpClean = TaskKey[Unit]("gulp-clean", "Task to run gulp clean")
  lazy val gulpTest = TaskKey[Unit]("gulp-test", "Task to run gulp test")

  val playGulpSettings: Seq[Setting[_]] = Seq(

    // Specifies the location of the root directory of the Gulp project relative to the Play app root
    gulpDirectory <<= (baseDirectory in Compile) { _ / "ui" },

    gulpFile := "gulpfile.js",

    gulp := {
      val base = (gulpDirectory in Compile).value
      val gulpfileName = (gulpFile in Compile).value
      runGulp(base, gulpfileName, Def.spaceDelimited("<arg>").parsed.toList).exitValue()
    },

    gulpBuild := {
      val base = (gulpDirectory in Compile).value
      val gulpfileName = (gulpFile in Compile).value
      val result = runGulp(base, gulpfileName, List("build")).exitValue()
      if (result == 0) {
        result
      } else throw new Exception("gulp failed")
    },

    gulpClean := {
      val base = (gulpDirectory in Compile).value
      val gulpfileName = (gulpFile in Compile).value
      val result = runGulp(base, gulpfileName, List("clean")).exitValue()
      if (result != 0) throw new Exception("gulp failed")
    },

    gulpTest := {
      val base = (gulpDirectory in Compile).value
      val gulpfileName = (gulpFile in Compile).value
      val result = runGulp(base, gulpfileName, List("test")).exitValue()
      if (result != 0) throw new Exception("gulp failed")
    },

    // Executes `gulp build` before `sbt dist`
    dist <<= dist dependsOn gulpBuild,

    // Executes `gulp build` before `sbt stage`
    stage <<= stage dependsOn gulpBuild,

    // Executes `gulp clean` before `sbt clean`
    clean <<= clean dependsOn gulpClean,

    // Executes `gulp test` before `sbt test` (optional)
    //(test in Test) <<= (test in Test) dependsOn gulpTest,

    // Ensures that static assets in the ui/dist directory are packaged into
    // target/scala-2.11/play-gulp_2.11-1.0.0-web-asset.jar/public when the play app is compiled
    unmanagedResourceDirectories in Assets <+= (gulpDirectory in Compile)(_ / "dist"),
  
    // Starts the gulp watch task before sbt run
    playRunHooks <+= (gulpDirectory, gulpFile).map {
      (base, fileName) => GulpWatch(base, fileName)
    },

    // Allows all the specified commands below to be run within sbt in addition to gulp
    commands <++= gulpDirectory {
      base =>
        Seq(
          "npm",
          "bower",
          "yo"
        ).map(cmd(_, base))
    }
  )

  val withTemplates: Seq[Setting[_]] = Seq(
    // Added ui/src/views/*.scala.html to the target of Scala view template compilation
    sourceDirectories in TwirlKeys.compileTemplates in Compile <+= (gulpDirectory in Compile)(_ / "src"),
    includeFilter in sources in TwirlKeys.compileTemplates := "*.scala.html",
    gulpExcludes <<= gulpDirectory(gd => Seq(
      gd + "/src/app/",
      gd + "/src/assets/",
      gd + "/src/bower_components/"
    )),
    excludeFilter in unmanagedSources <<=
      (excludeFilter in unmanagedSources, gulpExcludes) {
        (currentFilter: FileFilter, ge) =>
          currentFilter || new FileFilter {
            def accept(pathname: File): Boolean = {
              (true /: ge.map(s => pathname.getAbsolutePath.startsWith(s)))(_ && _)
            }
          }
      },
    // Makes play autoreloader to compile Scala view templates and reload the browser
    // upon changes in the view files ui/src/views/*.scala.html
    // Adds ui/src/views directory's scala view template files to continuous hot reloading
    watchSources <++= gulpDirectory map { path => ((path / "src/views") ** "*.scala.html").get}
  )

  private def runGulp(base: sbt.File, fileName: String, args: List[String] = List.empty): Process = {
    if (System.getProperty("os.name").startsWith("Windows")) {
      val process: ProcessBuilder = Process("cmd" :: "/c" :: "gulp" :: "--gulpfile=" + fileName :: args, base)
      println(s"Will run: ${process.toString} in ${base.getPath}")
      process.run()
    } else {
      val process: ProcessBuilder = Process("gulp" :: "--gulpfile=" + fileName :: args, base)
      println(s"Will run: ${process.toString} in ${base.getPath}")
      process.run()
    }
  }

  import scala.language.postfixOps

  private def cmd(name: String, base: File): Command = {
    if (!base.exists()) {
      base.mkdirs()
    }
    Command.args(name, "<" + name + "-command>") {
      (state, args) =>
        if (System.getProperty("os.name").startsWith("Windows")) {
          Process("cmd" :: "/c" :: name :: args.toList, base) !<
        } else {
          Process(name :: args.toList, base) !<
        }
        state
    }
  }

  object GulpWatch {

    def apply(base: File, fileName: String): PlayRunHook = {

      object GulpSubProcessHook extends PlayRunHook {

        var process: Option[Process] = None

        override def beforeStarted(): Unit = {
          process = Some(runGulp(base, fileName, "watch" :: Nil))
        }

        override def afterStopped(): Unit = {
          process.foreach(_.destroy())
          process = None
        }
      }

      GulpSubProcessHook
    }

  }

}
```

build.sbtでこの設定をプロジェクトに読み込みます。

```scala:build.sbt
import PlayGulp._

...

lazy val root = (project in file("."))
  .enablePlugins(PlayScala)
  .settings(playGulpSettings ++ withTemplates) //この行を追記
```

## 3. app/controllers/GulpAssets.scalaの作成

続いて、スタティックアセットへのアクセスをuiフォルダのgulp-angularプロジェクトにルーティングするためのGulpAssetsコントローラを作成します。

```scala:app/controllers/GulpAssets.scala
package controllers

import java.io.File
import javax.inject.Singleton

import play.api.Play.current
import play.api.{Logger, _}
import play.api.http.DefaultHttpErrorHandler
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.mvc.{Action, AnyContent}

import scala.concurrent.Future

object GulpAssets extends controllers.Assets(DefaultHttpErrorHandler) {

  private lazy val logger = Logger(getClass)

  def index = Action.async { request =>
    if (request.path.endsWith("/")) {
      at("index.html").apply(request)
    } else {
      Future(Redirect(request.path + "/"))
    }
  }

  // List of UI directories from which static assets are served in development mode
  // (A directory in higher priority comes first.)
  val devBasePaths: List[java.io.File] = List(
    Play.application.getFile("ui/.tmp/serve"),
    Play.application.getFile("ui/src"),
    Play.application.getFile("ui")
  )

  def devAssetHandler(file: String): Action[AnyContent] = Action { request =>
    // Generates a non-strict list of the full paths
    val targetPaths = devBasePaths.view map {
      new File(_, file)
    }

    // Generates responses returning the file in the dev and test modes only (not in the production mode)
    val responses = targetPaths filter { file =>
      file.exists()
    } map { file =>
      if (file.isFile) {
        logger.info(s"Serving $file")
        Ok.sendFile(file, inline = true).withHeaders(CACHE_CONTROL -> "no-store")
      } else {
        Forbidden(views.html.defaultpages.unauthorized())
      }
    }

    // Returns the first valid path if valid or NotFound otherwise
    responses.headOption getOrElse NotFound("404 - Page not found error\n" + request.path)
  }

  def prodAssetHandler(file: String): Action[AnyContent] = Assets.at("/public", file)

  lazy val atHandler: String => Action[AnyContent] = if (Play.isProd) prodAssetHandler(_: String) else devAssetHandler(_: String)

  def at(file: String): Action[AnyContent] = atHandler(file)

}

@Singleton
class GulpAssets extends controllers.Assets(DefaultHttpErrorHandler) {
  def at(file: String) = GulpAssets.at(file: String)
}
```

## 4. conf/routesの変更

デフォルトで設定されているスタティックアセット用のルーティング(Assets.versioned)は削除し、かわりに先ほど作成したGulp用のスタティックアセットのルーティングの設定(GulpAssets.at)を一番下に加えます。

```scala:routes
GET        /                           controllers.Application.index
GET        /oldhome                    controllers.Application.oldhome
GET        /*file                      controllers.GulpAssets.at(file)

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

routeを修正したので、viewsのテンプレートファイルも合わせて修正します。

Before

```html:app/views/main.scala.html
@(title: String)(content: Html)

<!DOCTYPE html>

<html lang="en">
  <head>
    <title>@title</title>
    <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
    <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
    <script src="@routes.Assets.versioned("javascripts/hello.js")" type="text/javascript"></script>
  </head>
  <body>
    @content
  </body>
</html>
```

After

```html:app/views/main.scala.html
@(title: String)(content: Html)

<!DOCTYPE html>

<html lang="en">
  <head>
    <title>@title</title>
    <link rel="stylesheet" media="screen" href="@routes.GulpAssets.at("assets/styles/oldhome-style.css")">
    <link rel="shortcut icon" type="image/png" href="@routes.GulpAssets.at("assets/favicon.png")">
    <script type="text/javascript" src="@routes.GulpAssets.at("assets/scripts/oldhome-script.js")"></script>
  </head>
  <body>
    @content
  </body>
</html>
```

上記のroutes.GulpAssets.at("assets/~/~.xxx")の実パスはui/src/assets/~/~.xxxに相当するので、この場所に適当にoldhome-style.cssやoldhome-script.jsをつくっておきます。

```bash
$ touch ui/src/assets/styles/oldhome-style.css
$ touch ui/src/assets/scripts/oldhome-script.js
```

そしてApplication.indexはGulpAssets.indexに設定して、URLのルート/へのアクセスは、開発時はui/.tmp/serve/index.htmlにリダイレクトされるようにします。従来のindexページは適当に/oldhomeでアクセスできるようにしておきます。

```scala:app/controllers/Application.scala
class Application extends Controller {

  def index = GulpAssets.index

  def oldhome = Action {
    Ok(views.html.index("Play Framework"))
  }

}
```

## 5. gulp-angularプロジェクトの微修正
さすがにgulp-angularのプロジェクト構成そのままではPlayアプリと統合できないので、以下を修正します。

* ui/.bowerrc

  Before

  ```json:ui/.bowerrc
{
  "directory": "bower_components"
}
  ```

  After

  ```json:ui/.bowerrc
{
  "directory": "src/bower_components"
}
  ```

* bower_componentsはsrcフォルダの中へ移動
  
  ```bash
mv ui/bower_components ui/src/bower_components
  ```

* ui/gulp/conf.js
  
  Before

  ```js:ui/gulp/conf.js
exports.wiredep = {
  exclude: [/bootstrap.js$/, /bootstrap-sass-official\/.*\.js/, /bootstrap\.css/],
  directory: 'bower_components'
};
  ```

  After

  ```js:ui/gulp/conf.js
exports.wiredep = {
  exclude: [/bootstrap.js$/, /bootstrap-sass-official\/.*\.js/, /bootstrap\.css/],
  directory: 'src/bower_components'
};
  ```
  
* ui/gulp/build.js
  
  Before

  ```js:ui/gulp/build.js
gulp.task('fonts', function () {
  return gulp.src($.mainBowerFiles())
    .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
    .pipe($.flatten())
    .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/')));
});

...

gulp.task('build', ['html', 'fonts', 'other']);
  ```

  After

  ```js:ui/gulp/build.js
gulp.task('fonts', function () {
  return gulp.src(path.join(conf.paths.src, '/bower_components/**/*'))
    .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
    .pipe($.flatten())
    .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/')));
});

gulp.task('otherassets', function () {
  return gulp.src(path.join(conf.paths.src, '/assets/**/*'))
    .pipe(gulp.dest(path.join(conf.paths.dist, '/assets/'), {overwrite: false}));
});

...

gulp.task('build', ['html', 'fonts', 'other', 'otherassets']);
  ```

必須ではないですが、各スタティックファイルがどのディレクトリからクライアントに送信させているのかがコンソールにリアルタイムに表示されたほうが慣れるまではいいと思うので、logの出力レベルをERRORからINFOに緩めておきます。

```xml:conf/logback.xml
...
<root level="INFO">
  <appender-ref ref="STDOUT" />
</root>
...
```

これでプロジェクト設定は完了です。

# 使い方

sbt/activatorのコンソールに入ると、

```bash
$ sbt/activator
```

build, clean, serve, testなどのgulpのタスクをuiディレクトリをルートとして実行できるようになっています。gulpのデフォルトタスク(ここではgulp build)を実行すると、ui/buildディレクトリにng-annotate, uglify等がされたその他スタティックファイルと、JavaScript/CSSをinjectされたindex.htmlファイルができます。

```bash
[play-gulp] > gulp
Will run: [gulp, --gulpfile=gulpfile.js, build] in /home/user/dev//play-gulp/ui
[23:41:29] Using gulpfile ~/dev/play-gulp/ui/gulpfile.js
[23:41:29] Starting 'clean'...
[23:41:29] Finished 'clean' after 53 ms
[23:41:29] Starting 'default'...
[23:41:29] Starting 'scripts'...
[23:41:29] Starting 'styles'...
[23:41:29] Starting 'partials'...
[23:41:29] Starting 'fonts'...
[23:41:29] Starting 'other'...
[23:41:29] Starting 'otherassets'...
[23:41:29] Finished 'default' after 480 ms
[23:41:29] gulp-inject 2 files into index.scss.
[23:41:31] Finished 'styles' after 1.87 s
[23:41:31] all files 7.88 kB
[23:41:31] Finished 'scripts' after 2.04 s
[23:41:31] Starting 'inject'...
[23:41:31] Finished 'partials' after 1.73 s
[23:41:31] gulp-inject 1 files into index.html.
[23:41:31] Finished 'otherassets' after 1.64 s
[23:41:31] gulp-inject 10 files into index.html.
[23:41:31] Finished 'inject' after 121 ms
[23:41:31] Starting 'html'...
[23:41:31] gulp-inject 1 files into index.html.
[23:41:40] 'dist/' styles/app-c90c023e53.css 120.34 kB
[23:41:40] 'dist/' styles/vendor-1dddaadd0b.css 58.62 kB
[23:41:40] 'dist/' scripts/app-10800ada38.js 6.26 kB
[23:41:40] 'dist/' scripts/vendor-48a98d1612.js 438.69 kB
[23:41:40] 'dist/' index.html 636 B
[23:41:40] 'dist/' all files 624.56 kB
[23:41:40] Finished 'html' after 8.83 s
[23:41:40] Finished 'other' after 11 s
[23:41:40] Finished 'fonts' after 11 s
[23:41:40] Starting 'build'...
[23:41:40] Finished 'build' after 7.29 μs
```

runにより開発モードでアプリケーションを実行すると、`PlayGulp.scala`の設定

```scala:project/PlayGulp.scala
...
playRunHooks <+= (gulpDirectory, gulpFile).map {
      (base, fileName) => GulpWatch(base, fileName)
    }
...
```

によりgulp watchタスクが呼ばれ、http://localhost:9000 を開くと、


```bash
[play-gulp] > run

Will run: [gulp, --gulpfile=gulpfile.js, watch] in /home/user/dev/play-gulp/ui
--- (Running the application, auto-reloading is enabled) ---

[info] p.a.l.c.ActorSystemProvider - Starting application default Akka system: application
[info] p.c.s.NettyServer - Listening for HTTP on /localhost:9000

(Server started, use Ctrl+D to stop and go back to the console...)
```

Yeomanのシンボルキャラクターのひげおじさんと昔の人気British sitcomのタイトルが飾る(なぜこのタイトルがここで出てくるのか理由は知りません...)トップ画面になります。

![Selection_014.png](https://qiita-image-store.s3.amazonaws.com/0/38958/c98e6a03-a677-2832-9f5f-faebdac04d39.png)

ターミナルを見てみると、GulpAssetsコントローラ経由でどこのパスからファイルがserveされているかわかります。

```bash
[23:52:50] Using gulpfile ~/dev/play-gulp/ui/gulpfile.js
[23:52:50] Starting 'scripts'...
[23:52:50] Starting 'styles'...
[23:52:50] gulp-inject 2 files into index.scss.
[23:52:51] all files 7.88 kB
[23:52:51] Finished 'scripts' after 530 ms
[23:52:52] Finished 'styles' after 1.43 s
[23:52:52] Starting 'inject'...
[23:52:52] gulp-inject 1 files into index.html.
[23:52:52] gulp-inject 10 files into index.html.
[23:52:52] Finished 'inject' after 61 ms
[23:52:52] Starting 'watch'...
[23:52:52] Finished 'watch' after 15 ms
...
[info] - play.api.Play - Application started (Dev)
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/.tmp/serve/index.html
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/src/bower_components/toastr/toastr.css
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/src/bower_components/angular-animate/angular-animate.js
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/.tmp/serve/app/index.css
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/src/bower_components/animate.css/animate.css
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/src/bower_components/angular/angular.js
[info] - controllers.GulpAssets - Serving /home/user/dev/play-gulp/ui/src/bower_components/jquery/dist/jquery.js
...
```

見えているトップページの実体は、デバッグ時にGulpが一時的に生成する`ui/.tmp/serve/index.html`です。

デバッグをやめて、sbt(またはactivator)の`stage`タスクを実行すると、

```bash
[play-gulp] > stage
```

`target/scala-2.11/play-gulp_2.11-1.0-web-assets.jar`がほかのjarファイルとともに生成されます。`play-gulp_2.11-1.0-web-assets.jar`をzipファイルとして解凍すると、Gulpによりビルドされたスタティックファイルが入っている`ui/dist`フォルダの中身がそのままそっくりjar内のpublicフォルダにコピーされているのがわかります。

![Selection_015.png](https://qiita-image-store.s3.amazonaws.com/0/38958/33e374fe-f7ae-d35b-eb40-0dd0fae4b980.png)

このjar/publicには本来はプロジェクトルートのpublicフォルダ内のスタティックファイルがコピーされますが、このプロジェクトでは、プロジェクトルート/publicフォルダを削除して、代わりに下記のプロジェクト設定

```scala:project/PlayGulp.scala
unmanagedResourceDirectories in Assets <+= (gulpDirectory in Compile)(_ / "dist")
```

を追加しているため、このようになります。


Gulpでビルドしたスタティックアセットは、最終的には`dist`タスクにより、デプロイ用ファイル一式`target/universal/play-gulp-1.0.zip`の中のlibに格納されます。

リリースモードで実行し、ブラウザでディベロッパーツールを開くと、JavaScript/CSSがきちんとminify, concatenate, obfuscateされているのがわかります。

```bash
[play-gulp] > testProd
```
![Selection_016.png](https://qiita-image-store.s3.amazonaws.com/0/38958/a536258a-6788-1647-b3f2-cab5d515fdd1.png)

app/views/~.scala.htmlのテンプレートからスタティックアセットにアクセスしたい時は、JavaScript, CSS, PDF, 画像、音楽ファイルなど(たとえばphoto.png)をui/src/assets以下のディレクトリに置いておけば、GulpAssetsコントローラ経由で開発時もリリース時もアクセスできます。(routesで/をgulp-angularのトップページに設定しているので、この設定のままであればGulpAssetsを使わず"/assets/images/photo.png"でも可。)

```scala:app/views/~.scala.html
<img src="@routes.GulpAssets.at("assets/images/photo.png")">
<img src="/assets/images/photo.png">
```

```scala:app/views/~.scala.html
<img src="@routes.Assets.at("assets/images/photo.png")">
<img src="/assets/images/photo.png">
```

# サーバサイドとフロントエンドの分離開発

このプロジェクト構成のいいところとして、Gulpをフルに使える点に加えて、サーバサイドとフロントエンドの分離開発がしやすいという点も挙げられます。

プロジェクトルートのuiフォルダ内は一般的なGulpプロジェクトなので、Play Frameworkプロジェクトには触らず、uiディレクトリ内のgulp-angularプロジェクトだけをgulpコマンドで開発していくことができます。

```bash
$ cd ui
$ gulp clean
$ gulp serve
$ gulp test
$ gulp build
```

ui以下をgit submoduleとして管理しておくと、デザイナーさんとの分業がはかどると思います。

# その他

GitHubにあげたサンプルプロジェクトでは、おまけとして、AngularJSのhtml5modeを有効にしたり、twirlのScala htmlテンプレートファイルをui/src/views/~~/~.scala.html に置いてみたり、ApplicationコントローラにREST APIっぽいアクションを追加してみたりしています。開発モードで http://localhost:9000/routes からいろんなパスを叩いてみてください。

大してファイルを追加していませんし、Gulpのプロジェクト構成は人それぞれだと思いますので、改変しやすいようあえてプラグインにはしていません。ご自由にカスタマイズしていただいて結構です。