アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Onsen UIが生まれたきっかけ



本記事はOnsen UI Advent Calendar 2016のエントリーです。Onsen UIが生まれたきっかけについて、簡単に紹介したいと思います。

当時PhoneGapといわれていたCordovaに足りなかったもの



2009年頃からWebViewを用いた、今でいうHTML5ハイブリッドアプリ開発をしていた訳ですが、当時のデバイスはパフォーマンスも低く、ブラウザーの機能が貧弱だったことも相まって、満足のいくアプリをHTML5「だけ」で作ることがほぼ不可能と言ってもいい状況でした。

思い返すとiOS 4Android 1.6の時代です。まだposition:fixedも使えず、CSSトランジションも怪しい動きをしていました。

一方で、コンテンツをHTML5で記述することのメリットは多く、アプリのマルチプラットフォーム対応や、サーバーからの動的配信で内容をアップデートすることができるなど、この分野の可能性を感じていました。

それがPhoneGap/Cordova/Monacaにつながっていくわけですが、とはいえUIWebViewだけでは、iOSAndroidのネイティブUIにかなう表現ができなかったのは事実です。

そこでネイティブUIフレームワーク



そこで2010年からMonacaを開発するに当たり、どのようにハイブリッドアプリでUI部分でブレークスルーを実現するかが特に大きな課題だと考えていました。その結果、私たちのチームが開発したのは「ネイティブUIとUIWebViewを組み合わせる」というアプローチです。

具体的には、コンテンツの中身はUIWebViewで記述しつつも、ナビゲーターやタブといったネイティブなトランジションが圧倒的に有利な部分はネイティブで描画する、という仕組みでした。

PhoneGapの拡張エンジンとして、オープンソースでリリースしました(メンテナンスはしていませんが、いまでも公開は続けています)。



このフレームワークでは、複数ページをスムーズに遷移するため、マルチWebView構成になっていました。ようするに、次のページに遷移した場合には別のWebViewが作られ、ページスタックが作られます。

ネイティブUIフレームワークの課題からOnsen UIへ



ただしこの仕組みには一つ大きな問題がありました。それは、WebViewが複数に分かれるため、JavaScriptでスコープを管理するのが大変難しくなったということでした。新しいページが全く別のJavaScriptインスタンスとして表示されるため、同じアプリであるにもかかわらず、ページ間でリソースを共有することが難しかったのです。

また、JSON形式でUI定義を記述できる仕組みとしていましたが、その結果デザインのカスタマイズに弱く、多様な表現をしたいアプリにとって制約になってしまうことも問題でした。

一方、iOS 7やAndroid 4が登場しフラットデザインが主流になるなか、WebViewでフルにUIを実装することの現実味が帯びてきました。そこで、社内で議論を行った末、これまで作っていたネイティブUIフレームワークを捨て、Onsen UIに移行するという決断を行いました。

これからのOnsen UI



最初はAngular 1のディレクティブ機能を使って実装したOnsen UIですが、当初は日本語の文献も全くなく、Angularは難しすぎるのではないか?という危惧もありました。しかし一方で、Angular以上にうまく部品をコンポーネント化できる仕組みがなく、Angularを選定したという経緯があります。

昨今ではCustom Elementsも安定したことから、Onsen UI 2.0よりAngularへの依存をなくし、ピュアなWeb Componentsフレームワークとなりました。そのメリットを生かし、Angular 2やReactへの対応とともに、Vue 2といったメジャーなフレームワークのサポートを進めていきたいと考えています。

ぜひ進化するOnsen UIにご期待ください。そして、日本発の世界的フレームワークの挑戦に向けて、GitHubのスターで応援ください!

MA(マッシュアップアワード)11でMonacaを使おう

こんにちは、アシアルの岡本です

MA(マッシュアップアワード)11に参加される方向けにMonacaの情報をお伝えいたします。
この記事は随時更新してまいります。

Monacaとは



Monacaスマホのアプリを開発するためのサービスです。HTML5JavaScriptといったWeb技術を応用してアプリを作成でき、各プラットフォーム向けにアプリをビルドすることでマーケットにリリースすることが可能です。

開発環境がクラウド上に用意されるためハッカソンのような短期間でチームを組んでアプリを作り上げる時に大変便利です。Goldプラン以上ならプロジェクトを共有することもできるためコンテストに向けてリモートでチーム開発を進めていく場合にもお勧めです。

テンプレート機能の使い方



Monacaアプリ開発をプロジェクトという単位で行います。そして、プロジェクトを立てる時のひな型になるのが『テンプレート』です。テンプレートは新規でプロジェクトを立てる時に選択することが出来ます

テンプレートはRSSリーダーブロック崩しのような具体的なアプリになっているものから、SNS連携機能だけを用意したものなど大小様々に用意されています。まっさらな状態から始めたいときは『最小限のテンプレート』を選択してください


サンプルアプリのインポート



Monacaにはプロジェクトをインポートしたりエクスポートする機能があります。インポートはプロジェクトを新規で立ち上げる際に選択が可能です。テンプレートを選択するタイミングで『Import Project』というボタンが画面右上にでてきますので、そちらを選択してください。




インポートできるプロジェクトの例


Monaca公式ガイドブックで紹介されているアプリをインポートすることが出来ます。


https://ja.monaca.io/book/support/



アプリの詳細について知りたい方はこの機会にぜひ書籍をお求めください。
クラウドでできるHTML5ハイブリッドアプリ開発

他のAPIとの連携もバッチリ


例えばニフティクラウドmobile backend と組み合わせればプッシュ通知を簡単に実装できます。また、会員管
理・認証、データストア機能なども充実しており、迅速なアプリ開発が行えます。

ニフティクラウドmobile backend

チュートリアルにてMonaca用のアプリのサンプルも用意されています。

見た目の良いアプリを作るコツ


HTML5技術で見た目の良いアプリを作るときにはUIフレームワークの利用がお勧めです。
MonacaではOnsen UIの利用を推奨しております。

HTML5モバイルアプリをもっと速く、もっと美しく | Onsen

PHPで日付時刻の処理を書くなら Carbon がおすすめ



どうも、筋トレにハマっているたきゃはしです。
本日はPHPにおける日付時刻のライブラリについて書いていきたいと思います。

突然ですが、日付や時刻が絡む処理って意外とやっかいだと思いませんか?おそらく皆さんもいくつか思い当たるフシがあるかと思いますが、そんなやっかい事も Carbon(カーボン)を使えば解消できるかもしれません!

Carbon - A simple PHP API extension for DateTime.

Carbon とはPHPのDateTimeクラスを継承して拡張された日時操作ライブラリです。

「Carbonってどうなの?流行ってるの?」という方向けに予め補足致します。
Carbonはすでに人気のフレームワークに統合されていたり、GitHubで☆3000に迫る評価もありますのでDateTimeライブラリとしてデファクトになるんじゃないかと予想できます。

ではインストールからはじめます。



composer require nesbot/carbon

Composerでインストールすれば



<?php
require 'vendor/autoload.php';

use Carbon\Carbon;

echo Carbon::now(); // 2015-05-13 10:14:28(現在の日時)

Carbon::setTestNow(Carbon::create(2015, 5, 9, 15, 0, 0));
echo Carbon::now(); // 2015-05-09 15:00:00
echo Carbon::today()->toDateString(); // 2015-05-09

Carbon::setTestNow();
$carbon = Carbon::create(1980, 4, 15, 10, 20, 30);
echo $carbon->year;        // 1980
echo $carbon->age;         // 35

echo Carbon::today()->subYears(13)->addMonths(2)->subDays(10)->toDateString(); // 2002-07-02

Carbonはすぐに使えます。

基本的にCarbonのメソッドはインスタンスを返します。なのでメソッドチェーンによる直感的な表現が可能です。またインスタンスを文字列型へとキャストするとデフォルトフォーマットに変換してくれます。

もちろんフォーマットを指定することも可能です。



<?php
require 'vendor/autoload.php';

use Carbon\Carbon;

$carbon = Carbon::create(1980, 4, 15, 10, 20, 30);
echo $carbon; // 1980-04-15 10:20:30
echo $carbon->format('Y年m月d日'); // 1980年04月15日

$carbon->setToStringFormat('Y/m/d H:i:s'); // デフォルトフォーマットを変更
echo $carbon; // 1980/04/15 10:20:30

$carbon->resetToStringFormat();
echo $carbon; // 1980-04-15 10:20:30


フォーマットの書式は date() と同じです。
- http://php.net/manual/ja/function.date.php

またCarbonではインスタンスを生成するための方法がいくつか準備されています。



<?php

use Carbon\Carbon;

class CarbonTest extends PHPUnit_Framework_TestCase
{

    /**
     * @test
     */
    public function instance()
    {
        $dt = new \DateTime();
        $carbon = Carbon::instance($dt);
        $carbon2 = Carbon::instance($dt);
        $this->assertTrue($carbon2 instanceof Carbon);
        $this->assertTrue($carbon !== $carbon2);
    }

    /**
     * @test
     */
    public function copy()
    {
        $carbon = new Carbon();
        $copiedCarbon = $carbon->copy();
        $this->assertTrue($copiedCarbon instanceof Carbon);
        $this->assertTrue($copiedCarbon !== $carbon);
    }

