更新ボタンも押すのが面倒な人のためのJavaScript 開発環境


最近、気にっている JavaScript の開発環境を紹介したいと思います。
Yeomen をベースラインとしたAnguler JSとNode JSの環境です。Yeomenが生成するGruntベースの環境は、ソースコードWatchされており、ソースを変更すると自動的にブラウザが更新され、ソースの変更がブラウザに反映されます。それにNodeJSも組み込み、
サーバサイドの開発もできるようになったので、重宝しています。

1. Yeomenの Angular JS の環境を作る

Yeomen をインストールして、

 yo angular:app

を実行すると、

├── Gruntfile.js
├── app
├── bower.json
├── karma-e2e.conf.js
├── karma.conf.js
├── node_modules
├── package.json
└── test

という、ディレクトリ構成の Yeomen の Angular JSの環境が構築されます。

そして、

grunt server

と実行すると、Web Server が立ち上がり、Angular JS アプリの雛形となる画面が表示されます。

Yeomen を使って作成した環境が便利なのは、Grunt の設定が予め入っているので、いろいろと便利です。その一つが、livereload機能で、 HTMLやJavaScriptを編集すると、自動的にブラウザーの画面がリフレッシュされ、更新ボタンを押す必要はありません。

2. Yeomenの Angular JS 環境と Node JSを連携させる

Grunt の設定を書き換えると、Livereloadを Node JS でも利用することができます。

Expressを組み込む

はじめに、Expressを組み込むために、package.json

{
 ....
  "dependencies": {
    "karma-script-launcher": "~0.1.0",
    "karma-firefox-launcher": "~0.1.0",
    "karma-chrome-launcher": "~0.1.0",
    "karma-html2js-preprocessor": "~0.1.0",
    "karma-jasmine": "~0.1.3",
    "karma-requirejs": "~0.1.0",
    "karma-coffee-preprocessor": "~0.1.0",
    "karma-phantomjs-launcher": "~0.1.0",
    "karma": "~0.10.2",
    "grunt-karma": "~0.6.1",
    "express": "latest",
    "ejs": "*"
  },
 .....
}

と、dependenciesにexpressとejbを追加します。

次に、下記のようなexpressのプログラムを作成し、app.jsとして保存します。

var express = require('express');
var http = require('http');
var path = require('path');

var app = express();

app.set('port', process.env.PORT || 3000);
app.use(express.bodyParser());

// フォルダを静的コンテンツのフォルダとする	
app.use(express.static(path.join(__dirname, 'dist')));

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

静的コンテンツのフォルダは、"dist"にしてください。

そして、

 grunt build

と、実行し、JavaScriptCSSコンパイルしたら、

 node app

と実行すると、Express経由で、Angular JSの静的コンテンツを参照することができます。

しかし、これでは、Livereloadとは連携できていません。

Grunt 経由で Expressを起動する

package.json

  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-coffee": "~0.7.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-contrib-compass": "~0.3.0",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-cssmin": "~0.6.0",
    "grunt-contrib-connect": "~0.3.0",
    "grunt-contrib-clean": "~0.4.1",
    "grunt-contrib-htmlmin": "~0.1.3",
    "grunt-contrib-imagemin": "~0.1.4",
    "grunt-contrib-watch": "~0.4.0",
    "grunt-usemin": "~0.1.11",
    "grunt-svgmin": "~0.2.0",
    "grunt-rev": "~0.1.0",
    "grunt-karma": "~0.4.3",
    "grunt-open": "~0.2.0",
    "grunt-concurrent": "~0.3.0",
    "matchdep": "~0.1.2",
    "connect-livereload": "~0.2.0",
    "grunt-google-cdn": "~0.2.0",
    "grunt-ngmin": "~0.0.2",
    "grunt-express-server": "latest"
  }

