Technote

by sizuhiko

htmldayのイベントに参加してきた

htmldayとは、日本全国でWebに関する多数のイベントを同じ日(6/13)に開催することで、日本のWebを一層盛り上げようという「お祭り」です。(毎年6月の第2土曜日に開催しています。)

ということで、以下のイベントに参加してきました。

当日の講演資料

まとめサイト ができました。ありがたやー。

サービスワーカーが今後のフロントエンドを支える

Google I/Oからの流れもあると思うのですが、サービスワーカー(ServiceWorker)に関連する発表が3連続になったりと、これから取り組まないといけない技術だと思います。 SafariとIEのサポートがまだなのですが、IEに関してはEdgeがChrome相当なので、MSではそちらに期待し、Appleはそのうち対応してくれるのではないかと期待しています。

サービスワーカーとはブラウザにインストールできるタスクで、アプリケーションのバックグラウンドで動作するもので、例えばPush通知とかキャッシュとかオフラインモードの実装なんかをするのに、とても役立つ技術です。

サービスワーカーに関しての詳しい内容はググるとたくさん出てくるので、それらを参照してください。

気になるキーワード

発表の中で気になったキーワードと、関連URLを紹介します。

  • Cache API
    • サービスワーカーの紹介記事の中で「Service Worker のインストール」以降にキャッシュを使う例が出ています
  • Fetch API
  • Push API
  • Lovefield
    • Indexed Databaseに対してSQLライクにアクセスできるようにしたライブラリ、Googleが開発した
  • Babel
    • ES6のコードをES5形式にコンパイルしてクロスブラウザで動作できるようにする
    • gulpとの相性が良いので便利
  • VORLON.JS
    • ブラウザをリモートでバッグするツール。マイクロソフトが開発している
    • スマートフォンのエミュレータを使ったときDOMやJavaScriptのデバッグが難しいが、それをリモートデバッグできるようにする(昔も似た技術はあった気がする)
  • manifoldjs
    • HTML5アプリケーションを各プラットフォーム用のアプリケーションに変換するツール
    • Windowsストアアプリ、Androidアプリ、iOSアプリ、Chromeアプリ、FirefoxOSアプリに変換できる

Polymer1.0がキタ

そしてPolymerな訳です。私はPolymer大好きで0.5をけっこう弄っていたのですが、1.0は互換性ないということで、個人的な利用にとどめておいて良かったと思っています。

PolymerはWebコンポーネントを実装するための仕組みにフォーカスしているので、いわゆるMVCフレームワークとは違います。 今流行のReact.jsとはかぶる部分が多いと思っています。

Polymerが適用されている実例としては

  • Atavist
  • salesforce
  • vaadin
  • youtube

そして今回のGoogle I/Oサイトということだそうです。

で、Polymerを簡単に始めるためにはPolymer Starter Kitというものがあります。 これを使うと、ブラウザで簡単にPolymerを使ったサイトのディレクトリ構成やファイルの配置、gulpのビルドファイルなどを初期構築できます。 Yeoman的な役割ですね。

Googleが提唱するMaterial designをベースにPolymerでサイトを作ると、あらゆるデバイスに最適なGoogle I/Oのようなサイトを作れるので、サイトを例としてPolymerを使ってみるのが良さそうです。

Polymerの注意点としては、ShadowDOMの実装がアレということで、Polymer1.0ではShady DOMという疑似ShadowDOMを実装しています。ShadowDOMと思ってるとそうではないのです。

最後に

ServiceWorker、ES6、Polymerあたりの技術は今後重要になってくると思っているので、何か作りたいなーと思う収穫多きイベントでした。

あと懇親会が2度(イベントが午後の部と、夜の部にわかれていたので)あって、おいしいお酒とたくさんのビールに囲まれて幸せでした。 スポンサー様、協賛ありがとうございました!

htmlday 2015 のイベントTシャツをジャンケン大会、最後の最後、ほんとの最後の1枚でGETしました!ありがたやー。早速着てこのBlogを書いています。

CakeFest2015に参加してきた

今年はCakePHP生誕10周年ということもあり、かねてよりアニバーサリーCakeFestとも言われていました。

開催場所は世界一の都市ニューヨークです。 私個人でも初のニューヨークです。

今年の第一印象

初参加が多い。CakeFestでは毎年会う馴染みの人もいるのですが、今年は特に初参加が多かったようです。 参加者が多い。さすが10周年というべきか、ニューヨークなのでアメリカの人が多かったようです。