    /**
     * @test
     */
    public function parse()
    {
        $carbon = Carbon::parse('2002-07-02');
        $this->assertSame(2002, $carbon->year);
        $this->assertSame(7, $carbon->month);
        $this->assertSame(2, $carbon->day);
    }

    /**
     * @test
     */
    public function create()
    {
        $carbon = Carbon::create(2002, 7, 2);
        $this->assertSame(2002, $carbon->year);
        $this->assertSame(7, $carbon->month);
        $this->assertSame(2, $carbon->day);
    }

}


基本的にcreate()もしくはparse()を使って生成することになりそうです。
parse()なんかはDBのDATE型やDATETIME型の値をそのまま引数に使えるので便利そうです。

次は Carbon::setTestNow() についてです。
setTestNow() はCarbonが基準とする現在の日時をモックで設定できる機能です。つまりロジック内でCarbonを適切に扱うことで時間に関するテストをユニットテストでも書けるようになります。

ユニットテストの例を用意しました。
賞味期限(BestBefore)が過ぎていないか確認するだけのシンプルな機能です。



<?php

namespace Services;

use DateTime;

class BestBefore
{

    protected $dt;

    public function __construct(DateTime $dt)
    {
        $this->dt = $dt;
    }

    public function isSafe(DateTime $dt)
    {
        return $this->dt < $dt;
    }

}


次に BestBefore::isSafe() のテストを書いてみます。



<?php

use Carbon\Carbon;
use Services\BestBefore;

class BestBeforeTest extends PHPUnit_Framework_TestCase
{

    /**
     * @test
     */
    public function 賞味期限が切れていないかどうか()
    {
        Carbon::setTestNow(Carbon::createFromDate(2015, 5, 1));
        $expire = new BestBefore(new Carbon());

        $this->assertFalse($expire->isSafe(Carbon::createFromDate(2015, 4, 30)));
        $this->assertFalse($expire->isSafe(Carbon::createFromDate(2015, 5, 1)));
        $this->assertTrue($expire->isSafe(Carbon::createFromDate(2015, 5, 2)));

        Carbon::setTestNow(Carbon::createFromDate(2015, 4, 30));
        $expire2 = new BestBefore(new Carbon());

        $this->assertFalse($expire2->isSafe(Carbon::createFromDate(2015, 4, 30)));
        $this->assertTrue($expire2->isSafe(Carbon::createFromDate(2015, 5, 1)));
        $this->assertTrue($expire2->isSafe(Carbon::createFromDate(2015, 5, 2)));
    }

}


ご覧の通り Carbon::setTestNow() を設定することで冪等なテストを書くことができました。もしBestBeforeクラスのロジックでPHP標準の date() や time() を使っていたならこのようなテストは書けないですよね。個人的にすごく魅力的な仕組みだと感じました。

また例のBestBeforeクラスではタイプヒンティングにDateTimeを指定していますので、Carbonを拡張したMyCarbonを使いたいといった場合でもBestBeforeクラスに手を加えることなく利用することができます。

Carbonは単に「DateTime扱いやすいぞーやったー」というだけでなく、日時処理まわりのメンテナンス性およびコード品質の向上に一役買うライブラリであるということをお伝えできたかと思います。

ということで概ね私が伝えたかったのはここまでです。
ここからは覚えておくと便利なことや調べた機能をメモとして記載します。

アクセサ




<?php
require 'vendor/autoload.php';

use Carbon\Carbon;

$dt = Carbon::parse('2012-9-5 23:26:11.123789');

var_dump($dt->year);                                         // int(2012)
var_dump($dt->month);                                        // int(9)
var_dump($dt->day);                                          // int(5)
var_dump($dt->hour);                                         // int(23)
var_dump($dt->minute);                                       // int(26)
var_dump($dt->second);                                       // int(11)
var_dump($dt->micro);                                        // int(123789)
var_dump($dt->dayOfWeek);                                    // int(3)          曜日。数値。0 (日曜)から 6 (土曜)
var_dump($dt->dayOfYear);                                    // int(248)        年間の通算日。数字。(ゼロから開始)
var_dump($dt->weekOfMonth);                                  // int(1)          月間の週番号。
var_dump($dt->weekOfYear);                                   // int(36)         ISO-8601に基づく月曜日に始まる年単位の週番号。
var_dump($dt->daysInMonth);                                  // int(30)         指定した月の日数。28 から 31。
var_dump($dt->timestamp);                                    // int(1346901971)
var_dump(Carbon::createFromDate(1975, 5, 21)->age);          // int(39)
var_dump($dt->quarter);                                      // int(3)          四半期。


小ネタですが、上記プロパティはメンバとして定義されておらず __get() で動的に処理されています。age等は計算が必要だからですね。またIDEでプロパティが補完されたのですがどうやらDocで宣言すれば出来るようです。なるほど〜

- https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Carbon.php#L42

比較




<?php
require 'vendor/autoload.php';

use Carbon\Carbon;

$first = Carbon::create(2012, 9, 5, 23, 26, 11);
$second = Carbon::create(2012, 9, 5, 7, 26, 11, 'America/Vancouver');

echo $first->toDateTimeString();                   // 2012-09-05 23:26:11
echo $first->tzName;                               // Asia/Tokyo
echo $second->toDateTimeString();                  // 2012-09-05 07:26:11
echo $second->tzName;                              // America/Vancouver

var_dump($first->eq($second));                     // bool(true)
var_dump($first->ne($second));                     // bool(false)
var_dump($first->gt($second));                     // bool(false)
var_dump($first->gte($second));                    // bool(true)
var_dump($first->lt($second));                     // bool(false)
var_dump($first->lte($second));                    // bool(true)

$first->setDateTime(2012, 1, 1, 0, 0, 0);
$second->setDateTime(2012, 1, 1, 0, 0, 0);         // Remember tz is 'America/Vancouver'

var_dump($first->eq($second));                     // bool(false)
var_dump($first->ne($second));                     // bool(true)
var_dump($first->gt($second));                     // bool(false)
var_dump($first->gte($second));                    // bool(false)
var_dump($first->lt($second));                     // bool(true)
var_dump($first->lte($second));                    // bool(true)


範囲比較




<?php
require 'vendor/autoload.php';

use Carbon\Carbon;

$first = Carbon::create(2012, 9, 5, 1);
$second = Carbon::create(2012, 9, 5, 5);
var_dump(Carbon::create(2012, 9, 5, 3)->between($first, $second));          // bool(true)
var_dump(Carbon::create(2012, 9, 5, 5)->between($first, $second));          // bool(true)
var_dump(Carbon::create(2012, 9, 5, 5)->between($first, $second, false));   // bool(false)