と、grunt-express-serverを追加し、Gruntfile.js に express-serverの設定を、

  .... 
  // Expressの設定を追加
  grunt.initConfig({
    express: {
      options: {
        // Override defaults here
      },
      dev: {
        options: {
          script: 'app.js'
        }
      },
    },
    yeoman: yeomanConfig,
  .....
    // expressのリロードの設定を組み込む
    watch: {
      express: {
        files:  ['routes/{,*/}*.js', 'app-client/{,*/}*.js','app.js' ],
        tasks:  [ 'express:dev'],
        options: {
          nospawn: true
        }
      },
  .....
  // Serverコマンドにexpressを組み込む
    grunt.registerTask('server', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'open', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'concurrent:server',
      'connect:livereload',
      'express:dev',
      'open',
      'watch'
    ]);
  });

と設定を追加し、grunt server コマンドで express を起動するようにします。
これで、app.jsを変更すると、自動的にexpressは再起動され、変更が反映されます。

しかし、expressが別のポートで立ち上がっているので、grunt serverのコンテンツとは、クロスドメインとなってしまっています。

Proxy 経由でExpressにアクセスする

同じポートでアクセスするためには、Grunt に proxy の設定を追加します。
まず、package.json

  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-coffee": "~0.7.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-contrib-compass": "~0.3.0",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-cssmin": "~0.6.0",
    "grunt-contrib-connect": "~0.3.0",
    "grunt-contrib-clean": "~0.4.1",
    "grunt-contrib-htmlmin": "~0.1.3",
    "grunt-contrib-imagemin": "~0.1.4",
    "grunt-contrib-watch": "~0.4.0",
    "grunt-usemin": "~0.1.11",
    "grunt-svgmin": "~0.2.0",
    "grunt-rev": "~0.1.0",
    "grunt-karma": "~0.4.3",
    "grunt-open": "~0.2.0",
    "grunt-concurrent": "~0.3.0",
    "matchdep": "~0.1.2",
    "connect-livereload": "~0.2.0",
    "grunt-google-cdn": "~0.2.0",
    "grunt-ngmin": "~0.0.2",
    "grunt-express-server": "latest",
    "grunt-connect-proxy": "latest"
 },
||< 
と、 "grunt-connect-proxy"を追加します。
次に、Gruntfile.jsを開き、connect-proxyの設定を,
>|javascript|
// 
var proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest;

 .....

   connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost'
      },
      // /api 以下のリクエストを 3000 ポートにする
      proxies: [{
        context: '/api',
        host: 'localhost',
        port: '3000',
        https: false,
        changeOrigin: false
      },],
      // proxySnippetを組み込む
      livereload: {
        options: {
          middleware: function (connect) {
            return [
              lrSnippet,
              proxySnippet,
              mountFolder(connect, '.tmp'),
              mountFolder(connect, yeomanConfig.app)
            ];
          }
        }
      },

 ....
 // proxy を grunt server コマンドに組み込む
    grunt.task.run([
      'clean:server',
      'configureProxies',
      'concurrent:server',
      'connect:livereload',
      'express:dev',
      'open',
      'watch'
    ]);
  });

と、組み込みます。

そして、

 grunt server

と、実行し、app.js を

var express = require('express');
var http = require('http');
var path = require('path');

var app = express();

app.set('port', process.env.PORT || 3000);
app.use(express.bodyParser());

app.use(express.static(path.join(__dirname, 'dist')));

// Expressの実装を追加する
app.get("/api/test",function(req,res){
	res.send("test");
})

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

と、/api/test で test と返すような実装を追加し、ファイルを保存すると、Expressは自動的に再起動され、http://localhost:9000/api/testにアクセスすると、Proxy経由でtestと返されます。

作成したプロジェクトは、https://github.com/takeshi/yeoman-angular-express-sample にアップしました。

Yeomenの使い方は下記の本で紹介されています。

AngularJS

AngularJS

Closure TemplateがJavaScriptのテンプレートエンジンでいい気がしてきた

Closure TemplateをEclipseで簡単に自動ビルドさせる方法を見つけた。Eclipse上でJavaScriptの開発を行うなら、これが最高のJavaScriptのテンプレートな気がしてきた。

JavaScriptを生成する

Closure Templateを使うとテンプレートからJavaScriptが生成されるので、JavaScriptが読めれば、動作を確認することができる。

