パンダのメモ帳

技術系のネタをゆるゆると

Play Framework 2.4 + コンパイル時DIで Play Module を利用する方法

Play Framework 2.4 がリリースされたのは2015年5月なのですごく今更感はあるのですが結構ハマった&わかりやすい記事が見当たらなかったのでまとめておくことにします。

忙しい人は最後のまとめだけ読めばOKかもしれないです。

Play Framework 2.4 の Dependency Injection

Play Framework 2.4 では Dependency Injection (以下DI)がフレームワークでサポートされました。 ……というよりも、フレームワークの標準機能でガッツリ利用されるようになりました。

マニュアルを読むと、2種類のDIが案内されています。

前者は Google Guice を利用した実装で、@Singleton やら @Inject やらのアノテーションがたくさん出てきてすごく Java っぽいです(GuiceJava 向けなので当たり前ですが)。 DIの方法は所謂「コンストラクタDI」で、依存性の注入先となるクラスのコンストラクタのパラメータとして依存するオブジェクトを受け取ります。 誰かもどこかで言っていましたが言語レベルでシングルトンオブジェクトが定義できる Scala@Singleton というアノテーションには少し違和感があります。

後者は特にこれと言った機能やライブラリは標準では提供されておらず、「コンパイル時に好きなようにDIすればいいじゃない」というスタイルです。 play.api.ApplicationLoader を継承した独自クラスを作成し、標準のものと差し替えることで実現します。 詳しい方法などはマニュアルを参照してください。

型安全教徒としてはもちろん後者を選択したのですが、それによって苦労したというお話。

Play Module について

Play Framework 2.4 ではDIの他にも大きな変更がいくつかあり、そのうちのひとつが Plugin に代わって登場した Module です。

Plugin は play.api.Plugin を継承し play.plugins に記述することで利用していましたが Module は play.api.inject.Module を継承し application.conf に次のような記述をすることで利用することになりました。

play.modules.enabled += "my.module.MyModule"

今回利用しようとした Module は flyway-playscalikejdbc-play-support の2つですが、いずれも README にて上記のような設定をすることで使える……と案内されていました。 ……が、それはあくまで Guice による実行時DIを利用している場合のみのお話だった、というわけです。

Play Module がどのように読み込まれるか

さて、というわけで各モジュールを libraryDependencies に追加して play.modules.enabled += ... を記述しても一向にモジュールが動作している様子がないことがわかりました。

最初は原因もわからず右往左往していたのですが、どうにもならないので Playframework のコードを調べてみました。

まずは application.conf の play.modules.enabled がどこで読み込まれているかです。 play.modules.enabled で検索してみると Modules オブジェクトの locate メソッドで設定が読まれているようです。

def locate(environment: Environment, configuration: Configuration): Seq[Any] = {

  val includes = configuration.getStringSeq("play.modules.enabled").getOrElse(Seq.empty)
  val excludes = configuration.getStringSeq("play.modules.disabled").getOrElse(Seq.empty)

  ...

次に Modules.locate がどこで呼び出されているかですが、これは GuiceableModule オブジェクトの loadModules メソッドです。

def loadModules(environment: Environment, configuration: Configuration): Seq[GuiceableModule] = {
  Modules.locate(environment, configuration) map guiceable
}

オブジェクト名に Guice と出てきて嫌な予感が……と思ったのも束の間、さらに辿ると GuiceApplicationBuilder そして GuiceApplicationLoader に辿り着きました。

つまりコンパイル時DIを使って独自の ApplicationLoader を定義、利用している場合はいくら application.conf に記述しても Module が読み込まれることはないのです。

コンパイル時DI + Play Module を利用する方法

いよいよ本題です。

しばらく悩んだのですが、flyway-play と scalikejdbc-play-support の各モジュールのコードを読んでいるといずれも PlayModule というクラスから PlayInitializer という初期化クラスを呼び出している、という構造になっています。

いずれの PlayModulePlayInitializer のためのエントリポイント、というカンジかつ PlayInitializerコンストラクタさえ呼び出してしまえば Module の機能が利用できそうです。

というわけで次のようになります。

import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext

class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) {

  ...