他には日時の差(diff)やis〜系メソッド(isBirthday()等の面白いメソッドもありました笑)で使えそうな機能もありますが、今回の記事ではこれくらいにしておきます。詳細は公式のドキュメント(http://carbon.nesbot.com/docs)をどうぞ。

・参考



- https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Carbon.php
- http://php.net/manual/ja/class.datetime.php
- http://php.net/manual/ja/function.date.php
- http://carbon.nesbot.com/docs

非エンジニアのWebディレクターにオススメのGUIツール(Windows) MySQL/PostgreSQL編

こんにちは、鴨田です。

大分久しぶりの投稿になりますが、今回はコマンドラインが苦手な非エンジニアのWebディレクターにオススメする、MySQLPostgreSQLGUIツール「HeidiSQL」をご紹介いたします。



インストールの前に



これから行う説明に関して、PuTTYなどで普段からリモートサーバーへ接続を行っている前提で、話が進んだりしますので、その点だけご注意をお願いします。ローカル環境であれば、特に気にしないで問題ありません。また、今回の説明はMySQLを用います。



インストール



まずはHeidiSQLのサイトから、インストーラーをダウンロードします。

http://www.heidisql.com/download.php


「Installer」ボタンをクリックすると、最新版がダウンロードできます。32+64bit installerになっているので、OSのバージョンは気にしないでも大丈夫です。今回は、バージョン9.2.0.4948を使用します。

インストーラーを起動すると、こんな画面が表示されます。


インストール方法で気をつけるところは特にありません。どんどん進めます。


インストールが完了すると、この画面になります。早速起動して、設定を行いましょう。



設定



起動すると、こんな画面が表示されます。


「Unnamed」セッションを選択して、接続したいデータベースの情報を入力します。

○ローカルPC内のDBに接続する場合

[設定]タブ
ネットワーク種別:MySQL(TCP/IP)
ホスト名 / IP:127.0.0.1
ユーザー:DBのユーザー名
パスワード:DBのパスワード
ポート:DBのポート番号
データベース:DB名

DBを作成したときに設定した情報を入力してください。リモートサーバーの場合もほぼ同様で、「Hostname /IP」をサーバーのIPに変えるだけです。

SSH経由でリモートサーバーに接続する場合

[設定]タブ
ネットワーク種別:MySQL(SSH tunnel)
ホスト名 / IP:リモートサーバーのIP
ユーザー:DBのユーザー名
パスワード:DBのパスワード
ポート:DBのポート番号
データベース:DB名

[SSHトンネル]タブ
plink.exe:PuTTYplink.exeを参照
SSHホスト+ポート:踏み台サーバーのIP:port番号
ユーザー名:踏み台サーバーのユーザー名
パスワード:踏み台サーバーのパスワード
秘密鍵ファイル:踏み台サーバーの秘密鍵を参照
ローカルポート:ローカルポート番号

リモートサーバーの情報は、インフラエンジニアに聞くか、プログラムのDBの接続設定を覗いて、入力してください。踏み台サーバーの方は、PuTTYなどの設定を流用してください。

設定が完了したら、「保存」ボタンを押して、セッション情報を保存しておきましょう。



テーブルの内容表示



データベースに接続すると、画面表示が変わります。


画面左側にデータベースのテーブルが表示され、画面右側に選択したテーブルの詳細が表示されます。画面下部はGUIで操作した内容のコマンドなどが出力されます。

サンプルとして、郵便番号情報を格納した「post」テーブルを用意しました。約125000件のカラムが格納されています。

データの中身を見るには、画面左側のビューで「post」テーブルを選択します。


画面右側のビューでは、「データ」タブをクリックします。
すると、テーブルの内容が見られるようになります。




検索(フィルターパネル)



該当のテーブルから、検索したい文字列を含むカラムを絞り込みたいときは、フィルターパネルを表示します。メニューから「編集」→「フィルターパネル」を選択するか、「Ctrl+Alt+F」です。


パネルに、検索したい文字列を入力すると、カラムが絞り込まれます。




並び替え



カラムを並び替えることも出来ます。


並び替えパネルをクリックし、表示されたウィンドウで、「カラムの追加」をクリックすると、フィールドの選択をすることが出来ます。フィールドを選択したら、「ASC(昇順)」、「DESC(降順)」をトグル式で選択し、「OK」ボタンを押すと、データが並び替えられて表示されます。




データの操作



データの変更は変更したい値のところで、シングルクリックを2回(ゆっくりダブルクリック)すると、データを編集することが出来ます。


データの編集が終わったら、Enterもしくは違う場所をクリックすると、編集が完了します。


この状態だとまだ変更が確定していない(左上に赤い三角マークが付いている状態)ので、違う場所をクリックなどすると、コマンドが流れて確定します。


データのコピーや貼り付けなどは、右クリックから行うことが出来ます。NULLの挿入も右クリック→値の挿入→NULLで可能です。同様に、行の追加や削除などをすることが可能です。




データの取得範囲



初期設定では、カラムの取得件数は1,000件となっており、「次」で、次の1,000件を取得したり、「すべて表示」で10,000件まで取得します。

レスポンスが遅くなりますが、この設定は変更することが出来ます。メニューからツール→環境設定を選択してください。「Text formatting」タブを選択し、「Number of rows displayed in data tab(Step, Maximum)を変更してください。




以上です。

他にも、いろいろ機能はあるのですが、基本的な使い方のご紹介でした。なにか問題が起こったときの調査や、テスト時にデータベースの内容を変更したいときなどで使う分には、この程度の操作で充分だと思います。

次回は、GitのGUIツールをご紹介したいと思っています。それではまた。

Laravel5でシンプルなCRUDアプリを開発する

こんにちは〜たきゃはしです〜最近もホントにとにかくビール最高!って感じです!
今回はついにリリースされたLaravel5(以降はL5と略記する)を早速使ってみました!

◯ この記事の概要



L5の基本機能を扱いつつシンプルなブログアプリを作ってみようと思います。

こんな感じになります

この記事の目的はブログアプリの開発を通してL5でCRUDを作れるようになることです。構成をなるべくシンプルにしたかったこともあり沢山の機能は取り扱ったわけではありませんが「PHPMySQLは分かるけどFWはよくわからないな〜」とか「Laravel5 気になってるんだよな〜」という人には特におすすめですよ!

さっそくインストールからはじめたいと思います。

◯ インストール





	$ composer create-project laravel/laravel l5blog --prefer-dist


l5blog というディレクトリを作成しインストールします
以降に登場するファイルパスはこの l5blog を基準とします。

◯ 初期設定



・ storageのパーミッションの変更





$ chmod -R 777 storage


・ データベースの設定



・ .env


	DB_HOST=localhost
	DB_DATABASE=l5blog
	DB_USERNAME=root
	DB_PASSWORD=secret


データベースのドライバーはデフォルトで mysql です。
他のドライバーを使用したい場合は config/database.php で設定してください。

◯ データベースの作成





	mysql> CREATE DATABASE `l5blog`;


マイグレーション



ここでいうマイグレーションとは、データベースのテーブルやカラムの追加や変更の定義を管理することを指します。

マイグレーションファイルの生成





	$ php artisan make:migration create_articles_table
	Created Migration: 2015_02_17_114821_create_articles_table


・ テーブル定義の作成



・ database/migrations/2015_02_17_114821_create_articles_table.php


	<?php
	
	use Illuminate\Database\Schema\Blueprint;
	use Illuminate\Database\Migrations\Migration;
	
	class CreateArticlesTable extends Migration
	{
	
	    /**
	     * Run the migrations.
	     *
	     * @return void
	     */
	    public function up()
	    {
	        Schema::create('articles', function (Blueprint $table) {
	            $table->increments('id');
	            $table->string('title');
	            $table->text('body');
	            $table->timestamps();
	        });
	    }
	
	    /**
	     * Reverse the migrations.
	     *
	     * @return void
	     */
	    public function down()
	    {
	        Schema::drop('articles');
	    }
	
	}


マイグレーションの実行





	$ php artisan migrate
	Migration table created successfully.
	Migrated: 2014_10_12_000000_create_users_table
	Migrated: 2014_10_12_100000_create_password_resets_table
	Migrated: 2015_02_17_114821_create_articles_table


これでデータベースに articles というテーブルが追加されました。
同時にいくつか追加されたテーブルがありますが今回使用しないため気にする必要はありません。

シーダー(初期データ)



シーダーを生成するコマンドは標準でないので通常通りファイルを作成します。

・ 記事シーダーの作成



・ database/seeds/ArticlesTableSeeder.php


	<?php
	
	use Illuminate\Database\Seeder;
	
	class ArticlesTableSeeder extends Seeder
	{
	
	    public function run()
	    {
	        DB::table('articles')->truncate();
	
	        DB::table('articles')->insert([
	            [
	                'title'      => 'Laozi',
	                'body'       => 'When there is no desire, all things are at peace.',
	                'created_at' => '2015-01-31 23:59:59',
	                'updated_at' => '2015-01-31 23:59:59',
	            ],
	            [
	                'title'      => 'Leonardo da Vinci',
	                'body'       => 'Simplicity is the ultimate sophistication.',
	                'created_at' => '2015-02-01 00:00:00',
	                'updated_at' => '2015-02-01 00:00:00',
	            ],
	            [
	                'title'      => 'Cedric Bledsoe',
	                'body'       => 'Simplicity is the essence of happiness.',
	                'created_at' => '2015-02-01 00:00:01',
	                'updated_at' => '2015-02-01 00:00:01',
	            ],
	        ]);
	
	    }
	
	}


記事シーダーを作成したら下記のコマンドを実行してください。



$ composer dump-autoload
//もしくは
$ php artisan optimize

※ 8egsさん、ご報告ありがとうございます!

・ database/seeds/DatabaseSeeder.php


	<?php
	
	use Illuminate\Database\Seeder;
	use Illuminate\Database\Eloquent\Model;
	
	class DatabaseSeeder extends Seeder
	{
	
	    /**
	     * Run the database seeds.
	     *
	     * @return void
	     */
	    public function run()
	    {
	        Model::unguard();
	
	        $this->call('ArticlesTableSeeder');
	    }
	
	}


シーダーの実行





	$ php artisan db:seed
	Seeded: ArticlesTableSeeder


デフォルトでDatabaseSeederが実行されるので記事データがインサートされます。
2回目以降であれば下記コマンドが便利です。



	$ php artisan migrate:refresh --seed
	Rolled back: 2015_02_17_114821_create_articles_table
	Rolled back: 2014_10_12_100000_create_password_resets_table
	Rolled back: 2014_10_12_000000_create_users_table
	Nothing to rollback.
	Migrated: 2014_10_12_000000_create_users_table
	Migrated: 2014_10_12_100000_create_password_resets_table
	Migrated: 2015_02_17_114821_create_articles_table
	Seeded: ArticlesTableSeeder


いよいよブログアプリの作成に進みます。

◯ 記事モデルの作成



はじめはモデルを作りましょう。



	$ php artisan make:model Article
	Model created successfully.


さっそく生成されたArticleモデルを確認してみます。

・ app/Article.php


	<?php namespace App;
	
	use Illuminate\Database\Eloquent\Model;
	
	class Article extends Model
	{
	
	    //
	
	}


モデルはappディレクトリ直下に生成されます。モデルがアプリの中枢を担うことになりますので妥当かもしれませんね。もちろん新たにModelディレクトリを作成してそこに配置してもLaravelでは何も問題ありません。

ひな形へ肉付けしていきます。

・ app/Article.php


	<?php namespace App;
	
	use Illuminate\Database\Eloquent\Model;
	
	class Article extends Model
	{
	
	    /**
	     * The table associated with the model.
	     *
	     * @var string
	     */
	    protected $table = 'articles';
	
	    /**
	     * The attributes that are mass assignable.
	     *
	     * @var array
	     */
	    protected $fillable = ['title', 'body'];
	
	}


対応するテーブル名や保存が可能なフィールドを定義しました。(テーブル名はDRYに従っていれば実質不要なのですが明示的にするため記載しました。)
もし時間があるならばAricleモデルが継承している Illuminate\Database\Eloquent\Model を確認してみてください。他にどのようなプロパティが設定できるのか知っておくことは大切です。例えば今回定義しなかった「$perPage」はページネーションで1ページに表示する件数を指定することができます。

◯ コントローラー



コントローラーのひな形を生成します。



	$ php artisan make:controller ArticlesController
	Controller created successfully.


生成されたファイルはリソースコントローラーを前提とするアクションがずらっと定義されています。ですが今回は暗黙的コントローラーとして実装していきたいので一旦すべてのメソッドを削除してください。(暗黙的コントローラーについては後ほど簡単に説明します)

・ app/Http/Controllers/ArticlesController.php


	<?php namespace App\Http\Controllers;
	
	use App\Http\Requests;
	use App\Http\Controllers\Controller;
	
	use Illuminate\Http\Request;
	
	class ArticlesController extends Controller
	{
	}


すっきりしたところで 一覧、詳細、作成、編集、削除 のアクションをそれぞれ定義していきます。



	<?php namespace App\Http\Controllers;
	
	use App\Article;
	use App\Http\Requests;
	use App\Http\Controllers\Controller;
	
	use Illuminate\Http\Request;
	
	class ArticlesController extends Controller
	{
	
	   /**
	    * @var Article
	    */
	   protected $article;
	
	   /**
	    * @param Article $article
	    */
	   public function __construct(Article $article)
	   {
	       $this->article = $article;
	   }
	
	   public function getIndex()
	   {
	
	   }
	
	   public function getShow()
	   {
	
	   }
	
	   public function getCreate()
	   {
	
	   }
	
	   public function postCreate()
	   {
	
	   }
	
	   public function getEdit()
	   {
	
	   }
	
	   public function postEdit()
	   {
	
	   }
	
	   public function postDelete()
	   {
	
	   }
	
	}


暗黙的コントローラーでは、URIとHTTPメソッドで対応するアクションが決定します。上記ではgetCreate()とpostCreate()では対応するURIは同じになりますが対応するHTTPメソッドが異なります。またPATCHやDELETEといった他のHTTPメソッドにも対応可能ですが今回は割愛します。

◯ ルーティング



作成したコントローラーをルーティングへ追加します。
'/'へのアクセスは記事一覧、'/articles'は暗黙的コントローラーとして定義します。

・ app/Http/routes.php


	<?php
	
	Route::get('/', 'ArticlesController@getIndex');
	Route::controller('articles', 'ArticlesController');


ここでコントローラーのネームスペースが指定する必要がないのは、 routes.php が App\Providers\RouteServiceProvider で読み込まれているためです。App\Providers\RouteServiceProviderで 'App\Http\Controllers' をネームスペースとするグループのルーティングだと定義されているおかげです。

◯ 記事の一覧



次に一覧の作成に取り掛かります。インサートした記事をすべて表示させましょう。
すべて記事を取得してビューへ渡すアクションを作成します。

・ 記事一覧アクション





<?php ...

    /**
     * 記事の一覧
     *
     * @return \Illuminate\View\View
     */
    public function getIndex()
    {
        $articles = $this->article->all();

        return view('articles.index')->with(compact('articles'));
    }


view()の中身はドット繋ぎでディレクトリ構造を表現できます。with()にはビューで使いたいデータを配列で渡してあげればOKです。

次にビューを作成します。テンプレートエンジンはLaravelデファクトのBladeを使用します。
アプリらしくViewExtends、共通のビューと個別のビューを別々に作成します。

・ 共通のビュー



・ resources/views/app.blade.php


	<!DOCTYPE html>
	<html lang="ja">
	<head>
	    <meta charset="UTF-8">
	    <title>L5Blog</title>
	    <link href="/css/app.css" rel="stylesheet">
	</head>
	<body>
	    <div class="container">
	        <div class="row">
	            <h1>L5Blog</h1>
	            <div class="col-md-12">
	                @yield('content')
	            </div>
	        </div>
	    </div>
	</body>
	</html>


・ 記事一覧のビュー(個別のビュー)



・ resources/views/articles/index.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事一覧</h2>
	    <table class="table table-striped table-hover">
	        <thead>
	        <tr>
	            <th>タイトル</th>
	            <th>本文</th>
	            <th>作成日時</th>
	            <th>更新日時</th>
	        </tr>
	        </thead>
	        <tbody>
	        @foreach($articles as $article)
	            <tr>
                   <td>{{{ $article->title }}}</td>
                   <td>{{{ $article->body }}}</td>
                   <td>{{{ $article->created_at }}}</td>
                   <td>{{{ $article->updated_at }}}</td>
	            </tr>
	        @endforeach
	        </tbody>
	    </table>
	@endsection


ホストのルート'/'へアクセスしてみてください!一覧が表示されるはずです!

◯ 記事の詳細



次は記事の詳細を表示するページを作成していきます。

・ 記事詳細アクション





<?php ...

    /**
     * 記事の詳細
     *
     * @param $id
     * @return \Illuminate\View\View
     */
    public function getShow($id)
    {
        $article = $this->article->find($id);

        return view('articles.show', compact('article'));
    }


データはview()の第二引数から渡す方法もあります。

・ 記事詳細のビュー



・ resources/views/articles/show.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事詳細</h2>
	    <table class="table table-striped">
	        <tbody>
	        <tr>
	            <th>タイトル</th>
	            <td>{{{ $article->title }}}</td>
	        </tr>
	        <tr>
	            <th>本文</th>
	            <td>{{{ $article->body }}}</td>
	        </tr>
	        <tr>
	            <th>作成日時</th>
	            <td>{{{ $article->created_at }}}</td>
	        </tr>
	        <tr>
	            <th>更新日時</th>
	            <td>{{{ $article->updated_at }}}</td>
	        </tr>
	        </tbody>
	    </table>
	@endsection


これで 'articles/show/2' へアクセスしてみてください!暗黙的コントローラーにより id:2 の記事の詳細が表示されます!

記事の一覧に詳細ページへのリンクを追加してみましょう。

・ resources/views/articles/index.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事一覧</h2>
	    <table class="table table-striped table-hover">
	        <thead>
	        <tr>
	            <th>タイトル</th>
	            <th>本文</th>
	            <th>作成日時</th>
	            <th>更新日時</th>
	            <th></th>
	        </tr>
	        </thead>
	        <tbody>
	        @foreach($articles as $article)
	            <tr>
                   <td>{{{ $article->title }}}</td>
                   <td>{{{ $article->body }}}</td>
                   <td>{{{ $article->created_at }}}</td>
                   <td>{{{ $article->updated_at }}}</td>
                   <td><a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a></td>
	            </tr>
	        @endforeach
	        </tbody>
	    </table>
	@endsection


一覧から詳細リンクをクリックすると詳細ページに遷移されるようになりました。
Laravelが初めての方でもなんとなくイメージは掴めてきたでしょうか?

次は投稿処理です。

◯ 記事の投稿



投稿は一覧や詳細と違いPOSTを受け付けます。

- GET /articles/create の場合は getCreate()
- POST /articles/create の場合は postCreate()

上記を理解していればアクションとフォームで何をしなくてはいけないかハッキリしますね。

・ 記事投稿のアクション





<?php ...

    /**
     * 記事の投稿
     *
     * @return \Illuminate\View\View
     */
    public function getCreate()
    {
        return view('articles.create');
    }

    /**
     * 記事の投稿
     *
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postCreate(Request $request)
    {
        $data = $request->all();
        $this->article->fill($data);
        $this->article->save();

        return redirect()->to('articles/index');
    }


L5ではアクションなどでメソッドインジェクションによる依存性の注入が可能です。

・ 記事投稿のビュー



と、ここで問題発生です。

記事投稿用のフォームの作成をしていきますが、ワタクシここでハマりました。。。
L4ではFormクラスとHTMLクラスといういわゆるヘルパークラスがありそれを使おうとしてもなぜか使えずに悩んでいました。IDEファサードが補完されない時点で「オヤ?」と思いサービスプロバイダーを確認したところ見当たらずロードされていないことに気が付きました。どうやらL5からFormクラスとHTMLクラスは統合されなくなったようで別途Composerによるインストールが必要なことが分かりました。

という訳で、あった方が便利なので準備します。

インストール




	$ composer require "laravelcollective/html:5.0.*"


サービスプロバイダーとファサードの追加


・ config/app.php


<?php ...

    'Illuminate\Hashing\HashServiceProvider',
    Collective\Html\HtmlServiceProvider::class,
    'Illuminate\Mail\MailServiceProvider',


ファサードも変更します。

・ config/app.php


<?php ...

    'File'      => 'Illuminate\Support\Facades\File',
    'Form'      => Collective\Html\FormFacade::class,
    'Hash'      => 'Illuminate\Support\Facades\Hash',
    'HTML'      => Collective\Html\HtmlFacade::class,
    'Input'     => 'Illuminate\Support\Facades\Input',


これでHTMLクラスとFormクラスの準備は完了です。
L4からやってる人はハマるだろうなぁと思いました。笑

※ L5.1 リリース頃にまたリポジトリが変わってしまったことの指摘があり修正しました。トイキン君さんありがとうございます。


気を取り直して、記事投稿ビューの作成です。

・ resources/views/articles/create.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事投稿</h2>
	    {!! Form::open() !!}
	        <div class="form-group">
	            <label>タイトル</label>
	            {!! Form::input('text', 'title', null, ['required', 'class' => 'form-control']) !!}
	        </div>
	        <div class="form-group">
	            <label>本文</label>
	            {!! Form::textarea('body', null, ['required', 'class' => 'form-control']) !!}
	        </div>
	        <button type="submit" class="btn btn-default">投稿</button>
	    {!! Form::close() !!}
	@endsection


'/articles/create'へアクセスしてください!フォームが表示され投稿が可能になったはずです。

◯ 記事の編集



編集は既に投稿を作っているため特筆することがありませんが、せっかくなのでソースを見ないで自分で作ってみるのも理解への近道かもしれませんね。

・ 記事編集のアクション





<?php ...

    /**
     * 記事の編集
     *
     * @param $id
     * @return \Illuminate\View\View
     */
    public function getEdit($id)
    {
        $article = $this->article->find($id);

        return view('articles.edit')->withArticle($article);
    }

    /**
     * 記事の編集
     *
     * @param Request $request
     * @param         $id
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postEdit(Request $request, $id)
    {
        $article = $this->article->find($id);
        $data = $request->all();
        $article->fill($data);
        $article->save();

        return redirect()->to('articles/index');
    }


with()はマジックメソッドとしても利用可能です。
上記の withArticle($article) は with('article' => $article) と同じです。

・ 記事編集のビュー



・ resources/views/articles/edit.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事編集</h2>
	    {!! Form::open(['action' => ['ArticlesController@postEdit', $article->id]]) !!}
	        <div class="form-group">
	            <label>タイトル</label>
	            {!! Form::input('text', 'title', $article->title, ['required', 'class' => 'form-control']) !!}
	        </div>
	        <div class="form-group">
	            <label>本文</label>
	            {!! Form::textarea('body', $article->body, ['required', 'class' => 'form-control']) !!}
	        </div>
	        <button type="submit" class="btn btn-default">編集</button>
	    {!! Form::close() !!}
	@endsection


編集ページヘの導線を作成しましょう。(先程の投稿もついでに)

・ resources/views/articles/index.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事一覧</h2>
	    <div>
	        <a href="/articles/create" class="btn btn-primary">投稿</a>
	    </div>
	    <table class="table table-striped table-hover">
	        <thead>
	        <tr>
	            <th>タイトル</th>
	            <th>本文</th>
	            <th>作成日時</th>
	            <th>更新日時</th>
	            <th></th>
	        </tr>
	        </thead>
	        <tbody>
	        @foreach($articles as $article)
	            <tr>
	                <td>{{{ $article->title }}}</td>
	                <td>{{{ $article->body }}}</td>
	                <td>{{{ $article->created_at }}}</td>
	                <td>{{{ $article->updated_at }}}</td>
	                <td>
	                    <a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a>
	                    <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-success btn-xs">編集</a>
	                </td>
	            </tr>
	        @endforeach
	        </tbody>
	    </table>
	@endsection


これで編集も出来ましたね!最後は削除です。

◯ 記事の削除



もしGETで受け付けた場合は'/articles/delete/1'にアクセスした時点で削除されます。それは厳しいと思いますのでPOSTでのみ受け付けることにします。

・ 記事削除のアクション





<?php ...

    /**
     * 記事の削除
     *
     * @param $id
     * @return \Illuminate\Http\RedirectResponse
     */
    public function postDelete($id)
    {
        $article = $this->article->find($id);
        $article->delete();

        return redirect()->to('articles/index');
    }


・ 記事削除のビュー



・ resources/views/articles/index.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事一覧</h2>
	    <div>
	        <a href="/articles/create" class="btn btn-primary">投稿</a>
	    </div>
	    <table class="table table-striped table-hover">
	        <thead>
	        <tr>
	            <th>タイトル</th>
	            <th>本文</th>
	            <th>作成日時</th>
	            <th>更新日時</th>
	            <th></th>
	        </tr>
	        </thead>
	        <tbody>
	        @foreach($articles as $article)
	            <tr>
	                <td>{{{ $article->title }}}</td>
	                <td>{{{ $article->body }}}</td>
	                <td>{{{ $article->created_at }}}</td>
	                <td>{{{ $article->updated_at }}}</td>
	                <td>
	                    <a href="/articles/show/{{{ $article->id }}}" class="btn btn-default btn-xs">詳細</a>
	                    <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-success btn-xs">編集</a>
	                    {!! Form::open(['action' => ['ArticlesController@postDelete', $article->id]]) !!}
	                    <button type="submit" class="btn btn-danger btn-xs">削除</button>
	                    {!! Form::close() !!}
	                </td>
	            </tr>
	        @endforeach
	        </tbody>
	    </table>
	@endsection


・ resources/views/articles/show.blade.php


	@extends('app')
	
	@section('content')
	    <h2 class="page-header">記事詳細</h2>
	    <ul class="list-inline">
	        <li>
	            <a href="/articles/edit/{{{ $article->id }}}" class="btn btn-primary pull-left">編集</a>
	        </li>
	        <li>
	            {!! Form::open(['action' => ['ArticlesController@postDelete', $article->id]]) !!}
	            <button type="submit" class="btn btn-danger pull-left">削除</button>
	            {!! Form::close() !!}
	        </li>
	    </ul>
	    <table class="table table-striped">
	        <tbody>
	        <tr>
	            <th>タイトル</th>
	            <td>{{{ $article->title }}}</td>
	        </tr>
	        <tr>
	            <th>本文</th>
	            <td>{{{ $article->body }}}</td>
	        </tr>
	        <tr>
	            <th>作成日時</th>
	            <td>{{{ $article->created_at }}}</td>
	        </tr>
	        <tr>
	            <th>更新日時</th>
	            <td>{{{ $article->updated_at }}}</td>
	        </tr>
	        </tbody>
	    </table>
	@endsection


削除ボタンをクリックすればデータが削除されるようになりましたでしょうか?
確認ボックスもなしにイキナリ削除なんて驚いたと思いますが、これにてブログアプリは完成です!お疲れさまでした〜!

◯ まとめ



L5でブログアプリを開発してみましたがどうでしょうか?正直シンプルすぎる構成ゆえL5の魅力をお伝え出来たとは思っていません(自分も全然満足していません)。個人的に注目しているのは新しくなったバリデーション、扱いやすくなったサービスプロバイダー、モデルとルーティングのバインド、メソッドインジェクションによる依存性の注入、他にも様々な機能が盛りだくさんで正直一記事で説明するのは無理があると思いました。なので、それらも今後発信していけたらと思います。

記事のソースでエラーが出たとかあればコメントください!それでは〜!(ソースは欲しい方いれば公開します。)

iOSデバッガー3.2の変更点について

1月24日より、iOS版の最新デバッガーである3.2.0の配信を開始しました。このデバッガーではCordova 4.1への対応やステータスバーの搭載をとりやめるなど、いくつか大きな変更点があります。本記事にまとめていますので、ご参考にしていただきますようお願いいたします。

変更点の概要



iOSMonacaデバッガー3.2.0は、以前のバージョンと比べて下記の変更が行われています。

Cordova 4.1にアップデート

デバッガーが搭載しているCordovaを、最新のCordova 4.1.0にアップデートいたしました。その結果、最新のCordova APIに対応し、バグ修正や安定性の向上が行われています。

CordovaのStatus Barプラグインの取り外し

これまでiOS 6との互換性をとるために、Status Barプラグインを標準で組み込んでいました。それにより、iOS 7やiOS 8で動かした場合にも、画面上部にiOS 6と同様のステータスバーが常に表示されていました。今回からStatus Barプラグインをデバッガーから取り除いたことで、アプリの見た目がiOS 7以降のフラットデザインに最適化されるようになります。

Android版やビルド機能のアップデートは近日対応

ビルドサーバーのCordova 4.1へのアップデートは近日中に行う予定です。改めて告知いたしますので、お待ちいただければ幸いです。なおCordova 3.5のプロジェクトは、引き続き維持されるように配慮いたします。

既存のプロジェクトへの影響



この変更により、これまでのデバッガーで動かしていたアプリには、下記の影響がでる可能性があります。いずれもビルド時には問題ありませんのでご安心ください。

ステータスバーの見た目が変わる

画面一杯にHTMLコンテンツが表示されるように変更になります。

旧バージョン
Status Barプラグインあり
新バージョン
Status Barプラグインなし


動かないCordova APIがある可能性がある

Cordova APIの関数やメソッドのなかには、新しいバージョンで対応しないものがある可能性があります。

対応策



既存プロジェクトの対応方針として、下記の2点を推奨いたします。

1. 既存のアプリを変更したくない場合

カスタムビルド版デバッガーを利用することで、以前のバージョンのデバッガーと同じものを作成することができます。この場合、Status Barプラグインも組み込まれ、Cordovaのバージョンも3.5.1となります。

カスタムビルド版デバッガーの使い方については、こちらのドキュメントを参照してください。

2.新しい見た目(画面いっぱいの表示領域)に合わせる場合

[設定]→[Cordovaプラグインの管理] より、Status Barプラグイン(org.apache.cordova.statusbar)を無効にすることで、ビルド時にもStatus Barプラグインが取り除かれ、デバッガーと見た目を共有できます。

ただしこの場合、プログラムコードに下記の通り修正が必要になります。

- Onsen UIを使用している場合は、ons.disableAutoStatusbarFill()関数の呼び出しを削除(コメントアウト)してください。
- Onsen UIを使用していない場合は、CSSのmargin-topなどを使い、iOSの場合にはページ上部に20px分の余白を作成してください。

■ ons.disableAutoStatusbarFill()をコメントアウト



本件に関するお問い合わせ



本件に関するお問い合わせは、Monacaサポートをご利用ください。

以上、よろしくお願いいたします。

Google の Chrome Dev Summit ~ Service Workers に関して ~

GoogleChrome Dev Summit に、先月参加してきました! 
こちらは、Chrome の新機能の紹介や開発者コミュニティーからの要望などをフィードバックする、技術者向けのイベントで、カリフォルニア州のマウンテンビューにある Google の本拠地で開催されました。



ミーティングの内容もさることながら、たくさんの技術者とお会いすることもでき、大変有意義な時間を過ごすことができました。今回が初めてのアメリカへの海外出張でしたが、いい思い出を作ることができました。私たちが滞在したのは、ビクトリア調の建物が立ち並ぶ、サンフランシスコの中でも、ちょっぴり歴史が古い一角でした。この一角は、他にも、壁面に描かれたモダンな絵 ( 落書き? いいえ、グラフィティと呼ばれる、プロの芸術家による創作絵画です! ) が有名なところです。



滞在中は、サンフランシスコらしい?ご飯も楽しむことができました。

Google のミーティングの中で、2 つほど、印象に残った新機能があります。
1 つ目は、Material Design と呼ばれる、新たなデザインの仕様です。2 つ目は、Service Worker API です。どちらも、来年も引き続き、注目すべき機能となるのではないでしょうか。
また、カスタム Web コンポーネントを作成するときに使用する Polymer ( JavaScript ライブラリー ) に関しても、熱い議論がありました。

なかでも、私が興味を持ったのは、Service Worker API の方です。ここでは、こちらの API を中心にお話をします。この API は、オフラインでも、Web ページが継続して動作できるように、ページのキャッシュを行います。各種アセットとアプリのデータをキャッシュするときに使用できます。
よって、基本的には、デスクトップ上でも、携帯端末上でも、Web アプリのデータをキャッシュするとき ( install 時 ) には、このAPI を 使用できます ( install とは、 Service Worker の用語で、キャッシュ処理を開始するときに使用します )。

この API を使用すれば、Web アプリを、より 「 アプリ的 」 ( appy !! ) にできます ( appy とは、Google 技術者の造語です^^。名詞の app に y をつけて、形容詞にしています )。「 アプリ的 」 とは、簡単にいうと、Web アプリだけど、ホーム画面上に置けるなど、ネイティブアプリのように、振る舞うこと・操作することができることを指します。
ネイティブアプリは、ホーム画面上から直接アクセスできたり、オフラインでも実行できるのは周知のとおりです。一方、Web アプリでは、インターネット接続を必要としない処理もありますが、それでも、アセットをダウンロードするときなどには、インターネット接続をしなければならないのが現状です。

Service Workers の現状


API 自体が発展・開発途中なので、正式リリース版のブラウザーではサポートされていませんが、Firefox Nightly や Chrome Canary など、一部のベータ版で、実際に、API を試すことができます。ブラウザー上で Service Workers を有効化する方法は、こちらのページ を参照してください。

最新版の Chrome Canary 上で試したところ、動作が怪しいので、ここでは、Firefox Nightly を使用します。

余談ですが、Chrome 41 から、この API を使用できるようです ( 来年の初旬ごろにリリース )。また、こちらのページ上で、現時点での、主なブラウザーのサポート状況を確認できます。

Service Worker の実装方法


API 自体はシンプルなので、 API のサポート状況を確認した後であれば ( 検証用のライブラリーなどを使用 )、既存の Web アプリにも安全に実装できます ( とはいっても、現時点では、ほとんどのブラウザーでサポートしていませんが... )。



if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/my-app/sw.js', {
    scope: '/'
  }).then(function() {
    console.log('Registration succeeded.');
  }).catch(function(error) {
    console.log('Registration failed with ' + error);
  });
};


