ES6のクラスとモジュールを使ってMVを実装してみる


jQueryを呪文のように書いていて以来フロントエンドは時が止まった状態だったけど先日ES6を入門した.
モダンJavascriptの技術背景をいくつかキャッチアップしてみたので、まだまだなところはあるけど学習したものを公開します.

Goal

  • ブラウザで動くアプリ
  • よくあるフォームに入力した項目がリストに表示されるサンプルを作成する
  • gulp, babel, browserifyでES6 => ES5にコンパイルする

成果物

ES5のサンプルコード
https://github.com/metheglin/js-module-challenge/tree/es5-module

ES6のサンプルコード
https://github.com/metheglin/js-module-challenge/tree/es6-module

ディレクトリ構成

はじめに

モダンjavascript環境を作るには、プロジェクトを作成、直下に移動してnpm initするとよい.
package.jsonが作成され、ライブラリの依存を管理してくれる.
gulpやgulpのタスクで使われるライブラリはnpm install --save-dev $LIBRARY_NAMEでインストールする.
-gオプションをつけずにnpm installするとカレントディレクトリのnode_modules以下にインストールされる.
--save-devと付与してインストールすると、package.jsondevDependenciesに開発用依存ライブラリとして自動で登録される.
package.jsonを保存しておけば、node_modulesを消してもnpm installすると依存ライブラリを取得することができる.

上のサンプルコードを落とせばpackage.jsonはコミットしてあるのでnpm installするだけで環境を再現できる.

gulpタスクについて

gulpfile.js
var gulp        = require("gulp");
var browserify  = require("browserify");
var babelify    = require("babelify");
var source      = require("vinyl-source-stream");

var assets_path     = "public/assets/";
var assets_src_path = "public/assets/src/";

gulp.task("browserify", function(){
  browserify( assets_src_path + "js/main.js")
    .transform( babelify )
    .bundle()
    .on("error", function(err) { console.log("Error: " + err.message); })
    .pipe( source("main.js") )
    .pipe( gulp.dest( assets_path + "/javascripts/" ) )
});

gulp.task("watch", function(){
  gulp.watch( assets_src_path + "js/*.js", ["browserify"] );
});

gulp.task("default", ["browserify", "watch"]);

browserifyはブラウザでモジュール記法をサポートするツール. ビルド時にrequire部分を書き換えてブラウザでも動くようになるらしい.
babelifyはbrowserifyでbabelを動かすツール. babelはES6=>ES5変換をする.
vinyl-source-streamはbrowserifyのストリームをvinylオブジェクト(gulp用のオブジェクト?)に変換するツール.

アプリケーションロジックを記述するjavascriptはpublic/assets/src/js以下に配置する. コンパイル対象にするのはmain.jsのみ. main.jsは依存するモジュールをrequire(ES5)やimport(ES6)で読み込む.
コンパイルされたmain.jspublic/assets/javascripts以下に配置され、htmlのscriptタグで読み込まれる.

Model Viewについて

フレームワークを使わずに簡単なものをObserverパターンで実装する.

Customer.js
import EventObserver from "./EventObserver";

export default class Customer {

  constructor( obj ) {
    this.name = obj.name;
  }

  static add( name ) {
    var customer = new Customer({ name: name });

    Customer.list.push( customer );
    this.trigger( "add", [customer] );
  }
}

Customer.list = [];

$.extend( Customer.prototype, EventObserver.prototype )
$.extend( Customer, EventObserver.prototype )

CustomerFormView.js
import Customer from "./Customer";

export default class CustomerFormView {
  constructor( $element ) {
    this.$element = $element;
    this.$input   = this.$element.find( "#input" );
    this.$element.submit( this.onsubmit.bind(this) );
  }

  onsubmit( e ) {
    e.preventDefault();
    Customer.add( this.$input.val() );
  }
}
CustomerListView.js
import Customer from "./Customer";

export default class CustomerListView {
  constructor( $element ) {
    this.$element = $element;
    Customer.on( 'add', this.add.bind(this) );
  }

  add( customer ) {
    var item = $('<li>' + customer.name + '</li>');
    this.$element.append( item );
  }
}

References