  new scalikejdbc.PlayInitializer(applicationLifecycle, configuration)
  new org.flywaydb.play.PlayInitializer(configuration, environment, webCommands)

}

上記はコンパイル時DIを手動でゴリゴリ頑張っている人向けです。macwire を使っているなら次のようにすればOKです。

import com.softwaremill.macwire._
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext

class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) {

  ...

  wire[scalikejdbc.PlayInitializer]
  wire[org.flywaydb.play.PlayInitializer]

}

macwire 便利だよね!

まとめ

  • application.conf に play.modules.enabled を記述してモジュールが読み込まれるのは標準の ApplicationLoader を使っている場合のみ。
  • コンパイル時DIを使う場合は自作の ApplicationLoader の中に自力でなんとかする必要がある。
  • macwire 便利(大事なことなので)。

Scala + sbt-android + IntelliJ で快適Androidアプリ開発

まえがき

このエントリは Android Advent Calendar 2013 ならびに Scala Advent Calendar 2013 の6日目(12/6)の記事になります。

僕が Scala に触れ始めたのは2012年の11月頃なのでかれこれちょうど1年前ぐらいです。 以前から Android アプリを開発していた身としては当然 Android アプリも Scala で書けたらなぁ、と考えるわけですが当時「Scala Android」とかでググる現状 #Scala で #Android をやるのは思っている以上に罠が多いという話 みたいまとめがあったりして「初心者お断り」感がすごかったのを覚えています。

その年の Android Advent Calendar 2012 でこんな記事を書いてる人がいたり、 今年に入ってこんな記事を見かけたりして、 改めて「あれ、意外と簡単にできるんじゃね?」と思って手を出したら結局やっぱり色々と罠に遭ったのでその辺のノウハウをまとめてみました。

本記事の内容

  • Android Support Library, revision 19 を使用して API Level 9 をサポートしつつ Fragment や ActionBar を使用したアプリを Scala で書くための環境構築
  • Scala + sbt-android + IntelliJ によるアプリ開発の簡単な説明と待ち受ける罠

開発環境

開発環境の概要は次の通り。

開発環境を構築する

Android SDK

これがないと始まらない。

公式サイト からそれぞれのプラットフォーム向けのアーカイブ(ADT Bundle じゃない方)をダウンロードして適当な場所に展開してください。 自分の場合(OS X)は /opt/local/android-sdk-macosx に設置しています。

設置後、.bashrc なり .zshrc なりを編集して設置パスを環境変数 ANDROID_HOME として export します。ついでにコマンドにもパスを通しておきます。

export ANDROID_HOME=/opt/local/android-sdk-macosx
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools

この段階では各バージョンのSDKが未導入の状態なのでターミナルから android update sdk -u を実行して一式ダウンロードしてください(結構時間が掛かります)。

Maven 3.1.x

次の Maven Android SDK Deployer を利用するために必要になります。 OS X の場合は brew install maven でOK。他の環境のことはよく知らないので「Maven インストール」とかでググってください。 3.0.x だとうまく動かないので 3.1.x を使用する必要があります。

Maven Android SDK Deployer

Android Support Library など Maven Central Repository にないパッケージをローカルの Maven Repository から解決できるようにしてくれるユーティリティです。

github から最新のソースコード一式を git clone して mvn install するだけでそんなに時間も掛からずに終わるはずです。

$ git clone git@github.com:mosabua/maven-android-sdk-deployer.git
$ cd maven-android-sdk-deployer
$ mvn install

sbt

Android のビルドと言えば最近はもっぱら Gradle なわけですが今回は Scala の定番ビルドツール sbt を使用します。 大きな理由としては Scala で書いた Android アプリケーションをビルドするためのプラグイン sbt-android がすでに存在するためです。

OS X の場合はこれも brew install sbt でOKです。 その他の環境の場合、公式サイトで各環境向けのパッケージが配布されているのでそちらを使えば多分OKです。

giter8

giter8 は Scala 製のプロジェクト生成ツールです。使わなくても困りはしないですがあった方が圧倒的に便利。 これも OS X なら brew install giter8 でインストールできます。その他の環境の場合は Conscript を使ってインストールするのがオススメらしいんですが、公式READMEでも紹介されているこちらの記事(日本語)が参考になるんじゃないかと思います。