上述したように、Service Worker API が利用可能か、確認する処理を最初に行います。
次に、navigator.serviceWorker.register() メソッドを呼び出して、Service Worker を登録します。このメソッドは、Promise ( プロミス/未解決・保留された処理 ) を返します。最初の引数は、ダウンロードして、実行する JavaScript ファイルです。

Service Worker を使用したアセットのキャッシュ方法


Service Worker の登録後に、「 install 」 イベントが発火します。sw.js の中で、このイベントのリスナーを設定します。



// sw.js
this.addEventListener('install', function(event) {
  event.waitUntil(
   caches.create('v1').then(function(cache) {
         return cache.add(
        '/my-app/',
        '/my-app/index.html',
        '/my-app/style.css',
        '/my-app/app.js',
        '/my-app/logo.jpg',
        ... // other assets that we might want to cache.
     );
   })
   );
});



  • caches.create(name) では、Promise を返します。この Promise は、キャッシュ処理が完了したときに、解決 ( 保留の解除 ) します。また、キャッシュに名前を割り当てています。複数のキャッシュを持つことを想定しているため、名前を割り当てることにより、アプリ側での管理が容易になります。

  • cache.add() を使用して、アセットをキャッシュに追加します。



キャッシュの使用方法


ここまでの処理で、キャッシュの作成、アセットのキャッシュへの追加が完了しました。そこで、ブラウザーに対して、外部へアセットをリクエストする代わりに、キャッシュ内部のデータを使うように、命令してみましょう。