例えば、

{namespace sample}

/**
* Sample Template
* @param name
*/
{template .helloWorld}
<div>
 {$name} Hello World.
</div>
{/template}

コンパイルすると以下のJavaScriptが生成される。

goog.provide('sample');

goog.require('soy');
goog.require('soy.StringBuilder');


sample.helloWorld = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div>', soy.$$escapeHtml(opt_data.name), ' Hello World.</div>');
  return opt_sb ? '' : output.toString();
};

JavaScriptが生成されるので処理の流れが掴みやすく理解しやすい。

手間なのはコンパイルの実行

Closure TemplateはJavaScriptが生成されるので、JSRender等のJavaScriptテンプレートに比べると、コンパイルの実行に一手間かかるので、コーディングのリズムが崩れる。「ファイル保存(ctlr-s)」 →「 更新(F5)」というリズムでの開発がいい。

Eclipse のAnt BuilderでClosure Templateのコンパイルを自動化する

Ecilpseにはファイル保存のイベントをフックして、Antを実行することができるAnt Builderという仕組みがある。これを使うとClosure Templateのコンパイルを簡単に自動化できる。

Ant Builderを利用するためには、はじめにbuild.xmlを用意する。ポイントは、パラメータ化したい変数をプロパティとしておくこと。下記のbuild.xmlでは、targetをパラメータ化している。

<?xml version="1.0"?>
<project basedir="." default="soy_to_javascript">
	<target name="sample">
		<property name="target" value="sample.html" />
		<antcall target="_create_soy" />
	</target>

	<target name="soy_to_javascript">
		<java jar="SoyToJsSrcCompiler.jar" fork="true">
			<arg value="--outputPathFormat" />
			<arg value="script/${target}.js" />
			<arg value="--shouldProvideRequireSoyNamespaces" />
			<arg value="soy/${target}" />
		</java>
	</target>
</project>
  • 手順2. Ant Builderを登録する

Ant Builderを登録するには、Projectを選択し、メニューの「project」を選択し「properties」をクリックし、プロジェクトのプロパティを表示し、Buildersを選択する。

Buildersの「New」ボタンをクリックし、「Ant Builder」を選択する。

そして、Edit Configurationのダイアログが開いたら、「Buildfile」に手順1で作成したbuild.xmlを、「Arguments」に「-Dtarget=${resource_name}」と設定する。AntのJVMの起動オプションで-Dでシステムプロパティを設定すると、${target}のフォーマットでプロパティを参照することができる仕組みを利用する。

「Targets」タブを選択し、「Manual Build」のSetTargetをクリックする。

ここで、ファイル保存時に実行するAnt Taskを指定することができる。targetがパラメータ化されているAnt Taskを指定する。

次に「Build Options」のタブを選択し、「Specific Resources」をクリックする。



「Specific Resources」でコンパイル対象のリソースを選択することができるので、Closure Templateのファイルを格納しているディレクトリを選択する。

これで設定完了。

  • 動作確認

soyディレクトリ以下にあるファイルを編集し、保存すると自動的にAnt Builderが動き、Closure Templeteのファイルがコンパイルされる。

まとめ

Closure Templateを使うと生成されるJavaScriptを確認することができるので、テンプレートの挙動を簡単に確認することができます。また、デバッグが簡単です。それにClosure Toolsのファミリーなので、Closure Compilerとも連携しており、ADVANCED_OPTIMIZATIONSでも利用可能です(JSRenderはADVANCED_OPTIMIZATIONSにすると、動かなくなります)。

Closure Templateの欠点はコンパイルの手間でしたが、Eclipseを利用すれば自動ビルドさせることもできるので、それほど手間ではなくなりました。

おまけ

EclipseJavaScript Editorは、Closure LibraryのJavaScriptの開発方法と相性がいいですよね。例えば、下記のようにJSDocに@constructorと付けると、

/**
 * @constructor
 */
sample.HelloWorld = function(){
};

Eclipseがsample.HelloWorldをクラスとして認識するようになり、コード補完も有効になる。Closure Tools × Eclipseはなかなか便利です。