[IntelliJ IDEA]

Android 開発者にとっては Android Studio のベースとなっていることでも注目されている最近流行のIDE(総合開発環境)です。 Eclipse?いえ、知らない子ですね……。

自分の場合は元々 Scala での開発用に使っていたのもあって IntelliJ IDEA + Android Plugin + Scala Plugin で開発しています。 どちらのプラグインも無料版の Community Edition でサポートされているので今回は無料版で十分です。今回はエディタ兼デバッガとして使用します。

未導入の場合はこのページからダウンロードしてインストールしてください。 Android Support/Android Designer プラグインは標準でインストールされているので Scala Plugin を追加するだけですぐに使えます。

ところで、この記事を書いている最中に IntelliJ IDEA 13 がリリースされましたが今回は IntelliJ IDEA 12 ベースで話を進めます。IntelliJ IDEA 13 では sbt が標準でサポートされるなど気になる新機能も多いので早く試したいです。

プロジェクトを作成する

開発環境が構築できたら giter8 を使ってプロジェクトを作成します。 今回の目的である「Android Support Library を使ったアプリ」のためのテンプレートを GitHub で公開しているので今回はそれを使用します。パッケージ名は「com.example.helloworld」プロジェクト名は「Hello World」とします。デフォルト値で問題ない場合はなにも入力せずにエンターキーを押せばOKです。

$ g8 shogogg/scala-android-app.g8
(中略・初回起動時は必要なライブラリ等をダウンロードするのでちょっと時間が掛かる)

Template for Android apps in Scala 

package [com.example.android.app]: com.example.helloworld
name [My Android Project]: Hello World
keyalias [alias]: <リリース時の署名に使用するキーストアのエイリアス>
scalaVersion [2.10.3]: <ビルドに使用する Scala のバージョン>
versionCode [0]: <アプリのバージョン(管理用・整数)>
version [0.1.0]: <アプリのバージョン(表示用・文字列)>
targetSdkVersion [19]: <アプリの targetSdkVersion>
mainActivity [MainActivity]: <アプリのメインアクティビティ名>
minSdkVersion [9]: <アプリの minSdkVersion>

Template applied in ./hello-world

デフォルトでは targetSdkVersion を最新の 19 [Android 4.4 KitKat], minSdkVersion を 9 [Android 2.3.x Gingerbread] としています。

ディレクトリ構成など

sbt-android ディレクトリ構成はこんなカンジになります。

$ cd /path/to/hello-world
$ tree .
.
├── project
│   ├── build.properties
│   ├── build.scala
│   └── plugins.sbt
└── src
    └── main
        ├── AndroidManifest.xml
        ├── res
        │   ├── layout
        │   │   └── main.xml
        │   └── values
        │       ├── strings.xml
        │       └── styles.xml
        └── scala
            └── com
                └── example
                    └── helloworld
                        ├── MainActivity.scala
                        └── support
                            ├── ActivitySupport.scala
                            ├── AndroidFuture.scala
                            ├── AndroidPromise.scala
                            ├── AsyncSupport.scala
                            ├── ScalaAsyncTask.scala
                            ├── TypedViewContainer.scala
                            └── package.scala

11 directories, 15 files

Eclipse + ADT などで作成するアプリのディレクトリ構成なんかとは随分違います。

src ディレクトリ以下にあるのがソースコードとリソース関連のファイルになります。 ちなみに src/main/java ディレクトリを作成すれば Javaソースコードを混在させることもできますし, Assets を使用したい場合は src/main/assets ディレクトリを作成すればOKです。 src/main/scala/com/example/helloworld/support 以下にあるのは補助用のクラスです。後で解説します。

project ディレクトリ以下にあるのがビルド関連の設定・定義ファイルです。以下、それぞれについて簡単に解説します。

project/build.properties

project/build.properties では使用する sbt のバージョンを指定しています。

sbt.version=0.12.4

project/plugins.sbt

project/plugins.sbt では sbt で使用するプラグインを指定しています。

resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)

addSbtPlugin("org.scala-sbt" % "sbt-android" % "0.7")

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")

