2015.01.22

ECMAScript 6 で Backbone.js を利用してみる


こんにちは。社内で開発をしている D.M. です。

前回は次期 Javascript の仕様である ECMAScript 6 の新機能を利用して既存のコードを書き換えることに挑戦しました。今回は ECMAScript 6 で書かれた JS の モジュールに MVC フレームワーク Backbone.js を適用してみたいと思います。


今回利用している ECMAScript 6 の機能


前回は ECMAScript 6 の以下の機能を利用しました。
class 修飾子
for of 構文
Map 変数
let 変数

今回は以下の機能を利用して、 Backbone.js との連携を図っていきます。
class の継承(extends)
アロー関数構文
const 変数

class の継承(extends)


現状 Backbone.js の各モジュールを利用する際には extends ファンクションにより継承するのですが、 ECMAScript 6 では言語仕様的に class と extends が定義されているため、他の言語で見られるような継承の形式で記述することができます。

Backbone.js についてはフレームワークの詳細を掘り下げずに、コアな機能についてだけ触れたいと思います。
Backbone.js では以下のモジュールを継承して画面ごとに MVC を形成します。

◆Model
・個別のデータを格納する役割があります。
・ defaults ファンクションによって扱う内部で変数を定義して利用します。
◆Collection
・ Model のデータをまとめて扱う役割があります。
・ url を設定しておくと fetch ファンクションで取得を行うことができます。取得したデータは自動的に Model にセットされます。
・ fetch が正常完了した際( success の場合)、コールバックファンクションを実行できます。今回は取得したデータで View の書き換えを行うファンクションを定義しています。
◆View
・ Model と Collection を利用して HTML の描画を行う役割があります。
・今回はユーザ操作に関わる onclick イベント等のバインドを行っています。

Backbone.js で定義されている各モジュールのファンクションは、 ECMAScript 6 の文法に則って記述することで、従来と同じ機能を実現することができます。

アロー関数構文


アロー関数構文は無名関数の略式表記です。 function や return キーワードを省いて簡単に記述できるメリットがあります。 (x) => {} と書くだけで関数を作ることができます。

//今までの書き方 
var fn = function (x, y) { return x*y };
//ES6での書き方
var fn = (x, y) => x*y;

アロー関数は他に以下の特徴があります。
・初期化のためのコンストラクタがない。
・引数を扱う arguments オブジェクトがない。
・ this 参照が呼び出し元から固定となる( self = this を書かないでも大丈夫になります)

const 変数


いわゆる定数を定義することができます。
変更することができません。

//円周率を固定値で定義する。
const PI =3.14;


ES6 モジュールローダー


前回のコンパイラ 6to5 を利用し、 ECMAScript 5 の形式に変換してからブラウザで実行しました。今回は Google の traceur.js と動的モジュールローダ es6-module-loader.js を利用して、 ES6 で書かれたコードをそのままブラウザで実行してみようと思います。

検証サイト
http://9199.jp/js-test/es6-test-2.html

<head>
<script src="/common/js/traceur.js" type="text/javascript" ></script>
<script src="/common/js/es6-module-loader.js" type="text/javascript" ></script>
</head>

body タグの中で今回使用した JS を読み込みます。


<script>
    System.import('common/js/pref_line_es6_2');
</script>


※ファイル名は pref_line.js ですが拡張子.jsをつけない形式で書きます。相対パスで記述します。

HTML 側にはこれだけを記述し、JS ファイル内部に ES6 のコードを全部集約して実行します。

pref_line.js の中身です。

//定数に分割代入する
const { Model, View, Collection } = Backbone;

class Prefecture {

    constructor(prefCode,prefName){
        this.prefName = prefName;
        this.prefCode = prefCode;
    }
    
    initLineList( callbackFn ){
        this.lineList = new LineList( this.prefCode );
        this.lineList.init( callbackFn );
    }

}

class PrefectureList{
  constructor( prefSelectBox, callbackRenderFn ){ //prefList  の初期化
      this.prefList = [];
      var map = new Map( [[1,"北海道"], [2,"青森県"]] );
      for (var [code, name] of map){ 
          this.prefList.push(new Prefecture( code,name ));
      }
      callbackRenderFn( this.prefList , prefSelectBox );
  }
  change(prefCode, callbackFn ){   //Prefectureが変更されたらLineを取得し初期化する
      console.log( prefCode ) ;
      this.selectPref = this.prefList[prefCode];
      console.log( this.selectPref ) ;
      this.selectPref.initLineList( callbackFn );
  }