Service Worker API では、「 fetch 」 イベントを使用して、リクエストされたリソースを取得します。シンプルな処理ですが、キャッシュされたアセットがあれば、それを返してくれます。



// sw.js
this.addEventListener('fetch', function(event) {
  event.respondWith(
    // this will respond with what is in the cache.
    caches.match(event.request);
  );
});


cache.match() では、例外処理時に使用できる Promise を返します。以下の例では、キャッシュされたアセットがない場合、通常のリクエストを使用した、リソースの取得を行います。



// sw.js
this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).catch(function() {
      return event.default();
    });
  );
});


上述の処理に加え、動的に、リソースをキャッシュすることも可能です。キャッシュ処理を段階的に行い、リソースを節約しつつ、オフライン時のアプリの実行に備えることもできます。また、クロスドメインからでもリソースをキャッシュできるため、たとえば、CDN のコンテンツなど、リモートのアセットもキャッシュできます。

他の処理の記法に関しては、こちらのページ をご確認ください。

使用勝手もさることならが、オフラインでも Web アプリを実行できる機能を提供してくれる、この API は、Web アプリの可能性を広げてくれるのではないでしょうか。今後も、目が離せませんね。今回は、Service Worker API の話を主にしましたが、今後の大きな流れとして、ネイティブ側が使用していた機能・リソースが、より外側(ブラウザー側)に解放され、ブラウザー上で動作するアプリもネイティブライクなものになっていくのではないしょうか。
そんな事を感じさせてくれるミーティングでした。