1行目はリポジトリの追加、3,5行目はそれぞれ sbt-android プラグインと sbt-idea プラグインをプロジェクトに追加しています。 sbt インターフェース上で plugins.sbt ファイルでは各コマンドの間に空行が必要になるので編集する場合は注意してください。

project/build.scala

project/build.scala は各種ビルド情報を定義するファイルです。 通常構成の Android プロジェクトでは AndroidManifest.xml に記述していたような内容の一部もこちらに記述します。 ちょっと長いのでここには載せませんが少しお節介な程度にコメントしておいたので参考にしてください。

src/main/AndroidManifest.xml

通常構成の Android プロジェクトではルートディレクトリ直下にある AndroidManifest.xml ですが sbt-android なプロジェクトでは src/main 以下に配置されています。 一部の内容を project/build.scala に記述する以外は通常のプロジェクトと同様です。

ビルドしてみる

g8 コマンドで生成されたプロジェクトディレクトリに移動後, sbt コマンドを実行して sbt インターフェースを起動します。 プロジェクトを git で管理する場合はこのタイミングで first commit するのがいいと思います。

# ディレクトリに移動
$ cd ./hello-world

# git リポジトリを作成して初回コミット
$ git init
$ git add --all
$ git commit -m 'first commit'

# sbt を起動
$ sbt
[info] Loading project definition from /path/to/hello-world/project
[info] Set current project to Hello World (in build file:/path/to/hello-world/)
> 

この sbt インターフェースでは様々なタスク(コマンド)を実行することができます。主なタスクは次の通り。

 compile: 各種ソースコードをコンパイルし .class ファイルを生成します。
     apk: JAR, DEX, APKファイルを生成します。未コンパイルの場合は自動でコンパイルも実行します。
 install: 生成されたAPKファイルを実機に転送します。APKが生成されていない場合は自動で生成します。
   start: 生成されたAPKファイルを転送し、実機上でアプリを起動します。APKが生成されていない場合は自動で生成します。
  reload: ビルド定義ファイルの再読込を行います。ビルド定義を修正したりした場合に使用します。
 console: REPLを起動します。
    exit: sbt インターフェースを終了します。

雛形のプロジェクトはそのままビルドできる状態なので start コマンドでコンパイル → APK生成 → 実機に転送 → 実行までを行ってみます。

> start
(中略)
[info] Dexing /path/to/hello-world/target/classes-hello-world-compile-0.1.0.dex
[info] Packaging /path/to/hello-world/target/hello-world-compile-0.1.0.apk
[info] Installing hello-world-compile-0.1.0.apk
[success] Total time: 30 s, completed 2013/12/06 18:07:50
> 

実機上でこんな画面が表示されれば成功です。

ちなみに sbt の各コマンド(タスク)の前にチルダ(~)を付けると、ファイルの変更を監視して変更をトリガーに自動でタスクを実行してくれます。 例えば

> ~compile

としておくことで自動でコンパイルが走ります。便利。

IntelliJ IDEA でプロジェクトを開く

gen-idea タスクを実行することで IntelliJ IDEA のプロジェクトに必要なメタファイルを生成することができます。 この辺はもしかしたら最新の IntelliJ IDEA 13 ではいらなくなるかも。

> gen-idea
[info] Creating IDEA module for project 'Hello World' ...
[info] Running compile:managed-sources ...
[info] Extracting library compatibility-v7-appcompat-19.apklib
[info] Generated /path/to/hello-world/target/scala-2.10/src_managed/main/AndroidManifest.xml
[info] Generated 0 source files from 1 ApkLibs
[info] Running AAPT for package com.example.helloworld
[info] Wrote /path/to/hello-world/target/scala-2.10/src_managed/main/scala/com/example/helloworld/TR.scala
[info] Running AAPT for package android.support.v7.appcompat
[info] Resolving android.support#compatibility-v7-appcompat;19 ...
[info] Excluding folder target
[info] Created /path/to/hello-world/.idea/IdeaProject.iml
[info] Created /path/to/hello-world/.idea
[info] Excluding folder /path/to/hello-world/target/scala-2.10/aarlib_managed
[info] Excluding folder /path/to/hello-world/target/scala-2.10/cache
[info] Excluding folder /path/to/hello-world/target/scala-2.10/classes
[info] Excluding folder /path/to/hello-world/target/scala-2.10/apklib_managed/res
[info] Excluding folder /path/to/hello-world/target/resolution-cache
[info] Excluding folder /path/to/hello-world/target/streams
[info] Created /path/to/hello-world/.idea_modules/Hello World.iml
[info] Created /path/to/hello-world/.idea_modules/Hello World-build.iml
>