印象に残ったセッション

昨年までと同様に、CakePHPに関する主なセッションはコアチームから、その他Webに関するトピックをCFPで通ったスピーカーからという流れでした。 今年はコミュニティマネージャのJamesが急に来れなくなってしまい、イベントの仕切りに不安を覚えるスタートだったのですが、Larryが八面六臂の働きで素晴らしいCakeFestになりました。LarryがLTのタイムキーパーやったり、抽選会の司会やったり、それはそれは大活躍でした。

コアチームの中でも

についてはCakePHPを利用する人には一読を薦める内容です。

Mark Storyのセッション

とは言えMarkのスライドはいつもの通りお題しか書いていないので、少し詳細にふれておきます。

  • CakePHP3の開発期間3年はとても長かった
  • 周辺の変化
    • PHPのバージョンも変わったし、機能も変わった
      • 3年前のPHPは5.4.4(Released: 14 June 2012)あたりです。
      • 現在は5.6.9で、その間にはジェネレータや可変引数などが導入されました。
    • 他のフレームワークの流れも変わった
  • CakePHP3は2ヶ月で8万ダウンロードされた。すごいね。
  • 今後のバージョンの話(詳しくはgithubのロードマップを見てね)
    • PSR-7対応は3.2の予定
  • 大きく変わったところ
    • Mailers:メール送信がこれまでよりも簡単になる
    • CLI:出力フォーマットの機能が増える。例えばプログレスバーとか簡単に出せるようになる
    • ORM:Joseが明日話すけど、関連のロードとかマッチングクエリが書けるようになるよ
    • ElasticSearch:ORMと似た呼び出し方法が使えるようになる
  • 今後注力していくところ
    • プラグインのリリース
    • 3.x系の開発
      • これにより2系については3からのバックポートが中心となる
      • 3.x内での互換性は重視している
    • PHP7

Jose Lorenzo Rodriguezのセッション

Joseのセッションはコード多めで、解説だけではわかりづらいと思うのでサンプルコードが提供されています。

サンプルコード::github

スライドの最初に出てくる3つのタイプのORM

  • いらっとする
  • おもちゃみたいな
  • 流行の(通の)
  • すばらしい

もちろんCakePHP3のORMはすばらしいものだよ。という流れで掴みはOKな展開。ちなみにQ&AでDoctrineは?という質問に「Hipster」と即座に回答していました。 ちなみに日本だとAgileはアジャイルと発音するけど、Joseのセッションではずっとエイジールと聞こえていた。

スライドやサンプルを見てもらえると、CakePHP3のORMがかなり強力になっているのがわかると思います。 特に気になったのは

  • タイプヒント
  • カスタムファインダー
  • 集合検索
  • 多重階層の集合検索
  • 他のDBへのアソシエーション定義
  • バーチャルフィールド

あたりです。サンプルを使って実際に動かしてみるのが良いですね。まだ全部試せていないけど、CakePHP3もくもく会で少しずつ試せたら良いなーと思っています。

10周年

ノベルティが久々にたくさんあった。きっとシカゴ以来。

で、私はそのシカゴから参加して6回目の参加となりました。 最後の抽選会でシカゴに参加した人、というときに私含め3人しか立たなかった(コアチームは除く)。さらにそのうち1人はマリアーノです。

アニバーサリーということで最後の目玉商品は来年のCakeFest招待券!!。なんと交通費込み(上限あるけど)。 当選者は

奥山さん!!!、来年はVIP待遇ですなー。来年はドイツらしいですよ。

感謝

10周年ということで、ここ数年は安藤さんと私の2人だったのを、もっと多くのCakePHPユーザにCakeFestに参加して欲しいと思い、様々なところでくどいぐらい誘いました(ご迷惑だった皆様すみません)。結果、私含め5人が日本から参加ということで、とても楽しかったです。

安藤さんの紹介で、pivotal lab.にも訪問できたし、ピザ屋さんも(すごい)美味しかったです。

また、日本でCakeRadioGaGaの中継を受け取ってくれたCo-Edoの田中さん、中継を見てくれた皆様、ありがとうございました。 別途報告会(もくもく会の中で?)、できたら良いなーと思っています。

そのほかGoogle社の前で写真撮ったり、チェルシーマーケットでおみやげ買ったり、MOMA行ったり、ちゃんとニューヨークも楽しむことができました。

