babelify で ES6 を試す
React v0.13.0 から ECMAScript 6 (以下 ES6) の class
によるコンポーネント定義がサポートされた。また Node v0.12 でも一部の機能が有効となっている。このように ES6 もだいぶ身近になってき。先行学習もかねて趣味の開発では積極採用してゆきたい。
とはいえある程度は広範な環境で動いてほしい。そのため実装は ES6 でおこない実行用スクリプトは ES5 に変換したものを使用する。これを実現するため Browserify transform の babel/babelify を試す。
Babel
Babel は ES6 として記述されたスクリプトを ES5 に変換するためのツールである。以前は 6to5 と呼ばれていたが諸事情により名称変更したそうだ。ES6 機能を先取りするという意味で AltJS だと TypeScript が近いのかもしれない。
Babel は CLI を提供しており、npm で配布されている。単体利用するのであれば、これをインストールして、
$ npm i -g babel
以下のようにコンパイルする。CLI 版のオプションはこちらを参照のこと。
$ babel script.js --out-file script-compiled.js
Browserify と babelify
babelify は Babel の Browserify transform 版になる。Browserify は JavaScript 間の依存管理だけでなく transform により AltJS や React の JSX など様々な形式のスクリプトを標準的な JavaScript へコンパイルできる。現時点では ES6 も AltJS の一種とみなせるため babelify でコンパイルするようにしておく。
Babel と同様に Browserify も CLI ツールを提供している。これをインストールして
$ npm i -g browserify
Babel CLI と組み合わせると以下のようにコンパイルできる。
$ browserify script.js -t babelify --outfile bundle.js
gulp
私は Web フロントエンドや nw.js アプリ開発のタスク管理に gulp を利用している。
gulp タスクの内、JavaScript 依存解決とコンパイルは Browserify で管理しているため babelify もそこに組み込む。なお CLI を利用せず gulp タスクのみで参照するなら Browserify と babelify のグローバルなインストール npm i -g
は不要。
はじめにタスクで利用する npm をインストール。
$ npm i --save gulp gulp-load-plugins gulp-util gulp-notify gulp-if gulp-sourcemaps gulp-uglify
$ npm i --save browserify watchify babelify vinyl-source-stream vinyl-buffer
次にタスクを定義。
var gulp = require( 'gulp' );
var $ = require( 'gulp-load-plugins' )();
/**
* JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
* このタスクは開発用で、JavaScript は Minify されません。
* Minify するとデバッガで Source Maps を展開したとき、変数名を復元できず不便なので開発時はそのまま結合します。
*
* @return {Object} ストリーム。
*/
gulp.task( 'js', function() {
return compile( false );
} );
/**
* JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
* このタスクはリリース用で、JavaScript は Minify されます。
*
* @return {Object} ストリーム。
*/
gulp.task( 'js-release', function() {
return compile( true );
} );
/**
* JavaScript の変更を監視して差分コンパイルします。
*
* @return {Object} ストリーム。
*/
gulp.task( 'watchify', function() {
return compile( false, true );
} );
/**
* JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
*
* @param {Boolean} isMinify 圧縮を有効にする場合は true。
* @param {Boolean} isWatch 差分監視モードで実行する場合は true。
*
* @return {Object} ストリーム。
*/
function compile( isUglify, isWatch ) {
var src = './src/js/app.js';
var dest = './dest/js';
var bundlejs = 'bundle.js';
var browserify = require( 'browserify' );
var source = require( 'vinyl-source-stream' );
var buffer = require( 'vinyl-buffer' );
var watchify = require( 'watchify' );
var logger = new bundleLogger( src, bundlejs );
var option = {
basedir: './',
debug: true,
transform: [ 'babelify' ]
};
var bundler = null;
if( isWatch ) {
option.cache = {};
option.packageCache = {};
option.fullPaths = true;
bundler = watchify( browserify( src, option ) );
logger.watch();
} else {
bundler = browserify( src, option );
}
/**
* Browserify による JavaScript コンパイルを実行します。
*
* @return {Object} ストリーム。
*/
function bundle() {
logger.begin();
return bundler
.bundle()
.on( 'error', handleError )
.pipe( source( bundlejs ) )
.pipe( buffer() )
.pipe( $.sourcemaps.init( { loadMaps: true } ) )
.pipe( $.if( isUglify, $.uglify() ) )
.pipe( $.sourcemaps.write( '.' ) )
.on( 'end', logger.end )
.pipe( gulp.dest( dest ) );
}
bundler.on( 'update', bundle );
return bundle();
}
/**
* Browserify による JavaScript コンパイルの進捗をコンソールに出力します。
*
* @param {String} src コンパイルの起点となるファイル。
* @param {String} bundle コンパイル結果となるファイル。
*/
var bundleLogger = function( src, bundle ) {
var prettyHrtime = require( 'pretty-hrtime' );
var beginTime;
this.begin = function() {
beginTime = process.hrtime();
$.util.log( 'Bundling', $.util.colors.green( src ) + '...' );
};
this.watch = function() {
$.util.log( 'Watching files required by', $.util.colors.yellow( src ) );
};
this.end = function() {
var taskTime = process.hrtime( beginTime );
var prettyTime = prettyHrtime( taskTime );
$.util.log( 'Bundled', $.util.colors.green( bundle ), 'in', $.util.colors.magenta( prettyTime ) );
};
};
/**
* Browserify のエラーが発生した時、タスク実行を維持するための関数です。
* エラーを検知して通知します。
*/
function handleError() {
var notify = require( 'gulp-notify' );
var args = Array.prototype.slice.call( arguments );
notify
.onError( {
title: 'Task Error',
message: "<%= error %>"
} )
.apply( this, args );
// タスク維持
this.emit( 'end' );
}
JavaScript のビルドは gulp js
または gulp js-release
、ファイル監視による差分ビルドなら gulp watchify
コマンドを実行。
Bable/babelify は React JSX をコンパイルできるため reactify が不要になる。React ユーザーとしてはありがたいのだけど ES6 ではないライブラリをサポートするのはありなのだろうか?
ES6
ES6 を試すにあたり、ある程度の規模と複雑さをもつ既存コード (ES5 相当) を対象としたほうが参考になると考えた。そこでこの間の記事で作成した nw.js による音楽プレーヤーのサンプル実装を ES6 化してみた。このプロジェクトでは UI へ React、データ管理に Flux ( npm flux だけの簡素な実装) を採用している。
class
これまでクラス風に定義していたオブジェクトは
var AudioPlayer = function() {
var _audioContext = ( () => {
var audioContext = ( window.AudioContext || window.webkitAudioContext );
if( audioContext ) { return new audioContext(); }
throw new Error( 'Web Audio API is not supported.' );
} )();
var _gainNode = _audioContext.createGain();
// ... 実装
};
以下のように ES6 クラス化できる。
class AudioPlayer {
constructor() {
this._audioContext = ( () => {
let audioContext = ( window.AudioContext || window.webkitAudioContext );
if( audioContext ) { return new audioContext(); }
throw new Error( 'Web Audio API is not supported.' );
} )();
this._gainNode = this._audioContext.createGain();
}
// ... 実装
}
JavaScript でクラス風なオブジェクト定義でさまざまな流派があったけど、今後は ES6 クラスに統一されてゆくのではなかろうか。
function
でクラス定義する方法だと private
にしたい変数や関数は最外周の関数ローカルに定義していたが、ES6 クラスでこれを実現する方法がわからなかった。そのため this
所属させつつ接頭語にアンダースコアをつけることで private
なことを明示している。
ES6 クラスではコンストラクタも導入されている。そのためメンバー変数の宣言と初期化はここで実行するのがよさそう。
export と import
CommonJS モジュールでは
var AudioPlayer = function() {
};
module.exports = AudioPlayer;
のように定義したものを
var AudioPlayer = require( './AudioPlayer.js' );
のように参照していた。ES6 ではモジュール定義と参照が export/import
として標準化された。定義は以下のようになる。
export default class AudioPlayer {
}
参照は import
を使用。from
キーワードの後に対象へのパスを指定。
import AudioPlayer from './AudioPlayer.js';
CommonJS と同様にモジュールから複数のクラスやオブジェクトを公開できる。default
を付けておくとそれが import
時の既定となる。複数公開されているものから選択、または公開オブジェクトの子を明示的に参照するときは波括弧で指定する。
import {PlayState} from '../model/constants/AudioPlayerConstants.js';
import
はスクリプトのスコープ最上段へ定義しなければならない。関数ローカルなどに定義すると Syntax Error になるので注意すること。C# の using
や Java の import
的にファイル先頭へ定義するとよいだろう。
nw.js において Node モジュールを window.require
で参照しているのだが、これも import
にすべきか悩んだ。結局 nw.js に属することを明示化するため window.require
のままにしてある。外部モジュール参照という意味では import
と並べてファイル先頭にすべきだけど、これらが混在すると分かりにくいので関数ローカルで定義している。
それと import
した変数の命名は PascalCase にしている。通常の変数や関数は camelCase で命名しているため、こうすると名前衝突を避けられるうえ視覚的にも区別しやすい。これに慣れてから Node でプログラミングするときもモジュール スコープ単位で参照される require
ではこのように名付けるようになった。例えば fs
は Fs
、path
は Path
という感じで。
let と const
いままで var
で宣言していた変数を let
、const
に置き換えた。変更されるものは let
、再代入を禁止したいものは const
にしてある。
変数が Object
や Array
へ const
を指定するとするとそれ自体への代入は禁止されるのだが、子要素の変更は可能である。そのため let
にするか迷ったけど const
を採用した。インスタンス参照だけでも不変であることを明示したいので let
より const
のほうが好ましく感じる。
const
は単体で定義できる点が気に入っている。構文が const var
や const let
だったら多用しなかっただろう。C# の const
や Java の final
は型と一緒に指定するため記述が長く面倒。そのため不変であることを強く明示したいときだけ利用していた。
とはいえ、あまりに const
だらけなので他の言語と往復したとき違和感ある。いまはこうしているけど将来は考えかたが変わって let
中心にするかもしれない。CSS プリプロセッサに Stylus を導入したときも波括弧まで省略していたが、インデントだけではスコープがわかりにくいと感じて現在は省略をやめている。
React コンポーネント
React v0.13.0 Beta 1 からで React コンポーネントを ES6 クラスとして定義できるようになった。従来、React.createClass で定義していたものは
var MainViewModel = React.createClass( {
getInitialState: function() {
return {
musics: [],
current: null,
};
},
render: function() {
return (
<div>
<ToolbarViewModel />
<MusicListViewModel musics={this.state.musics} />
</div>
);
}
} );
module.exports = MainViewModel;
以下のように React.Component
派生クラスとして定義できる。
export default class MainViewModel extends React.Component {
constructor( props ) {
super( props );
this.state = {
musics: [],
current: null,
};
}
render() {
return (
<div>
<ToolbarViewModel />
<MusicListViewModel musics={this.state.musics} />
</div>
);
}
}
機能面でもいろいろ変更がある。詳しくは以下の記事を参照のこと。
- React v0.13.0 Beta 1 blog.koba04.com
- React v0.13.0 Beta1でclassでComponentが作れるようになった
クラス内の関数が自動で bind
されなくなるためイベント ハンドラ指定でハマるかも。
とはいえ Array.map
などの function
では明示的に bind(this)
していたわけで this
参照が必要な関数なら常に bind
指定すべし、というルールに統一されるのはよいことだと思う。
Flux の Store とシングルトン
Flux の Store を class 化したくなった。元の実装は facebook/flux のサンプルを踏襲した簡素なもので、こんな感じ。
// Store 単位のデータ
var _audioPlayer = new AudioPlayer();
// Action に応じて呼ばれる関数
function play( music ) {
_audioPlayer.play();
AudioPlayerStore.emitChange();
}
// Action からのリクエスト処理
AppDispatcher.register( function( action ) {
switch( action.type ) {
case ActionTypes.PLAY:
play( action.music );
breakl
}
} );
// Store 実装 ( npm object-assign で Mixin している )
var AudioPlayerStore = assign( {}, EventEmitter.prototype, {
// 更新通知やデータ取得などの実装
} );
module.exports = AudioPlayerStore;
Store の利用側はシングルトンであることを期待する。これを ES6 class で実装する場合、以下に紹介されている方法がよさそうだ。
非公開の Symbol を識別子にすることでモジュール外から new
されることを禁止する (例外が発生する)。インスタンスは static
メソッドからのみ参照できる。static
メソッドのみでインスタンス公開するのは他の言語でもよく見るパターンだ。これを参考に Store を書きなおしてみる。
const SINGLETON_PROP = Symbol();
const SINGLETON_ENFORCER = Symbol();
class AudioPlayerStore extends EventEmitter {
constructor( enforcer ) {
// AudioPlayerStore.instance 以外でインスタンス生成したら例外発生
if( enforcer != SINGLETON_ENFORCER ) {
throw new Error( 'Cannot construct singleton' );
}
// 自身のメソッドで Action からのリクエスト処理
AppDispatcher.register( this._onAction.bind( this ) );
// 自身のプロパティとしてデータ管理
this._audioPlayer = new AudioPlayer();
}
// 唯一の AudioPlayerStore インスタンスを返す
static get instance() {
if( !( this[ SINGLETON_PROP ] ) ) {
this[ SINGLETON_PROP ] = new AudioPlayerStore( SINGLETON_ENFORCER );
}
return this[ SINGLETON_PROP ];
}
_onAction( action ) {
switch( action.type ) {
case ActionTypes.PLAY:
this._play( action.music );
break;
}
}
_play( music ) {
this._audioPlayer.play();
this._emitChange();
}
// 更新通知やデータ取得などの実装
}
// クラスの代りにシングルトン インスタンスを公開
export default AudioPlayerStore.instance;
従来方式だとデータや関数が分散しており、同一ファイル内であっても参照関係やライフサイクルを把握しにくかった。これらをクラスに集約することで参照は this
に明示され、ライフ サイクルもクラス単位となり非常に分かりやすくなった。ES6 class にはアクセス指定子がないためクラスに含めたものは外部参照できてしまうのだが集約で得られるメリットのほうが大きいので、これは受け入れることにした。
シングルトン、利用側としては楽だけど一意性の担保にオブジェクトの本質ではない実装 (しかも定形で冗長なもの) が必要なため忌避する開発者も多い。オブジェクトをシングルトンにするぐらいなら、それを所持・参照する箇所を一意にすべきという思想もある。
例えばアプリの最上層となるエントリー ポイントで管理用のインスタンスを生成して下層にはその参照を伝搬してゆくとか。エントリー ポイントはひとつなので、そこで生成されるものも一意になるという設計。iOS なら AppDelegate
、Android だと Application
派生クラスをそのように実装したりする。
Store の class
化を検討しているときに見つけた azu/material-flux という Flux 実装ではシングルトンを避けて Context
というクラスに Store や Action を集約している。サンプルだと Context
をエントリー ポイントでインスタンス化し、それを React コンポーネントの prop
に指定している。
Store や Action 間に依存があったとしても、その管理を React コンポーネントに晒すことなく Context
内に閉じられる点が好ましい。Context
という命名と porp
による伝搬は Android の設計を彷彿とさせる。
次に実装する機会があったらこれを採用するかもしれない。
サンプル
今回のサンプルを以下に公開する。Before が元、After が ES6 化されたプロジェクトになる。
- Before examples-nw/audio-player at audio-player-v1.0
- After examples-nw/audio-player at audio-player-v1.0.1-es6
clone して README.md の Installation & Build に書かれた手順を実行するとアプリが生成される。