gen-idea タスクが無事に完了したら IntelliJ IDEA でプロジェクトを開いてみます。 Open Project から hello-world ディレクトリを選択すればOKです。

デバッガを使う

IntelliJ からデバッガを起動してみます。 まずメニューから RunEdit Configurations... を選択します。

左上の をクリックして Android Application を選択。

各項目の設定は次の通り。

  • Name には適当に名前を入力
  • Module は今回作成したプロジェクト(Hello World)を選択
  • Launche default Activity にチェックを入れる
  • Deploy Application のチェックを外す
  • Target Device は好みで
  • Before launchMake を選択して《−》をクリックして削除する
  • OK ボタンをクリックして完了

これだけで使えれば完璧なんですが、あともう1カ所設定が必要です。左のプロジェクトツリーのルートを右クリックして Open Module Settings をクリック。

ModulesAndroidStructure タブに Manifest file という項目があるのでこれを /path/to/hello-world/target/scala-2.10/src_managed/main/AndroidManifest.xml に変更します。

これで準備は完了です。 RunDebug 'Hello World' を選択してデバッガを起動します。 こんな画面が表示され、端末上でアプリが起動すれば成功です。

もちろんブレークポイントを設定することもできます。

以上で環境設定は完了です。これでアプリ開発をスタートできますね!

待ち受ける罠

実は、ここまで至るまでにいくつかの罠にハマりました。

ライブラリプロジェクトのリソースが参照できない(未解決)

Scala + sbt-android はマルチプロジェクトに対応しています。 もちろんライブラリプロジェクトを作成し、それを利用することもできるのですがメインのプロジェクトからライブラリプロジェクトのリソースを参照することができません(おそらくバグ)。

Scala で Android アプリを作ろうと思い立ったとき Android Support Library v7 appcompat を使うことは必須条件でした。 しかし Android Support Library v7 appcompat は ActionBar に対応するために通常の手順ではライブラリプロジェクトとして参照し、そのリソースを使う必要があります。

今回は Maven Android SDK Deployer を使ってローカルリポジトリを構築し、依存関係に jar と apklib を含めることで対応しました。

Scala の Future は使えない(使いづらい)

Scala 勢はご存じかと思いますが Scala には非同期処理を簡単に扱える scala.concurrent.Future というクラスがあります。 下記の様に future { ... } で括った部分が別スレッドで実行され、その結果を使って別の計算を行う、というコードを下記の様に簡単に書くことができます。

import scala.concurrent._
import ExecutionContext.Implicits.global

val x: Future[String] = future {
  "Hello, %s".format("World")
}
x onSuccess { msg =>
  println(msg)
}

ところがこれを Android で使おうと思ったときに一点問題があります。 それは Future を使った処理は onSuccessonFailure, onComplete で実行される処理も別スレッドで行われる=UIスレッドでない、という問題です。

Android ではUIに関する処理はメインスレッドであるUIスレッドで行う必要がありますが Future を使うとUIスレッドに処理が帰ってくることはありません。 おとなしく AsyncTask を使いましょう。

2013-12-10 追記: AsyncTaskActivity#runOnUiThread を使った ExecutionContext を独自実装し、 onSuccess の引数に明示的に指定すれば Future を使って非同期に処理をしつつ onSuccess を UIスレッド上で実行できるみたいです(参考: @OE_uia さんの Gist)。

ところが AsyncTask が素直には使えない

Future が使いづらいことがわかりガッカリしたのも束の間, AsyncTask を使うのにも罠が待ち受けていました。 まず最初は次のように素直に AsyncTask を継承したクラスを作成しました。

import android.os.AsyncTask

class SomeAsyncTask extends AsyncTask[Void, Void, String] {
  def doInBackground(params: Void*): String = "Hello, %s".format("World")
  override def onPostExecute(result: String): Unit = { /* Something to do */ }
}

val task = new SomeAsyncTask
task.execute()

