r.jsでビルドする際に設定として使うbuild.jsを動的にする。さらに部品化する。

r.js

RequireJS用のmoduleをビルドする際にはr.jsを使います。

build.js

この時にビルド設定の記述に使用するのがbuild.jsでこんな感じで書きます。
簡単な例だとこんなの

({
  baseUrl:'./src',
  dir : './build',
  paths:{
    jquery:"../lib/jquery-1.8.2",
    text:'../lib/plugins/text',
    underscore:'../lib/underscore-1.3.3'
  }
})

動的なbuild.js

build.jsはJSONではなくてJSです。
JSなので処理を書くことが出来ます。
例えば関数その場実行を含む下記のbuild.jsは、上記のものと同じ設定を表します。

({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    return paths;
  })()
})

ちなみに全体を関数その場実行で包むことは出来ません。
以下のコードはbuild.jsとしては使えません。

(function () {
  return ({
    baseUrl:'./src',
    dir : './build',
    paths:{
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    }
  });
})();

さて、現状ではbuild.jsの中に処理を書けるようになったものの、何を以て設定を変更するかが分からないので、動的というほどではなくてあまり嬉しくありません。
r.jsはNode.jsで実行されているので、process.argvでコマンドライン引数を参照することも出来ます。

r.js -o build.js optimize='none' os='ios5'
({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    var os = (process.argv.filter(function (arg) {
      return arg.match(/^os=/);
    })[0] || '').replace(/^os=/,'');
    if (os === 'ios5') {
      // 何らかの処理
    }
    return paths;
  })()
})

これでコマンドラインオプションによってビルド時の設定を細かく変えることが出来るようになりました。

build.jsのNode.js module化

繰り返しますが全体を関数その場実行で包むことは出来ません。
objectのvalueしか関数その場実行で包めません。
つまり、処理を関数として共通化したくても、それを置いておく場所がありません。
例えば各項目でos optionによる振り分けを行いたいなら、各項目のvalueの関数その場実行の中に以下を入れる必要があります。

    var os = (process.argv.filter(function (arg) {
      return arg.match(/^os=/);
    })[0] || '').replace(/^os=/,'');

これではちょっとやりたいことが増えればすぐに肥大化してしまいます。

さて、繰り返しますがr.jsはNode.jsで実行されています。共通化したい処理はNode.jsのmoduleにしてしまいましょう。
以下のような内容のoptions.jsを作ります。

var os = (process.argv.filter(function (arg) {
  return arg.match(/^os=/);
})[0] || '').replace(/^os=/,'');
module.exports = function options(){
  return {
    os:os
  }
};

これを使う際の注意点は二つあります。

  • Node.jsのmodule読み込み関数requireはr.jsではrequire.jsのそれで上書きされています。
    • nodeRequireが元々のrequire関数なのでこれを使います。
  • Node.jsで実行されているのはr.jsなので、build.jsからの相対pathでmoduleを読み込もうとしても無駄です。
    • process.env.PWDを使ってr.jsを実行したときのカレントディレクトリ(build.jsはここにある前提)を参照しましょう。

こんな感じでもりもりbuild.jsを動的にしていくことが出来ます。
複数環境向けビルドをしたいとか行った場合に捗りますね。

({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    var options = nodeRequire(process.env.PWD+'/build-tools/options')();
    var os = options['os'];
    if (os === 'ios5') {
      // 何らかの処理
    }
    return paths;
  })()
})

Require JSで設定をmodule外に出して、それをビルド時に同梱する

モジュール別の設定を読み込む

RequireJSではモジュールの名前として"module"が予約されていて、モジュール内でmoduleモジュールを取得するとそのモジュールの情報を参照することが出来ます。

define('foo',['module'], function (module) {
});

RequireJS API
config methodで設定できる項目の中のconfigにモジュール名をkey何らかのオブジェクトをvalueという辞書を設定しておくと、
moduleモジュールのconfig methodでそのオブジェクトを取得することが出来ます。
今回はmodule moduleとかconfig configとか見てて混乱するのが多いですね。
こんな設定をすると

requirejs.config({
    config: {
        'baz': {
            color: 'blue'
        }
    }
});

こんなふうに取れます。

define('baz',['module'], function (module) {
    //Will be the value 'blue'
    var color = module.config().color;
});

こういった設定をr.jsでのビルド時に同梱してrequirejs.config/require.configでの設定と一緒に使われるデフォルト値のような形にすることは出来ないようです。
設定込みのビルドというのは出来ないわけです。

ビルド時に設定を同梱する