  getPrefLineList(){
      return this.selectPref.lineList;
  }

}

//継承!!!
class Line extends Model{
    defaults() {
        return {
            line_name : "",
            line_cd : ""
        };
    }
    
}

//継承!!!
class LineList extends Collection{

   constructor( prefCode ){
       super();
       this.prefCode = prefCode;
       this.url = this.requestUrl(prefCode);
       this.model = Line;
   }

   requestUrl( prefCode  ){
       if($.browser.mozilla)   return '/common/json/station/line_mozilla/' + prefCode + '.json';
       else                    return '/common/json/station/line_ie/' + prefCode + '.json';
   }

   init( callbackFn ){
       console.log("initLineList this");
       console.log(this);
//       this.fetch( { success : function(){ callbackFn(); } } );
       this.fetch( { success : () =&gt; callbackFn() } );
   }

   parse(json){
        console.log(json);
        return json.line;
   }

}

class ListView extends View{

    constructor( prefSelectBox , lineSelectBox , stationSelectBox ) {
        this.prefList = new PrefectureList( prefSelectBox, this.renderPrefList );
        this.lineSelectBox = lineSelectBox;
        this.stationSelectBox = stationSelectBox;
        self=this;
        $( '#'+ prefSelectBox  ).change( function(){ 
            console.log( self );
            self.prefList.change( $(this).children(':selected').val(), self.renderLine );
             
        } ) ;   
    }

    //県リストのHTML描画
    renderPrefList( prefList , prefSelectBox ){
 
      var prefBox = document.getElementById( prefSelectBox );
 
      //初期化
      var prefIdx = prefBox.options.length;
      for ( var i = 0 ; i <= prefIdx; i++ )   prefBox.options[0] = null;
      prefBox.options[0] = new Option("県名を選択", 0);
 
        var count = 0;
        prefBox.options[count++] = new Option("路線を選択", "0");
        $.each( prefList, function(key,pref){
                if( pref ){
                        prefBox.options[count++] = new Option(pref.prefName,pref.prefCode);
                }
        });
    }

    //路線リストのHTML描画
    renderLine( ){
      
      if(!self){ self = this ;}

      console.log(&quot;renderLine&quot;);
      console.log( self );
      var line_box = document.getElementById( self.lineSelectBox  );
      var station_box = document.getElementById( self.stationSelectBox  );

      //初期化
      var line_idx = line_box.options.length;
      var staion_idx = station_box.options.length;
      for ( var i = 0 ; i &lt;= line_idx; i++ )  line_box.options[0] = null;
      for ( var i = 0 ; i &lt;= staion_idx; i++ )    station_box.options[0] = null;
      station_box.options[0] = new Option("駅名を選択", 0);

      //選択されたPrefにおうじてListBoxのOptionを初期化する
      if ( self.prefCode === 0){
          line_box.options[0] = new Option("路線を選択", 0);
      }else{
          var count = 0;
          line_box.options[count++] = new Option("路線を選択", 0);

          var lineList = self.prefList.getPrefLineList();
          console.log(&quot;models&quot;);
          console.log(lineList.models);
          _( lineList.models ).each( function( l ){
              if( l ){
                  line_box.options[count++] = new Option(l.get('line_name'),l.get('line_cd'));
              }
          });
      }
    }
}

//アロー関数構文で jQuery の画面初期処理 $(function(){}); を記述!!!
$(() => {
    new ListView( "stationpref" , 'linecode' , 'stationcode' );
});

ECMAScript 6 を利用して Backbone.js を実行することができました。

今後の展望


ECMAScript 6 を利用しても既存のJSフレームワークと連携することができることが確認できました。今後はHTMLの新機能である Web Componets や Polymer と連携していくことに挑戦してみたいと思います。


次世代システム研究室では、グループ全体のアプリケーション開発を担うアーキテクトやアプリケーションエンジニアを募集しています。Webアプリケーション開発経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。