これをビルドし、実行すると AbstractMethodError が発生し doInBackground メソッドがない と怒られます。 定義してるのにも関わらず。ビルドが通っているのにも関わらず、です。

原因は Scala と Javaジェネリクス(型パラメータ)を使った可変長引数のビルド結果が異なる ことです。 詳しいことはこちらのページが詳しいです。

解決方法として、次のようなクラスを定義してこちらを継承することで問題なく AsyncTask を使うことができます。

import android.os.AsyncTask

abstract class ScalaAsyncTask[Params,Progress,Result] extends AsyncTask[Params, Progress, Result] {

  // doInBackground では Scala で新たに定義した asyncAction メソッドに処理を委譲する
  final def doInBackground(params: Params*): Result = asyncAction(params: _*)

  // onProgressUpdate でも同様に Scala で新たに定義した onProgress に処理を委譲する
  override final def onProgressUpdate(progress: Progress*) = onProgress(progress: _*)

  // Scala で新たなメソッドを定義する
  // doInBackground/onProgressUpdate はオーバーライドせず、こちらをオーバーライドして使用する
  def asyncAction(params: Params*): Result
  def onProgress(progress: Progress*): Unit = {}

}

具体的には可変長引数の部分を型パラメータのままオーバーライドし、実際に使用するサブクラスで定義・オーバーライドするのは Scala で新たに定義したメソッドにすることでビルド結果の差をなくしています。

今回用意した g8 テンプレート には上記の ScalaAsyncTask を改良したクラスが含まれています。 また、以下のようにして単純な非同期処理であれば前述の Future 風に(とかいうと Scala ガチ勢からマサカリ飛んできそうですが……)簡単に書けるようにしてあります。

import android.widget.Toast
import com.example.helloworld.support._

val x = async {
  "Hello, %s".format("World")
}
x onSuccess { text =>
  Toast.makeText(context, text, Toast. LENGTH_SHORT).show()
}

最後に

現在, Android アプリ「電話帳R」を Scala で書き直しています。 最初に書いたとおり少し前まで Scala で Android アプリを作る=ネタ扱いでしたが、個人的な感触では意外に実用的(になった)なんじゃないかなーと思います。 なにより Scala は書いていて楽しいです。

2014年はもっと Scala が流行りますように! あと、この世から Gingerbread 搭載端末が駆逐されますように!!

参考URL一覧

AngularJS の $locationProvider.html5mode について

AngularJS で構築するアプリケーションで $locationProvider.html5Mode をどう設定するか小一時間悩んだ結果をまとめてみました。

導入

AngularJS では $routeProvider を使用してクライアントサイドでのルーティングが可能です。

angular.module('app').config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/aaa/', {controller: 'AaaController', templateUrl: 'aaa.html'})
    .when('/bbb/', {controller: 'BbbController', templateUrl: 'bbb.html'});
}]);

こんなカンジで指定することでURLに応じて Controller と View が変更されるようになります。便利だね!

ところで、このURLですが標準ではこんなカンジになります。

http://www.example.com/#/aaa/