Crosswalkを使うとどれくらいパフォーマンスがアップするかテストしてみました

Intelが開発、提供しているハイブリッドアプリ用のWebViewがCrosswalkです。





Crosswalkの公式サイト



Crosswalk - build world class hybrid apps



Crosswalk登場の経緯



ハイブリッドアプリでは通常、OS標準のWebViewを使いますが、Crosswalk版はCrosswalkをレンダリングおよびJavaScriptエンジンとして使うようになります。図にすると次のようになります。





通常版とCrosswalk版の違い



通常のハイブリッドアプリがHTML5/JavaScriptのコードを持ち、Android OSが提供するWebViewを使うのに対して、さらにCrosswalk版はChromiumベースのレンダリング/JavaScriptエンジン(Crosswalk)をアプリ内に内包します。



なぜCrosswalkが登場したのかと言うと、これまでハイブリッドアプリにおいてOS間におけるレンダリング/JavaScriptエンジンの違いによる動作、パフォーマンスの違いが大きな問題になってきました。Android 4.4になり、Chromiumベースになったことでパフォーマンスは大きく向上しましたが、それでも端末間の動作差異が微妙に残ってしまっています。4.4以前の端末においてもハイブリッドアプリのパフォーマンス、機能を大きく改善してくれるのがCrosswalkです。



Crosswalkにより、大きく分けて以下の3つの利便性があります。



1. レンダリングエンジンの統一



CrosswalkはPhoneGap/Cordova、そしてMonacaアプリに同梱して一つのアプリとして配布できます。つまり異なるAndroid OSにおいてもWebViewのバージョンが統一されるようになります。