最後に、CakeRadioGaGaの中継動画を。そういえばMarkとJoseに今年もメッセージちょうだいと言っていたのに、すっかり忘れたのは内緒です…

CakePHP3 のアプリケーションを Behat でテストする

CakePHP3の変更点として大きく取り上げられるのが、モデル層の変更でしょう。 しかしそれ以上に私たちが受けられる恩恵で大きいのが、PSR-2の採択です。

CakePHP3 is fully adopt PSR-2

例えばCakePHP2で単体テストを実行するときは、以下のようにcakeコマンドを使って実行する必要がありました。

Console/cake test app Model/Articles

cakeコマンド内でPHPUnitへの依存関係を解決し、PHPUnitからCakePHPのクラスが参照可能になるように作られていました。

ところがCakePHP3からは、以下のようにphpunitコマンドを使って実行します。

vendor/bin/phpunit

PSR-0のオートロードに対応したことで、PHPUnitからCakePHP3のクラスが参照可能になるのです。

はじめてみよう

同様の理由で、CakePHP2のアプリケーションをBehatでテストしたい場合は、私が作成したBdd Pluginを使ってBehatのステップ記述からCakePHPのクラスを参照可能になるようにしていました。

しかしBehatにおいても直接実行したステップ定義から、CakePHP3のクラスが参照可能になるのです。

CakePHP3のアプリケーションをどのようにBehatからアクセスするのか、CakePHP3のブログチュートリアルを例にサンプルアプリを作成しました。

cakephp3-bdd-example

サンプルアプリケーションは以前記事にもしたCakeboxを使って構築しました。 またサンプルアプリケーションの実行にもCakeboxを使うと簡単に実行環境を構築することができます。

サンプルアプリケーションのGithubページに書いてあるとおりの手順で進むことができます。 本ブログでは日本語で補足します。

必要なアプリケーションのインストール

以下のアプリケーションをホストOSにインストールします。

  • VirtualBox
  • Vagrant
  • Cakebox

詳しくはCakebox を使ってCakePHP3アプリケーションを作ってみようの記事を参照してください。

サンプルアプリケーションのインストール

CakeboxのゲストOSにログインして、cakeboxコマンドでアプリケーションをインストールします。

localhost:cakebox $ vagrant ssh
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-24-generic x86_64)

vagrant@cakebox $ cakebox application add blog-tutorial.app --source https://github.com/sizuhiko/cakephp3-bdd-example.git --webroot blog-tutorial.app

すると、以下のように表示されます。

Creating application http://blog-tutorial.app

Configuring installer
Creating installation directory
Git installing user specified application sources
Creating virtual host
* Successfully created PHP-FPM virtual host
Creating databases
* Successfully created main database
* Successfully created test database
Configuring permissions
Updating configuration files
Application created using:
  database => blog-tutorial_app
  framework_human => user specified
  framework_short => custom
  installation_method => git
  path => /home/vagrant/Apps/blog-tutorial.app
  source => https://github.com/sizuhiko/cakephp3-bdd-example.git
  url => blog-tutorial.app
  webroot => blog-tutorial.app
Please note:
  => Configuration files are not automatically updated for user specified applications.
  => Make sure to manually update your database credentials, plugins, etc.

Remember to update your hosts file with: 10.33.10.10 http://blog-tutorial.app

Installation completed successfully

新規アプリケーションの構築と同じように、データベースやNginxの設定ファイルも生成してくれるので、すぐにアプリケーションを実行できる環境が整います。

あとはアプリケーションのルートディレクトリに移動して、不足しているディレクトリを作ってcomposerでライブラリをインストールします。

vagrant@cakebox $ cd Apps/blog-tutorial.app
vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir tmp 
vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir logs
vagrant@cakebox:~/Apps/blog-tutorial.app$ cp config/app.default.php config/app.php
vagrant@cakebox:~/Apps/blog-tutorial.app$ composer install 

サンプルアプリケーションの環境設定

データベース接続設定の変更