これは Hashbang Mode と呼ばれ、ハッシュ(#)以降の部分をアプリケーションのパスとして利用するモードです。 ちなみに Hashbang とは "#!" のことで、一昔前に Ajax を多用したサイトでURLによく使われていました(なぜ多用され、現在廃れつつあるのかは "Hashbang URL" とかでググりましょう)。

AngularJS の標準では "!" つまりbang部分がないのでなんだかちょっと違和感があります。 ちなみに hashPrefix という設定をすることで正当な(?) Hashbang にすることもできます。

angular.module('app').config(['$locationProvider', function($locationProvider) {
  $locationProvider.hashPrefix('!');
}]);

本題

前述の Hashbang を利用したURLは現在では「あまり使うべきではない」と言われています。 また HTML5 で pushState という機能が採用され Hashbang を使わずともブラウザヒストリを記録しつつクライアントサイドで画面を書き換えることができるようになりました。

AngularJS では $locationProvider.html5Mode を変更することで Hashbang Mode から HTML5 Mode に変更することができます。

angular.module('app').config(['$locationProvider', function($locationProvider) {
  $locationProvider.html5Mode(true);
}]);

Hashbang Mode と HTML5 Mode それぞれでURLは次のようになります。

# Hashbang Mode
http://www.example.com/#/aaa/
# HTML5 Mode
http://www.example.com/aaa/

ただし pushState に対応していない IE9 などのブラウザもまだまだ現役です。 AngularJS ではそういった pushState 未対応ブラウザの場合は自動で Hashbang Mode にフォールバックしてくれます。超便利だね!

Hashbang Mode と HTML5 Mode の違い

先述の通り Hashbang を使用したURLは廃れつつあります。……が、HTML5 Mode も万能というわけではありません。

先ほどのように HTML5 Mode を使うと http://www.example.com/aaa/ の様にURLにハッシュが含まれなくなります。 その結果、Hashbang Mode と HTML5 Mode で次のような違いが現れてしまいます。

# Hashbang Mode
http://www.example.com/#/aaa/ -> WEBサーバー www.example.com の / にリクエストが飛ぶ
# HTML5 Mode
http://www.example.com/aaa/ -> WEBサーバー www.example.com の /aaa/ にリクエストが飛ぶ

つまり次の通り。

  • Hashbang Mode ではクライアントサイドでページを生成すればOK。
  • HTML5 Mode ではサーバー側で /aaa/ に対応するページを生成する必要がある。所謂 pjax にサーバー側で対応しなくてはならない。

場合によっては後者はとても面倒です。

Hashbang URL の問題点とは?

ところで Hashbang を使うべきではない理由とはなんでしょうか。参考サイトによればざっくり次のようなカンジです。

  • GoogleBot 以外の Hashbang に対応していないクローラー、ボットなどがコンテンツにアクセスできなくなる
  • ブラウザ外のキャッシュができなくなる
  • Microformats が使い難くなる
  • Facebook の Like widgets などが難しくなる
  • Referer にページレベルの情報が無くなる
  • JavaScript にエラーがあるとなにも表示されない
    • arrayの終りに,が残っているだけでIE利用者には真っ白な画面しかみえなくなる
    • 開発者が console.log を間違えて残してしまうとほとんどのユーザー(IE利用者)が真っ白な画面を見ることになる
    • サイト管理者のコントロール下にない広告のJavaScriptにエラーがあるとコンテンツが見れなくなる

結論:どちらのモードを使うべきか

先ほどの参考サイトにもほぼ同じ事が書いてありますが、次のように考えることができると思います。

  • 商品ページなどSEO等が重要になる所謂「コンテンツ」の場合は積極的に HTML5 Mode を採用するべき。
  • 管理画面や、フォームなどコンテンツというよりは「アプリケーション」と呼ぶべきページの場合は場合によって Hashbang Mode の採用を検討する。
    • ただし、一般に公開されるお問い合わせフォームなどで、外部の広告用スクリプトなどを利用する場合は細心の注意を払う(できるなら HTML5 Mode)を使う。

以上。

参考

VirtualBox + nginx で静的ファイルが正常に送信されないときは

Vagrant + Chef Solo を使って VirtualBox 上に開発環境を構築したけど、なにやら .js や .css などの静的ファイルが途中で途切れて正常に送信されてこない(受信できない)。

調べて見たところ、どうやら nginx が利用している sendfile という機能が VirtualBox と相性が悪いらしい。そんなわけで nginx.conf を次のように修正して再起動したところ、無事に静的ファイルが読めるようになった。

nginx.conf 【修正前】

sendfile on

nginx.conf 【修正後】

sendfile off

めでたしめでたし。

Vagrant を導入する

WEB+DB PRESS vol.75 でも紹介されていた Vagrant を遅ればせながら導入しました。 Vagrant の導入記事なんて巷に溢れていますが、記事が古かったりしてかなり振り回されたので自分でもまとめてみることにしました。

ポイント

  • gem を使わず公式のパッケージからインストールする
  • VirtualBox を 4.2.14 にアップグレードしてはいけない
    • 2013-07-09 追記:VirtualBox 4.2.16 がリリースされました(参考)。VirtualBox 4.2.16 + Vagrant 1.2.2 の組み合わせで正常に動作することを確認しました。

Vagrant とは?

Vagrant は仮想マシンの立ち上げを自動化するコマンドラインツールです。 元々は Oracle VirtualBox 向けのツールだったようですが、現在では VMWare でも使えるようになったみたいです。

インストール

公式のインストールパッケージを使いましょう。

2013年6月現在 Vagrant の最新バージョンは 1.2.2 ですが、導入記事によくある gem install vagrant で導入した場合古い 1.0.7 がインストールされてしまいました。

古いバージョンを使用すると、この後の手順でダウンロードする box ファイルに互換性がないため長時間待った挙げ句に失敗して何も残らないという悲惨な目に遭います(遭いました)。

また gem を使うために OS X + Homebrew 環境で ruby を導入したのですがその状態で gem install vagrant しても vagrant コマンドにパスが通らず、別途作業が必要になるなど非常に手間が掛かります。

box を追加する

Vagrant では新しく仮想マシンを作成する際、雛形となる box と呼ばれるものが必要となり、あらかじめローカルマシンに box が追加されていなくてはなりません。 これらの box は様々なOS・バージョン・構成のものが公開されており、その一覧も http://www.vagrantbox.es/ で見ることができます。

box を追加するためのコマンドは次の通り。box名は好きに指定できます。

$ vagrant box add <box名> <url>

今回は VirtualBox 向けの CentOS 6.4 x86_64 Minimal を導入してみます。 なお各種 box ファイルは小さいものでも約300MB程度あるので、なるべく高速な通信環境で行う事をオススメします。

$ vagrant box add centos-6.4-x86_64 http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130427.box
Downloading or copying the box...
Extracting box...te: 684k/s, Estimated time remaining: 0:00:01))
Successfully added box 'centos-64-x86_64' with provider 'virtualbox'!