2. 最新のAPIが利用できる



CrosswalkはGoogle Chromeベースではなく、Chromiumベースになります。ChromiumGoogle Chromeのベースになるオープンソース・プロジェクトですが、その成果がGoogle Chromeに反映されています。そのため、より先進的なAPIを(しかもOSのバージョン差異を気にすることなく)利用できます。



3. パフォーマンスが向上



今回の記事ネタになりますが、Chromiumは最新の技術を使い、パフォーマンスが向上しています。どれくらい向上しているかは後述します。



デメリットとしては、



1. Android 4.0以降限定



CrosswalkがサポートしているのはAndroid 4.0以降になります。2.3などをターゲットして考えると使えないということになります(またはアプリを分割しないといけません)。



2. アプリサイズが増えます



レンダリングエンジンを内包しますので、その分アプリのサイズが大きくなります。10MBくらいは大きくなるようです。



といったデメリットがあります。とはいえAndroid 4.0以降向けに提供されるハイブリッドアプリであれば選択しない手はないのではないでしょうか。



3. 最新の環境下ではCrosswalk相当な場合があります



例えばAndroid 4.4.4の場合、Chromeのバージョンが33.0.0.0にあがっており、WebKitは537.36になっています。これはCrosswalk版と同じになります。このように最新のAndroid OS環境の場合、Crosswalk相当になることがあります。



Android 4.1でチェック



Android 4.1、Nexus 7相当の環境でテストをしました。手元にAndroid 4.1実機がなかったのでGenemotionで仮想環境を作っています。この環境にMonacaデバッガーMonacaデバッガー(ハイパフォーマンス版)をインストールしています。



バージョン



WebViewのバージョンは次のようになっています。





数字にすると3くらいしか変わらないのですが、バージョン番号の付け方が通常のソフトウェアとは異なるので、その性能差は相当大きいというのが以下の内容で分かります。



javascriptパフォーマンス



JavaScriptがどれくらい高速化したのか、Googleが提供するJavaScriptベンチマークソフトウェアのOctane 2.0でチェックしました。



まずは通常版での結果です。





通常版のOctane 2.0の結果



次にCrosswalk版。





Crosswalk版のOctane 2.0の結果



スコアにして1.7倍近く向上しています。2回目は通常のWebViewが9,265で、Crosswalkが18,851となり2倍以上の高速化となりました。



|回数|通常のWebView版|Crosswalk版|



|1 | 9,365| 16,338|
|2 | 9,265| 18,851|
|3 | 10,599| 17,784|



APIの実装具合



HTML5は2014年10月にようやく勧告になったこともあり、実装の度合いがブラウザによって異なってきます。そのため、Android OSのバージョンが違うと使えない機能があるというのはよくあります。なおAndroid 4.1は2012年6月ごろにリリースされたようです(via Wikipedia)。



Crosswalk版はレンダリングエンジンがどのAndroid OSにおいても統一できるのでそういった心配が大幅に軽減されるでしょう。



そこでAPIの実装具合を確認します。テストはHTML5Testで行いました。





通常版のHTML5実装状況



通常版の場合、278ポイントなのに対して、





Crosswalk版のHTML5実装状況



Crosswalk版は493ポイントになっています。実際、利用不可の数が圧倒的に少なくなっているという印象があります。これでHTML5を活かした機能が躊躇なく実装できるようになるでしょう。






ハイブリッドアプリの場合、ネイティブに対するパフォーマンスが常に話題にあがります。それだけにレンダリングエンジンを内包するだけで高速化し、さらにAPIの面で多機能化、OS標準ブラウザごとの差異を気にすることなく実装できるのは大きなメリットと言えそうです。



MonacaではAndroidアプリのデバッグ時、コンパイル時にCrosswalk版が選択できるようになっています。より高いパフォーマンスを目指すためにもぜひご利用ください。

Monacaでのアプリ開発をローカルで可能にする「Monaca Localkit」ベータ版の紹介

MonacaはこれまでWeb IDEを使ってWebブラウザ上で開発するスタイルが基本でした。WebDAVも提供はしているものの、修正したらオンラインのMonaca上に反映して、それを手元のスマートフォンでデバッガーを通して確認するという流れだったと思います。



つまりこのような形です。





Monaca IDEを使った開発



Webブラウザでの修正は Monaca Cloud へ反映されて、その結果をスマートデバイスに送ります。データ通信としては2段階を経ていました。



Monaca Localkit ベータ版リリース



その開発スタイルが Monaca Localkit によって一変します。具体的には次のようになります。





Monaca Localkitを使った開発



大きな変更点は2つあります。



1. ローカルだけで完結する開発スタイル



これまであった Monaca Cloud へのデータ送信がなくなり、直接スマートデバイスに修正が反映されます。



2. 好きなエディタが選べる



ローカルコンピュータ上のファイルを編集しますので、これまでのWebブラウザからの編集ではなく自分の好きなエディタを使って開発が行えるようになります。



Monaca Localkit は Windows/Mac OSX に対応しています。今回はWindowsを使って紹介します(Mac OSX版でもほとんど変わりません)。なお Monaca Localkit はベータ版となっておりますので、正式リリースの際には仕様に変更が行われている可能性があります。



インストーラーをダウンロード&インストール



まずローカルコンピュータにMonaca Localkitをインストールします。こちらからインストーラーがダウンロードできます。



Macをご利用の方は、こちらからMonaca Localkitベータ版利用マニュアルをご覧ください。



ダウンロード画面



インストールはウィザードに沿って進めます。なお、あらかじめGoogle ChromeiTunesがインストールされている必要があります。





順番に進めます



インストールが終わると、Chromeウェブストアが開いて、Monaca Localkitのページが開きます。そのまま無料ボタンを押してインストールします。インストールが終わると、Chromeアプリランチャーの中にMonaca Localkitが追加されます。





Chromeアプリランチャー



これで開発環境が整いました!



早速開発しましょう



まずChromeアプリランチャーからMonaca Localkitを起動します。





Monaca Localkit起動画面



起動したらメールアドレス、パスワードを入力してLoginボタンを押してください。まだアカウントをお持ちでなければSign Upボタンを押してください。



ログインするとプロジェクト一覧が表示されます。今回はOnsen UI M/Dを使ってみます。プロジェクト名をクリックします。





プロジェクト一覧



プロジェクトの詳細が表示されます。まずDownloadボタンを押して、Monaca Cloud上にあるコンテンツをダウンロードします。





プロジェクト詳細



最初に保存先を聞かれます。





保存先指定





ディレクトリ選択



ディレクトリを選んでOKボタンを押すと、コンテンツのダウンロードが開始します。





ダウンロード中…





ダウンロード完了



ダウンロードが終わりました。後はStart debuggingボタンを押すだけです!





デバッグスタート



デバッグが開始すると、このようにボタンが赤くなります。ここまで終わったら、次にAndroid/iOS側での作業になります。今回はAndroid版で紹介します。



スマートフォン/タブレット側の準備



Google PlayにてMonacaデバッガーをダウンロードします。ハイパフォーマンス版と、通常版の2つがあります。ハイパフォーマンス版はCrosswalkエンジンを組み込んだ版で、最新のChromiumAPIが使えるのが特徴です。今回はハイパフォーマンス版をインストールしています。





Monacaデバッガー(ハイパフォーマンス版)



インストールが終わったら起動します。ID/パスワードはデスクトップ側で使ったものと合っている必要があります。





ログイン画面



ログインするとプロジェクトの一覧が表示されます。今回は左上にあるメニューアイコンをタップします。





プロジェクト一覧



メニューが開くと、そこにローカルPCという選択肢があるのか分かるかと思います。これをタップします。





メニュー



そうすると同じLAN内にあるMonaca Localkitを検索します。見つからない場合は手動でIPアドレスを指定することもできます。その際のポート番号は8000番になります。





PC検索中



無事見つかったら、そのPCと接続します。その際、スマートフォン側のMonaca Debuggerでペアリングコードが表示されますので、デスクトップのMonaca Localkitにてコードを入力してください。





ペアリング





デスクトップ側で入力



ペアリングが正しく行われれば完了です。





接続完了



使ってみる!



いよいよLocalkitを試せるようになりました。スマートフォンにて、配信中になっているコンピュータアイコンをタップします。初回はコンテンツのダウンロードが実行されます。





コンテンツダウンロード



コンテンツがダウンロードされれば作成したアプリがそのまま表示されます。





アプリの実行



さて、この状態になったらローカルのファイル(先ほど指定したディレクトリにダウンロードされています)を好きなエディタで開いて編集してみましょう。





ファイルを編集



ファイルの保存をトリガーにlive reloadが行われます。つまりローカルで編集してすぐに表示確認ができるようになります。なおリロードがかかりますので画面遷移をしていたりすると、初期表示に戻る可能性がありますのでご注意ください。





ライブリロード実行



しかしこの開発のさくさく感はとても気持ちが良いと思います。Webシステムを開発しているのと変わらない感じではないでしょうか。



Monaca Cloudの使いどころ