config/app.phpのデータベース接続設定をCakeboxで生成された内容に変更します。 以下のとおりusernamedatabaseの部分のみ変更します(それ以外はそのまま)。

    'Datasources' => [
        'default' => [
            // 省略
            'username' => 'cakebox',
            'database' => 'blog-tutorial_app',
            // 省略
        ],
        'test' => [
            // 省略
            'username' => 'cakebox',
            'database' => 'test_blog-tutorial_app',
            // 省略
        ],
ホストOSのhostsファイルの変更

ホストOSのhostsファイルに指示されたように 10.33.10.10 blog-tutorial.app の行を追加します。

Cakebox環境のチューニング

Cakeboxのデフォルト設定ではBehatを使ってアプリケーションをテストしようとすると、いくつか動かない箇所があったので、設定値をチューニングします。 まずボックスファイルのメモリを2048Mにアップします(デフォルトは1024M)。 次にxdebug.iniのxdebug.maxnestinglevelの値を調整します。READMEでは500を設定しています。もう少し値は小さくても大丈夫かもしれないですが、とりあえず500あれば大丈夫です。

具体的な設定例は、githubのREADMEを参照してください。

Webサーバの設定

Behatからアプリケーションをテストするときは、ブラウザから通常操作するのと同じようにWebサーバを通過します。 そのため、アプリケーションが通常操作としてアクセスされたのか、Behatのテストでアクセスされたのかを識別して環境を切り替えてあげないと、データベースのデータがテストによって変更するので、通常操作のデータが失われてしまいます。

このあたりの話(理由や手法)は、過去に何度か記事にしていたり、書籍CakePHPで学ぶ継続的インテグレーションでも詳しく解説していますので、CakePHP2の内容ですが、一度手に取ってみてください。

で、このサンプルはnginx用の設定ファイルをblog-tutorial.app.testというファイルで用意しておいたので、これをCakeboxのnginxの設定ディレクトリにコピーして再起動するだけで大丈夫です。

環境切り替え用にblog-tutorial.app.testというホスト名でアクセスされたら、nginxで環境変数CAKE_ENVtestという文字列を設定するようにしています。 CakePHP3のアプリケーションではconfig/bootstrap.phpで環境変数の設定値を見てDBの接続先がtestになるように設定します。

if (getenv('CAKE_ENV') === 'test') {
    ConnectionManager::alias('test', 'default');
}

CakePHP3ではConnectionManagerのaliasという機能でdefaultへ接続しようとしたときに、実際はtestの接続内容を参照するように設定することができるので、この機能を利用し、間違ってdefaultのテーブルが書き変わらないようにしています。

より詳しい手順や、設定ファイルの内容はgithubのREADMEや設定ファイルを参照してください。

Behatから参照可能なホスト名としてblog-tutorial.app.testをCakeboxのVM側の/etc/hostsに追加します。

データベースのマイグレーション

データベースの生成はマイグレーションコマンドで一発です。

bin/cake migrations migrate

Behatのテストを実行する

ここまで設定できれば、後はテストを実行するだけです。

vagrant@cakebox:~/Apps/blog-tutorial.app$ vendor/bin/behat

おそらくすべてグリーンで成功するはずです。 うまくいかなかったら、お気軽にgithubのissueに日本語で書いてください。

どうやったのか?

まずCakePHP3で最初に注目したのは、単体テストがPHPUnitのコマンドから実行できるようになっていたことです。 これは過去に外部の様々なツールやアプリケーションとCakePHPを結合するときに一番悩んでいたところでした。

PHPUnitからCakePHP3にどのように連動しているのか?を調べることから始めました。 PHPUnitは実行すると、カレントディレクトリのphpunit.xml(もしくはphpunit.xml.dist)を参照します。

PHPUnitがCakePHP3を呼び出す仕組みを知る

CakePHP3ではアプリケーションスケルトンを生成すると、ルートディレクトリにphpunit.xml.distが生成されます。

<!-- phpunit.xml.dist -->
<phpunit
    colors="true"
    processIsolation="false"
    stopOnFailure="false"
    syntaxCheck="false"
    bootstrap="./tests/bootstrap.php"  // (1)
    >
    <php>
        <ini name="memory_limit" value="-1"/>
        <ini name="apc.enable_cli" value="1"/>
    </php>

    <!-- Add any additional test suites you want to run here -->
    <testsuites>
        <testsuite name="App Test Suite">
            <directory>./tests/TestCase</directory>
        </testsuite>
        <!-- Add plugin test suites here. -->
    </testsuites>

    <!-- Setup a listener for fixtures (2) -->
    <listeners>
        <listener
        class="\Cake\TestSuite\Fixture\FixtureInjector"
        file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
            <arguments>
                <object class="\Cake\TestSuite\Fixture\FixtureManager" />
            </arguments>
        </listener>
    </listeners>
</phpunit>

このファイルを読むと、2つ重要な箇所があるのに気がつきます。

まず(1)の bootstrap="./tests/bootstrap.php" という部分。 bootstrap属性にはPHPUnitが実行されるとき呼び出されるPHPコードを指定することができます。 ここからCakePHP3アプリケーションをテスト用にロードする場合、このファイルを呼び出せば外部ツールからCakePHP3が操作できるようになることがわかります。

実はこのファイルを実際に見てみると、以下の1行しかありません。

require dirname(__DIR__) . '/config/bootstrap.php';

テストとは関係なく、アプリケーションのconfig/bootstrap.phpをロードしています。 おそらく将来テストに関する何か差分が必要になったときに、テスト側にだけ変更が発生すると思うので、テスト用にCakePHP3をロードする場合はtests/bootstrap.phpをロードしておいた方が良いでしょう。

次に(2)のリスナー設定です。 PHPUnitのリスナーはPHPUnitのフックポイントでコールバックされる処理を記述できるクラスです。 CakePHP3ではフィクスチャ(DBのテストテーブルとデータを準備する仕組み)を投入するのに利用しています。 以下のようにFixtureInjectorクラスのstartTestとendTestでテストケース開始/終了ごとにフィクスチャのロードとアンロードが対応するようになっています。

class FixtureInjector implements PHPUnit_Framework_TestListener
{

    /**
     * Adds fixtures to a test case when it starts.
     *
     * @param \PHPUnit_Framework_Test $test The test case
     * @return void
     */
    public function startTest(PHPUnit_Framework_Test $test)
    {
        $test->fixtureManager = $this->_fixtureManager;
        if ($test instanceof TestCase) {
            $this->_fixtureManager->fixturize($test);
            $this->_fixtureManager->load($test);
        }
    }

    /**
     * Unloads fixtures from the test case.
     *
     * @param \PHPUnit_Framework_Test $test The test case
     * @param float $time current time
     * @return void
     */
    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        if ($test instanceof TestCase) {
            $this->_fixtureManager->unload($test);
        }
    }

}