$ vagrant box list
centos-64-x86_64 (virtualbox)

Vagrantfile を作成する

次のコマンドを実行するとカレントディレクトリに Vagrantfile という名前のファイルが作成されます。仮想マシンが作成されるわけではありません。

$ vagrant init centos-64-x86_64
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

Vagrantfile には「どんな仮想マシンを設定するのか」といった設定情報が Ruby で記述されています。 仮想マシンの作成や起動はこの Vagrantfile のあるディレクトリに移動してから実行することになります。

仮想マシンを起動する

仮想マシンを起動したい場合は Vagrantfile のあるディレクトリに移動してから次のコマンドを実行します。 初回に実行した場合のみ自動で env_1234567890 みたいな名前の仮想マシンが作成されます。

Vagrantfile を特に編集しない場合ネットワークアダプタは NAT に設定され、SSH接続を行うためにゲストの22番ポートからホストの2222番ポートへのフォワーディングが設定されます。

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'centos-64-x86_64'...
[default] Matching MAC address for NAT networking...
[default] Setting the name of the VM...
[default] Clearing any previously set forwarded ports...
[default] Creating shared folders metadata...
[default] Clearing any previously set network interfaces...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)
[default] Booting VM...
[default] Waiting for VM to boot. This can take a few minutes.
[default] VM booted and ready for use!
[default] Configuring and enabling network interfaces...
[default] Mounting shared folders...
[default] -- /vagrant

仮想マシンSSHで接続する

無事に仮想マシンが起動したら次のコマンドでSSH接続することができます。

デフォルトの状態では Vagrantfile を設置したディレクトリが VirtualBox の共有フォルダー機能を使用して /vagrant にマウントされています。Vagrantfile を編集することで共有フォルダを変更・追加することもできそうです。

$ vagrant ssh
Welcome to your Vagrant-built virtual machine.

[vagrant@localhost ~]$ pwd
/home/vagrant

[vagrant@localhost ~]$ ls -l /vagrant
合計 8
-rw-r--r-- 1 vagrant vagrant 4359  6月 27 09:01 2013 Vagrantfile

[vagrant@localhost ~]$ exit
logout
Connection to 127.0.0.1 closed.

仮想マシンを停止する

仮想マシンを停止する場合は次のいずれかのコマンドを実行します。

  • vagrant suspend …… 仮想マシンの状態を保存して終了します。
  • vagrant halt …… 仮想マシンをシャットダウンします。
  • vagrant destroy …… 仮想マシンを強制終了し、破棄(削除)します。

参考サイト