同梱出来ない設定をどうやって同梱するか?
設定を行うJavaScriptのコードを同梱してしまえば良いのです。
RequireJS2.1からはビルド後のmodule全体を囲むwrapの設定で複数ファイルを指定することが出来るようになっています。
r.js/example.build.js at master · requirejs/r.js · GitHub
wrap.startFileの2つ目以降に設定を行うJavaScriptのファイルを入れれば同梱出来ます。

環境別ビルド等でどんどん活用していきましょう。

CommonJS AMDとDeferred

Writing Modular JavaScript With AMD, CommonJS & ES HarmonyのModules With Deferred Dependenciesが便利なので活用してる。
初期化処理が非同期処理でrequireしてきても即使えるとは限らない場合に使う。
モジュール側ではモジュールそのものではなくてpromiseを返しておいて、モジュールの実体が完成したらresolveで渡す。
使う側はrequireしてきたpromiseのthenメソッドのcallbackでモジュールの実体を受け取って使う。

// 何らかの非同期処理を経て初期化されるモジュール
define('someModule',['jquery'],function($){
  var dfd = $.Deferred();
  setTimeout(function(){
    // モジュールとして実際使いたいobject
    var module = {a:1};
    dfd.resolve(module);
  },500);
  return dfd.promise();
});
// モジュールを使う側
require(['someModule'],function(someModule){
  someModule.then(function(module){
    console.log(module); // {a:1}
  });
});

こういったモジュール複数に依存している場合は

require(['someModule1','someModule2','someModule3'],function(someModule1,someModule2,someModule3){
  $.when(someModule1,someModule2,someModule3).then(function(module1,module2,module3){
    console.log(module1,module2,module3);
  });
});

ね、簡単でしょう?

JavaScriptでコマンドを作って実行する

前置き

Firebug1.10にhelpコマンドなるものが入ってました。
consoleでhelpって入力して実行するとFirebugで使える関数等が表示されます。
help();じゃないんですよ。
関数じゃなくてコマンド。
どうやって実現しているんだろうかって気になったのでエスパーして似たようなのを作ってみました。
Firebugのソースは読んでないけど多分おんなじようなことをしているはず。

コード

以下、with文の中でcommandって書いて実行するとcommand is executed.って出力されます。

var obj={};
Object.defineProperty(obj,'command',{
  get : function() {
    console.log('command is executed.');
  }
});
with (obj) {
  command // command is executed.
}

解説

Object.definePropertyでgetアクセサのあるpropertyを定義すれば、getアクセサの関数を関数・メソッド呼び出しではなくproperty参照で実行できます。
さらにwith文でそのpropertyを持ったobjectを指定することで、.や[]なしでproperty参照できるので、コマンドのように実行することが出来るようになります。
Firebugでは少なくとも以前は入力した内容をwith文の中でevalすることでFirebug専用の関数等を使えるようにしていたので、helpコマンドもおそらく同じです。

JavaScriptでのbuilt-in/DOM objectのprototype拡張

@rosylillyが気にしていた

のでまとめた。

built-in/DOM objectのprototype拡張による弊害

追加したプロパティ/メソッドがfor inで列挙される
var obj = {a:1};
for (var i in obj) {
  console.log(i);
}

こうするとaだけ出るはずが、

Object.prototype.b=function(){};

こうした後だとa,bが出てしまうって奴ですね。
そのまま代入しないでObject.defineProperty/definePropertiesでenumerable:falseのプロパティとして定義すれば列挙されなくなるので特に問題ありません。
今回挙げるprototype拡張の弊害の内唯一これだけはECMAScript5時代になって解消されました。唯一これだけは。

built-in/DOM objectの新しい仕様で追加されたプロパティ/メソッドとバッティング

JSどうこうというかどの言語のmonkey patchでも起こりうる問題。
よく見かけた例

  • Prototype.js(1.6.0.3以前)でのArray#reduceとECMAScript5のArray#reduce
  • Prototype.js(1.7未満)によるECMAScript5のJSON.stringifyの破壊
    • Ooharabucyou «
    • 何だか知らんが壊れる
    • あまり使わないがJSON.stringifyでJSON文字列化されるobjectのどこかの階層にtoJSONメソッドを持つobjectがあるとその戻り値でその階層が置き換えられる。このtoJSONメソッドの戻り値はプリミティブ or objectであってJSON文字列ではない。一方Prototype.js(1.7未満)ではJSON文字列を返すArray#toJSONを実装していたためJSON.stringifyがまともに機能しなくなっていた。
    • 問題が発生するメソッドと拡張された箇所が違うため知らなければ原因特定は困難