ではローカルで開発できるようになったことで、Monaca Cloudの役割はどうなるでしょうか。Monaca Cloudはこれまでと変わらずiOS/Android/Windows Phone/Google App向けのビルド環境をクラウドで提供します。しかもMonaca Localkitからのアップロードに対応し、アプリケーションビルドもローカルから実行することができます。





プラグインの選択





ビルド設定





ビルドターゲット指定





ビルド





ビルド完了



これまでMonaca IDE上で行っていたほとんどの操作がMonaca Localkit上で行えるようになります。もはやWebブラウザいらずかも知れませんね。ビルドが終わったらQRコードからダウンロードしてインストールもできます。





バイスへのインストール






Monaca Localkitを使うことで、これまでのMonacaを使った開発スタイルが一変するのがお分かりいただけたかと思います。Webブラウザ上で開発することでいつでもどこでも開発できるというのがIDEの魅力ならば、Monaca LocalkitはLAN内だけで、スクラッチ&ビルドによる開発のスピード感が売りになるでしょう。



もちろん使い慣れたプログラミングエディタやIDEを使うことで生産性の向上も狙えます。ぜひMonaca Localkitをお試しください!

AngularJS 1.3のフォームまわりの機能強化

こんにちは中川です。

先日、AngularJS 1.3 がリリースされましたね。

動作速度の改善や、メモリ消費量の削減などパフォーマンス面での改善もうれしいところですが、
機能的にはフォーム関連の機能強化が特にうれしく感じましたので、紹介したいと思います。

■ ngModel.$validators



https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

ngModel.$validators を使うと、独自のバリデーション関数を簡単に定義することができるようになりました。

以下の例のように、入力値を引数で受け取り、返り値で真偽値を返す関数を$validatorsオブジェクトに定義します。
$validatorsのキー(ここではvalidCharacters)が、エラーメッセージ表示時などの参照用に利用できます。



ngModel.$validators.validCharacters = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value)  & &
       /[a-z]+/.test(value)  & &
       /[A-Z]+/.test(value)  & &
       /\W+/.test(value);
};


■ ngModel.$asyncValidators



サーバへの問い合わせが必要な場合など、非同期の処理がある場合のバリデーションにも対応しています。
以下の例のように、ngModel.$asyncValidators にpromiseを返す関数を定義します。



ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;

// Lookup user by username
return $http.get('/api/users/' + value).
   then(function resolved() {
     //username exists, this means validation fails
     return $q.reject('exists');
   }, function rejected() {
     //username does not exist, therefore this validation passes
     return true;
   });
};


今まで、パスワード確認入力やユーザー名重複のサーバ問い合わせなど、
controller(service)の値を組み合わせたバリデーションをdirectiveで
指定するのが面倒で、ui-validateというcontrollerの関数定義を指定できる外部モジュールを利用していました。
ui-vaidateでも今まで対応できていたので、別にそれでもいいといえばいいのですが、
しかし、1.3からは以下のような汎用的なdirectiveを定義すれば、とても素直に指定することができます。



app.directive('appValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appValidators: '=',
        },
        link: function (scope, elem, attrs, ctrl) {
            var validators = scope.appValidators || {};
            angular.forEach(validators, function (val, key) {
                ctrl.$validators[key] = val;
            });
        }
    };
});

app.directive('appAsyncValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appAsyncValidators: '='
        },
        link: function (scope, elem, attrs, ctrl) {
            var asyncValidators = scope.appAsyncValidators || {};
            angular.forEach(asyncValidators, function (val, key) {
                ctrl.$asyncValidators[key] = val;
            });
        }
    };
});


利用時は以下のように、controllerでオブジェクトをテンプレート側のapp-validators属性で渡せます。



app.controller('AppCtrl', function($scope) {
	$scope.user_name_validators = {
		hoge: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'hoge';
		},
		fuga: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'fuga';
		}
	};
});



<input type="text" ng-model="user_name" app-validators="user_name_validators">


■ ngMessages



https://docs.angularjs.org/api/ngMessages

フォームのエラーメッセージの表示対応について、
従来のng-showやng-ifで行う方法では、同時に複数エラーが出た場合にもひとつだけ表示するような制御が面倒でしたが、
ngMessagesを利用すると、ずいぶん簡単に記述できるようになりました。

ngMessagesを利用するには別途angular-messages.jsを読み込む必要があります。


<script src="angular.js"></script>
<script src="angular-messages.js"></script>

<form name="myForm">
<input type="text" ng-model="field" name="myField" required minlength="5" />
<div ng-messages="myForm.myField.$error">
  <div ng-message="required">You did not enter a field</div>
  <div ng-message="minlength">The value entered is too short</div>
</div>
</form>


■サンプル



これらの機能を利用したフォームのサンプルを作ってみました。
※ユーザー名:「aaaa」「bbbb」「cccc」を重複エラーとしています。




<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-messages.js"></script>
        <script src="app.js"></script>
        <style>
            .container { margin-top: 50px;}
        </style>
    </head>
    <body ng-controller="AppCtrl">

        <div class="container">
            <div class="row">
                <div class="col-xs-6">
                    <form name="userForm" novalidate ng-submit="submit()">
                        <div class="form-group" ng-class="{'has-error': userForm.user_name.$dirty  & & userForm.user_name.$invalid}">
                            <label class="control-label">ユーザー名</label>
                            <span class="help-inline text-danger" ng-messages="userForm.user_name.$error" ng-if="userForm.user_name.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="pattern">不正な値です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="duplicate">既に利用されているユーザー名です</span>
                            </span>
                            <input type="text" class="form-control"
                                   name="user_name" ng-model="user.user_name"
                                   required minlength="4" ng-pattern="/^[a-zA-Z0-9]+$/" app-async-validators="asyncValidators.user_name" />
                            <p class="help-block">※必須、英数字4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password.$dirty  & & userForm.password.$invalid}">
                            <label class="control-label">パスワード</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password.$error" ng-if="userForm.password.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="joe">ユーザー名と一緒はダメー</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password" ng-model="user.password"
                                   required minlength="4" app-validators="validators.password" />
                            <p class="help-block">※必須、4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password_confirm.$dirty  & & userForm.password_confirm.$invalid}">
                            <label class="control-label">パスワード(確認)</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password_confirm.$error" ng-if="userForm.password_confirm.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="confirm">パスワード確認が一致しません</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password_confirm" ng-model="user.password_confirm"
                                   required minlength="4" app-validators="validators.password_confirm" />
                        </div>

                        <button class="btn btn-primary btn-block" ng-disabled="userForm.$dirty  & & userForm.$invalid">送信</button>
                    </form>
                </div>
                <div class="col-xs-6">
                    <pre>user = {{user|json}}</pre>
                    <pre>userForm.$error = {{userForm.$error|json}}</pre>
                </div>
            </div>
        </div>

    </body>
</html>




(function() {
    'use strict';

    var app = angular.module('app', ['ngMessages']);

    app.controller('AppCtrl', function ($scope, $q) {
        // モデル
        $scope.user = {};

        // バリデータ
        $scope.validators = {
            password: {
                // ユーザー名とパスワードは一緒はダメ
                joe: function (modelValue, viewValue) {
                    var val = modelValue || viewValue;
                    var user = $scope.user || {};

                    return val != user.user_name;
                }
            },
            password_confirm: {
                // パスワード確認
                confirm: function (modelValue, viewValue) {
                    var user = $scope.user || {};
                    var val = modelValue || viewValue;

                    return val == user.password;
                }
            }
        };

        // 非同期バリデータ
        $scope.asyncValidators = {
            user_name: {
                duplicate: function (modelValue, viewValue) {
                    var users = ['aaaa', 'bbbb', 'cccc'];
                    var val = modelValue || viewValue;
                    return $q(function (resolve, reject) {
                        setTimeout(function () {
                            if (users.indexOf(val) === -1) {
                                resolve('ok');
                            } else {
                                reject('ng');
                            }
                        }, 1000);
                    });
                }
            }
        };

        // user_name != password判定のため
        $scope.$watch('user.user_name', function() {
            $scope.userForm.password.$validate();
        });

        // password == password_confirm判定のため
        $scope.$watch('user.password', function() {
            $scope.userForm.password_confirm.$validate();
        });

        // 送信ボタンイベント
        $scope.submit = function () {
            // 何も変更しないで、送信ボタン時にエラーを表示してあげる
            if ($scope.userForm.$invalid) {
                $scope.userForm.$setDirty();
                return;
            }

            // 成功!!
            console.log($scope.user);
            alert('成功');
        };
    });

    /**
     * validators
     */
    app.directive('appValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appValidators: '=',
            },
            link: function (scope, elem, attrs, ctrl) {
                var validators = scope.appValidators || {};
                angular.forEach(validators, function (val, key) {
                    ctrl.$validators[key] = val;
                });
            }
        };
    });

    /**
     * asyncValidators
     */
    app.directive('appAsyncValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appAsyncValidators: '='
            },
            link: function (scope, elem, attrs, ctrl) {
                var asyncValidators = scope.appAsyncValidators || {};
                angular.forEach(asyncValidators, function (val, key) {
                    ctrl.$asyncValidators[key] = val;
                });
            }
        };
    });

})();


今回のバージョンアップで、ますます使いやすくなったと思いますので、
ぜひみなさん試してみてはいかがでしょうか。