BehatからCakePHP3を呼び出す仕組みに流用する

ここまでの内容が外部ツールからCakePHP3のアプリケーションをテストするのに重要な部分です。 PHPUnitがCakePHP3を呼び出すのと同じようにする仕組みをBehatのFeatureContextクラスに用意します。

features/bootstrap/FeatureContext.phpというBehatが読み込むファイルに記述します。 CakePHP2とBDDプラグインによるインテグレーションではBehatのバージョンが2系でしたが、CakePHP3との連携では最新の3系を利用しています。 Behat3からはBehat1系、2系で利用していたファイル構成と異なっています。従来、support/bootstrap.phpsupport/hooks.phpあたりに書いていたコードはすべてContextクラス内に記述することになります。

Behat3からはFeatureContextにブートストラップ記述を、それ以外のコンテキストは用途に応じて別のコンテキストクラスに分割する方がスマートに記述できそうです。 Behat2では複数のコンテキストクラスを使う場合、FeatureContextでインクルードしないといけなかったのですが、Behat3ではbehat.yml上で記述できるのでより簡単になっています。

class FeatureContext implements Context, SnippetAcceptingContext
{
    public function __construct()
    {
        require_once dirname(dirname(__DIR__)) . '/tests/bootstrap.php'; // (1)

        // Always connect test database
        ConnectionManager::alias('test', 'default'); // (2)

        Fabricate::config(function($config) { // (3)
            $config->adaptor = new CakeFabricateAdaptor([
                CakeFabricateAdaptor::OPTION_FILTER_KEY => true,
                CakeFabricateAdaptor::OPTION_VALIDATE   => false
            ]);
        });

        $this->fixtureInjector = new FixtureInjector(new FixtureManager()); //(4)
        $this->fixture = new BddAllFixture();
    }
}
  • (1)は、phpunit.xmlのbootstrapと同様にCakePHP3のtests/bootstrap.phpを呼び出します。
  • (2)は、Behatのステップ定義からテストデータを投入するときに、testの接続設定を参照するようにエイリアスを設定します。
  • (3)は、テストデータジェネレータFabricateの初期設定です。FabricateもCakePHP3対応されています。
  • (4)は、phpunit.xmlのリスナー部分を模して、Behatのシナリオ毎にフィクスチャが動くようにFixtureInjectorのインスタンスを生成しています。