JSON.stringify({
  b:{
    toJSON:function(){return 1;}
  }
}); // '{"b":1}'

ECMAScript6でも便利メソッドは追加されるので今後も似たような状況にはなりうるので、prototype拡張として便利メソッドを実装するのはお勧めしません。

代案としてはやはりラッパーオブジェクト系のライブラリか関数群を提供するライブラリが良いと思います。


ラッパーオブジェクト系のライブラリ

正直Underscore.jsもそろそろラッパーオブジェクト/メソッドチェーンのレイヤと便利メソッドを生やすレイヤを分けてUnderscore.js的なライブラリを作る土壌になったほうが良いのでは?などと。

同じようにbuilt-in/DOM objectのprototype拡張をしている他ライブラリとのバッティング

これもJSどうこうというかどの言語のmonkey patchでも起こりうる問題。
バッティングが起こらないように気を付けろというのは対策になりません。
手動でscriptタグを並べていた時代ならいざしらず今はCommonJS AMD再帰的に依存関係を辿る真っ当なモジュールシステムがある時代なので、依存関係グラフの途中のどこかとどこかのモジュールでバッティングしているとか目も当てられません。
積極的なmonkey patchが忌避されるのはJavaScriptに限った話ではないということを忘れるべきではありません。

monkey patchでのバッティングの対策は以下のようなものだと思います

JavaScriptでは上記3対策のうち事実上prefixを付けるくらいしか有効ではないので、便利メソッドを無邪気に生やすのは考えものです。

Re:prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log

prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log
ついでにこのエントリに言及しておくと、このエントリでやってるのは典型的な藁人形メソッドです。


prototype拡張を使わない冗長なコードとprototype拡張を使った自作ライブラリを使った簡潔なコードを比較していますが、冗長なのはprototype拡張を使っていないからではありません。
冗長な使い捨てコードと簡潔なAPIのライブラリを比較しているだけです。


Number#to(prototype拡張あり)とUnderscore.js+ECMAScript5標準(prototype拡張なし)を比較してみましょう。
prototype拡張によって越えた壁はどこかにありますか?

function isX5(n) { // Multiples of 5 filter
  return n % 5 === 0;
}

1..to(100);       // -> [1, 2 .. 99, 100]
100..to(1);       // -> [100, 99 .. 2, 1]
1..to(100, 2);    // -> [1, 3 .. 97, 99] (skip 2)
1..to(100, isX5); // -> [5, 10, .. 95, 100] (filter)
function isX5(n) { // Multiples of 5 filter
  return n % 5 === 0;
}

_.range(1,100+1); // [1, 2 .. 99, 100]
_.range(1,100+1).reverse(); // [100, 99 .. 2, 1]
_.range(1,100+1,2); // [1, 3 .. 97, 99] (skip 2)
_.range(1,100+1).filter(isX5); // [5, 10, .. 95, 100] (filter)

built-in/DOM objectのprototype拡張をしてよさそうな箇所

最後にprototype拡張を活用できる・できそうな箇所についても言及しておきます。

shim/pollyfill

標準仕様に入ったAPIを未実装の環境に提供するのは何も問題ありません。バッティングして問題が発生するなら相手のほうが悪いです。どんどんやればいいし、そういうライブラリを提供している人はたくさんいます。

internalプロパティ/メソッド

APIとして公開するものでなくライブラリの中でのみ使うプロパティ/メソッドをまず他とバッティングしないような長い名前で追加するのは問題ありません。もちろんenumerableはfalseにする。例えばGitHub - monjudoh/BeautifulProperties.jsでObject.prototypeに対してEvents.onしたら'BeautifulProperties::internalObjectKey'というkeyのプロパティが追加されるがこれはバッティングしないでしょう。

小規模なWebWorker内

この場合DOM objectのprototype拡張はそもそもありません。Worker間・Worker/window間でbuilt-in objectのprototypeは共有されないので、Worker内での拡張が他に影響することはありません。個々のWorkerのscriptのサイズをprototype拡張の影響を充分に追いきれる規模に止めれば良いでしょう。

BPStudy#60 ECMAScript5時代のJavaScriptライブラリ

2012年未だ誤解されているプログラミング言語JavaScript

嫌われている"JavaScript"とは何か?
  • ECMAScript3
    • 13年前・前世紀(1999年)の言語仕様
  • IE6
    • 11年前(2001年)のランタイム