Closure Tools, JSRender, JSView, Backbone.js, JQuery UI,

いろいろなライブラリ、フレームワーク、ツールが必要だった。
"ちゃんと"JavaScriptでWeb Applicationで開発するには、いろいろと準備する必要がある。
最近、Amazon S3のWeb Consoleがバグっていたので、Amazon S3のブラウザをChrome Extension「S3 Tree」を作っていた。参考にしていたのは、下記の本。


JavaScript Web Applications: jQuery Developers' Guide to Moving State to the Client

JavaScript Web Applications: jQuery Developers' Guide to Moving State to the Client

Web Application開発に必要なもの

Web Applicationを、"ちゃんと"作ろうとすると、次の5つが足りない。
- モジュールロード
- テンプレート エンジン
- ViewとModelのバインディグ
- MVCフレームワーク
- UI Component

Javaもそうだけど、いくつものフレームワークやライブラリがある。フレームワークやライブラリに何を使うかを決めることから始めなければならない。しかも、Javaよりもたちが悪く、デファクト・スタンダードと呼べるものがなさそう。「JavaScript Web Applications」の中でも、"Great Alternative"が何回も登場する。どれを使えばよいか分からない。いろいろ調べながら開発を進めていた。最後は、Closure Tools, JSRender, JSView, JQuery UI, Backbone.jsを組み合わせたカタチになっていた。

モジュールロード「Closure Tools」

JavaScriptはモジュールロードする仕組みがなく、自分で用意しなければならない。「JavaScript Web Applications」では、サーバサイドJavaScriptの標準仕様を作ろうとしているCommonJSの仕様ベースのYabbleRequiteJSが紹介されていた。使ってみたが、非同期でモジュールを読み込むので、モジュールのロードと実行のタイミングが入れ替わるとエラーが発生する。多段にモジュール読み込みがあると使い物にならない。

それで、Closure Toolsを使うことにした。Closure ToolsはライブラリからテストツールまでそろったAll In Oneのツールと思っていたが、モジュールロードの仕組みを使うだけであれば、goog/base.jsとcalcdeps.pyだけで、使うことができる。Closure Toolsは一手間かかるが、現実的なアプローチでモジュールのロードをする。

  • モジュールの依存関係の定義

goog.provideとgoog.requireでモジュールの依存関係を定義していく。goog.provideが公開するための名前、goog.requireが依存しているモジュールの名前となる。

 goog.provide("s3.app");
 goog.require("s3.load");
  • 自動生成されるファイル

上記のようにモジュールの依存関係を定義しておき、calcdeps.pyを動かすと、モジュール依存関係を表すdeps.jsを生成することができる。

goog.addDependency("../../s3/app.js", ['s3.app'], ['s3.load', 'downtown.EventBus', 's3.view.login', 's3.view.bucketselector', 's3.view.treeview', 's3.view.listview', 's3.view.S3Router']);
goog.addDependency("../../s3/deps.js", [], []);
goog.addDependency("../../s3/load.js", ['s3.load'], ['downtown.Loader']);

この自動生成されるdeps.jsがあることで、起動時にすべての依存モジュールを読み込むことができる。非同期読み込みの問題は発生しない。さすがGoogleのツールだ。

モジュールのロードが使えるようになることでファイルの分割が簡単になり、1クラス1ファイルで管理することができる。

こんなファイルになる。

goog.provide("downtown.EventBus");
/**
*@constructor
*/
downtown.EventBus = function() {
  this.dispacher = jQuery("<div></div>");
};

(function($) {

  downtown.EventBus.prototype.bind = function(type, data, callback) {
    this.dispacher.bind(type, data, callback);
  };
  downtown.EventBus.prototype.trigger = function(type, data) {
    this.dispacher.trigger(type, data);
  };

}(jQuery));

テンプレート エンジン「JSRender」 ViewとModelのバインディグ「JSView」

ここはミーハーに、JSRenderJSViewを使ってみて、機能としては、不便はなかったのでそのまま利用した。