BehatからCakePHP3のフィクスチャを利用する

(4)で書いたとおり、FixtureInjectorのインスタンスを生成したので、Behatのフックポイントを使ってシナリオ開始時にフィクスチャをロードし、シナリオ終了時にフィクスチャをアンロードするようにします。

    /** @BeforeScenario */
     public function beforeScenario(BeforeScenarioScope $scope)
     {
        $this->fixtureInjector->startTest($this->fixture);
     }

     /** @AfterScenario */
     public function afterScenario(AfterScenarioScope $scope)
     {
        $this->fixtureInjector->endTest($this->fixture, time());
     }

実際にフィクスチャを利用するためには、$this->fixtureのクラスがCakePHP3のTestCaseでなければならないので、$fixturesという利用するフィクスチャファイルの配列を定義しただけのクラスを用意してFixtureInjectorに渡すようにします。

class BddAllFixture extends TestCase {
    public $fixtures = [
        'Categories' => 'app.categories',
        'Articles'   => 'app.articles',
        'Users'      => 'app.users',
        'Categories' => 'app.categories'
    ];
}

このあたりの話も、書籍CakePHPで学ぶ継続的インテグレーションでも詳しく解説していますので、CakePHP2の内容ですが、一度手に取ってみてください。 CakePHP3になって、メソッドやクラスが一部変わりましたが、BehatとCakePHPをインテグレーションするためにおさえておかないといけないポイントはほとんど変わっていません。

後はBehat3のドキュメント、CakePHP3のドキュメントを見ながら進めていくと、エンド to エンドのテストが容易に記述できるようになります。

さいごに

GithubのREADMEに書いた内容をすべて日本語にした訳ではないのですが、要所をかいつまんで重要な部分を解説しました。 より詳しい内容などはREADMEを見ていただければと思います。

また、BDDプラグインのサンプルアプリにはあった、日本語のシナリオや、JavaScriptを使ったテストなど、Behat3になって大きく変わってはいませんが、サンプルアプリケーションに少しずつ載せられたらなぁと思っています。 何かうまく動かないなどあれば、気軽にGithubのissueに投稿お願いします(日本語でOKです)。

AngularJSでngDialog中の値をngModelでバインドしたいとき注意すること

AngularJS でモーダルダイアログを表示するために、何を使うでしょうか? 多くの場合 ngDialog というコンポーネントを使うのではないかと思います。

で、ダイアログ上の値は、それを表示したコントローラのスコープにバインドする、という良くあるシナリオを想定してください。

まずうまく動作するサンプルを紹介します。

Open Dialogというリンクをクリックして、ダイアログを表示したら、チェックボックスをON/OFFしてください。 ダイアログ背景のページで check: truecheck: false がトグルするはずです。

<div data-ng-app="myApplication">
    <div data-ng-controller="MainController">
        <a href="" ng-click="ShowNgDialog()">Open Dialog</a>
        <span>check: {{FormData.allcheck}}</span>
    </div>
</div>
var myApplication = angular.module('myApplication', ['ngDialog']);

myApplication.controller('MainController', function ($scope, ngDialog) {
    $scope.FormData={allcheck: false};
    $scope.ShowNgDialog = function () {
        ngDialog.open({            
            template: '<div><input type="checkbox" ng-model="FormData.allcheck"/></div>',
            plain: true,
            scope:$scope

        });
    }    
});

とても簡単な例ですが、AngularJSを使ってモーダルダイアログを表示して、チェックボックスの値をコントローラのスコープ変数 FormData.allcheck にバインドしています。

なぜか変数だとバインドされない

一方で、こちらは動作しないサンプルです。

Open Dialogというリンクをクリックして、ダイアログを表示したら、チェックボックスをON/OFFしてください。 ダイアログ背景のページは check: false のままです。

<div data-ng-app="myApplication">
    <div data-ng-controller="MainController">
        <a href="" ng-click="ShowNgDialog()">Open Dialog</a>
        <span>check: {{allcheck}}</span>
    </div>
</div>
var myApplication = angular.module('myApplication', ['ngDialog']);

myApplication.controller('MainController', function ($scope, ngDialog) {
    $scope.allcheck = false;
    $scope.ShowNgDialog = function () {
        ngDialog.open({            
            template: '<div><input type="checkbox" ng-model="allcheck"/></div>',
            plain: true,
            scope:$scope

        });
    }    
});