嫌われている"JavaScript"と同世代の各言語のバージョンは?
  • Python2.1
  • Ruby1.6
  • Perl5.6
  • J2SE1.3

現在のJavaScriptはECMAScript5

どの環境で使える?

http://kangax.github.com/es5-compat-table/
ES5 features on iOS/Android's default browser - 愛と勇気と缶ビール

  • IE9+(Strict modeを除く)
  • Google Chrome13+
    • 「変数名にゼロ幅スペース使用可能」を除く
  • Firefox6+
  • Safari5.1.4+
    • 「変数名にゼロ幅スペース使用可能」を除く
      • 込みなら6+
  • MobileSafari iOS6+
  • Android 標準ブラウザ 4.0+
    • 「変数名にゼロ幅スペース使用可能」を除く
    • strict modeを除く
      • 込みなら4.1+
  • Opera12+
    • 「変数名にゼロ幅スペース使用可能」を除く

shimライブラリを使えば対応環境はもっと増える
GitHub - es-shims/es5-shim: ECMAScript 5 compatibility shims for legacy (and modern) JavaScript engines

  • IE9+(Strict modeを除く)
  • Google Chrome13+
    • 「変数名にゼロ幅スペース使用可能」を除く-Firefox6+
  • Safari5.1+
    • 「変数名にゼロ幅スペース使用可能」を除く
      • 込みなら6+
  • MobileSafari iOS5.0+
    • 「変数名にゼロ幅スペース使用可能」を除く
      • 込みなら6+-Android 標準ブラウザ 4.0+
    • 「変数名にゼロ幅スペース使用可能」を除く
    • strict modeを除く
      • 込みなら4.1+
  • Opera12+
    • 「変数名にゼロ幅スペース使用可能」を除く
何が出来る?
  • strict mode
  • Array#map等のarrayの反復メソッド
  • その他builtin objectの便利メソッドが若干
  • native JSON
  • Function#bind
    • thisと引数の束縛
  • Object.create
    • コンストラクタを経由せずにprototype継承が出来る
    • Object.prototypeを継承しないobject、純粋な辞書を作れる
  • object保護関数群
    • Object.preventExtensions
      • propertyの追加定義禁止
    • Object.seal
      • propertyの追加定義・再定義禁止
    • Object.freeze
      • propertyの追加定義・再定義・値の変更禁止
  • Object.Object.defineProperty
    • writable
    • enumerable
    • configurable
    • value
    • get
    • set

ECMAScript5の豊かさを活かしていきたい

ECMAScript5をターゲットにしたライブラリ、BeautifulProperties.js

v0.1.0を先ほどリリースしました
GitHub - monjudoh/BeautifulProperties.js at v0.1.0

インスタンスに依存した初期値を持つ書き換え可能propertyの定義

インスタンスに依存してなければこれで済むから簡単ですよねー

function A(){}
var proto = A.prototype;
Object.defineProperty(proto,'key',{
  value : 'default',
  writable : true
});

インスタンスに依存している場合はprototype定義時にそのインスタンスが存在しないのでvalueで初期値を定義できません。
単純なコードだと、実際の値を保持する別propertyとget/setを定義して、まだ保持していなかったら設定するとかそうなるでしょう。

function foo(val) {
  // valの内容によって戻り値が変わると思いねえ
  return 'default';
}
function A(){}
var proto = A.prototype;
Object.defineProperty(proto,'key',{
  get : function () {
    var self = this;
    if (self._key === undefined) {
      self._key = foo(self);
    }
    return self._key;
  },
  set : function (val) {
    var self = this;
    self._key = val;
  }
});

しかし、無駄にpropertyを増やしたくありません。
こうします。
prototypeに対してconfigurable:trueにしてget/setありのpropertyを定義、
その中でインスタンスの(!=prototypeの)propertyを上書き定義します。

function foo(val) {
  // valの内容によって戻り値が変わると思いねえ
  return 'default';
}

function A(){}
var proto = A.prototype;
Object.defineProperty(proto,'key',{
  get : function () {
    var self = this;
    var val = foo(self);
    Object.defineProperty(self,'key',{
      value:val,
      writable:true
    });
    return val;
  },
  set : function (val) {
    var self = this;
    Object.defineProperty(self,'key',{
      value:val,
      writable:true
    });
  },
  configurable : true
});

ばっちり動きますね

a= new A;
b= new A;
a.key; // 'default'
a.key= 1;
a.key; // 1
b.key; // 'default'

参考http://d.hatena.ne.jp/Constellation/20101205/1291564928