JSRenderは1つだけ使い方を工夫して、HTMLファイルを独立して管理できるようにした。

  • 別のファイルからHTMLのテンプテートを読み込む
  var loadedHtml = {};
  downtown.Loader.loadHtml = function(src, callback) {
    var path = settings["basePath"] + src + ".html";
   // すでに読み込んでいたら読み込まない。
    if (loadedHtml[src]) {
      // コールバックは非同期で
      setTimeout(function() {
        callback(src);
      }, 0);
      return;
    }
    // HTMLをロードする
    $.ajax({
      async : true,
      dataType : "text",
      url : path,
      success : function(data) {
        loadedHtml[src] = true;
        // JSRenderに登録する
        $.template(src, data);
        callback(src);
      }
    });
  };

このメソッドがあると、下記のようなコードでHTMLをファイルから読み込むことができる。

  • HTMLファイルのロード
downtown.Loader.loadHtml("s3/view/bucketselector/list", function(src) {
      var html = $($.render(self.model, src));
      html.appendTo(self.el);
      self.bindEvent();
    });

MVCフレームワーク「Backbone.js」

ここまでで、JavaScriptファイルとHTMファイルを分割して開発を進めることができる仕組みが準備でいた。次はプログラムに構造を持たせることができるフレームワークを導入した。
JavaScript Web Application」では、Backbone.jsの他にもknockout.jsJavaScriptMVCが紹介されていた。
knockout.jsは使いやすそうでしたがMVVMモデルのため、HTMLにロジックを持たせることになりそうだったので辞めました。MVVMは画面の開発ツールが"しっかり"していないと、使いこなせない気がします。
JavaScriptMVCはクラスの宣言が独特でした。

  • JavaScriptMVCのクラスの宣言
$.Model("Todo",{ findAll : "/recipes" }, {});
Todo.findAll(function(todos){ ... });

クラスの宣言が独特だと、Closure ToolsやEclipseと相性が悪くなるので辞めました。

  • Closure ToolsとBackbone.jsを組み合わせる

Closure Toolsのクラスの宣言方法に従って、Backbone.jsのクラスを使えば、組み合わせることができます。

/**
 * @constructor
 */
s3.view.bucketselector.BucketSelectView = function(attr) {
  var self = this;
  // Backbone.jsのベースクラスのコンストラクタを呼び出す
  Backbone.View.call(this, attr);
  this.eventBus = new downtown.EventBus();
};
// Backbone.jsのベースクラスを継承する。
goog.inherits(s3.view.bucketselector.BucketSelectView, Backbone.View);

Closure Toolsのクラスの宣言方法は、Eclipseとも相性が良くEclipseでコード補完も使えるようになり便利です。

Backbone.jsのモジュール構造

S3 Treeでは有効に使えなかったですが、Backbone.jsはJavaScriptでのWeb ApplicationのMVCの考え方を教えてくれます。Backbone.jsを使って開発を進めると、ModelとViewとRouterを作ることになります。

  • Model

Modelはサーバと接続部分を担当し、通信前のバリデーションのチェックやXMLデータのパース、aJaxリクエストの送信を担当します。

  • View

HTMLのDomとのやりとりはViewに実装します。Viewはthis.elでDomのエレメントを、this.modelでModelを持ち、ModelとテンプレートからのHTMLの生成や、ModelとDomのデータのバインディング、DOMイベントのバインディング等、ユーザインタフェースに関係する処理をすべてViewが担当します。

  • Router

aJaxのアプリケーションは戻るボタンとの連携ができない問題があります。Routerを使うと戻るボタンとの連携ができます。
aJaxアプリケーションで戻るボタンを利用するには、window.location.hashを利用します。hashを利用すると画面遷移なしにURLを変更することができ、戻るボタンとも連携することができるため、aJaxアプリケーションで戻るボタンやブックマークが使えるようになります。
Routerはhashの変更のイベントを受け取り、hashの種類ごとに実行するロジックを振り分けを行います。
# ただし、hashに"/"が入っているとRouterの振り分け機能が動作しなかったので、
# 利用するのは辞めました。
# 同じような処理は、jQueryで$(window).hashchange(function(event){})を
# 実装すればできます。

  • Backbone.jsを使うと、