変わったのは、コントローラのスコープ変数にバインドするオブジェクトです。

うまく動作するのは $scope.FormData={allcheck: false}; のようにスコープのプロパティはオブジェクトで、オブジェクトに値を保持しているケースです。 一方うまく動作しないのは $scope.allcheck = false; のようにスコープのプロパティに変数で値を保持しているケースです。

ngDialogでなければ変数でバインドできる

ngDialogでなく普通に表示される範囲にある場合は、動作するサンプルです。

チェックボックスをON/OFFしてください。ページで check: truecheck: false がトグルするはずです。

<div data-ng-app="myApplication">
    <div data-ng-controller="MainController">
        <div><input type="checkbox" ng-model="allcheck"/></div>
        <span>check: {{allcheck}}</span>
    </div>
</div>
var myApplication = angular.module('myApplication', ['ngDialog']);

myApplication.controller('MainController', function ($scope, ngDialog) {
    $scope.allcheck = false;
});

まとめ

ngDialogを使うときのちょっとした小ネタなのですが、解決策を見つけるまで結構時間がかかりました。 もし、ngDialogを使ってうまくデータバィンディングできない!という人の参考になればと思います。

gulp-rev-replace を使ってリビジョン管理をするときに注意したいこと

JavaScriptでモダンな開発をするとき、gulpというビルドシステムを使うと、簡単にビルド過程を自動化できます。 さらに、JavaScriptでアプリケーションを作ったとき、ライブラリはCDNなどから取得するとして、自分で作ったスクリプトファイルは1つのファイルにまとめてミニファイズする、ということをgulpのタスクで書くでしょう。 一般的にJavaScriptやCSSをHTML上に記述するとき、以下のようにします。

<script type="text/javascript" src="/js/app.js"></script>
<link rel="stylesheet" href="/css/app.css" type="text/css">

リリースしたのにファイルの変更が読み込まれない

良くあるシーンとして、JavaScriptファイルやCSSファイルを差し替えたのに、変更がブラウザに反映されない、というケースです。 これはブラウザのキャッシュが有効になっていて、JavaScriptやCSSのファイルをWebサーバへ取得しに行かないために発生します。

そこで、この課題に対応するため、以下のどちらかの方法を採用すると思います。

  • ファイル名のGETパラメータに、乱数を付加して /js/app.js?_リビジョン番号 のようにする
  • ファイル名にリビジョン番号を入れて /js/app-リビジョン番号.js のようにする

こうすると、リビジョンが変更になった(リリースした)ときにファイルが必ず読み込まれるようになります。

GETパラメータの付加は推奨されない

前記の対応のうち、GETパラメータにリビジョン番号を追加する方法はあまり推奨されません(参照:High Performance Web Sites)。 この方法は、ブラウザやWebサーバがキャッシュを利用しないため、サイトの負荷につながります。

もちろん毎回リクエストが来ても問題ないサイトや、利用者が想定されていれば問題ないかもしれないですが、利用できるのであればキャッシュが有効になっていてページが速く表示できた方が良いことはいうまでもありません。

gulp-revを使ってGETパラメータにリビジョン番号を入れたい場合は、gulp-rev-appendを使うとクエリ文字列としてハッシュ値を入れられるようになります。

ビルド時にファイル名を変更する

推奨される方法は /js/app-リビジョン番号.js のように、ファイル名を変更することです。 gulpのタスク上に、gulp-revのREADMEに書いてあるとおりの方法で対応します。

var gulp = require('gulp');
var rev = require('gulp-rev');

gulp.task('default', function () {
    // by default, gulp would pick `assets/css` as the base, 
    // so we need to set it explicitly: 
    return gulp.src(['assets/css/*.css', 'assets/js/*.js'], {base: 'assets'})
        .pipe(gulp.dest('build/assets'))  // copy original assets to build dir 
        .pipe(rev())
        .pipe(gulp.dest('build/assets'))  // write rev'd assets to build dir 
        .pipe(rev.manifest())
        .pipe(gulp.dest('build/assets')); // write manifest to build dir 
});

プロジェクトによっては、このように単純な構成ではなく、複数のストリームを使ってビルドすることもあるでしょう。