Viewに複雑になりがちな画面イベントのハンドリングやテンプテートを寄せることができ、Modelに通信部分を寄せることができるので、役割をキレイに分離します。Modelはサーバを見ながら設計し、Viewは画面とModelの関係から設計するので、通信と画面が分離されて開発しやすくなります。

  • 実際のViewのコード
goog.provide("s3.view.login.LoginView");

goog.require("downtown.Loader");
goog.require("downtown.EventBus");
goog.require("s3.model.LoginUser");

/**
 * @constructor
 */
s3.view.login.LoginView = function(attr) {
  this.model = new s3.model.LoginUser();
  Backbone.View.call(this, attr);
  this.eventBus = new downtown.EventBus();
  if (attr.eventBus) {
    this.eventBus = attr.eventBus;
  }
};
goog.inherits(s3.view.login.LoginView, Backbone.View);

(function($, undefined) {
  /**
   * 画面の表示
   */
  s3.view.login.LoginView.prototype.render = function() {
    var self = this;
    downtown.Loader.loadHtml("s3/view/login/login", function(src) {
      var html = $($.render({}, src));
      // テンプテートからHTMLを生成し、画面に描画する。
      $(self.el).html(html);
      var userJson = localStorage["s3.login.user"];
      if (userJson) {
        self.model = new s3.model.LoginUser(JSON.parse(userJson));
      }
      // ModelとViewをバインドする
      html.link(self.model);
      self.bindEvent();
    });
  };

  /**
   * 画面のイベントをバインドする
   */
  s3.view.login.LoginView.prototype.bindEvent = function() {
    var self = this;
    // loginボタンのクリック
    $(this.el).find(".login").click(function(e) {
      if (self.clicked) {
        return;
      }
      self.clicked = true;
      self.model.accessKey = self.model.accessKey.trim();
      self.model.secretKey = self.model.secretKey.trim();
      self.model.loadBuckets({
        success : function(buckets) {
          self.eventBus.trigger("buckets_select", buckets);
          $(self.el).children().remove();
        },
        error : function(req, status) {
          alert("Login Error status:" + status);
          self.clicked = false;
        }
      });
    });
    // saveボタンのクリック
    $(this.el).find(".save").click(function(e) {
      localStorage["s3.login.user"] = JSON.stringify(self.model);
    });
  };

}(jQuery));

UI Component 「jQuery UI」

jQueryを利用するとViewをカプセル化することがことができ、Viewを部品化するのに便利でした。
jQuery UIのUI Compoentを作るのは簡単です。

goog.provide("s3.view.login");

goog.require("s3.view.login.LoginView");

(function($) {
  // $(xxx).s3login()で呼び出せるように登録する
  $.widget("ui.s3login", {
    // 外部からのパラメータ
    options : {
       eventBus:null
    },
    // 初期化ロジック
    _create : function() {
      // Backbone.jsのViewにブリッジする
      this.view = new s3.view.login.LoginView({
        el : this.element,
        eventBus:this.options.eventBus
      });
      this.view.render();
    }
  });

}(jQuery));

Viewを実装していれば、jQuery UIからBackbone.jsへディスパッチするコードを書くだけです。jQuery UIで登録しておけば、次のコードで呼び出せます。

  • UI Compoentの呼び出し
  var eventBus = new downtown.EventBus();
  $("#login").s3login({
      eventBus : eventBus
   });

Backbone.jsの作法を知らなくても、Viewを利用することができるようになります。
Viewの公開インタフェースとして、UI Compoentを利用するのは便利ではないかと思います。

まとめ

紆余曲折した結果、モジュールロード「Closure Tools」、 テンプレート エンジン「JSRender」、ViewとModelのバインディグ「JSView」、MVCフレームワーク「Backbone.js」、 UI Component「jQuery UI」、と様々なツールを使うことになりました。ツールの組み合わせで罠があるので注意が必要ですね。