gulp.task('build_js', function () {
    return gulp.src('src/*.js')
        .pipe(sourcemaps.init())
        .pipe(concat({path: 'bundle.js', cwd: ''}))
        .pipe(rev())
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest('dist'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('dist'));

たとえばjsとcssを分けてビルドしなくてはいけないようなケースですね。 元ファイル名と、置き換えられたファイル名のマッピングを出力するために、rev.manifest()というAPIを呼び出します。 出力先は、そのあとのdestAPI呼び出しで指定します。ファイル名は省略時には manifest.json というファイル名になります。

{
    "app.css": "app-098f6bcd.css",
    "app.js": "app-273c2cin.js"
}

ファイル名の変更を反映する

HTMLファイルのjsやcssのファイル名を書き換えるのに使うのが、gulp-rev-replaceです。 マニフェストファイルを入力として、HTMLファイルのビルド(コピー)過程で差し込むことができるようになっています。

gulp.task("revreplace", ["revision"], function(){
  var manifest = gulp.src("./" + opt.distFolder + "/rev-manifest.json");

  return gulp.src(opt.distFolder + "/index.html")
    .pipe(revReplace({manifest: manifest}))
    .pipe(gulp.dest(opt.distFolder));
});

revReplace() というAPIを使って、指定したマニフェストの内容と一致する部分を置換します。

<script type="text/javascript" src="/js/app-273c2cin.js"></script>
<link rel="stylesheet" href="/css/app-098f6bcd.css" type="text/css">

とても便利、でも…

私が遭遇したケースで説明しましょう。 ビルド済みファイル名が domain.js というファイル名でそれにリビジョン番号を追加する必要がありました。 さらにドメイン名のチェック用に、is-valid-domain.jsというライブラリも読み込んでいました。

<script type="text/javascript" src="/lib/is-valid-domain.js"></script>
<script type="text/javascript" src="/js/domain.js"></script>

ここでビルドしたところ

<script type="text/javascript" src="/lib/is-valid-domain-リビジョン番号.js"></script>
<script type="text/javascript" src="/js/domain-リビジョン番号.js"></script>

のようになってしまいました。domain.js だけでなく、is-valid-domain.js も変わってしまいます。 なぜこうなるか、ソースを見てみました。

renames.forEach(function replaceOnce(rename) {
  contents = contents.split(rename.unreved).join(rename.reved);
  if (options.prefix) {
    contents = contents.split('/' + options.prefix).join(options.prefix + '/');
  }
});

まぁですよね。ファイルを読み込んで domain.js に一致するところで分割、domain-リビジョン番号.js を追加して繰り返す、という実装です。 ファイルのどこに入っているか厳密に識別するのは困難(正規表現を使えばできなくはないかもしれないけど)です。

で、このようなケースにならなそうなら、そのまま gulp-rev-replace を使ってもらえば問題ないと思います。 ライブラリの挙動がわかっていれば利用するのも安心ですね。

私は gulp-template を使いました

で、私は gulp-rev-replace 使うのやめました。

ちょっと予期しない動作をするのは怖かったので、リビジョン番号が入って欲しいところを明示するようにしたかったのです。 そこで使ったのが gulp-template です。

<h1>Hello <%= name %></h1>

<%= => で囲んだ部分に値を差し込むことができるので、以下のように記述します。

<script type="text/javascript" src="/lib/is-valid-domain.js"></script>
<script type="text/javascript" src="/js/<%= data['domain.js'] %>"></script>

そこにマニフェストJSONをfs-extraで読み込んで、templateAPIに流し込むようにします。 そのままだと、変換前JSファイル名が変数名になって取り出しずらいので、variableオプションを指定してdataという変数名にバインドするようにします。

var gulp = require('gulp');
var template = require('gulp-template');
var fs = require('fs-extra');

gulp.task('build_html', function () {
    var manifest = fs.readJsonSync('./' + opt.distFolder + '/rev-manifest.json', {throws: false})
    return gulp.src('src/*.html')
        .pipe(template(manifest, {variable: 'data'}))
        .pipe(gulp.dest('dist'));
});

まとめ

静的ファイルのリビジョン管理って結構面倒なんですが、gulp使うと便利なライブラリあって簡単に実装できます。 今回は私が遭遇した特殊なケースかもしれないので、そのままgulp-revだけで完結できることも多々あるでしょう。 gulp-revや、その関連ライブラリには便利な機能がまだあるので、一度使ってみてください。